diff --git a/lib/python3.10/site-packages/absl/__init__.py b/lib/python3.10/site-packages/absl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1849be666cc38eb5ea89f1d9640e835dab46669b --- /dev/null +++ b/lib/python3.10/site-packages/absl/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '2.3.1' diff --git a/lib/python3.10/site-packages/absl/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/absl/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c18095b4b8d52f8319e8602b85e4020d8497b154 Binary files /dev/null and b/lib/python3.10/site-packages/absl/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/__pycache__/app.cpython-310.pyc b/lib/python3.10/site-packages/absl/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22ca9f0d63ce0f191c11c7d64dd23f24f7ff2b1e Binary files /dev/null and b/lib/python3.10/site-packages/absl/__pycache__/app.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/__pycache__/command_name.cpython-310.pyc b/lib/python3.10/site-packages/absl/__pycache__/command_name.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b04128e0fbee74b5a7a077166bef3e028a2c4b6 Binary files /dev/null and b/lib/python3.10/site-packages/absl/__pycache__/command_name.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/app.py b/lib/python3.10/site-packages/absl/app.py new file mode 100644 index 0000000000000000000000000000000000000000..8eefb9e7a28a917971e9f0ba1855c43a72cd9ee5 --- /dev/null +++ b/lib/python3.10/site-packages/absl/app.py @@ -0,0 +1,488 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generic entry point for Abseil Python applications. + +To use this module, define a ``main`` function with a single ``argv`` argument +and call ``app.run(main)``. For example:: + + def main(argv): + if len(argv) > 1: + raise app.UsageError('Too many command-line arguments.') + + if __name__ == '__main__': + app.run(main) +""" + +import collections +import errno +import os +import pdb +import sys +import textwrap +import traceback + +from absl import command_name +from absl import flags +from absl import logging + +try: + import faulthandler +except ImportError: + faulthandler = None + +FLAGS = flags.FLAGS + +flags.DEFINE_boolean('run_with_pdb', False, 'Set to true for PDB debug mode') +flags.DEFINE_boolean('pdb_post_mortem', False, + 'Set to true to handle uncaught exceptions with PDB ' + 'post mortem.') +flags.DEFINE_alias('pdb', 'pdb_post_mortem') +flags.DEFINE_boolean('run_with_profiling', False, + 'Set to true for profiling the script. ' + 'Execution will be slower, and the output format might ' + 'change over time.') +flags.DEFINE_string('profile_file', None, + 'Dump profile information to a file (for python -m ' + 'pstats). Implies --run_with_profiling.') +flags.DEFINE_boolean('use_cprofile_for_profiling', True, + 'Use cProfile instead of the profile module for ' + 'profiling. This has no effect unless ' + '--run_with_profiling is set.') +flags.DEFINE_boolean('only_check_args', False, + 'Set to true to validate args and exit.', + allow_hide_cpp=True) + + +# If main() exits via an abnormal exception, call into these +# handlers before exiting. +EXCEPTION_HANDLERS = [] + + +class Error(Exception): + pass + + +class UsageError(Error): + """Exception raised when the arguments supplied by the user are invalid. + + Raise this when the arguments supplied are invalid from the point of + view of the application. For example when two mutually exclusive + flags have been supplied or when there are not enough non-flag + arguments. It is distinct from flags.Error which covers the lower + level of parsing and validating individual flags. + """ + + def __init__(self, message, exitcode=1): + super().__init__(message) + self.exitcode = exitcode + + +class HelpFlag(flags.BooleanFlag): + """Special boolean flag that displays usage and raises SystemExit.""" + NAME = 'help' + SHORT_NAME = '?' + + def __init__(self): + super().__init__( + self.NAME, + False, + 'show this help', + short_name=self.SHORT_NAME, + allow_hide_cpp=True, + ) + + def parse(self, arg): + if self._parse(arg): + usage(shorthelp=True, writeto_stdout=True) + # Advertise --helpfull on stdout, since usage() was on stdout. + print() + print('Try --helpfull to get a list of all flags.') + sys.exit(1) + + +class HelpshortFlag(HelpFlag): + """--helpshort is an alias for --help.""" + NAME = 'helpshort' + SHORT_NAME = None + + +class HelpfullFlag(flags.BooleanFlag): + """Display help for flags in the main module and all dependent modules.""" + + def __init__(self): + super().__init__('helpfull', False, 'show full help', allow_hide_cpp=True) + + def parse(self, arg): + if self._parse(arg): + usage(writeto_stdout=True) + sys.exit(1) + + +class HelpXMLFlag(flags.BooleanFlag): + """Similar to HelpfullFlag, but generates output in XML format.""" + + def __init__(self): + super().__init__( + 'helpxml', + False, + 'like --helpfull, but generates XML output', + allow_hide_cpp=True, + ) + + def parse(self, arg): + if self._parse(arg): + flags.FLAGS.write_help_in_xml_format(sys.stdout) + sys.exit(1) + + +def parse_flags_with_usage(args): + """Tries to parse the flags, print usage, and exit if unparsable. + + Args: + args: [str], a non-empty list of the command line arguments including + program name. + + Returns: + [str], a non-empty list of remaining command line arguments after parsing + flags, including program name. + """ + try: + return FLAGS(args) + except flags.Error as error: + message = str(error) + if '\n' in message: + final_message = 'FATAL Flags parsing error:\n%s\n' % textwrap.indent( + message, ' ') + else: + final_message = 'FATAL Flags parsing error: %s\n' % message + sys.stderr.write(final_message) + sys.stderr.write('Pass --helpshort or --helpfull to see help on flags.\n') + sys.exit(1) + + +_define_help_flags_called = False + + +def define_help_flags(): + """Registers help flags. Idempotent.""" + # Use a global to ensure idempotence. + global _define_help_flags_called + + if not _define_help_flags_called: + flags.DEFINE_flag(HelpFlag()) + flags.DEFINE_flag(HelpshortFlag()) # alias for --help + flags.DEFINE_flag(HelpfullFlag()) + flags.DEFINE_flag(HelpXMLFlag()) + _define_help_flags_called = True + + +def _register_and_parse_flags_with_usage( + argv=None, + flags_parser=parse_flags_with_usage, +): + """Registers help flags, parses arguments and shows usage if appropriate. + + This also calls sys.exit(0) if flag --only_check_args is True. + + Args: + argv: [str], a non-empty list of the command line arguments including + program name, sys.argv is used if None. + flags_parser: Callable[[List[str]], Any], the function used to parse flags. + The return value of this function is passed to `main` untouched. It must + guarantee FLAGS is parsed after this function is called. + + Returns: + The return value of `flags_parser`. When using the default `flags_parser`, + it returns the following: + [str], a non-empty list of remaining command line arguments after parsing + flags, including program name. + + Raises: + Error: Raised when flags_parser is called, but FLAGS is not parsed. + SystemError: Raised when it's called more than once. + """ + # fmt: on + if _register_and_parse_flags_with_usage.done: + raise SystemError('Flag registration can be done only once.') + + define_help_flags() + + original_argv = sys.argv if argv is None else argv + args_to_main = flags_parser(original_argv) + if not FLAGS.is_parsed(): + raise Error('FLAGS must be parsed after flags_parser is called.') + + # Exit when told so. + if FLAGS.only_check_args: + sys.exit(0) + # Immediately after flags are parsed, bump verbosity to INFO if the flag has + # not been set. + if FLAGS['verbosity'].using_default_value: + FLAGS.verbosity = 0 + _register_and_parse_flags_with_usage.done = True + + return args_to_main + +_register_and_parse_flags_with_usage.done = False + + +def _run_main(main, argv): + """Calls main, optionally with pdb or profiler.""" + if FLAGS.run_with_pdb: + sys.exit(pdb.runcall(main, argv)) + elif FLAGS.run_with_profiling or FLAGS.profile_file: + # Avoid import overhead since most apps (including performance-sensitive + # ones) won't be run with profiling. + # pylint: disable=g-import-not-at-top + import atexit + if FLAGS.use_cprofile_for_profiling: + import cProfile as profile + else: + import profile + profiler = profile.Profile() + if FLAGS.profile_file: + atexit.register(profiler.dump_stats, FLAGS.profile_file) + else: + atexit.register(profiler.print_stats) + sys.exit(profiler.runcall(main, argv)) + else: + sys.exit(main(argv)) + + +def _call_exception_handlers(exception): + """Calls any installed exception handlers.""" + for handler in EXCEPTION_HANDLERS: + try: + if handler.wants(exception): + handler.handle(exception) + except: # pylint: disable=bare-except + try: + # We don't want to stop for exceptions in the exception handlers but + # we shouldn't hide them either. + logging.error(traceback.format_exc()) + except: # pylint: disable=bare-except + # In case even the logging statement fails, ignore. + pass + + +def run( + main, + argv=None, + flags_parser=parse_flags_with_usage, +): + """Begins executing the program. + + Args: + main: The main function to execute. It takes an single argument "argv", + which is a list of command line arguments with parsed flags removed. + The return value is passed to `sys.exit`, and so for example + a return value of 0 or None results in a successful termination, whereas + a return value of 1 results in abnormal termination. + For more details, see https://docs.python.org/3/library/sys#sys.exit + argv: A non-empty list of the command line arguments including program name, + sys.argv is used if None. + flags_parser: Callable[[List[str]], Any], the function used to parse flags. + The return value of this function is passed to `main` untouched. + It must guarantee FLAGS is parsed after this function is called. + Should be passed as a keyword-only arg which will become mandatory in a + future release. + - Parses command line flags with the flag module. + - If there are any errors, prints usage(). + - Calls main() with the remaining arguments. + - If main() raises a UsageError, prints usage and the error message. + """ + # fmt: on + try: + args = _run_init( + sys.argv if argv is None else argv, + flags_parser, + ) + while _init_callbacks: + callback = _init_callbacks.popleft() + callback() + try: + _run_main(main, args) + except UsageError as error: + usage(shorthelp=True, detailed_error=error, exitcode=error.exitcode) + except: + exc = sys.exc_info()[1] + # Don't try to post-mortem debug successful SystemExits, since those + # mean there wasn't actually an error. In particular, the test framework + # raises SystemExit(False) even if all tests passed. + if isinstance(exc, SystemExit) and not exc.code: + raise + + # Check the tty so that we don't hang waiting for input in an + # non-interactive scenario. + if FLAGS.pdb_post_mortem and sys.stdout.isatty(): + traceback.print_exc() + print() + print(' *** Entering post-mortem debugging ***') + print() + pdb.post_mortem() + raise + except Exception as e: + _call_exception_handlers(e) + raise + +# Callbacks which have been deferred until after _run_init has been called. +_init_callbacks = collections.deque() + + +def call_after_init(callback): + """Calls the given callback only once ABSL has finished initialization. + + If ABSL has already finished initialization when ``call_after_init`` is + called then the callback is executed immediately, otherwise `callback` is + stored to be executed after ``app.run`` has finished initializing (aka. just + before the main function is called). + + If called after ``app.run``, this is equivalent to calling ``callback()`` in + the caller thread. If called before ``app.run``, callbacks are run + sequentially (in an undefined order) in the same thread as ``app.run``. + + Args: + callback: a callable to be called once ABSL has finished initialization. + This may be immediate if initialization has already finished. It + takes no arguments and returns nothing. + """ + if _run_init.done: + callback() + else: + _init_callbacks.append(callback) + + +def _run_init( + argv, + flags_parser, +): + """Does one-time initialization and re-parses flags on rerun.""" + if _run_init.done: + return flags_parser(argv) + command_name.make_process_name_useful() + # Set up absl logging handler. + logging.use_absl_handler() + args = _register_and_parse_flags_with_usage( + argv=argv, + flags_parser=flags_parser, + ) + if faulthandler: + try: + faulthandler.enable() + except Exception: # pylint: disable=broad-except + # Some tests verify stderr output very closely, so don't print anything. + # Disabled faulthandler is a low-impact error. + pass + _run_init.done = True + return args + + +_run_init.done = False + + +def usage(shorthelp=False, writeto_stdout=False, detailed_error=None, + exitcode=None): + """Writes __main__'s docstring to stderr with some help text. + + Args: + shorthelp: bool, if True, prints only flags from the main module, + rather than all flags. + writeto_stdout: bool, if True, writes help message to stdout, + rather than to stderr. + detailed_error: str, additional detail about why usage info was presented. + exitcode: optional integer, if set, exits with this status code after + writing help. + """ + if writeto_stdout: + stdfile = sys.stdout + else: + stdfile = sys.stderr + + doc = sys.modules['__main__'].__doc__ + if not doc: + doc = '\nUSAGE: %s [flags]\n' % sys.argv[0] + doc = flags.text_wrap(doc, indent=' ', firstline_indent='') + else: + # Replace all '%s' with sys.argv[0], and all '%%' with '%'. + num_specifiers = doc.count('%') - 2 * doc.count('%%') + try: + doc %= (sys.argv[0],) * num_specifiers + except (OverflowError, TypeError, ValueError): + # Just display the docstring as-is. + pass + if shorthelp: + flag_str = FLAGS.main_module_help() + else: + flag_str = FLAGS.get_help() + try: + stdfile.write(doc) + if flag_str: + stdfile.write('\nflags:\n') + stdfile.write(flag_str) + stdfile.write('\n') + if detailed_error is not None: + stdfile.write('\n%s\n' % detailed_error) + except OSError as e: + # We avoid printing a huge backtrace if we get EPIPE, because + # "foo.par --help | less" is a frequent use case. + if e.errno != errno.EPIPE: + raise + if exitcode is not None: + sys.exit(exitcode) + + +class ExceptionHandler: + """Base exception handler from which other may inherit.""" + + def wants(self, exc): + """Returns whether this handler wants to handle the exception or not. + + This base class returns True for all exceptions by default. Override in + subclass if it wants to be more selective. + + Args: + exc: Exception, the current exception. + """ + del exc # Unused. + return True + + def handle(self, exc): + """Do something with the current exception. + + Args: + exc: Exception, the current exception + + This method must be overridden. + """ + raise NotImplementedError() + + +def install_exception_handler(handler): + """Installs an exception handler. + + Args: + handler: ExceptionHandler, the exception handler to install. + + Raises: + TypeError: Raised when the handler was not of the correct type. + + All installed exception handlers will be called if main() exits via + an abnormal exception, i.e. not one of SystemExit, KeyboardInterrupt, + FlagsError or UsageError. + """ + if not isinstance(handler, ExceptionHandler): + raise TypeError('handler of type %s does not inherit from ExceptionHandler' + % type(handler)) + EXCEPTION_HANDLERS.append(handler) diff --git a/lib/python3.10/site-packages/absl/app.pyi b/lib/python3.10/site-packages/absl/app.pyi new file mode 100644 index 0000000000000000000000000000000000000000..934be90ad8692a32e4ee03fb04f5531977b90380 --- /dev/null +++ b/lib/python3.10/site-packages/absl/app.pyi @@ -0,0 +1,88 @@ +from typing import Any, Callable, Collection, Iterable, List, NoReturn, Optional, TypeVar, Union, overload + +from absl.flags import _flag + +_MainArgs = TypeVar('_MainArgs') +_Exc = TypeVar('_Exc', bound=Exception) + +class ExceptionHandler(): + + def wants(self, exc: _Exc) -> bool: + ... + + def handle(self, exc: _Exc): + ... + +EXCEPTION_HANDLERS: List[ExceptionHandler] = ... + +class HelpFlag(_flag.BooleanFlag): + def __init__(self): + ... + +class HelpshortFlag(HelpFlag): + ... + +class HelpfullFlag(_flag.BooleanFlag): + def __init__(self): + ... + +class HelpXMLFlag(_flag.BooleanFlag): + def __init__(self): + ... + +def define_help_flags() -> None: + ... + +@overload +def usage(shorthelp: Union[bool, int] = ..., + writeto_stdout: Union[bool, int] = ..., + detailed_error: Optional[Any] = ..., + exitcode: None = ...) -> None: + ... + +@overload +def usage(shorthelp: Union[bool, int], + writeto_stdout: Union[bool, int], + detailed_error: Optional[Any], + exitcode: int) -> NoReturn: + ... + +@overload +def usage(shorthelp: Union[bool, int] = ..., + writeto_stdout: Union[bool, int] = ..., + detailed_error: Optional[Any] = ..., + *, + exitcode: int) -> NoReturn: + ... + +def install_exception_handler(handler: ExceptionHandler) -> None: + ... + +class Error(Exception): + ... + +class UsageError(Error): + exitcode: int + +def parse_flags_with_usage(args: List[str]) -> List[str]: + ... + +def call_after_init(callback: Callable[[], Any]) -> None: + ... + +# Without the flag_parser argument, `main` should require a List[str]. +@overload +def run( + main: Callable[[List[str]], Any], + argv: Optional[List[str]] = ..., +) -> NoReturn: + ... + +@overload +def run( + main: Callable[[_MainArgs], Any], + argv: Optional[List[str]] = ..., + *, + flags_parser: Callable[[List[str]], _MainArgs], +) -> NoReturn: + ... diff --git a/lib/python3.10/site-packages/absl/command_name.py b/lib/python3.10/site-packages/absl/command_name.py new file mode 100644 index 0000000000000000000000000000000000000000..86f81af8ed9fbc98dd46a3171631163ae8f693a5 --- /dev/null +++ b/lib/python3.10/site-packages/absl/command_name.py @@ -0,0 +1,63 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A tiny stand alone library to change the kernel process name on Linux.""" + +import os +import sys + +# This library must be kept small and stand alone. It is used by small things +# that require no extension modules. + + +def make_process_name_useful(): + """Sets the process name to something better than 'python' if possible.""" + set_kernel_process_name(os.path.basename(sys.argv[0])) + + +def set_kernel_process_name(name): + """Changes the Kernel's /proc/self/status process name on Linux. + + The kernel name is NOT what will be shown by the ps or top command. + It is a 15 character string stored in the kernel's process table that + is included in the kernel log when a process is OOM killed. + The first 15 bytes of name are used. Non-ASCII unicode is replaced with '?'. + + Does nothing if /proc/self/comm cannot be written or prctl() fails. + + Args: + name: bytes|unicode, the Linux kernel's command name to set. + """ + if not isinstance(name, bytes): + name = name.encode('ascii', 'replace') + try: + # This is preferred to using ctypes to try and call prctl() when possible. + with open('/proc/self/comm', 'wb') as proc_comm: + proc_comm.write(name[:15]) + except OSError: + try: + import ctypes # pylint: disable=g-import-not-at-top + except ImportError: + return # No ctypes. + try: + libc = ctypes.CDLL('libc.so.6') + except OSError: + return # No libc.so.6. + pr_set_name = ctypes.c_ulong(15) # linux/prctl.h PR_SET_NAME value. + zero = ctypes.c_ulong(0) + try: + libc.prctl(pr_set_name, name, zero, zero, zero) + # Ignore the prctl return value. Nothing we can do if it errored. + except AttributeError: + return # No prctl. diff --git a/lib/python3.10/site-packages/absl/flags/__init__.py b/lib/python3.10/site-packages/absl/flags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..06401900d2d4c6da61f6c532e330eb6a3fcc39e7 --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/__init__.py @@ -0,0 +1,220 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This package is used to define and parse command line flags. + +This package defines a *distributed* flag-definition policy: rather than +an application having to define all flags in or near main(), each Python +module defines flags that are useful to it. When one Python module +imports another, it gains access to the other's flags. (This is +implemented by having all modules share a common, global registry object +containing all the flag information.) + +Flags are defined through the use of one of the DEFINE_xxx functions. +The specific function used determines how the flag is parsed, checked, +and optionally type-converted, when it's seen on the command line. +""" + +import sys + +from absl.flags import _argument_parser +from absl.flags import _defines +from absl.flags import _exceptions +from absl.flags import _flag +from absl.flags import _flagvalues +from absl.flags import _helpers +from absl.flags import _validators + +__all__ = ( + 'DEFINE', + 'DEFINE_flag', + 'DEFINE_string', + 'DEFINE_boolean', + 'DEFINE_bool', + 'DEFINE_float', + 'DEFINE_integer', + 'DEFINE_enum', + 'DEFINE_enum_class', + 'DEFINE_list', + 'DEFINE_spaceseplist', + 'DEFINE_multi', + 'DEFINE_multi_string', + 'DEFINE_multi_integer', + 'DEFINE_multi_float', + 'DEFINE_multi_enum', + 'DEFINE_multi_enum_class', + 'DEFINE_alias', + # Flag validators. + 'register_validator', + 'validator', + 'register_multi_flags_validator', + 'multi_flags_validator', + 'mark_flag_as_required', + 'mark_flags_as_required', + 'mark_flags_as_mutual_exclusive', + 'mark_bool_flags_as_mutual_exclusive', + # Flag modifiers. + 'set_default', + 'override_value', + # Key flag related functions. + 'declare_key_flag', + 'adopt_module_key_flags', + 'disclaim_key_flags', + # Module exceptions. + 'Error', + 'CantOpenFlagFileError', + 'DuplicateFlagError', + 'IllegalFlagValueError', + 'UnrecognizedFlagError', + 'UnparsedFlagAccessError', + 'ValidationError', + 'FlagNameConflictsWithMethodError', + # Public classes. + 'Flag', + 'BooleanFlag', + 'EnumFlag', + 'EnumClassFlag', + 'MultiFlag', + 'MultiEnumClassFlag', + 'FlagHolder', + 'FlagValues', + 'ArgumentParser', + 'BooleanParser', + 'EnumParser', + 'EnumClassParser', + 'ArgumentSerializer', + 'FloatParser', + 'IntegerParser', + 'BaseListParser', + 'ListParser', + 'ListSerializer', + 'EnumClassListSerializer', + 'CsvListSerializer', + 'WhitespaceSeparatedListParser', + 'EnumClassSerializer', + # Helper functions. + 'get_help_width', + 'text_wrap', + 'flag_dict_to_args', + 'doc_to_help', + # The global FlagValues instance. + 'FLAGS', +) + +# Initialize the FLAGS_MODULE as early as possible. +# It's only used by adopt_module_key_flags to take SPECIAL_FLAGS into account. +_helpers.FLAGS_MODULE = sys.modules[__name__] + +# Add current module to disclaimed module ids. +_helpers.disclaim_module_ids.add(id(sys.modules[__name__])) + +# DEFINE functions. They are explained in more details in the module doc string. +# pylint: disable=invalid-name +DEFINE = _defines.DEFINE +DEFINE_flag = _defines.DEFINE_flag +DEFINE_string = _defines.DEFINE_string +DEFINE_boolean = _defines.DEFINE_boolean +DEFINE_bool = DEFINE_boolean # Match C++ API. +DEFINE_float = _defines.DEFINE_float +DEFINE_integer = _defines.DEFINE_integer +DEFINE_enum = _defines.DEFINE_enum +DEFINE_enum_class = _defines.DEFINE_enum_class +DEFINE_list = _defines.DEFINE_list +DEFINE_spaceseplist = _defines.DEFINE_spaceseplist +DEFINE_multi = _defines.DEFINE_multi +DEFINE_multi_string = _defines.DEFINE_multi_string +DEFINE_multi_integer = _defines.DEFINE_multi_integer +DEFINE_multi_float = _defines.DEFINE_multi_float +DEFINE_multi_enum = _defines.DEFINE_multi_enum +DEFINE_multi_enum_class = _defines.DEFINE_multi_enum_class +DEFINE_alias = _defines.DEFINE_alias +# pylint: enable=invalid-name + +# Flag validators. +register_validator = _validators.register_validator +validator = _validators.validator +register_multi_flags_validator = _validators.register_multi_flags_validator +multi_flags_validator = _validators.multi_flags_validator +mark_flag_as_required = _validators.mark_flag_as_required +mark_flags_as_required = _validators.mark_flags_as_required +mark_flags_as_mutual_exclusive = _validators.mark_flags_as_mutual_exclusive +mark_bool_flags_as_mutual_exclusive = _validators.mark_bool_flags_as_mutual_exclusive + +# Flag modifiers. +set_default = _defines.set_default +override_value = _defines.override_value + +# Key flag related functions. +declare_key_flag = _defines.declare_key_flag +adopt_module_key_flags = _defines.adopt_module_key_flags +disclaim_key_flags = _defines.disclaim_key_flags + +# Module exceptions. +# pylint: disable=invalid-name +Error = _exceptions.Error +CantOpenFlagFileError = _exceptions.CantOpenFlagFileError +DuplicateFlagError = _exceptions.DuplicateFlagError +IllegalFlagValueError = _exceptions.IllegalFlagValueError +UnrecognizedFlagError = _exceptions.UnrecognizedFlagError +UnparsedFlagAccessError = _exceptions.UnparsedFlagAccessError +ValidationError = _exceptions.ValidationError +FlagNameConflictsWithMethodError = _exceptions.FlagNameConflictsWithMethodError + +# Public classes. +Flag = _flag.Flag +BooleanFlag = _flag.BooleanFlag +EnumFlag = _flag.EnumFlag +EnumClassFlag = _flag.EnumClassFlag +MultiFlag = _flag.MultiFlag +MultiEnumClassFlag = _flag.MultiEnumClassFlag +FlagHolder = _flagvalues.FlagHolder +FlagValues = _flagvalues.FlagValues +ArgumentParser = _argument_parser.ArgumentParser +BooleanParser = _argument_parser.BooleanParser +EnumParser = _argument_parser.EnumParser +EnumClassParser = _argument_parser.EnumClassParser +ArgumentSerializer = _argument_parser.ArgumentSerializer +FloatParser = _argument_parser.FloatParser +IntegerParser = _argument_parser.IntegerParser +BaseListParser = _argument_parser.BaseListParser +ListParser = _argument_parser.ListParser +ListSerializer = _argument_parser.ListSerializer +EnumClassListSerializer = _argument_parser.EnumClassListSerializer +CsvListSerializer = _argument_parser.CsvListSerializer +WhitespaceSeparatedListParser = _argument_parser.WhitespaceSeparatedListParser +EnumClassSerializer = _argument_parser.EnumClassSerializer +# pylint: enable=invalid-name + +# Helper functions. +get_help_width = _helpers.get_help_width +text_wrap = _helpers.text_wrap +flag_dict_to_args = _helpers.flag_dict_to_args +doc_to_help = _helpers.doc_to_help + +# Special flags. +_helpers.SPECIAL_FLAGS = FlagValues() + +DEFINE_string( + 'flagfile', '', + 'Insert flag definitions from the given file into the command line.', + _helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types + +DEFINE_string('undefok', '', + 'comma-separated list of flag names that it is okay to specify ' + 'on the command line even if the program does not define a flag ' + 'with that name. IMPORTANT: flags in this list that have ' + 'arguments MUST use the --flag=value format.', + _helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types + +#: The global FlagValues instance. +FLAGS = _flagvalues.FLAGS diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16370fe30d9aeef15afe61a9374d4b46098fea6d Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_argument_parser.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_argument_parser.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2c28398cbcc5d243544e89dc749e5d2b972ae5d Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_argument_parser.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_defines.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_defines.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4430a80db10db0110a24e8493b47ccf3e671a9e9 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_defines.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_exceptions.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_exceptions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c247d6499d3178750dc1b2336ab9a0d9cb5af8f6 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_exceptions.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_flag.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_flag.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1b07b0214ddfc2199aba77bd586a0b08426dab2 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_flag.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_flagvalues.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_flagvalues.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..663890edb1d3a97d52c447e50d2e475ca1314660 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_flagvalues.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_helpers.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_helpers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..269570157fb215ee3995e08d19f712dd887e09a2 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_helpers.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_validators.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_validators.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9eb7e61015b04f0c007e7e452bb1d03ef3abf596 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_validators.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/_validators_classes.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/_validators_classes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa021c397dbf3b148c1c6881d8a7d2080375b242 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/_validators_classes.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/__pycache__/argparse_flags.cpython-310.pyc b/lib/python3.10/site-packages/absl/flags/__pycache__/argparse_flags.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e6e5ab8da9fd10e584183d9918a7d555942fa07 Binary files /dev/null and b/lib/python3.10/site-packages/absl/flags/__pycache__/argparse_flags.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/flags/_argument_parser.py b/lib/python3.10/site-packages/absl/flags/_argument_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..8a10e50c4a638b0c21a4a029c0542c7618789d3b --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_argument_parser.py @@ -0,0 +1,633 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Contains base classes used to parse and convert arguments. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +import collections +import csv +import enum +import io +import string +from typing import Any, Dict, Generic, Iterable, List, Optional, Sequence, Type, TypeVar, Union +from xml.dom import minidom + +from absl.flags import _helpers + +_T = TypeVar('_T') +_ET = TypeVar('_ET', bound=enum.Enum) +_N = TypeVar('_N', int, float) + + +class _ArgumentParserCache(type): + """Metaclass used to cache and share argument parsers among flags.""" + + _instances: Dict[Any, Any] = {} + + def __call__(cls, *args, **kwargs): + """Returns an instance of the argument parser cls. + + This method overrides behavior of the __new__ methods in + all subclasses of ArgumentParser (inclusive). If an instance + for cls with the same set of arguments exists, this instance is + returned, otherwise a new instance is created. + + If any keyword arguments are defined, or the values in args + are not hashable, this method always returns a new instance of + cls. + + Args: + *args: Positional initializer arguments. + **kwargs: Initializer keyword arguments. + + Returns: + An instance of cls, shared or new. + """ + if kwargs: + return type.__call__(cls, *args, **kwargs) + else: + instances = cls._instances + key = (cls,) + tuple(args) + try: + return instances[key] + except KeyError: + # No cache entry for key exists, create a new one. + return instances.setdefault(key, type.__call__(cls, *args)) + except TypeError: + # An object in args cannot be hashed, always return + # a new instance. + return type.__call__(cls, *args) + + +class ArgumentParser(Generic[_T], metaclass=_ArgumentParserCache): + """Base class used to parse and convert arguments. + + The :meth:`parse` method checks to make sure that the string argument is a + legal value and convert it to a native type. If the value cannot be + converted, it should throw a ``ValueError`` exception with a human + readable explanation of why the value is illegal. + + Subclasses should also define a syntactic_help string which may be + presented to the user to describe the form of the legal values. + + Argument parser classes must be stateless, since instances are cached + and shared between flags. Initializer arguments are allowed, but all + member variables must be derived from initializer arguments only. + """ + + syntactic_help: str = '' + + def parse(self, argument: str) -> Optional[_T]: + """Parses the string argument and returns the native value. + + By default it returns its argument unmodified. + + Args: + argument: string argument passed in the commandline. + + Raises: + ValueError: Raised when it fails to parse the argument. + TypeError: Raised when the argument has the wrong type. + + Returns: + The parsed value in native type. + """ + if not isinstance(argument, str): + raise TypeError('flag value must be a string, found "{}"'.format( + type(argument))) + return argument # type: ignore[return-value] + + def flag_type(self) -> str: + """Returns a string representing the type of the flag.""" + return 'string' + + def _custom_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + """Returns a list of minidom.Element to add additional flag information. + + Args: + doc: minidom.Document, the DOM document it should create nodes from. + """ + del doc # Unused. + return [] + + +class ArgumentSerializer(Generic[_T]): + """Base class for generating string representations of a flag value.""" + + def serialize(self, value: _T) -> str: + """Returns a serialized string of the value.""" + return str(value) + + +class NumericParser(ArgumentParser[_N]): + """Parser of numeric values. + + Parsed value may be bounded to a given upper and lower bound. + """ + + lower_bound: Optional[_N] + upper_bound: Optional[_N] + + def is_outside_bounds(self, val: _N) -> bool: + """Returns whether the value is outside the bounds or not.""" + return ((self.lower_bound is not None and val < self.lower_bound) or + (self.upper_bound is not None and val > self.upper_bound)) + + def parse(self, argument: Union[str, _N]) -> _N: + """See base class.""" + val = self.convert(argument) + if self.is_outside_bounds(val): + raise ValueError('%s is not %s' % (val, self.syntactic_help)) + return val + + def _custom_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = [] + if self.lower_bound is not None: + elements.append(_helpers.create_xml_dom_element( + doc, 'lower_bound', self.lower_bound)) + if self.upper_bound is not None: + elements.append(_helpers.create_xml_dom_element( + doc, 'upper_bound', self.upper_bound)) + return elements + + def convert(self, argument: Union[str, _N]) -> _N: + """Returns the correct numeric value of argument. + + Subclass must implement this method, and raise TypeError if argument is not + string or has the right numeric type. + + Args: + argument: string argument passed in the commandline, or the numeric type. + + Raises: + TypeError: Raised when argument is not a string or the right numeric type. + ValueError: Raised when failed to convert argument to the numeric value. + """ + raise NotImplementedError + + +class FloatParser(NumericParser[float]): + """Parser of floating point values. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = 'a' + number_name = 'number' + syntactic_help = ' '.join((number_article, number_name)) + + def __init__( + self, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ) -> None: + super().__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound)) + elif lower_bound == 0: + sh = 'a non-negative %s' % self.number_name + elif upper_bound == 0: + sh = 'a non-positive %s' % self.number_name + elif upper_bound is not None: + sh = '%s <= %s' % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = '%s >= %s' % (self.number_name, lower_bound) + self.syntactic_help = sh + + def convert(self, argument: Union[int, float, str]) -> float: + """Returns the float value of argument.""" + if ( + (isinstance(argument, int) and not isinstance(argument, bool)) + or isinstance(argument, float) + or isinstance(argument, str) + ): + return float(argument) + else: + raise TypeError( + 'Expect argument to be a string, int, or float, found {}'.format( + type(argument))) + + def flag_type(self) -> str: + """See base class.""" + return 'float' + + +class IntegerParser(NumericParser[int]): + """Parser of an integer value. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = 'an' + number_name = 'integer' + syntactic_help = ' '.join((number_article, number_name)) + + def __init__( + self, lower_bound: Optional[int] = None, upper_bound: Optional[int] = None + ) -> None: + super().__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound)) + elif lower_bound == 1: + sh = 'a positive %s' % self.number_name + elif upper_bound == -1: + sh = 'a negative %s' % self.number_name + elif lower_bound == 0: + sh = 'a non-negative %s' % self.number_name + elif upper_bound == 0: + sh = 'a non-positive %s' % self.number_name + elif upper_bound is not None: + sh = '%s <= %s' % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = '%s >= %s' % (self.number_name, lower_bound) + self.syntactic_help = sh + + def convert(self, argument: Union[int, str]) -> int: + """Returns the int value of argument.""" + if isinstance(argument, int) and not isinstance(argument, bool): + return argument + elif isinstance(argument, str): + base = 10 + if len(argument) > 2 and argument[0] == '0': + if argument[1] == 'o': + base = 8 + elif argument[1] == 'x': + base = 16 + return int(argument, base) + else: + raise TypeError('Expect argument to be a string or int, found {}'.format( + type(argument))) + + def flag_type(self) -> str: + """See base class.""" + return 'int' + + +class BooleanParser(ArgumentParser[bool]): + """Parser of boolean values.""" + + def parse(self, argument: Union[str, int]) -> bool: + """See base class.""" + if isinstance(argument, str): + if argument.lower() in ('true', 't', '1'): + return True + elif argument.lower() in ('false', 'f', '0'): + return False + else: + raise ValueError('Non-boolean argument to boolean flag', argument) + elif isinstance(argument, int): + # Only allow bool or integer 0, 1. + # Note that float 1.0 == True, 0.0 == False. + bool_value = bool(argument) + if argument == bool_value: + return bool_value + else: + raise ValueError('Non-boolean argument to boolean flag', argument) + + raise TypeError('Non-boolean argument to boolean flag', argument) + + def flag_type(self) -> str: + """See base class.""" + return 'bool' + + +class EnumParser(ArgumentParser[str]): + """Parser of a string enum value (a string value from a given set).""" + + def __init__( + self, enum_values: Iterable[str], case_sensitive: bool = True + ) -> None: + """Initializes EnumParser. + + Args: + enum_values: [str], a non-empty list of string values in the enum. + case_sensitive: bool, whether or not the enum is to be case-sensitive. + + Raises: + ValueError: When enum_values is empty. + """ + if not enum_values: + raise ValueError(f'enum_values cannot be empty, found "{enum_values}"') + if isinstance(enum_values, str): + raise ValueError(f'enum_values cannot be a str, found "{enum_values}"') + super().__init__() + self.enum_values = list(enum_values) + self.case_sensitive = case_sensitive + + def parse(self, argument: str) -> str: + """Determines validity of argument and returns the correct element of enum. + + Args: + argument: str, the supplied flag value. + + Returns: + The first matching element from enum_values. + + Raises: + ValueError: Raised when argument didn't match anything in enum. + """ + if self.case_sensitive: + if argument not in self.enum_values: + raise ValueError('value should be one of <%s>' % + '|'.join(self.enum_values)) + else: + return argument + else: + if argument.upper() not in [value.upper() for value in self.enum_values]: + raise ValueError('value should be one of <%s>' % + '|'.join(self.enum_values)) + else: + return [value for value in self.enum_values + if value.upper() == argument.upper()][0] + + def flag_type(self) -> str: + """See base class.""" + return 'string enum' + + +class EnumClassParser(ArgumentParser[_ET]): + """Parser of an Enum class member.""" + + def __init__( + self, enum_class: Type[_ET], case_sensitive: bool = True + ) -> None: + """Initializes EnumParser. + + Args: + enum_class: class, the Enum class with all possible flag values. + case_sensitive: bool, whether or not the enum is to be case-sensitive. If + False, all member names must be unique when case is ignored. + + Raises: + TypeError: When enum_class is not a subclass of Enum. + ValueError: When enum_class is empty. + """ + if not issubclass(enum_class, enum.Enum): + raise TypeError(f'{enum_class} is not a subclass of Enum.') + if not enum_class.__members__: + raise ValueError('enum_class cannot be empty, but "{}" is empty.' + .format(enum_class)) + if not case_sensitive: + members = collections.Counter( + name.lower() for name in enum_class.__members__) + duplicate_keys = { + member for member, count in members.items() if count > 1 + } + if duplicate_keys: + raise ValueError( + 'Duplicate enum values for {} using case_sensitive=False'.format( + duplicate_keys)) + + super().__init__() + self.enum_class = enum_class + self._case_sensitive = case_sensitive + if case_sensitive: + self._member_names = tuple(enum_class.__members__) + else: + self._member_names = tuple( + name.lower() for name in enum_class.__members__) + + @property + def member_names(self) -> Sequence[str]: + """The accepted enum names, in lowercase if not case sensitive.""" + return self._member_names + + def parse(self, argument: Union[_ET, str]) -> _ET: + """Determines validity of argument and returns the correct element of enum. + + Args: + argument: str or Enum class member, the supplied flag value. + + Returns: + The first matching Enum class member in Enum class. + + Raises: + ValueError: Raised when argument didn't match anything in enum. + """ + if isinstance(argument, self.enum_class): + return argument # pytype: disable=bad-return-type + elif not isinstance(argument, str): + raise ValueError( + '{} is not an enum member or a name of a member in {}'.format( + argument, self.enum_class)) + key = EnumParser( + self._member_names, case_sensitive=self._case_sensitive).parse(argument) + if self._case_sensitive: + return self.enum_class[key] + else: + # If EnumParser.parse() return a value, we're guaranteed to find it + # as a member of the class + return next(value for name, value in self.enum_class.__members__.items() + if name.lower() == key.lower()) + + def flag_type(self) -> str: + """See base class.""" + return 'enum class' + + +class ListSerializer(Generic[_T], ArgumentSerializer[List[_T]]): + + def __init__(self, list_sep: str) -> None: + self.list_sep = list_sep + + def serialize(self, value: List[_T]) -> str: + """See base class.""" + return self.list_sep.join([str(x) for x in value]) + + +class EnumClassListSerializer(ListSerializer[_ET]): + """A serializer for :class:`MultiEnumClass` flags. + + This serializer simply joins the output of `EnumClassSerializer` using a + provided separator. + """ + + _element_serializer: 'EnumClassSerializer' + + def __init__(self, list_sep: str, **kwargs) -> None: + """Initializes EnumClassListSerializer. + + Args: + list_sep: String to be used as a separator when serializing + **kwargs: Keyword arguments to the `EnumClassSerializer` used to serialize + individual values. + """ + super().__init__(list_sep) + self._element_serializer = EnumClassSerializer(**kwargs) + + def serialize(self, value: Union[_ET, List[_ET]]) -> str: + """See base class.""" + if isinstance(value, list): + return self.list_sep.join( + self._element_serializer.serialize(x) for x in value) + else: + return self._element_serializer.serialize(value) + + +class CsvListSerializer(ListSerializer[str]): + + def serialize(self, value: List[str]) -> str: + """Serializes a list as a CSV string or unicode.""" + output = io.StringIO() + writer = csv.writer(output, delimiter=self.list_sep) + writer.writerow([str(x) for x in value]) + serialized_value = output.getvalue().strip() + + # We need the returned value to be pure ascii or Unicodes so that + # when the xml help is generated they are usefully encodable. + return str(serialized_value) + + +class EnumClassSerializer(ArgumentSerializer[_ET]): + """Class for generating string representations of an enum class flag value.""" + + def __init__(self, lowercase: bool) -> None: + """Initializes EnumClassSerializer. + + Args: + lowercase: If True, enum member names are lowercased during serialization. + """ + self._lowercase = lowercase + + def serialize(self, value: _ET) -> str: + """Returns a serialized string of the Enum class value.""" + as_string = str(value.name) + return as_string.lower() if self._lowercase else as_string + + +class BaseListParser(ArgumentParser): + """Base class for a parser of lists of strings. + + To extend, inherit from this class; from the subclass ``__init__``, call:: + + super().__init__(token, name) + + where token is a character used to tokenize, and name is a description + of the separator. + """ + + def __init__( + self, token: Optional[str] = None, name: Optional[str] = None + ) -> None: + assert name + super().__init__() + self._token = token + self._name = name + self.syntactic_help = 'a %s separated list' % self._name + + def parse(self, argument: str) -> List[str]: + """See base class.""" + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + return [s.strip() for s in argument.split(self._token)] + + def flag_type(self) -> str: + """See base class.""" + return '%s separated list of strings' % self._name + + +class ListParser(BaseListParser): + """Parser for a comma-separated list of strings.""" + + def __init__(self) -> None: + super().__init__(',', 'comma') + + def parse(self, argument: Union[str, List[str]]) -> List[str]: + """Parses argument as comma-separated list of strings.""" + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + try: + return [s.strip() for s in list(csv.reader([argument], strict=True))[0]] + except csv.Error as e: + # Provide a helpful report for case like + # --listflag="$(printf 'hello,\nworld')" + # IOW, list flag values containing naked newlines. This error + # was previously "reported" by allowing csv.Error to + # propagate. + raise ValueError('Unable to parse the value %r as a %s: %s' + % (argument, self.flag_type(), e)) + + def _custom_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = super()._custom_xml_dom_elements(doc) + elements.append(_helpers.create_xml_dom_element( + doc, 'list_separator', repr(','))) + return elements + + +class WhitespaceSeparatedListParser(BaseListParser): + """Parser for a whitespace-separated list of strings.""" + + def __init__(self, comma_compat: bool = False) -> None: + """Initializer. + + Args: + comma_compat: bool, whether to support comma as an additional separator. + If False then only whitespace is supported. This is intended only for + backwards compatibility with flags that used to be comma-separated. + """ + self._comma_compat = comma_compat + name = 'whitespace or comma' if self._comma_compat else 'whitespace' + super().__init__(None, name) + + def parse(self, argument: Union[str, List[str]]) -> List[str]: + """Parses argument as whitespace-separated list of strings. + + It also parses argument as comma-separated list of strings if requested. + + Args: + argument: string argument passed in the commandline. + + Returns: + [str], the parsed flag value. + """ + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + if self._comma_compat: + argument = argument.replace(',', ' ') + return argument.split() + + def _custom_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = super()._custom_xml_dom_elements(doc) + separators = list(string.whitespace) + if self._comma_compat: + separators.append(',') + separators.sort() + for sep_char in separators: + elements.append(_helpers.create_xml_dom_element( + doc, 'list_separator', repr(sep_char))) + return elements diff --git a/lib/python3.10/site-packages/absl/flags/_defines.py b/lib/python3.10/site-packages/absl/flags/_defines.py new file mode 100644 index 0000000000000000000000000000000000000000..9e4e5201c591b87e4f8b4d90bce3a6e2905ce202 --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_defines.py @@ -0,0 +1,1702 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This modules contains flags DEFINE functions. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +import enum +import sys +import types +from typing import Any, Iterable, List, Literal, Optional, Type, TypeVar, Union, overload + +from absl.flags import _argument_parser +from absl.flags import _exceptions +from absl.flags import _flag +from absl.flags import _flagvalues +from absl.flags import _helpers +from absl.flags import _validators + +_helpers.disclaim_module_ids.add(id(sys.modules[__name__])) + +_T = TypeVar('_T') +_ET = TypeVar('_ET', bound=enum.Enum) + + +def _register_bounds_validator_if_needed(parser, name, flag_values): + """Enforces lower and upper bounds for numeric flags. + + Args: + parser: NumericParser (either FloatParser or IntegerParser), provides lower + and upper bounds, and help text to display. + name: str, name of the flag + flag_values: FlagValues. + """ + if parser.lower_bound is not None or parser.upper_bound is not None: + + def checker(value): + if value is not None and parser.is_outside_bounds(value): + message = '%s is not %s' % (value, parser.syntactic_help) + raise _exceptions.ValidationError(message) + return True + + _validators.register_validator(name, checker, flag_values=flag_values) + + +@overload +def DEFINE( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + name: str, + default: Any, + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + serializer: Optional[_argument_parser.ArgumentSerializer[_T]] = ..., + module_name: Optional[str] = ..., + required: Literal[True] = ..., + **args: Any +) -> _flagvalues.FlagHolder[_T]: + ... + + +@overload +def DEFINE( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + name: str, + default: Optional[Any], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + serializer: Optional[_argument_parser.ArgumentSerializer[_T]] = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[_T]]: + ... + + +def DEFINE( # pylint: disable=invalid-name + parser, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + serializer=None, + module_name=None, + required=False, + **args): + """Registers a generic Flag object. + + NOTE: in the docstrings of all DEFINE* functions, "registers" is short + for "creates a new flag and registers it". + + Auxiliary function: clients should use the specialized ``DEFINE_`` + function instead. + + Args: + parser: :class:`ArgumentParser`, used to parse the flag arguments. + name: str, the flag name. + default: The default value of the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + serializer: :class:`ArgumentSerializer`, the flag serializer instance. + module_name: str, the name of the Python module declaring this flag. If not + provided, it will be computed using the stack trace of this call. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + return DEFINE_flag( + _flag.Flag(parser, serializer, name, default, help, **args), + flag_values, + module_name, + required=True if required else False, + ) + + +@overload +def DEFINE_flag( # pylint: disable=invalid-name + flag: _flag.Flag[_T], + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: Literal[True] = ..., +) -> _flagvalues.FlagHolder[_T]: + ... + + +@overload +def DEFINE_flag( # pylint: disable=invalid-name + flag: _flag.Flag[_T], + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., +) -> _flagvalues.FlagHolder[Optional[_T]]: + ... + + +def DEFINE_flag( # pylint: disable=invalid-name + flag, + flag_values=_flagvalues.FLAGS, + module_name=None, + required=False): + """Registers a :class:`Flag` object with a :class:`FlagValues` object. + + By default, the global :const:`FLAGS` ``FlagValue`` object is used. + + Typical users will use one of the more specialized DEFINE_xxx + functions, such as :func:`DEFINE_string` or :func:`DEFINE_integer`. But + developers who need to create :class:`Flag` objects themselves should use + this function to register their flags. + + Args: + flag: :class:`Flag`, a flag that is key to the module. + flag_values: :class:`FlagValues`, the ``FlagValues`` instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: str, the name of the Python module declaring this flag. If not + provided, it will be computed using the stack trace of this call. + required: bool, is this a required flag. This must be used as a keyword + argument. + + Returns: + a handle to defined flag. + """ + if required and flag.default is not None: + raise ValueError( + 'Required flag --%s needs to have None as default' % flag.name + ) + # Copying the reference to flag_values prevents pychecker warnings. + fv = flag_values + fv[flag.name] = flag + # Tell flag_values who's defining the flag. + if module_name: + module = sys.modules.get(module_name) + else: + module, module_name = _helpers.get_calling_module_object_and_name() + flag_values.register_flag_by_module(module_name, flag) + flag_values.register_flag_by_module_id(id(module), flag) + if required: + _validators.mark_flag_as_required(flag.name, fv) + ensure_non_none_value = (flag.default is not None) or required + return _flagvalues.FlagHolder( + fv, flag, ensure_non_none_value=ensure_non_none_value) + + +def set_default(flag_holder: _flagvalues.FlagHolder[_T], value: _T) -> None: + """Changes the default value of the provided flag object. + + The flag's current value is also updated if the flag is currently using + the default value, i.e. not specified in the command line, and not set + by FLAGS.name = value. + + Args: + flag_holder: FlagHolder, the flag to modify. + value: The new default value. + + Raises: + IllegalFlagValueError: Raised when value is not valid. + """ + flag_holder._flagvalues.set_default(flag_holder.name, value) # pylint: disable=protected-access + + +def override_value(flag_holder: _flagvalues.FlagHolder[_T], value: _T) -> None: + """Overrides the value of the provided flag. + + This value takes precedent over the default value and, when called after flag + parsing, any value provided at the command line. + + Args: + flag_holder: FlagHolder, the flag to modify. + value: The new value. + + Raises: + IllegalFlagValueError: The value did not pass the flag parser or validators. + """ + fv = flag_holder._flagvalues # pylint: disable=protected-access + # Ensure the new value satisfies the flag's parser while avoiding side + # effects of calling parse(). + parsed = fv[flag_holder.name]._parse(value) # pylint: disable=protected-access + if parsed != value: + raise _exceptions.IllegalFlagValueError( + 'flag %s: parsed value %r not equal to original %r' + % (flag_holder.name, parsed, value) + ) + setattr(fv, flag_holder.name, value) + + +def _internal_declare_key_flags( + flag_names: List[str], + flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS, + key_flag_values: Optional[_flagvalues.FlagValues] = None, +) -> None: + """Declares a flag as key for the calling module. + + Internal function. User code should call declare_key_flag or + adopt_module_key_flags instead. + + Args: + flag_names: [str], a list of names of already-registered Flag objects. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flags listed in flag_names have registered (the value of the flag_values + argument from the ``DEFINE_*`` calls that defined those flags). This + should almost never need to be overridden. + key_flag_values: :class:`FlagValues`, the FlagValues instance that (among + possibly many other things) keeps track of the key flags for each module. + Default ``None`` means "same as flag_values". This should almost never + need to be overridden. + + Raises: + UnrecognizedFlagError: Raised when the flag is not defined. + """ + key_flag_values = key_flag_values or flag_values + + module = _helpers.get_calling_module() + + for flag_name in flag_names: + key_flag_values.register_key_flag_for_module(module, flag_values[flag_name]) + + +def declare_key_flag( + flag_name: Union[str, _flagvalues.FlagHolder], + flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS, +) -> None: + """Declares one flag as key to the current module. + + Key flags are flags that are deemed really important for a module. + They are important when listing help messages; e.g., if the + --helpshort command-line flag is used, then only the key flags of the + main module are listed (instead of all flags, as in the case of + --helpfull). + + Sample usage:: + + flags.declare_key_flag('flag_1') + + Args: + flag_name: str | :class:`FlagHolder`, the name or holder of an already + declared flag. (Redeclaring flags as key, including flags implicitly key + because they were declared in this module, is a no-op.) + Positional-only parameter. + flag_values: :class:`FlagValues`, the FlagValues instance in which the + flag will be declared as a key flag. This should almost never need to be + overridden. + + Raises: + ValueError: Raised if flag_name not defined as a Python flag. + """ + flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values) + if flag_name in _helpers.SPECIAL_FLAGS: + # Take care of the special flags, e.g., --flagfile, --undefok. + # These flags are defined in SPECIAL_FLAGS, and are treated + # specially during flag parsing, taking precedence over the + # user-defined flags. + _internal_declare_key_flags([flag_name], + flag_values=_helpers.SPECIAL_FLAGS, + key_flag_values=flag_values) + return + try: + _internal_declare_key_flags([flag_name], flag_values=flag_values) + except KeyError: + raise ValueError('Flag --%s is undefined. To set a flag as a key flag ' + 'first define it in Python.' % flag_name) + + +def adopt_module_key_flags( + module: Any, flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS +) -> None: + """Declares that all flags key to a module are key to the current module. + + Args: + module: module, the module object from which all key flags will be declared + as key flags to the current module. + flag_values: :class:`FlagValues`, the FlagValues instance in which the + flags will be declared as key flags. This should almost never need to be + overridden. + + Raises: + Error: Raised when given an argument that is a module name (a string), + instead of a module object. + """ + if not isinstance(module, types.ModuleType): + raise _exceptions.Error('Expected a module object, not %r.' % (module,)) + _internal_declare_key_flags( + [f.name for f in flag_values.get_key_flags_for_module(module.__name__)], + flag_values=flag_values) + # If module is this flag module, take _helpers.SPECIAL_FLAGS into account. + if module == _helpers.FLAGS_MODULE: + _internal_declare_key_flags( + # As we associate flags with get_calling_module_object_and_name(), the + # special flags defined in this module are incorrectly registered with + # a different module. So, we can't use get_key_flags_for_module. + # Instead, we take all flags from _helpers.SPECIAL_FLAGS (a private + # FlagValues, where no other module should register flags). + [_helpers.SPECIAL_FLAGS[name].name for name in _helpers.SPECIAL_FLAGS], + flag_values=_helpers.SPECIAL_FLAGS, + key_flag_values=flag_values) + + +def disclaim_key_flags() -> None: + """Declares that the current module will not define any more key flags. + + Normally, the module that calls the DEFINE_xxx functions claims the + flag to be its key flag. This is undesirable for modules that + define additional DEFINE_yyy functions with its own flag parsers and + serializers, since that module will accidentally claim flags defined + by DEFINE_yyy as its key flags. After calling this function, the + module disclaims flag definitions thereafter, so the key flags will + be correctly attributed to the caller of DEFINE_yyy. + + After calling this function, the module will not be able to define + any more flags. This function will affect all FlagValues objects. + """ + globals_for_caller = sys._getframe(1).f_globals # pylint: disable=protected-access + module = _helpers.get_module_object_and_name(globals_for_caller) + if module is not None: + _helpers.disclaim_module_ids.add(id(module.module)) + + +@overload +def DEFINE_string( # pylint: disable=invalid-name + name: str, + default: Optional[str], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[str]: + ... + + +@overload +def DEFINE_string( # pylint: disable=invalid-name + name: str, + default: None, + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[str]]: + ... + + +@overload +def DEFINE_string( # pylint: disable=invalid-name + name: str, + default: str, + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[str]: + ... + + +def DEFINE_string( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value can be any string.""" + parser = _argument_parser.ArgumentParser[str]() + serializer = _argument_parser.ArgumentSerializer[str]() + return DEFINE( + parser, + name, + default, + help, + flag_values, + serializer, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_boolean( # pylint: disable=invalid-name + name: str, + default: Union[None, str, bool, int], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[bool]: + ... + + +@overload +def DEFINE_boolean( # pylint: disable=invalid-name + name: str, + default: None, + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[bool]]: + ... + + +@overload +def DEFINE_boolean( # pylint: disable=invalid-name + name: str, + default: Union[str, bool, int], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[bool]: + ... + + +# pytype: disable=bad-return-type +def DEFINE_boolean( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + module_name=None, + required=False, + **args +): + """Registers a boolean flag. + + Such a boolean flag does not take an argument. If a user wants to + specify a false value explicitly, the long option beginning with 'no' + must be used: i.e. --noflag + + This flag will have a value of None, True or False. None is possible + if default=None and the user does not specify the flag on the command + line. + + Args: + name: str, the flag name. + default: bool|str|None, the default value of the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: str, the name of the Python module declaring this flag. If not + provided, it will be computed using the stack trace of this call. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + return DEFINE_flag( + _flag.BooleanFlag(name, default, help, **args), + flag_values, + module_name, + required=True if required else False, + ) + + +@overload +def DEFINE_float( # pylint: disable=invalid-name + name: str, + default: Union[None, float, str], + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[float]: + ... + + +@overload +def DEFINE_float( # pylint: disable=invalid-name + name: str, + default: None, + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[float]]: + ... + + +@overload +def DEFINE_float( # pylint: disable=invalid-name + name: str, + default: Union[float, str], + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[float]: + ... + + +def DEFINE_float( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + lower_bound=None, + upper_bound=None, + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value must be a float. + + If ``lower_bound`` or ``upper_bound`` are set, then this flag must be + within the given range. + + Args: + name: str, the flag name. + default: float|str|None, the default value of the flag. + help: str, the help message. + lower_bound: float, min value of the flag. + upper_bound: float, max value of the flag. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to :func:`DEFINE`. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.FloatParser(lower_bound, upper_bound) + serializer = _argument_parser.ArgumentSerializer() + result = DEFINE( + parser, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values, + serializer, + required=True if required else False, + **args, + ) + _register_bounds_validator_if_needed(parser, name, flag_values=flag_values) + return result + + +@overload +def DEFINE_integer( # pylint: disable=invalid-name + name: str, + default: Union[None, int, str], + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[int]: + ... + + +@overload +def DEFINE_integer( # pylint: disable=invalid-name + name: str, + default: None, + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[int]]: + ... + + +@overload +def DEFINE_integer( # pylint: disable=invalid-name + name: str, + default: Union[int, str], + help: Optional[str], # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[int]: + ... + + +def DEFINE_integer( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + lower_bound=None, + upper_bound=None, + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value must be an integer. + + If ``lower_bound``, or ``upper_bound`` are set, then this flag must be + within the given range. + + Args: + name: str, the flag name. + default: int|str|None, the default value of the flag. + help: str, the help message. + lower_bound: int, min value of the flag. + upper_bound: int, max value of the flag. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to :func:`DEFINE`. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.IntegerParser(lower_bound, upper_bound) + serializer = _argument_parser.ArgumentSerializer() + result = DEFINE( + parser, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values, + serializer, + required=True if required else False, + **args, + ) + _register_bounds_validator_if_needed(parser, name, flag_values=flag_values) + return result + + +@overload +def DEFINE_enum( # pylint: disable=invalid-name + name: str, + default: Optional[str], + enum_values: Iterable[str], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[str]: + ... + + +@overload +def DEFINE_enum( # pylint: disable=invalid-name + name: str, + default: None, + enum_values: Iterable[str], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[str]]: + ... + + +@overload +def DEFINE_enum( # pylint: disable=invalid-name + name: str, + default: str, + enum_values: Iterable[str], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[str]: + ... + + +def DEFINE_enum( # pylint: disable=invalid-name + name, + default, + enum_values, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + module_name=None, + required=False, + **args +): + """Registers a flag whose value can be any string from enum_values. + + Instead of a string enum, prefer `DEFINE_enum_class`, which allows + defining enums from an `enum.Enum` class. + + Args: + name: str, the flag name. + default: str|None, the default value of the flag. + enum_values: [str], a non-empty list of strings with the possible values for + the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: str, the name of the Python module declaring this flag. If not + provided, it will be computed using the stack trace of this call. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + result = DEFINE_flag( + _flag.EnumFlag(name, default, help, enum_values, **args), + flag_values, + module_name, + required=True if required else False, + ) + return result + + +@overload +def DEFINE_enum_class( # pylint: disable=invalid-name + name: str, + default: Union[None, _ET, str], + enum_class: Type[_ET], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + case_sensitive: bool = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[_ET]: + ... + + +@overload +def DEFINE_enum_class( # pylint: disable=invalid-name + name: str, + default: None, + enum_class: Type[_ET], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + case_sensitive: bool = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[_ET]]: + ... + + +@overload +def DEFINE_enum_class( # pylint: disable=invalid-name + name: str, + default: Union[_ET, str], + enum_class: Type[_ET], + help: Optional[str], # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + case_sensitive: bool = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[_ET]: + ... + + +def DEFINE_enum_class( # pylint: disable=invalid-name + name, + default, + enum_class, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + module_name=None, + case_sensitive=False, + required=False, + **args +): + """Registers a flag whose value can be the name of enum members. + + Args: + name: str, the flag name. + default: Enum|str|None, the default value of the flag. + enum_class: class, the Enum class with all the possible values for the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: str, the name of the Python module declaring this flag. If not + provided, it will be computed using the stack trace of this call. + case_sensitive: bool, whether to map strings to members of the enum_class + without considering case. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: dict, the extra keyword args that are passed to ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + # NOTE: pytype fails if this is a direct return. + result = DEFINE_flag( + _flag.EnumClassFlag( + name, default, help, enum_class, case_sensitive=case_sensitive, **args + ), + flag_values, + module_name, + required=True if required else False, + ) + return result + + +@overload +def DEFINE_list( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +@overload +def DEFINE_list( # pylint: disable=invalid-name + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[str]]]: + ... + + +@overload +def DEFINE_list( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +def DEFINE_list( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value is a comma-separated list of strings. + + The flag value is parsed with a CSV parser. + + Args: + name: str, the flag name. + default: list|str|None, the default value of the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.ListParser() + serializer = _argument_parser.CsvListSerializer(',') + return DEFINE( + parser, + name, + default, + help, + flag_values, + serializer, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_spaceseplist( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + comma_compat: bool = ..., + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +@overload +def DEFINE_spaceseplist( # pylint: disable=invalid-name + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + comma_compat: bool = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[str]]]: + ... + + +@overload +def DEFINE_spaceseplist( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + comma_compat: bool = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +def DEFINE_spaceseplist( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + comma_compat=False, + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value is a whitespace-separated list of strings. + + Any whitespace can be used as a separator. + + Args: + name: str, the flag name. + default: list|str|None, the default value of the flag. + help: str, the help message. + comma_compat: bool - Whether to support comma as an additional separator. If + false then only whitespace is supported. This is intended only for + backwards compatibility with flags that used to be comma-separated. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.WhitespaceSeparatedListParser( + comma_compat=comma_compat) + serializer = _argument_parser.ListSerializer(' ') + return DEFINE( + parser, + name, + default, + help, + flag_values, + serializer, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_multi( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + serializer: _argument_parser.ArgumentSerializer[_T], + name: str, + default: Iterable[_T], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[_T]]: + ... + + +@overload +def DEFINE_multi( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + serializer: _argument_parser.ArgumentSerializer[_T], + name: str, + default: Union[None, _T], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[_T]]: + ... + + +@overload +def DEFINE_multi( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + serializer: _argument_parser.ArgumentSerializer[_T], + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[_T]]]: + ... + + +@overload +def DEFINE_multi( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + serializer: _argument_parser.ArgumentSerializer[_T], + name: str, + default: Iterable[_T], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[_T]]: + ... + + +@overload +def DEFINE_multi( # pylint: disable=invalid-name + parser: _argument_parser.ArgumentParser[_T], + serializer: _argument_parser.ArgumentSerializer[_T], + name: str, + default: _T, + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[_T]]: + ... + + +def DEFINE_multi( # pylint: disable=invalid-name + parser, + serializer, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + module_name=None, + required=False, + **args +): + """Registers a generic MultiFlag that parses its args with a given parser. + + Auxiliary function. Normal users should NOT use it directly. + + Developers who need to create their own 'Parser' classes for options + which can appear multiple times can call this module function to + register their flags. + + Args: + parser: ArgumentParser, used to parse the flag arguments. + serializer: ArgumentSerializer, the flag serializer instance. + name: str, the flag name. + default: Union[Iterable[T], str, None], the default value of the flag. If + the value is text, it will be parsed as if it was provided from the + command line. If the value is a non-string iterable, it will be iterated + over to create a shallow copy of the values. If it is None, it is left + as-is. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: A string, the name of the Python module declaring this flag. If + not provided, it will be computed using the stack trace of this call. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + result = DEFINE_flag( + _flag.MultiFlag(parser, serializer, name, default, help, **args), + flag_values, + module_name, + required=True if required else False, + ) + return result + + +@overload +def DEFINE_multi_string( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +@overload +def DEFINE_multi_string( # pylint: disable=invalid-name + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[str]]]: + ... + + +@overload +def DEFINE_multi_string( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[str], str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +def DEFINE_multi_string( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value can be a list of any strings. + + Use the flag on the command line multiple times to place multiple + string values into the list. The 'default' may be a single string + (which will be converted into a single-element list) or a list of + strings. + + + Args: + name: str, the flag name. + default: Union[Iterable[str], str, None], the default value of the flag; see + :func:`DEFINE_multi`. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.ArgumentParser() + serializer = _argument_parser.ArgumentSerializer() + return DEFINE_multi( + parser, + serializer, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_multi_integer( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[int], int, str], + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[int]]: + ... + + +@overload +def DEFINE_multi_integer( # pylint: disable=invalid-name + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[int]]]: + ... + + +@overload +def DEFINE_multi_integer( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[int], int, str], + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[int] = ..., + upper_bound: Optional[int] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[int]]: + ... + + +def DEFINE_multi_integer( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + lower_bound=None, + upper_bound=None, + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value can be a list of arbitrary integers. + + Use the flag on the command line multiple times to place multiple + integer values into the list. The 'default' may be a single integer + (which will be converted into a single-element list) or a list of + integers. + + Args: + name: str, the flag name. + default: Union[Iterable[int], str, None], the default value of the flag; see + `DEFINE_multi`. + help: str, the help message. + lower_bound: int, min values of the flag. + upper_bound: int, max values of the flag. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.IntegerParser(lower_bound, upper_bound) + serializer = _argument_parser.ArgumentSerializer() + return DEFINE_multi( + parser, + serializer, + name, + default, + help, # pylint: disable=redefined-builtin + flag_values, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_multi_float( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[float], float, str], + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[float]]: + ... + + +@overload +def DEFINE_multi_float( # pylint: disable=invalid-name + name: str, + default: None, + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[float]]]: + ... + + +@overload +def DEFINE_multi_float( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[float], float, str], + help: str, # pylint: disable=redefined-builtin + lower_bound: Optional[float] = ..., + upper_bound: Optional[float] = ..., + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[float]]: + ... + + +def DEFINE_multi_float( # pylint: disable=invalid-name + name, + default, + help, # pylint: disable=redefined-builtin + lower_bound=None, + upper_bound=None, + flag_values=_flagvalues.FLAGS, + required=False, + **args +): + """Registers a flag whose value can be a list of arbitrary floats. + + Use the flag on the command line multiple times to place multiple + float values into the list. The 'default' may be a single float + (which will be converted into a single-element list) or a list of + floats. + + Args: + name: str, the flag name. + default: Union[Iterable[float], str, None], the default value of the flag; + see `DEFINE_multi`. + help: str, the help message. + lower_bound: float, min values of the flag. + upper_bound: float, max values of the flag. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.FloatParser(lower_bound, upper_bound) + serializer = _argument_parser.ArgumentSerializer() + return DEFINE_multi( + parser, + serializer, + name, + default, + help, + flag_values, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_multi_enum( # pylint: disable=invalid-name + name: str, + default: Union[None, Iterable[str], str], + enum_values: Iterable[str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +@overload +def DEFINE_multi_enum( # pylint: disable=invalid-name + name: str, + default: None, + enum_values: Iterable[str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[str]]]: + ... + + +@overload +def DEFINE_multi_enum( # pylint: disable=invalid-name + name: str, + default: Union[Iterable[str], str], + enum_values: Iterable[str], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[str]]: + ... + + +def DEFINE_multi_enum( # pylint: disable=invalid-name + name, + default, + enum_values, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + case_sensitive=True, + required=False, + **args +): + """Registers a flag whose value can be a list strings from enum_values. + + Use the flag on the command line multiple times to place multiple + enum values into the list. The 'default' may be a single string + (which will be converted into a single-element list) or a list of + strings. + + Args: + name: str, the flag name. + default: Union[Iterable[str], str, None], the default value of the flag; see + `DEFINE_multi`. + enum_values: [str], a non-empty list of strings with the possible values for + the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + case_sensitive: Whether or not the enum is to be case-sensitive. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + parser = _argument_parser.EnumParser(enum_values, case_sensitive) + serializer = _argument_parser.ArgumentSerializer() + return DEFINE_multi( + parser, + serializer, + name, + default, + '<%s>: %s' % ('|'.join(enum_values), help), + flag_values, + required=True if required else False, + **args, + ) + + +@overload +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name: str, + # This is separate from `Union[None, _ET, Iterable[str], str]` to avoid a + # Pytype issue inferring the return value to + # FlagHolder[List[Union[_ET, enum.Enum]]] when an iterable of concrete enum + # subclasses are used. + default: Iterable[_ET], + enum_class: Type[_ET], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[_ET]]: + ... + + +@overload +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name: str, + default: Union[None, _ET, Iterable[str], str], + enum_class: Type[_ET], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + *, + required: Literal[True], + **args: Any +) -> _flagvalues.FlagHolder[List[_ET]]: + ... + + +@overload +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name: str, + default: None, + enum_class: Type[_ET], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[Optional[List[_ET]]]: + ... + + +@overload +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name: str, + # This is separate from `Union[None, _ET, Iterable[str], str]` to avoid a + # Pytype issue inferring the return value to + # FlagHolder[List[Union[_ET, enum.Enum]]] when an iterable of concrete enum + # subclasses are used. + default: Iterable[_ET], + enum_class: Type[_ET], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[_ET]]: + ... + + +@overload +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name: str, + default: Union[_ET, Iterable[str], str], + enum_class: Type[_ET], + help: str, # pylint: disable=redefined-builtin + flag_values: _flagvalues.FlagValues = ..., + module_name: Optional[str] = ..., + required: bool = ..., + **args: Any +) -> _flagvalues.FlagHolder[List[_ET]]: + ... + + +def DEFINE_multi_enum_class( # pylint: disable=invalid-name + name, + default, + enum_class, + help, # pylint: disable=redefined-builtin + flag_values=_flagvalues.FLAGS, + module_name=None, + case_sensitive=False, + required=False, + **args +): + """Registers a flag whose value can be a list of enum members. + + Use the flag on the command line multiple times to place multiple + enum values into the list. + + Args: + name: str, the flag name. + default: Union[Iterable[Enum], Iterable[str], Enum, str, None], the default + value of the flag; see `DEFINE_multi`; only differences are documented + here. If the value is a single Enum, it is treated as a single-item list + of that Enum value. If it is an iterable, text values within the iterable + will be converted to the equivalent Enum objects. + enum_class: class, the Enum class with all the possible values for the flag. + help: str, the help message. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: A string, the name of the Python module declaring this flag. If + not provided, it will be computed using the stack trace of this call. + case_sensitive: bool, whether to map strings to members of the enum_class + without considering case. + required: bool, is this a required flag. This must be used as a keyword + argument. + **args: Dictionary with extra keyword args that are passed to the + ``Flag.__init__``. + + Returns: + a handle to defined flag. + """ + # NOTE: pytype fails if this is a direct return. + result = DEFINE_flag( + _flag.MultiEnumClassFlag( + name, + default, + help, + enum_class, + case_sensitive=case_sensitive, + **args, + ), + flag_values, + module_name, + required=True if required else False, + ) + return result + + +def DEFINE_alias( # pylint: disable=invalid-name + name: str, + original_name: str, + flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS, + module_name: Optional[str] = None, +) -> _flagvalues.FlagHolder[Any]: + """Defines an alias flag for an existing one. + + Args: + name: str, the flag name. + original_name: str, the original flag name. + flag_values: :class:`FlagValues`, the FlagValues instance with which the + flag will be registered. This should almost never need to be overridden. + module_name: A string, the name of the module that defines this flag. + + Returns: + a handle to defined flag. + + Raises: + flags.FlagError: + UnrecognizedFlagError: if the referenced flag doesn't exist. + DuplicateFlagError: if the alias name has been used by some existing flag. + """ + if original_name not in flag_values: + raise _exceptions.UnrecognizedFlagError(original_name) + flag = flag_values[original_name] + + class _FlagAlias(_flag.Flag): + """Overrides Flag class so alias value is copy of original flag value.""" + + def parse(self, argument): + flag.parse(argument) + self.present += 1 + + def _parse_from_default(self, value): + # The value was already parsed by the aliased flag, so there is no + # need to call the parser on it a second time. + # Additionally, because of how MultiFlag parses and merges values, + # it isn't possible to delegate to the aliased flag and still get + # the correct values. + return value + + @property + def value(self): + return flag.value + + @value.setter + def value(self, value): + flag.value = value + + help_msg = 'Alias for --%s.' % flag.name + # If alias_name has been used, flags.DuplicatedFlag will be raised. + return DEFINE_flag( + _FlagAlias( + flag.parser, + flag.serializer, + name, + flag.default, + help_msg, + boolean=flag.boolean), flag_values, module_name) diff --git a/lib/python3.10/site-packages/absl/flags/_exceptions.py b/lib/python3.10/site-packages/absl/flags/_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..e9edfce684a7ea351be35eb96b32c441b2ccda2a --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_exceptions.py @@ -0,0 +1,107 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exception classes in ABSL flags library. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +import sys + +from absl.flags import _helpers + + +_helpers.disclaim_module_ids.add(id(sys.modules[__name__])) + + +class Error(Exception): + """The base class for all flags errors.""" + + +class CantOpenFlagFileError(Error): + """Raised when flagfile fails to open. + + E.g. the file doesn't exist, or has wrong permissions. + """ + + +class DuplicateFlagError(Error): + """Raised if there is a flag naming conflict.""" + + @classmethod + def from_flag(cls, flagname, flag_values, other_flag_values=None): + """Creates a DuplicateFlagError by providing flag name and values. + + Args: + flagname: str, the name of the flag being redefined. + flag_values: :class:`FlagValues`, the FlagValues instance containing the + first definition of flagname. + other_flag_values: :class:`FlagValues`, if it is not None, it should be + the FlagValues object where the second definition of flagname occurs. + If it is None, we assume that we're being called when attempting to + create the flag a second time, and we use the module calling this one + as the source of the second definition. + + Returns: + An instance of DuplicateFlagError. + """ + first_module = flag_values.find_module_defining_flag( + flagname, default='') + if other_flag_values is None: + second_module = _helpers.get_calling_module() + else: + second_module = other_flag_values.find_module_defining_flag( + flagname, default='') + flag_summary = flag_values[flagname].help + msg = ("The flag '%s' is defined twice. First from %s, Second from %s. " + "Description from first occurrence: %s") % ( + flagname, first_module, second_module, flag_summary) + return cls(msg) + + +class IllegalFlagValueError(Error): + """Raised when the flag command line argument is illegal.""" + + +class UnrecognizedFlagError(Error): + """Raised when a flag is unrecognized. + + Attributes: + flagname: str, the name of the unrecognized flag. + flagvalue: The value of the flag, empty if the flag is not defined. + """ + + def __init__(self, flagname, flagvalue='', suggestions=None): + self.flagname = flagname + self.flagvalue = flagvalue + if suggestions: + # Space before the question mark is intentional to not include it in the + # selection when copy-pasting the suggestion from (some) terminals. + tip = '. Did you mean: %s ?' % ', '.join(suggestions) + else: + tip = '' + super().__init__("Unknown command line flag '%s'%s" % (flagname, tip)) + + +class UnparsedFlagAccessError(Error): + """Raised when accessing the flag value from unparsed :class:`FlagValues`.""" + + +class ValidationError(Error): + """Raised when flag validator constraint is not satisfied.""" + + +class FlagNameConflictsWithMethodError(Error): + """Raised when a flag name conflicts with :class:`FlagValues` methods.""" diff --git a/lib/python3.10/site-packages/absl/flags/_flag.py b/lib/python3.10/site-packages/absl/flags/_flag.py new file mode 100644 index 0000000000000000000000000000000000000000..548a325952eb5cea0c1cff56d53f5b468c74a9ae --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_flag.py @@ -0,0 +1,566 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Contains Flag class - information about single command-line flag. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +from collections import abc +import copy +import enum +import functools +from typing import Any, Dict, Generic, Iterable, List, Optional, Type, TypeVar, Union +from xml.dom import minidom + +from absl.flags import _argument_parser +from absl.flags import _exceptions +from absl.flags import _helpers + +_T = TypeVar('_T') +_ET = TypeVar('_ET', bound=enum.Enum) + + +@functools.total_ordering +class Flag(Generic[_T]): + """Information about a command-line flag. + + Attributes: + name: the name for this flag + default: the default value for this flag + default_unparsed: the unparsed default value for this flag. + default_as_str: default value as repr'd string, e.g., "'true'" + (or None) + value: the most recent parsed value of this flag set by :meth:`parse` + help: a help string or None if no help is available + short_name: the single letter alias for this flag (or None) + boolean: if 'true', this flag does not accept arguments + present: true if this flag was parsed from command line flags + parser: an :class:`~absl.flags.ArgumentParser` object + serializer: an ArgumentSerializer object + allow_override: the flag may be redefined without raising an error, + and newly defined flag overrides the old one. + allow_override_cpp: use the flag from C++ if available the flag + definition is replaced by the C++ flag after init + allow_hide_cpp: use the Python flag despite having a C++ flag with + the same name (ignore the C++ flag) + using_default_value: the flag value has not been set by user + allow_overwrite: the flag may be parsed more than once without + raising an error, the last set value will be used + allow_using_method_names: whether this flag can be defined even if + it has a name that conflicts with a FlagValues method. + validators: list of the flag validators. + + The only public method of a ``Flag`` object is :meth:`parse`, but it is + typically only called by a :class:`~absl.flags.FlagValues` object. The + :meth:`parse` method is a thin wrapper around the + :meth:`ArgumentParser.parse()` method. The + parsed value is saved in ``.value``, and the ``.present`` attribute is + updated. If this flag was already present, an Error is raised. + + :meth:`parse` is also called during ``__init__`` to parse the default value + and initialize the ``.value`` attribute. This enables other python modules to + safely use flags even if the ``__main__`` module neglects to parse the + command line arguments. The ``.present`` attribute is cleared after + ``__init__`` parsing. If the default value is set to ``None``, then the + ``__init__`` parsing step is skipped and the ``.value`` attribute is + initialized to None. + + Note: The default value is also presented to the user in the help + string, so it is important that it be a legal value for this flag. + """ + + # NOTE: pytype doesn't find defaults without this. + default: Optional[_T] + default_as_str: Optional[str] + default_unparsed: Union[Optional[_T], str] + + parser: _argument_parser.ArgumentParser[_T] + + def __init__( + self, + parser: _argument_parser.ArgumentParser[_T], + serializer: Optional[_argument_parser.ArgumentSerializer[_T]], + name: str, + default: Union[Optional[_T], str], + help_string: Optional[str], + short_name: Optional[str] = None, + boolean: bool = False, + allow_override: bool = False, + allow_override_cpp: bool = False, + allow_hide_cpp: bool = False, + allow_overwrite: bool = True, + allow_using_method_names: bool = False, + ) -> None: + self.name = name + + if not help_string: + help_string = '(no help available)' + + self.help = help_string + self.short_name = short_name + self.boolean = boolean + self.present = 0 + self.parser = parser # type: ignore[annotation-type-mismatch] + self.serializer = serializer + self.allow_override = allow_override + self.allow_override_cpp = allow_override_cpp + self.allow_hide_cpp = allow_hide_cpp + self.allow_overwrite = allow_overwrite + self.allow_using_method_names = allow_using_method_names + + self.using_default_value = True + self._value: Optional[_T] = None + self.validators: List[Any] = [] + if self.allow_hide_cpp and self.allow_override_cpp: + raise _exceptions.Error( + "Can't have both allow_hide_cpp (means use Python flag) and " + 'allow_override_cpp (means use C++ flag after InitGoogle)') + + self._set_default(default) + + @property + def value(self) -> Optional[_T]: + return self._value + + @value.setter + def value(self, value: Optional[_T]): + self._value = value + + def __hash__(self): + return hash(id(self)) + + def __eq__(self, other): + return self is other + + def __lt__(self, other): + if isinstance(other, Flag): + return id(self) < id(other) + return NotImplemented + + def __bool__(self): + raise TypeError('A Flag instance would always be True. ' + 'Did you mean to test the `.value` attribute?') + + def __getstate__(self): + raise TypeError("can't pickle Flag objects") + + def __copy__(self): + raise TypeError('%s does not support shallow copies. ' + 'Use copy.deepcopy instead.' % type(self).__name__) + + def __deepcopy__(self, memo: Dict[int, Any]) -> 'Flag[_T]': + result = object.__new__(type(self)) + result.__dict__ = copy.deepcopy(self.__dict__, memo) + return result + + def _get_parsed_value_as_string(self, value: Optional[_T]) -> Optional[str]: + """Returns parsed flag value as string.""" + if value is None: + return None + if self.serializer: + return repr(self.serializer.serialize(value)) + if self.boolean: + if value: + return repr('true') + else: + return repr('false') + return repr(str(value)) + + def parse(self, argument: Union[str, _T]) -> None: + """Parses string and sets flag value. + + Args: + argument: str or the correct flag value type, argument to be parsed. + """ + if self.present and not self.allow_overwrite: + raise _exceptions.IllegalFlagValueError( + 'flag --%s=%s: already defined as %s' % ( + self.name, argument, self.value)) + self.value = self._parse(argument) + self.present += 1 + + def _parse(self, argument: Union[str, _T]) -> Optional[_T]: + """Internal parse function. + + It returns the parsed value, and does not modify class states. + + Args: + argument: str or the correct flag value type, argument to be parsed. + + Returns: + The parsed value. + """ + try: + return self.parser.parse(argument) # type: ignore[arg-type] + except (TypeError, ValueError, OverflowError) as e: + # Recast as IllegalFlagValueError. + raise _exceptions.IllegalFlagValueError( + 'flag --%s=%s: %s' % (self.name, argument, e)) + + def unparse(self) -> None: + self.value = self.default + self.using_default_value = True + self.present = 0 + + def serialize(self) -> str: + """Serializes the flag.""" + return self._serialize(self.value) + + def _serialize(self, value: Optional[_T]) -> str: + """Internal serialize function.""" + if value is None: + return '' + if self.boolean: + if value: + return '--%s' % self.name + else: + return '--no%s' % self.name + else: + if not self.serializer: + raise _exceptions.Error( + 'Serializer not present for flag %s' % self.name) + return '--%s=%s' % (self.name, self.serializer.serialize(value)) + + def _set_default(self, value: Union[Optional[_T], str]) -> None: + """Changes the default value (and current value too) for this Flag.""" + self.default_unparsed = value + if value is None: + self.default = None + else: + self.default = self._parse_from_default(value) + self.default_as_str = self._get_parsed_value_as_string(self.default) + if self.using_default_value: + self.value = self.default + + # This is split out so that aliases can skip regular parsing of the default + # value. + def _parse_from_default(self, value: Union[str, _T]) -> Optional[_T]: + return self._parse(value) + + def flag_type(self) -> str: + """Returns a str that describes the type of the flag. + + NOTE: we use strings, and not the types.*Type constants because + our flags can have more exotic types, e.g., 'comma separated list + of strings', 'whitespace separated list of strings', etc. + """ + return self.parser.flag_type() + + def _create_xml_dom_element( + self, doc: minidom.Document, module_name: str, is_key: bool = False + ) -> minidom.Element: + """Returns an XML element that contains this flag's information. + + This is information that is relevant to all flags (e.g., name, + meaning, etc.). If you defined a flag that has some other pieces of + info, then please override _ExtraXMLInfo. + + Please do NOT override this method. + + Args: + doc: minidom.Document, the DOM document it should create nodes from. + module_name: str,, the name of the module that defines this flag. + is_key: boolean, True iff this flag is key for main module. + + Returns: + A minidom.Element instance. + """ + element = doc.createElement('flag') + if is_key: + element.appendChild(_helpers.create_xml_dom_element(doc, 'key', 'yes')) + element.appendChild(_helpers.create_xml_dom_element( + doc, 'file', module_name)) + # Adds flag features that are relevant for all flags. + element.appendChild(_helpers.create_xml_dom_element(doc, 'name', self.name)) + if self.short_name: + element.appendChild(_helpers.create_xml_dom_element( + doc, 'short_name', self.short_name)) + if self.help: + element.appendChild(_helpers.create_xml_dom_element( + doc, 'meaning', self.help)) + # The default flag value can either be represented as a string like on the + # command line, or as a Python object. We serialize this value in the + # latter case in order to remain consistent. + if self.serializer and not isinstance(self.default, str): + if self.default is not None: + default_serialized = self.serializer.serialize(self.default) + else: + default_serialized = '' + else: + default_serialized = self.default # type: ignore[assignment] + element.appendChild(_helpers.create_xml_dom_element( + doc, 'default', default_serialized)) + value_serialized = self._serialize_value_for_xml(self.value) + element.appendChild(_helpers.create_xml_dom_element( + doc, 'current', value_serialized)) + element.appendChild(_helpers.create_xml_dom_element( + doc, 'type', self.flag_type())) + # Adds extra flag features this flag may have. + for e in self._extra_xml_dom_elements(doc): + element.appendChild(e) + return element + + def _serialize_value_for_xml(self, value: Optional[_T]) -> Any: + """Returns the serialized value, for use in an XML help text.""" + return value + + def _extra_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + """Returns extra info about this flag in XML. + + "Extra" means "not already included by _create_xml_dom_element above." + + Args: + doc: minidom.Document, the DOM document it should create nodes from. + + Returns: + A list of minidom.Element. + """ + # Usually, the parser knows the extra details about the flag, so + # we just forward the call to it. + return self.parser._custom_xml_dom_elements(doc) # pylint: disable=protected-access + + +class BooleanFlag(Flag[bool]): + """Basic boolean flag. + + Boolean flags do not take any arguments, and their value is either + ``True`` (1) or ``False`` (0). The false value is specified on the command + line by prepending the word ``'no'`` to either the long or the short flag + name. + + For example, if a Boolean flag was created whose long name was + ``'update'`` and whose short name was ``'x'``, then this flag could be + explicitly unset through either ``--noupdate`` or ``--nox``. + """ + + def __init__( + self, + name: str, + default: Union[Optional[bool], str], + help: Optional[str], # pylint: disable=redefined-builtin + short_name: Optional[str] = None, + **args + ) -> None: + p = _argument_parser.BooleanParser() + super().__init__(p, None, name, default, help, short_name, True, **args) + + +class EnumFlag(Flag[str]): + """Basic enum flag; its value can be any string from list of enum_values.""" + + parser: _argument_parser.EnumParser + + def __init__( + self, + name: str, + default: Optional[str], + help: Optional[str], # pylint: disable=redefined-builtin + enum_values: Iterable[str], + short_name: Optional[str] = None, + case_sensitive: bool = True, + **args + ): + p = _argument_parser.EnumParser(enum_values, case_sensitive) + g: _argument_parser.ArgumentSerializer[str] + g = _argument_parser.ArgumentSerializer() + super().__init__(p, g, name, default, help, short_name, **args) + self.parser = p + self.help = '<%s>: %s' % ('|'.join(p.enum_values), self.help) + + def _extra_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = [] + for enum_value in self.parser.enum_values: + elements.append(_helpers.create_xml_dom_element( + doc, 'enum_value', enum_value)) + return elements + + +class EnumClassFlag(Flag[_ET]): + """Basic enum flag; its value is an enum class's member.""" + + parser: _argument_parser.EnumClassParser + + def __init__( + self, + name: str, + default: Union[Optional[_ET], str], + help: Optional[str], # pylint: disable=redefined-builtin + enum_class: Type[_ET], + short_name: Optional[str] = None, + case_sensitive: bool = False, + **args + ): + p = _argument_parser.EnumClassParser( + enum_class, case_sensitive=case_sensitive + ) + g: _argument_parser.EnumClassSerializer[_ET] + g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive) + super().__init__(p, g, name, default, help, short_name, **args) + self.parser = p + self.help = '<%s>: %s' % ('|'.join(p.member_names), self.help) + + def _extra_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = [] + for enum_value in self.parser.enum_class.__members__.keys(): + elements.append(_helpers.create_xml_dom_element( + doc, 'enum_value', enum_value)) + return elements + + +class MultiFlag(Generic[_T], Flag[List[_T]]): + """A flag that can appear multiple time on the command-line. + + The value of such a flag is a list that contains the individual values + from all the appearances of that flag on the command-line. + + See the __doc__ for Flag for most behavior of this class. Only + differences in behavior are described here: + + * The default value may be either a single value or an iterable of values. + A single value is transformed into a single-item list of that value. + + * The value of the flag is always a list, even if the option was + only supplied once, and even if the default value is a single + value + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.help += ';\n repeat this option to specify a list of values' + + def parse(self, arguments: Union[str, _T, Iterable[_T]]): # pylint: disable=arguments-renamed + """Parses one or more arguments with the installed parser. + + Args: + arguments: a single argument or a list of arguments (typically a + list of default values); a single argument is converted + internally into a list containing one item. + """ + new_values = self._parse(arguments) + if self.present: + assert self.value is not None + self.value.extend(new_values) + else: + self.value = new_values + self.present += len(new_values) + + def _parse(self, arguments: Union[str, _T, Iterable[_T]]) -> List[_T]: # pylint: disable=arguments-renamed + arguments_list: List[Union[str, _T]] + + if isinstance(arguments, str): + arguments_list = [arguments] + + elif isinstance(arguments, abc.Iterable): + arguments_list = list(arguments) + + else: + # Default value may be a list of values. Most other arguments + # will not be, so convert them into a single-item list to make + # processing simpler below. + arguments_list = [arguments] + + return [super(MultiFlag, self)._parse(item) for item in arguments_list] # type: ignore + + def _serialize(self, value: Optional[List[_T]]) -> str: + """See base class.""" + if not self.serializer: + raise _exceptions.Error( + 'Serializer not present for flag %s' % self.name) + if value is None: + return '' + + serialized_items = [ + super(MultiFlag, self)._serialize(value_item) # type: ignore[arg-type] + for value_item in value + ] + + return '\n'.join(serialized_items) + + def flag_type(self): + """See base class.""" + return 'multi ' + self.parser.flag_type() + + def _extra_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = [] + if hasattr(self.parser, 'enum_values'): + for enum_value in self.parser.enum_values: # pytype: disable=attribute-error + elements.append(_helpers.create_xml_dom_element( + doc, 'enum_value', enum_value)) + return elements + + +class MultiEnumClassFlag(MultiFlag[_ET]): # pytype: disable=not-indexable + """A multi_enum_class flag. + + See the __doc__ for MultiFlag for most behaviors of this class. In addition, + this class knows how to handle enum.Enum instances as values for this flag + type. + """ + + parser: _argument_parser.EnumClassParser[_ET] # type: ignore[assignment] + + def __init__( + self, + name: str, + default: Union[None, Iterable[_ET], _ET, Iterable[str], str], + help_string: str, + enum_class: Type[_ET], + case_sensitive: bool = False, + **args + ): + p = _argument_parser.EnumClassParser( + enum_class, case_sensitive=case_sensitive) + g: _argument_parser.EnumClassListSerializer + g = _argument_parser.EnumClassListSerializer( + list_sep=',', lowercase=not case_sensitive) + super().__init__(p, g, name, default, help_string, **args) + # NOTE: parser should be typed EnumClassParser[_ET] but the constructor + # restricts the available interface to ArgumentParser[str]. + self.parser = p + # NOTE: serializer should be non-Optional but this isn't inferred. + self.serializer = g + self.help = ( + '<%s>: %s;\n repeat this option to specify a list of values' % + ('|'.join(p.member_names), help_string or '(no help available)')) + + def _extra_xml_dom_elements( + self, doc: minidom.Document + ) -> List[minidom.Element]: + elements = [] + for enum_value in self.parser.enum_class.__members__.keys(): # pytype: disable=attribute-error + elements.append(_helpers.create_xml_dom_element( + doc, 'enum_value', enum_value)) + return elements + + def _serialize_value_for_xml(self, value): + """See base class.""" + if value is not None: + if not self.serializer: + raise _exceptions.Error( + 'Serializer not present for flag %s' % self.name + ) + value_serialized = self.serializer.serialize(value) + else: + value_serialized = '' + return value_serialized diff --git a/lib/python3.10/site-packages/absl/flags/_flagvalues.py b/lib/python3.10/site-packages/absl/flags/_flagvalues.py new file mode 100644 index 0000000000000000000000000000000000000000..dda928c32e3663f82123ac2c1a0533e68582ce0b --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_flagvalues.py @@ -0,0 +1,1486 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Defines the FlagValues class - registry of 'Flag' objects. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +import copy +import logging +import os +import sys +from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, Optional, Sequence, Set, TextIO, Tuple, TypeVar, Union +from xml.dom import minidom + +from absl.flags import _exceptions +from absl.flags import _flag +from absl.flags import _helpers +from absl.flags import _validators_classes +from absl.flags._flag import Flag + +# Add flagvalues module to disclaimed module ids. +_helpers.disclaim_module_ids.add(id(sys.modules[__name__])) + +_T = TypeVar('_T') +_T_co = TypeVar('_T_co', covariant=True) # pytype: disable=not-supported-yet + + +class FlagValues: + """Registry of :class:`~absl.flags.Flag` objects. + + A :class:`FlagValues` can then scan command line arguments, passing flag + arguments through to the 'Flag' objects that it owns. It also + provides easy access to the flag values. Typically only one + :class:`FlagValues` object is needed by an application: + :const:`FLAGS`. + + This class is heavily overloaded: + + :class:`Flag` objects are registered via ``__setitem__``:: + + FLAGS['longname'] = x # register a new flag + + The ``.value`` attribute of the registered :class:`~absl.flags.Flag` objects + can be accessed as attributes of this :class:`FlagValues` object, through + ``__getattr__``. Both the long and short name of the original + :class:`~absl.flags.Flag` objects can be used to access its value:: + + FLAGS.longname # parsed flag value + FLAGS.x # parsed flag value (short name) + + Command line arguments are scanned and passed to the registered + :class:`~absl.flags.Flag` objects through the ``__call__`` method. Unparsed + arguments, including ``argv[0]`` (e.g. the program name) are returned:: + + argv = FLAGS(sys.argv) # scan command line arguments + + The original registered :class:`~absl.flags.Flag` objects can be retrieved + through the use of the dictionary-like operator, ``__getitem__``:: + + x = FLAGS['longname'] # access the registered Flag object + + The ``str()`` operator of a :class:`absl.flags.FlagValues` object provides + help for all of the registered :class:`~absl.flags.Flag` objects. + """ + + _HAS_DYNAMIC_ATTRIBUTES = True + + # A note on collections.abc.Mapping: + # FlagValues defines __getitem__, __iter__, and __len__. It makes perfect + # sense to let it be a collections.abc.Mapping class. However, we are not + # able to do so. The mixin methods, e.g. keys, values, are not uncommon flag + # names. Those flag values would not be accessible via the FLAGS.xxx form. + + __dict__: Dict[str, Any] + + def __init__(self): + # Since everything in this class is so heavily overloaded, the only + # way of defining and using fields is to access __dict__ directly. + + # Dictionary: flag name (string) -> Flag object. + self.__dict__['__flags'] = {} + + # Set: name of hidden flag (string). + # Holds flags that should not be directly accessible from Python. + self.__dict__['__hiddenflags'] = set() + + # Dictionary: module name (string) -> list of Flag objects that are defined + # by that module. + self.__dict__['__flags_by_module'] = {} + # Dictionary: module id (int) -> list of Flag objects that are defined by + # that module. + self.__dict__['__flags_by_module_id'] = {} + # Dictionary: module name (string) -> list of Flag objects that are + # key for that module. + self.__dict__['__key_flags_by_module'] = {} + + # Bool: True if flags were parsed. + self.__dict__['__flags_parsed'] = False + + # Bool: True if unparse_flags() was called. + self.__dict__['__unparse_flags_called'] = False + + # None or Method(name, value) to call from __setattr__ for an unknown flag. + self.__dict__['__set_unknown'] = None + + # A set of banned flag names. This is to prevent users from accidentally + # defining a flag that has the same name as a method on this class. + # Users can still allow defining the flag by passing + # allow_using_method_names=True in DEFINE_xxx functions. + self.__dict__['__banned_flag_names'] = frozenset(dir(FlagValues)) + + # Bool: Whether to use GNU style scanning. + self.__dict__['__use_gnu_getopt'] = True + + # Bool: Whether use_gnu_getopt has been explicitly set by the user. + self.__dict__['__use_gnu_getopt_explicitly_set'] = False + + # Function: Takes a flag name as parameter, returns a tuple + # (is_retired, type_is_bool). + self.__dict__['__is_retired_flag_func'] = None + + def set_gnu_getopt(self, gnu_getopt: bool = True) -> None: + """Sets whether or not to use GNU style scanning. + + GNU style allows mixing of flag and non-flag arguments. See + http://docs.python.org/library/getopt.html#getopt.gnu_getopt + + Args: + gnu_getopt: bool, whether or not to use GNU style scanning. + """ + self.__dict__['__use_gnu_getopt'] = gnu_getopt + self.__dict__['__use_gnu_getopt_explicitly_set'] = True + + def is_gnu_getopt(self) -> bool: + return self.__dict__['__use_gnu_getopt'] + + def _flags(self) -> Dict[str, Flag]: + return self.__dict__['__flags'] + + def flags_by_module_dict(self) -> Dict[str, List[Flag]]: + """Returns the dictionary of module_name -> list of defined flags. + + Returns: + A dictionary. Its keys are module names (strings). Its values + are lists of Flag objects. + """ + return self.__dict__['__flags_by_module'] + + def flags_by_module_id_dict(self) -> Dict[int, List[Flag]]: + """Returns the dictionary of module_id -> list of defined flags. + + Returns: + A dictionary. Its keys are module IDs (ints). Its values + are lists of Flag objects. + """ + return self.__dict__['__flags_by_module_id'] + + def key_flags_by_module_dict(self) -> Dict[str, List[Flag]]: + """Returns the dictionary of module_name -> list of key flags. + + Returns: + A dictionary. Its keys are module names (strings). Its values + are lists of Flag objects. + """ + return self.__dict__['__key_flags_by_module'] + + def register_flag_by_module(self, module_name: str, flag: Flag) -> None: + """Records the module that defines a specific flag. + + We keep track of which flag is defined by which module so that we + can later sort the flags by module. + + Args: + module_name: str, the name of a Python module. + flag: Flag, the Flag instance that is key to the module. + """ + flags_by_module = self.flags_by_module_dict() + flags_by_module.setdefault(module_name, []).append(flag) + + def register_flag_by_module_id(self, module_id: int, flag: Flag) -> None: + """Records the module that defines a specific flag. + + Args: + module_id: int, the ID of the Python module. + flag: Flag, the Flag instance that is key to the module. + """ + flags_by_module_id = self.flags_by_module_id_dict() + flags_by_module_id.setdefault(module_id, []).append(flag) + + def register_key_flag_for_module(self, module_name: str, flag: Flag) -> None: + """Specifies that a flag is a key flag for a module. + + Args: + module_name: str, the name of a Python module. + flag: Flag, the Flag instance that is key to the module. + """ + key_flags_by_module = self.key_flags_by_module_dict() + # The list of key flags for the module named module_name. + key_flags = key_flags_by_module.setdefault(module_name, []) + # Add flag, but avoid duplicates. + if flag not in key_flags: + key_flags.append(flag) + + def _flag_is_registered(self, flag_obj: Flag) -> bool: + """Checks whether a Flag object is registered under long name or short name. + + Args: + flag_obj: Flag, the Flag instance to check for. + + Returns: + bool, True iff flag_obj is registered under long name or short name. + """ + flag_dict = self._flags() + # Check whether flag_obj is registered under its long name. + name = flag_obj.name + if name in flag_dict and flag_dict[name] == flag_obj: + return True + # Check whether flag_obj is registered under its short name. + short_name = flag_obj.short_name + if ( + short_name is not None + and short_name in flag_dict + and flag_dict[short_name] == flag_obj + ): + return True + return False + + def _cleanup_unregistered_flag_from_module_dicts( + self, flag_obj: Flag + ) -> None: + """Cleans up unregistered flags from all module -> [flags] dictionaries. + + If flag_obj is registered under either its long name or short name, it + won't be removed from the dictionaries. + + Args: + flag_obj: Flag, the Flag instance to clean up for. + """ + if self._flag_is_registered(flag_obj): + return + # Materialize dict values to list to avoid concurrent modification. + for flags_in_module in [ + *self.flags_by_module_dict().values(), + *self.flags_by_module_id_dict().values(), + *self.key_flags_by_module_dict().values(), + ]: + # While (as opposed to if) takes care of multiple occurrences of a + # flag in the list for the same module. + while flag_obj in flags_in_module: + flags_in_module.remove(flag_obj) + + def get_flags_for_module(self, module: Union[str, Any]) -> List[Flag]: + """Returns the list of flags defined by a module. + + Args: + module: module|str, the module to get flags from. + + Returns: + [Flag], a new list of Flag instances. Caller may update this list as + desired: none of those changes will affect the internals of this + FlagValue instance. + """ + if not isinstance(module, str): + module = module.__name__ + if module == '__main__': + module = sys.argv[0] + + return list(self.flags_by_module_dict().get(module, [])) + + def get_key_flags_for_module(self, module: Union[str, Any]) -> List[Flag]: + """Returns the list of key flags for a module. + + Args: + module: module|str, the module to get key flags from. + + Returns: + [Flag], a new list of Flag instances. Caller may update this list as + desired: none of those changes will affect the internals of this + FlagValue instance. + """ + if not isinstance(module, str): + module = module.__name__ + if module == '__main__': + module = sys.argv[0] + + # Any flag is a key flag for the module that defined it. NOTE: + # key_flags is a fresh list: we can update it without affecting the + # internals of this FlagValues object. + key_flags = self.get_flags_for_module(module) + + # Take into account flags explicitly declared as key for a module. + for flag in self.key_flags_by_module_dict().get(module, []): + if flag not in key_flags: + key_flags.append(flag) + return key_flags + + # TODO(yileiyang): Restrict default to Optional[str]. + def find_module_defining_flag( + self, flagname: str, default: Optional[_T] = None + ) -> Union[str, Optional[_T]]: + """Return the name of the module defining this flag, or default. + + Args: + flagname: str, name of the flag to lookup. + default: Value to return if flagname is not defined. Defaults to None. + + Returns: + The name of the module which registered the flag with this name. + If no such module exists (i.e. no flag with this name exists), + we return default. + """ + registered_flag = self._flags().get(flagname) + if registered_flag is None: + return default + for module, flags in self.flags_by_module_dict().items(): + for flag in flags: + # It must compare the flag with the one in _flags. This is because a + # flag might be overridden only for its long name (or short name), + # and only its short name (or long name) is considered registered. + if (flag.name == registered_flag.name and + flag.short_name == registered_flag.short_name): + return module + return default + + # TODO(yileiyang): Restrict default to Optional[str]. + def find_module_id_defining_flag( + self, flagname: str, default: Optional[_T] = None + ) -> Union[int, Optional[_T]]: + """Return the ID of the module defining this flag, or default. + + Args: + flagname: str, name of the flag to lookup. + default: Value to return if flagname is not defined. Defaults to None. + + Returns: + The ID of the module which registered the flag with this name. + If no such module exists (i.e. no flag with this name exists), + we return default. + """ + registered_flag = self._flags().get(flagname) + if registered_flag is None: + return default + for module_id, flags in self.flags_by_module_id_dict().items(): + for flag in flags: + # It must compare the flag with the one in _flags. This is because a + # flag might be overridden only for its long name (or short name), + # and only its short name (or long name) is considered registered. + if (flag.name == registered_flag.name and + flag.short_name == registered_flag.short_name): + return module_id + return default + + def _register_unknown_flag_setter( + self, setter: Callable[[str, Any], None] + ) -> None: + """Allow set default values for undefined flags. + + Args: + setter: Method(name, value) to call to __setattr__ an unknown flag. Must + raise NameError or ValueError for invalid name/value. + """ + self.__dict__['__set_unknown'] = setter + + def _set_unknown_flag(self, name: str, value: _T) -> _T: + """Returns value if setting flag |name| to |value| returned True. + + Args: + name: str, name of the flag to set. + value: Value to set. + + Returns: + Flag value on successful call. + + Raises: + UnrecognizedFlagError + IllegalFlagValueError + """ + setter = self.__dict__['__set_unknown'] + if setter: + try: + setter(name, value) + return value + except (TypeError, ValueError): # Flag value is not valid. + raise _exceptions.IllegalFlagValueError( + f'"{value}" is not valid for --{name}' + ) + except NameError: # Flag name is not valid. + pass + raise _exceptions.UnrecognizedFlagError(name, value) + + def append_flag_values(self, flag_values: 'FlagValues') -> None: + """Appends flags registered in another FlagValues instance. + + Args: + flag_values: FlagValues, the FlagValues instance from which to copy flags. + """ + for flag_name, flag in flag_values._flags().items(): # pylint: disable=protected-access + # Each flags with short_name appears here twice (once under its + # normal name, and again with its short name). To prevent + # problems (DuplicateFlagError) with double flag registration, we + # perform a check to make sure that the entry we're looking at is + # for its normal name. + if flag_name == flag.name: + try: + self[flag_name] = flag + except _exceptions.DuplicateFlagError: + raise _exceptions.DuplicateFlagError.from_flag( + flag_name, self, other_flag_values=flag_values) + + def remove_flag_values( + self, flag_values: 'Union[FlagValues, Iterable[str]]' + ) -> None: + """Remove flags that were previously appended from another FlagValues. + + Args: + flag_values: FlagValues, the FlagValues instance containing flags to + remove. + """ + for flag_name in flag_values: + self.__delattr__(flag_name) + + def __setitem__(self, name: str, flag: Flag) -> None: + """Registers a new flag variable.""" + fl = self._flags() + if not isinstance(flag, _flag.Flag): + raise _exceptions.IllegalFlagValueError( + f'Expect Flag instances, found type {type(flag)}. ' + "Maybe you didn't mean to use FlagValue.__setitem__?") + if not isinstance(name, str): + raise _exceptions.Error('Flag name must be a string') + if not name: + raise _exceptions.Error('Flag name cannot be empty') + if ' ' in name: + raise _exceptions.Error('Flag name cannot contain a space') + self._check_method_name_conflicts(name, flag) + if name in fl and not flag.allow_override and not fl[name].allow_override: + module, module_name = _helpers.get_calling_module_object_and_name() + if (self.find_module_defining_flag(name) == module_name and + id(module) != self.find_module_id_defining_flag(name)): + # If the flag has already been defined by a module with the same name, + # but a different ID, we can stop here because it indicates that the + # module is simply being imported a subsequent time. + return + raise _exceptions.DuplicateFlagError.from_flag(name, self) + # If a new flag overrides an old one, we need to cleanup the old flag's + # modules if it's not registered. + flags_to_cleanup = set() + short_name: Optional[str] = flag.short_name + if short_name is not None: + if (short_name in fl and not flag.allow_override and + not fl[short_name].allow_override): + raise _exceptions.DuplicateFlagError.from_flag(short_name, self) + if short_name in fl and fl[short_name] != flag: + flags_to_cleanup.add(fl[short_name]) + fl[short_name] = flag + if (name not in fl # new flag + or fl[name].using_default_value or not flag.using_default_value): + if name in fl and fl[name] != flag: + flags_to_cleanup.add(fl[name]) + fl[name] = flag + for f in flags_to_cleanup: + self._cleanup_unregistered_flag_from_module_dicts(f) + + def __dir__(self) -> List[str]: + """Returns list of names of all defined flags. + + Useful for TAB-completion in ipython. + + Returns: + [str], a list of names of all defined flags. + """ + return sorted(self._flags()) + + def __getitem__(self, name: str) -> Flag: + """Returns the Flag object for the flag --name.""" + return self._flags()[name] + + def _hide_flag(self, name): + """Marks the flag --name as hidden.""" + self.__dict__['__hiddenflags'].add(name) + + def __getattr__(self, name: str) -> Any: + """Retrieves the 'value' attribute of the flag --name.""" + flag_entry = self._flags().get(name) + if flag_entry is None: + raise AttributeError(name) + if name in self.__dict__['__hiddenflags']: + raise AttributeError(name) + + if self.__dict__['__flags_parsed'] or flag_entry.present: + return flag_entry.value + else: + raise _exceptions.UnparsedFlagAccessError( + 'Trying to access flag --%s before flags were parsed.' % name) + + def __setattr__(self, name: str, value: _T) -> _T: + """Sets the 'value' attribute of the flag --name.""" + self._set_attributes(**{name: value}) + return value + + def _set_attributes(self, **attributes: Any) -> None: + """Sets multiple flag values together, triggers validators afterwards.""" + fl = self._flags() + known_flag_vals = {} + known_flag_used_defaults = {} + try: + for name, value in attributes.items(): + if name in self.__dict__['__hiddenflags']: + raise AttributeError(name) + flag_entry = fl.get(name) + if flag_entry is not None: + orig = flag_entry.value + flag_entry.value = value + known_flag_vals[name] = orig + else: + self._set_unknown_flag(name, value) + for name in known_flag_vals: + self._assert_validators(fl[name].validators) + known_flag_used_defaults[name] = fl[name].using_default_value + fl[name].using_default_value = False + except: + for name, orig in known_flag_vals.items(): + fl[name].value = orig + for name, orig in known_flag_used_defaults.items(): + fl[name].using_default_value = orig + # NOTE: We do not attempt to undo unknown flag side effects because we + # cannot reliably undo the user-configured behavior. + raise + + def validate_all_flags(self) -> None: + """Verifies whether all flags pass validation. + + Raises: + AttributeError: Raised if validators work with a non-existing flag. + IllegalFlagValueError: Raised if validation fails for at least one + validator. + """ + all_validators = set() + for flag in self._flags().values(): + all_validators.update(flag.validators) + self._assert_validators(all_validators) + + def _assert_validators( + self, validators: Iterable[_validators_classes.Validator] + ) -> None: + """Asserts if all validators in the list are satisfied. + + It asserts validators in the order they were created. + + Args: + validators: Iterable(validators.Validator), validators to be verified. + + Raises: + AttributeError: Raised if validators work with a non-existing flag. + IllegalFlagValueError: Raised if validation fails for at least one + validator. + """ + messages = [] + bad_flags: Set[str] = set() + for validator in sorted( + validators, key=lambda validator: validator.insertion_index): + try: + if isinstance(validator, _validators_classes.SingleFlagValidator): + if validator.flag_name in bad_flags: + continue + elif isinstance(validator, _validators_classes.MultiFlagsValidator): + if bad_flags & set(validator.flag_names): + continue + validator.verify(self) + except _exceptions.ValidationError as e: + if isinstance(validator, _validators_classes.SingleFlagValidator): + bad_flags.add(validator.flag_name) + elif isinstance(validator, _validators_classes.MultiFlagsValidator): + bad_flags.update(set(validator.flag_names)) + message = validator.print_flags_with_values(self) + messages.append('%s: %s' % (message, str(e))) + if messages: + raise _exceptions.IllegalFlagValueError('\n'.join(messages)) + + def __delattr__(self, flag_name: str) -> None: + """Deletes a previously-defined flag from a flag object. + + This method makes sure we can delete a flag by using + + del FLAGS. + + E.g., + + flags.DEFINE_integer('foo', 1, 'Integer flag.') + del flags.FLAGS.foo + + If a flag is also registered by its the other name (long name or short + name), the other name won't be deleted. + + Args: + flag_name: str, the name of the flag to be deleted. + + Raises: + AttributeError: Raised when there is no registered flag named flag_name. + """ + fl = self._flags() + flag_entry = fl.get(flag_name) + if flag_entry is None: + raise AttributeError(flag_name) + del fl[flag_name] + + self._cleanup_unregistered_flag_from_module_dicts(flag_entry) + + def set_default(self, name: str, value: Any) -> None: + """Changes the default value of the named flag object. + + The flag's current value is also updated if the flag is currently using + the default value, i.e. not specified in the command line, and not set + by FLAGS.name = value. + + Args: + name: str, the name of the flag to modify. + value: The new default value. + + Raises: + UnrecognizedFlagError: Raised when there is no registered flag named name. + IllegalFlagValueError: Raised when value is not valid. + """ + fl = self._flags() + flag_entry = fl.get(name) + if flag_entry is None: + self._set_unknown_flag(name, value) + return + flag_entry._set_default(value) # pylint: disable=protected-access + self._assert_validators(flag_entry.validators) + + def __contains__(self, name: str) -> bool: + """Returns True if name is a value (flag) in the dict.""" + return name in self._flags() + + def __len__(self) -> int: + return len(self.__dict__['__flags']) + + def __iter__(self) -> Iterator[str]: + return iter(self._flags()) + + def __call__( + self, argv: Sequence[str], known_only: bool = False + ) -> List[str]: + """Parses flags from argv; stores parsed flags into this FlagValues object. + + All unparsed arguments are returned. + + Args: + argv: a tuple/list of strings. + known_only: bool, if True, parse and remove known flags; return the rest + untouched. Unknown flags specified by --undefok are not returned. + + Returns: + The list of arguments not parsed as options, including argv[0]. + + Raises: + Error: Raised on any parsing error. + TypeError: Raised on passing wrong type of arguments. + ValueError: Raised on flag value parsing error. + """ + if isinstance(argv, (str, bytes)): + raise TypeError( + 'argv should be a tuple/list of strings, not bytes or string.') + if not argv: + raise ValueError( + 'argv cannot be an empty list, and must contain the program name as ' + 'the first element.') + + # This pre parses the argv list for --flagfile=<> options. + program_name = argv[0] + args = self.read_flags_from_files(argv[1:], force_gnu=False) + + # Parse the arguments. + unknown_flags, unparsed_args = self._parse_args(args, known_only) + + # Handle unknown flags by raising UnrecognizedFlagError. + # Note some users depend on us raising this particular error. + for name, value in unknown_flags: + suggestions = _helpers.get_flag_suggestions(name, list(self)) + raise _exceptions.UnrecognizedFlagError( + name, value, suggestions=suggestions) + + self.mark_as_parsed() + self.validate_all_flags() + return [program_name] + unparsed_args + + def __getstate__(self) -> Any: + raise TypeError("can't pickle FlagValues") + + def __copy__(self) -> Any: + raise TypeError('FlagValues does not support shallow copies. ' + 'Use absl.testing.flagsaver or copy.deepcopy instead.') + + def __deepcopy__(self, memo) -> Any: + result = object.__new__(type(self)) + result.__dict__.update(copy.deepcopy(self.__dict__, memo)) + return result + + def _set_is_retired_flag_func(self, is_retired_flag_func): + """Sets a function for checking retired flags. + + Do not use it. This is a private absl API used to check retired flags + registered by the absl C++ flags library. + + Args: + is_retired_flag_func: Callable(str) -> (bool, bool), a function takes flag + name as parameter, returns a tuple (is_retired, type_is_bool). + """ + self.__dict__['__is_retired_flag_func'] = is_retired_flag_func + + def _parse_args( + self, args: List[str], known_only: bool + ) -> Tuple[List[Tuple[str, Any]], List[str]]: + """Helper function to do the main argument parsing. + + This function goes through args and does the bulk of the flag parsing. + It will find the corresponding flag in our flag dictionary, and call its + .parse() method on the flag value. + + Args: + args: [str], a list of strings with the arguments to parse. + known_only: bool, if True, parse and remove known flags; return the rest + untouched. Unknown flags specified by --undefok are not returned. + + Returns: + A tuple with the following: + unknown_flags: List of (flag name, arg) for flags we don't know about. + unparsed_args: List of arguments we did not parse. + + Raises: + Error: Raised on any parsing error. + ValueError: Raised on flag value parsing error. + """ + unparsed_names_and_args: List[Tuple[Optional[str], str]] = [] + undefok: Set[str] = set() + retired_flag_func = self.__dict__['__is_retired_flag_func'] + + flag_dict = self._flags() + args_it = iter(args) + del args + for arg in args_it: + value = None + + def get_value() -> str: + try: + return next(args_it) if value is None else value # pylint: disable=cell-var-from-loop + except StopIteration: + raise _exceptions.Error('Missing value for flag ' + arg) from None # pylint: disable=cell-var-from-loop + + if not arg.startswith('-'): + # A non-argument: default is break, GNU is skip. + unparsed_names_and_args.append((None, arg)) + if self.is_gnu_getopt(): + continue + else: + break + + if arg == '--': + if known_only: + unparsed_names_and_args.append((None, arg)) + break + + # At this point, arg must start with '-'. + if arg.startswith('--'): + arg_without_dashes = arg[2:] + else: + arg_without_dashes = arg[1:] + + if '=' in arg_without_dashes: + name, value = arg_without_dashes.split('=', 1) + else: + name, value = arg_without_dashes, None + + if not name: + # The argument is all dashes (including one dash). + unparsed_names_and_args.append((None, arg)) + if self.is_gnu_getopt(): + continue + else: + break + + # --undefok is a special case. + if name == 'undefok': + value = get_value() + undefok.update(v.strip() for v in value.split(',')) + undefok.update('no' + v.strip() for v in value.split(',')) + continue + + flag = flag_dict.get(name) + if flag is not None: + if flag.boolean and value is None: + value = 'true' + else: + value = get_value() + elif name.startswith('no') and len(name) > 2: + # Boolean flags can take the form of --noflag, with no value. + noflag = flag_dict.get(name[2:]) + if noflag is not None and noflag.boolean: + if value is not None: + raise ValueError(arg + ' does not take an argument') + flag = noflag + value = 'false' + + if retired_flag_func and flag is None: + is_retired, is_bool = retired_flag_func(name) + + # If we didn't recognize that flag, but it starts with + # "no" then maybe it was a boolean flag specified in the + # --nofoo form. + if not is_retired and name.startswith('no'): + is_retired, is_bool = retired_flag_func(name[2:]) + is_retired = is_retired and is_bool + + if is_retired: + if not is_bool and value is None: + # This happens when a non-bool retired flag is specified + # in format of "--flag value". + get_value() + logging.error( + 'Flag "%s" is retired and should no longer be specified. See ' + 'https://abseil.io/tips/90.', + name, + ) + continue + + if flag is not None: + # LINT.IfChange + flag.parse(value) + flag.using_default_value = False + # LINT.ThenChange(../testing/flagsaver.py:flag_override_parsing) + else: + unparsed_names_and_args.append((name, arg)) + + unknown_flags = [] + unparsed_args = [] + for arg_name, arg in unparsed_names_and_args: + if arg_name is None: + # Positional arguments. + unparsed_args.append(arg) + elif arg_name in undefok: + # Remove undefok flags. + continue + else: + # This is an unknown flag. + if known_only: + unparsed_args.append(arg) + else: + unknown_flags.append((arg_name, arg)) + + unparsed_args.extend(list(args_it)) + return unknown_flags, unparsed_args + + def is_parsed(self) -> bool: + """Returns whether flags were parsed.""" + return self.__dict__['__flags_parsed'] + + def mark_as_parsed(self) -> None: + """Explicitly marks flags as parsed. + + Use this when the caller knows that this FlagValues has been parsed as if + a ``__call__()`` invocation has happened. This is only a public method for + use by things like appcommands which do additional command like parsing. + """ + self.__dict__['__flags_parsed'] = True + + def unparse_flags(self) -> None: + """Unparses all flags to the point before any FLAGS(argv) was called.""" + for f in self._flags().values(): + f.unparse() + # We log this message before marking flags as unparsed to avoid a + # problem when the logging library causes flags access. + logging.info('unparse_flags() called; flags access will now raise errors.') + self.__dict__['__flags_parsed'] = False + self.__dict__['__unparse_flags_called'] = True + + def flag_values_dict(self) -> Dict[str, Any]: + """Returns a dictionary that maps flag names to flag values.""" + return {name: flag.value for name, flag in list(self._flags().items())} + + def __str__(self): + """Returns a help string for all known flags.""" + return self.get_help() + + def get_help( + self, prefix: str = '', include_special_flags: bool = True + ) -> str: + """Returns a help string for all known flags. + + Args: + prefix: str, per-line output prefix. + include_special_flags: bool, whether to include description of + SPECIAL_FLAGS, i.e. --flagfile and --undefok. + + Returns: + str, formatted help message. + """ + flags_by_module = self.flags_by_module_dict() + if flags_by_module: + modules = sorted(flags_by_module) + # Print the help for the main module first, if possible. + main_module = sys.argv[0] + if main_module in modules: + modules.remove(main_module) + modules = [main_module] + modules + return self._get_help_for_modules(modules, prefix, include_special_flags) + else: + output_lines: List[str] = [] + # Just print one long list of flags. + values = list(self._flags().values()) + if include_special_flags: + values.extend(_helpers.SPECIAL_FLAGS._flags().values()) # pylint: disable=protected-access + self._render_flag_list(values, output_lines, prefix) + return '\n'.join(output_lines) + + def _get_help_for_modules(self, modules, prefix, include_special_flags): + """Returns the help string for a list of modules. + + Private to absl.flags package. + + Args: + modules: List[str], a list of modules to get the help string for. + prefix: str, a string that is prepended to each generated help line. + include_special_flags: bool, whether to include description of + SPECIAL_FLAGS, i.e. --flagfile and --undefok. + """ + output_lines = [] + for module in modules: + self._render_our_module_flags(module, output_lines, prefix) + if include_special_flags: + self._render_module_flags( + 'absl.flags', + _helpers.SPECIAL_FLAGS._flags().values(), # pylint: disable=protected-access # pytype: disable=attribute-error + output_lines, + prefix, + ) + return '\n'.join(output_lines) + + def _render_module_flags(self, module, flags, output_lines, prefix=''): + """Returns a help string for a given module.""" + if not isinstance(module, str): + module = module.__name__ + output_lines.append('\n%s%s:' % (prefix, module)) + self._render_flag_list(flags, output_lines, prefix + ' ') + + def _render_our_module_flags(self, module, output_lines, prefix=''): + """Returns a help string for a given module.""" + flags = self.get_flags_for_module(module) + if flags: + self._render_module_flags(module, flags, output_lines, prefix) + + def _render_our_module_key_flags(self, module, output_lines, prefix=''): + """Returns a help string for the key flags of a given module. + + Args: + module: module|str, the module to render key flags for. + output_lines: [str], a list of strings. The generated help message lines + will be appended to this list. + prefix: str, a string that is prepended to each generated help line. + """ + key_flags = self.get_key_flags_for_module(module) + if key_flags: + self._render_module_flags(module, key_flags, output_lines, prefix) + + def module_help(self, module: Any) -> str: + """Describes the key flags of a module. + + Args: + module: module|str, the module to describe the key flags for. + + Returns: + str, describing the key flags of a module. + """ + helplist: List[str] = [] + self._render_our_module_key_flags(module, helplist) + return '\n'.join(helplist) + + def main_module_help(self) -> str: + """Describes the key flags of the main module. + + Returns: + str, describing the key flags of the main module. + """ + return self.module_help(sys.argv[0]) + + def _render_flag_list(self, flaglist, output_lines, prefix=' '): + fl = self._flags() + special_fl = _helpers.SPECIAL_FLAGS._flags() # pylint: disable=protected-access # pytype: disable=attribute-error + flaglist = [(flag.name, flag) for flag in flaglist] + flaglist.sort() + flagset = {} + for (name, flag) in flaglist: + # It's possible this flag got deleted or overridden since being + # registered in the per-module flaglist. Check now against the + # canonical source of current flag information, the _flags. + if fl.get(name, None) != flag and special_fl.get(name, None) != flag: + # a different flag is using this name now + continue + # only print help once + if flag in flagset: + continue + flagset[flag] = 1 + flaghelp = '' + if flag.short_name: + flaghelp += '-%s,' % flag.short_name + if flag.boolean: + flaghelp += '--[no]%s:' % flag.name + else: + flaghelp += '--%s:' % flag.name + flaghelp += ' ' + if flag.help: + flaghelp += flag.help + flaghelp = _helpers.text_wrap( + flaghelp, indent=prefix + ' ', firstline_indent=prefix) + if flag.default_as_str: + flaghelp += '\n' + flaghelp += _helpers.text_wrap( + '(default: %s)' % flag.default_as_str, indent=prefix + ' ') + if flag.parser.syntactic_help: + flaghelp += '\n' + flaghelp += _helpers.text_wrap( + '(%s)' % flag.parser.syntactic_help, indent=prefix + ' ') + output_lines.append(flaghelp) + + def get_flag_value(self, name: str, default: Any) -> Any: # pylint: disable=invalid-name + """Returns the value of a flag (if not None) or a default value. + + Args: + name: str, the name of a flag. + default: Default value to use if the flag value is None. + + Returns: + Requested flag value or default. + """ + + value = self.__getattr__(name) + if value is not None: # Can't do if not value, b/c value might be '0' or "" + return value + else: + return default + + def _is_flag_file_directive(self, flag_string): + """Checks whether flag_string contain a --flagfile= directive.""" + if isinstance(flag_string, str): + if flag_string.startswith('--flagfile='): + return 1 + elif flag_string == '--flagfile': + return 1 + elif flag_string.startswith('-flagfile='): + return 1 + elif flag_string == '-flagfile': + return 1 + else: + return 0 + return 0 + + def _extract_filename(self, flagfile_str): + """Returns filename from a flagfile_str of form -[-]flagfile=filename. + + The cases of --flagfile foo and -flagfile foo shouldn't be hitting + this function, as they are dealt with in the level above this + function. + + Args: + flagfile_str: str, the flagfile string. + + Returns: + str, the filename from a flagfile_str of form -[-]flagfile=filename. + + Raises: + Error: Raised when illegal --flagfile is provided. + """ + if flagfile_str.startswith('--flagfile='): + return os.path.expanduser((flagfile_str[(len('--flagfile=')):]).strip()) + elif flagfile_str.startswith('-flagfile='): + return os.path.expanduser((flagfile_str[(len('-flagfile=')):]).strip()) + else: + raise _exceptions.Error('Hit illegal --flagfile type: %s' % flagfile_str) + + def _get_flag_file_lines(self, filename, parsed_file_stack=None): + """Returns the useful (!=comments, etc) lines from a file with flags. + + Args: + filename: str, the name of the flag file. + parsed_file_stack: [str], a list of the names of the files that we have + recursively encountered at the current depth. MUTATED BY THIS FUNCTION + (but the original value is preserved upon successfully returning from + function call). + + Returns: + List of strings. See the note below. + + NOTE(springer): This function checks for a nested --flagfile= + tag and handles the lower file recursively. It returns a list of + all the lines that _could_ contain command flags. This is + EVERYTHING except whitespace lines and comments (lines starting + with '#' or '//'). + """ + # For consistency with the cpp version, ignore empty values. + if not filename: + return [] + if parsed_file_stack is None: + parsed_file_stack = [] + # We do a little safety check for reparsing a file we've already encountered + # at a previous depth. + if filename in parsed_file_stack: + sys.stderr.write('Warning: Hit circular flagfile dependency. Ignoring' + ' flagfile: %s\n' % (filename,)) + return [] + else: + parsed_file_stack.append(filename) + + line_list = [] # All line from flagfile. + flag_line_list = [] # Subset of lines w/o comments, blanks, flagfile= tags. + try: + file_obj = open(filename) + except OSError as e_msg: + raise _exceptions.CantOpenFlagFileError( + 'ERROR:: Unable to open flagfile: %s' % e_msg) + + with file_obj: + line_list = file_obj.readlines() + + # This is where we check each line in the file we just read. + for line in line_list: + if line.isspace(): + pass + # Checks for comment (a line that starts with '#'). + elif line.startswith('#') or line.startswith('//'): + pass + # Checks for a nested "--flagfile=" flag in the current file. + # If we find one, recursively parse down into that file. + elif self._is_flag_file_directive(line): + sub_filename = self._extract_filename(line) + included_flags = self._get_flag_file_lines( + sub_filename, parsed_file_stack=parsed_file_stack) + flag_line_list.extend(included_flags) + else: + # Any line that's not a comment or a nested flagfile should get + # copied into 2nd position. This leaves earlier arguments + # further back in the list, thus giving them higher priority. + flag_line_list.append(line.strip()) + + parsed_file_stack.pop() + return flag_line_list + + def read_flags_from_files( + self, argv: Sequence[str], force_gnu: bool = True + ) -> List[str]: + """Processes command line args, but also allow args to be read from file. + + Args: + argv: [str], a list of strings, usually sys.argv[1:], which may contain + one or more flagfile directives of the form --flagfile="./filename". + Note that the name of the program (sys.argv[0]) should be omitted. + force_gnu: bool, if False, --flagfile parsing obeys the + FLAGS.is_gnu_getopt() value. If True, ignore the value and always follow + gnu_getopt semantics. + + Returns: + A new list which has the original list combined with what we read + from any flagfile(s). + + Raises: + IllegalFlagValueError: Raised when --flagfile is provided with no + argument. + + This function is called by FLAGS(argv). + It scans the input list for a flag that looks like: + --flagfile=. Then it opens , reads all valid key + and value pairs and inserts them into the input list in exactly the + place where the --flagfile arg is found. + + Note that your application's flags are still defined the usual way + using absl.flags DEFINE_flag() type functions. + + Notes (assuming we're getting a commandline of some sort as our input): + + * For duplicate flags, the last one we hit should "win". + * Since flags that appear later win, a flagfile's settings can be "weak" + if the --flagfile comes at the beginning of the argument sequence, + and it can be "strong" if the --flagfile comes at the end. + * A further "--flagfile=" CAN be nested in a flagfile. + It will be expanded in exactly the spot where it is found. + * In a flagfile, a line beginning with # or // is a comment. + * Entirely blank lines _should_ be ignored. + """ + rest_of_args = argv + new_argv = [] + while rest_of_args: + current_arg = rest_of_args[0] + rest_of_args = rest_of_args[1:] + if self._is_flag_file_directive(current_arg): + # This handles the case of -(-)flagfile foo. In this case the + # next arg really is part of this one. + if current_arg == '--flagfile' or current_arg == '-flagfile': + if not rest_of_args: + raise _exceptions.IllegalFlagValueError( + '--flagfile with no argument') + flag_filename = os.path.expanduser(rest_of_args[0]) + rest_of_args = rest_of_args[1:] + else: + # This handles the case of (-)-flagfile=foo. + flag_filename = self._extract_filename(current_arg) + new_argv.extend(self._get_flag_file_lines(flag_filename)) + else: + new_argv.append(current_arg) + # Stop parsing after '--', like getopt and gnu_getopt. + if current_arg == '--': + break + # Stop parsing after a non-flag, like getopt. + if not current_arg.startswith('-'): + if not force_gnu and not self.__dict__['__use_gnu_getopt']: + break + else: + if ('=' not in current_arg and rest_of_args and + not rest_of_args[0].startswith('-')): + # If this is an occurrence of a legitimate --x y, skip the value + # so that it won't be mistaken for a standalone arg. + fl = self._flags() + name = current_arg.lstrip('-') + if name in fl and not fl[name].boolean: + current_arg = rest_of_args[0] + rest_of_args = rest_of_args[1:] + new_argv.append(current_arg) + + if rest_of_args: + new_argv.extend(rest_of_args) + + return new_argv + + def flags_into_string(self) -> str: + """Returns a string with the flags assignments from this FlagValues object. + + This function ignores flags whose value is None. Each flag + assignment is separated by a newline. + + NOTE: MUST mirror the behavior of the C++ CommandlineFlagsIntoString + from https://github.com/gflags/gflags. + + Returns: + str, the string with the flags assignments from this FlagValues object. + The flags are ordered by (module_name, flag_name). + """ + module_flags = sorted(self.flags_by_module_dict().items()) + s = '' + for unused_module_name, flags in module_flags: + flags = sorted(flags, key=lambda f: f.name) + for flag in flags: + if flag.value is not None: + s += flag.serialize() + '\n' + return s + + def append_flags_into_file(self, filename: str) -> None: + """Appends all flags assignments from this FlagInfo object to a file. + + Output will be in the format of a flagfile. + + NOTE: MUST mirror the behavior of the C++ AppendFlagsIntoFile + from https://github.com/gflags/gflags. + + Args: + filename: str, name of the file. + """ + with open(filename, 'a') as out_file: + out_file.write(self.flags_into_string()) + + def write_help_in_xml_format(self, outfile: Optional[TextIO] = None) -> None: + """Outputs flag documentation in XML format. + + NOTE: We use element names that are consistent with those used by + the C++ command-line flag library, from + https://github.com/gflags/gflags. + We also use a few new elements (e.g., ), but we do not + interfere / overlap with existing XML elements used by the C++ + library. Please maintain this consistency. + + Args: + outfile: File object we write to. Default None means sys.stdout. + """ + doc = minidom.Document() + all_flag = doc.createElement('AllFlags') + doc.appendChild(all_flag) + + all_flag.appendChild( + _helpers.create_xml_dom_element(doc, 'program', + os.path.basename(sys.argv[0]))) + + usage_doc = sys.modules['__main__'].__doc__ + if not usage_doc: + usage_doc = '\nUSAGE: %s [flags]\n' % sys.argv[0] + else: + usage_doc = usage_doc.replace('%s', sys.argv[0]) + all_flag.appendChild( + _helpers.create_xml_dom_element(doc, 'usage', usage_doc)) + + # Get list of key flags for the main module. + key_flags = self.get_key_flags_for_module(sys.argv[0]) + + flags_by_module = self.flags_by_module_dict() + # Sort flags by declaring module name and next by flag name. + for module_name in sorted(flags_by_module.keys()): + flag_list = [(f.name, f) for f in flags_by_module[module_name]] + flag_list.sort() + for unused_flag_name, flag in flag_list: + is_key = flag in key_flags + all_flag.appendChild( + flag._create_xml_dom_element( # pylint: disable=protected-access + doc, + module_name, + is_key=is_key)) + + outfile = outfile or sys.stdout + outfile.write( + doc.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8')) + outfile.flush() + + def _check_method_name_conflicts(self, name: str, flag: Flag): + if flag.allow_using_method_names: + return + short_name = flag.short_name + flag_names = {name} if short_name is None else {name, short_name} + for flag_name in flag_names: + if flag_name in self.__dict__['__banned_flag_names']: + raise _exceptions.FlagNameConflictsWithMethodError( + 'Cannot define a flag named "{name}". It conflicts with a method ' + 'on class "{class_name}". To allow defining it, use ' + 'allow_using_method_names and access the flag value with ' + "FLAGS['{name}'].value. FLAGS.{name} returns the method, " + 'not the flag value.'.format( + name=flag_name, class_name=type(self).__name__)) + + +FLAGS = FlagValues() + + +class FlagHolder(Generic[_T_co]): + """Holds a defined flag. + + This facilitates a cleaner api around global state. Instead of:: + + flags.DEFINE_integer('foo', ...) + flags.DEFINE_integer('bar', ...) + + def method(): + # prints parsed value of 'bar' flag + print(flags.FLAGS.foo) + # runtime error due to typo or possibly bad coding style. + print(flags.FLAGS.baz) + + it encourages code like:: + + _FOO_FLAG = flags.DEFINE_integer('foo', ...) + _BAR_FLAG = flags.DEFINE_integer('bar', ...) + + def method(): + print(_FOO_FLAG.value) + print(_BAR_FLAG.value) + + since the name of the flag appears only once in the source code. + """ + + value: _T_co + + def __init__( + self, + flag_values: FlagValues, + flag: Flag[_T_co], + ensure_non_none_value: bool = False, + ): + """Constructs a FlagHolder instance providing typesafe access to flag. + + Args: + flag_values: The container the flag is registered to. + flag: The flag object for this flag. + ensure_non_none_value: Is the value of the flag allowed to be None. + """ + self._flagvalues = flag_values + # We take the entire flag object, but only keep the name. Why? + # - We want FlagHolder[T] to be generic container + # - flag_values contains all flags, so has no reference to T. + # - typecheckers don't like to see a generic class where none of the ctor + # arguments refer to the generic type. + self._name = flag.name + # We intentionally do NOT check if the default value is None. + # This allows future use of this for "required flags with None default" + self._ensure_non_none_value = ensure_non_none_value + + def __eq__(self, other): + raise TypeError( + "unsupported operand type(s) for ==: '{0}' and '{1}' " + "(did you mean to use '{0}.value' instead?)".format( + type(self).__name__, type(other).__name__)) + + def __bool__(self): + raise TypeError( + "bool() not supported for instances of type '{0}' " + "(did you mean to use '{0}.value' instead?)".format( + type(self).__name__)) + + __nonzero__ = __bool__ + + @property + def name(self) -> str: + return self._name + + @property # type: ignore[no-redef] + def value(self) -> _T_co: + """Returns the value of the flag. + + If ``_ensure_non_none_value`` is ``True``, then return value is not + ``None``. + + Raises: + UnparsedFlagAccessError: if flag parsing has not finished. + IllegalFlagValueError: if value is None unexpectedly. + """ + val = getattr(self._flagvalues, self._name) + if self._ensure_non_none_value and val is None: + raise _exceptions.IllegalFlagValueError( + 'Unexpected None value for flag %s' % self._name) + return val + + @property + def default(self) -> _T_co: + """Returns the default value of the flag.""" + return self._flagvalues[self._name].default # type: ignore[return-value] + + @property + def present(self) -> bool: + """Returns True if the flag was parsed from command-line flags.""" + return bool(self._flagvalues[self._name].present) + + def serialize(self) -> str: + """Returns a serialized representation of the flag.""" + return self._flagvalues[self._name].serialize() + + +def resolve_flag_ref( + flag_ref: Union[str, FlagHolder], flag_values: FlagValues +) -> Tuple[str, FlagValues]: + """Helper to validate and resolve a flag reference argument.""" + if isinstance(flag_ref, FlagHolder): + new_flag_values = flag_ref._flagvalues # pylint: disable=protected-access + if flag_values != FLAGS and flag_values != new_flag_values: + raise ValueError( + 'flag_values must not be customized when operating on a FlagHolder') + return flag_ref.name, new_flag_values + return flag_ref, flag_values + + +def resolve_flag_refs( + flag_refs: Sequence[Union[str, FlagHolder]], flag_values: FlagValues +) -> Tuple[List[str], FlagValues]: + """Helper to validate and resolve flag reference list arguments.""" + fv = None + names = [] + for ref in flag_refs: + if isinstance(ref, FlagHolder): + newfv = ref._flagvalues # pylint: disable=protected-access + name = ref.name + else: + newfv = flag_values + name = ref + if fv and fv != newfv: + raise ValueError( + 'multiple FlagValues instances used in invocation. ' + 'FlagHolders must be registered to the same FlagValues instance as ' + 'do flag names, if provided.') + fv = newfv + names.append(name) + if fv is None: + raise ValueError('flag_refs argument must not be empty') + return names, fv diff --git a/lib/python3.10/site-packages/absl/flags/_helpers.py b/lib/python3.10/site-packages/absl/flags/_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..5c7d87fa56826b0e8bda49807558adecaa0b872c --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_helpers.py @@ -0,0 +1,427 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal helper functions for Abseil Python flags library.""" + +import os +import re +import struct +import sys +import textwrap +import types +from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence, Set +from xml.dom import minidom +# pylint: disable=g-import-not-at-top +fcntl: Optional[types.ModuleType] +try: + import fcntl +except ImportError: + fcntl = None +termios: Optional[types.ModuleType] +try: + # Importing termios will fail on non-unix platforms. + import termios +except ImportError: + termios = None +# pylint: enable=g-import-not-at-top + + +_DEFAULT_HELP_WIDTH = 80 # Default width of help output. +# Minimal "sane" width of help output. We assume that any value below 40 is +# unreasonable. +_MIN_HELP_WIDTH = 40 + +# Define the allowed error rate in an input string to get suggestions. +# +# We lean towards a high threshold because we tend to be matching a phrase, +# and the simple algorithm used here is geared towards correcting word +# spellings. +# +# For manual testing, consider " --list" which produced a large number +# of spurious suggestions when we used "least_errors > 0.5" instead of +# "least_erros >= 0.5". +_SUGGESTION_ERROR_RATE_THRESHOLD = 0.50 + +# Characters that cannot appear or are highly discouraged in an XML 1.0 +# document. (See http://www.w3.org/TR/REC-xml/#charsets or +# https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0) +_ILLEGAL_XML_CHARS_REGEX = re.compile( + '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]' +) + +# This is a set of module ids for the modules that disclaim key flags. +# This module is explicitly added to this set so that we never consider it to +# define key flag. +disclaim_module_ids: Set[int] = {id(sys.modules[__name__])} + + +# Define special flags here so that help may be generated for them. +# NOTE: Please do NOT use SPECIAL_FLAGS from outside flags module. +# Initialized inside flagvalues.py. +# NOTE: This cannot be annotated as its actual FlagValues type since this would +# create a circular dependency. +SPECIAL_FLAGS: Any = None + + +# This points to the flags module, initialized in flags/__init__.py. +# This should only be used in adopt_module_key_flags to take SPECIAL_FLAGS into +# account. +FLAGS_MODULE: Optional[types.ModuleType] = None + + +class _ModuleObjectAndName(NamedTuple): + """Module object and name. + + Fields: + - module: object, module object. + - module_name: str, module name. + """ + module: types.ModuleType + module_name: str + + +def get_module_object_and_name( + globals_dict: Dict[str, Any], +) -> Optional[_ModuleObjectAndName]: + """Returns the module that defines a global environment, and its name. + + Args: + globals_dict: A dictionary that should correspond to an environment + providing the values of the globals. + + Returns: + _ModuleObjectAndName - pair of module object & module name. + Returns None if the module could not be identified. + """ + try: + name = globals_dict['__name__'] + module = sys.modules[name] + except KeyError: + return None + # Pick a more informative name for the main module. + return _ModuleObjectAndName(module, + (sys.argv[0] if name == '__main__' else name)) + + +def get_calling_module_object_and_name() -> _ModuleObjectAndName: + """Returns the module that's calling into this module. + + We generally use this function to get the name of the module calling a + DEFINE_foo... function. + + Returns: + The module object that called into this one. + + Raises: + AssertionError: Raised when no calling module could be identified. + """ + for depth in range(1, sys.getrecursionlimit()): + # sys._getframe is the right thing to use here, as it's the best + # way to walk up the call stack. + globals_for_frame = sys._getframe(depth).f_globals # pylint: disable=protected-access + module = get_module_object_and_name(globals_for_frame) + if module is not None and id(module.module) not in disclaim_module_ids: + return module + raise AssertionError('No module was found') + + +def get_calling_module() -> str: + """Returns the name of the module that's calling into this module.""" + return get_calling_module_object_and_name().module_name + + +def create_xml_dom_element( + doc: minidom.Document, name: str, value: Any +) -> minidom.Element: + """Returns an XML DOM element with name and text value. + + Args: + doc: minidom.Document, the DOM document it should create nodes from. + name: str, the tag of XML element. + value: object, whose string representation will be used + as the value of the XML element. Illegal or highly discouraged xml 1.0 + characters are stripped. + + Returns: + An instance of minidom.Element. + """ + s = str(value) + if isinstance(value, bool): + # Display boolean values as the C++ flag library does: no caps. + s = s.lower() + # Remove illegal xml characters. + s = _ILLEGAL_XML_CHARS_REGEX.sub('', s) + + e = doc.createElement(name) + e.appendChild(doc.createTextNode(s)) + return e + + +def get_help_width() -> int: + """Returns the integer width of help lines that is used in TextWrap.""" + if not sys.stdout.isatty() or termios is None or fcntl is None: + return _DEFAULT_HELP_WIDTH + try: + data = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'1234') + columns = struct.unpack('hh', data)[1] + # Emacs mode returns 0. + # Here we assume that any value below 40 is unreasonable. + if columns >= _MIN_HELP_WIDTH: + return columns + # Returning an int as default is fine, int(int) just return the int. + return int(os.getenv('COLUMNS', _DEFAULT_HELP_WIDTH)) + + except (TypeError, OSError, struct.error): + return _DEFAULT_HELP_WIDTH + + +def get_flag_suggestions( + attempt: str, longopt_list: Sequence[str] +) -> List[str]: + """Returns helpful similar matches for an invalid flag.""" + # Don't suggest on very short strings, or if no longopts are specified. + if len(attempt) <= 2 or not longopt_list: + return [] + + option_names = [v.split('=')[0] for v in longopt_list] + + # Find close approximations in flag prefixes. + # This also handles the case where the flag is spelled right but ambiguous. + distances = [(_damerau_levenshtein(attempt, option[0:len(attempt)]), option) + for option in option_names] + # t[0] is distance, and sorting by t[1] allows us to have stable output. + distances.sort() + + least_errors, _ = distances[0] + # Don't suggest excessively bad matches. + if least_errors >= _SUGGESTION_ERROR_RATE_THRESHOLD * len(attempt): + return [] + + suggestions = [] + for errors, name in distances: + if errors == least_errors: + suggestions.append(name) + else: + break + return suggestions + + +def _damerau_levenshtein(a, b): + """Returns Damerau-Levenshtein edit distance from a to b.""" + memo = {} + + def distance(x, y): + """Recursively defined string distance with memoization.""" + if (x, y) in memo: + return memo[x, y] + if not x: + d = len(y) + elif not y: + d = len(x) + else: + d = min( + distance(x[1:], y) + 1, # correct an insertion error + distance(x, y[1:]) + 1, # correct a deletion error + distance(x[1:], y[1:]) + (x[0] != y[0])) # correct a wrong character + if len(x) >= 2 and len(y) >= 2 and x[0] == y[1] and x[1] == y[0]: + # Correct a transposition. + t = distance(x[2:], y[2:]) + 1 + if d > t: + d = t + + memo[x, y] = d + return d + return distance(a, b) + + +def text_wrap( + text: str, + length: Optional[int] = None, + indent: str = '', + firstline_indent: Optional[str] = None, +) -> str: + """Wraps a given text to a maximum line length and returns it. + + It turns lines that only contain whitespace into empty lines, keeps new lines, + and expands tabs using 4 spaces. + + Args: + text: str, text to wrap. + length: int, maximum length of a line, includes indentation. + If this is None then use get_help_width() + indent: str, indent for all but first line. + firstline_indent: str, indent for first line; if None, fall back to indent. + + Returns: + str, the wrapped text. + + Raises: + ValueError: Raised if indent or firstline_indent not shorter than length. + """ + # Get defaults where callee used None + if length is None: + length = get_help_width() + if indent is None: + indent = '' + if firstline_indent is None: + firstline_indent = indent + + if len(indent) >= length: + raise ValueError('Length of indent exceeds length') + if len(firstline_indent) >= length: + raise ValueError('Length of first line indent exceeds length') + + text = text.expandtabs(4) + + result = [] + # Create one wrapper for the first paragraph and one for subsequent + # paragraphs that does not have the initial wrapping. + wrapper = textwrap.TextWrapper( + width=length, initial_indent=firstline_indent, subsequent_indent=indent) + subsequent_wrapper = textwrap.TextWrapper( + width=length, initial_indent=indent, subsequent_indent=indent) + + # textwrap does not have any special treatment for newlines. From the docs: + # "...newlines may appear in the middle of a line and cause strange output. + # For this reason, text should be split into paragraphs (using + # str.splitlines() or similar) which are wrapped separately." + for paragraph in (p.strip() for p in text.splitlines()): + if paragraph: + result.extend(wrapper.wrap(paragraph)) + else: + result.append('') # Keep empty lines. + # Replace initial wrapper with wrapper for subsequent paragraphs. + wrapper = subsequent_wrapper + + return '\n'.join(result) + + +def flag_dict_to_args( + flag_map: Dict[str, Any], multi_flags: Optional[Set[str]] = None +) -> Iterable[str]: + """Convert a dict of values into process call parameters. + + This method is used to convert a dictionary into a sequence of parameters + for a binary that parses arguments using this module. + + Args: + flag_map: dict, a mapping where the keys are flag names (strings). + values are treated according to their type: + + * If value is ``None``, then only the name is emitted. + * If value is ``True``, then only the name is emitted. + * If value is ``False``, then only the name prepended with 'no' is + emitted. + * If value is a string then ``--name=value`` is emitted. + * If value is a collection, this will emit + ``--name=value1,value2,value3``, unless the flag name is in + ``multi_flags``, in which case this will emit + ``--name=value1 --name=value2 --name=value3``. + * Everything else is converted to string an passed as such. + + multi_flags: set, names (strings) of flags that should be treated as + multi-flags. + Yields: + sequence of string suitable for a subprocess execution. + """ + for key, value in flag_map.items(): + if value is None: + yield '--%s' % key + elif isinstance(value, bool): + if value: + yield '--%s' % key + else: + yield '--no%s' % key + elif isinstance(value, (bytes, str)): + # We don't want strings to be handled like python collections. + yield '--%s=%s' % (key, value) # type: ignore[str-bytes-safe] + else: + # Now we attempt to deal with collections. + try: + if multi_flags and key in multi_flags: + for item in value: + yield '--%s=%s' % (key, str(item)) + else: + yield '--%s=%s' % (key, ','.join(str(item) for item in value)) + except TypeError: + # Default case. + yield '--%s=%s' % (key, value) + + +def trim_docstring(docstring: str) -> str: + """Removes indentation from triple-quoted strings. + + This is the function specified in PEP 257 to handle docstrings: + https://www.python.org/dev/peps/pep-0257/. + + Args: + docstring: str, a python docstring. + + Returns: + str, docstring with indentation removed. + """ + if not docstring: + return '' + + # If you've got a line longer than this you have other problems... + max_indent = 1 << 29 + + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + + # Determine minimum indentation (first line doesn't count): + indent = max_indent + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < max_indent: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return '\n'.join(trimmed) + + +def doc_to_help(doc: str) -> str: + """Takes a __doc__ string and reformats it as help.""" + + # Get rid of starting and ending white space. Using lstrip() or even + # strip() could drop more than maximum of first line and right space + # of last line. + doc = doc.strip() + + # Get rid of all empty lines. + whitespace_only_line = re.compile('^[ \t]+$', re.M) + doc = whitespace_only_line.sub('', doc) + + # Cut out common space at line beginnings. + doc = trim_docstring(doc) + + # Just like this module's comment, comments tend to be aligned somehow. + # In other words they all start with the same amount of white space. + # 1) keep double new lines; + # 2) keep ws after new lines if not empty line; + # 3) all other new lines shall be changed to a space; + # Solution: Match new lines between non white space and replace with space. + doc = re.sub(r'(?<=\S)\n(?=\S)', ' ', doc, flags=re.M) + + return doc diff --git a/lib/python3.10/site-packages/absl/flags/_validators.py b/lib/python3.10/site-packages/absl/flags/_validators.py new file mode 100644 index 0000000000000000000000000000000000000000..d4a937acd6184a02366c11384f7e8421cdbc1af1 --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_validators.py @@ -0,0 +1,353 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to enforce different constraints on flags. + +Flags validators can be registered using following functions / decorators:: + + flags.register_validator + @flags.validator + flags.register_multi_flags_validator + @flags.multi_flags_validator + +Three convenience functions are also provided for common flag constraints:: + + flags.mark_flag_as_required + flags.mark_flags_as_required + flags.mark_flags_as_mutual_exclusive + flags.mark_bool_flags_as_mutual_exclusive + +See their docstring in this module for a usage manual. + +Do NOT import this module directly. Import the flags package and use the +aliases defined at the package level instead. +""" + +import warnings + +from absl.flags import _exceptions +from absl.flags import _flagvalues +from absl.flags import _validators_classes + + +def register_validator(flag_name, + checker, + message='Flag validation failed', + flag_values=_flagvalues.FLAGS): + """Adds a constraint, which will be enforced during program execution. + + The constraint is validated when flags are initially parsed, and after each + change of the corresponding flag's value. + + Args: + flag_name: str | FlagHolder, name or holder of the flag to be checked. + Positional-only parameter. + checker: callable, a function to validate the flag. + + * input - A single positional argument: The value of the corresponding + flag (string, boolean, etc. This value will be passed to checker + by the library). + * output - bool, True if validator constraint is satisfied. + If constraint is not satisfied, it should either ``return False`` or + ``raise flags.ValidationError(desired_error_message)``. + + message: str, error text to be shown to the user if checker returns False. + If checker raises flags.ValidationError, message from the raised + error will be shown. + flag_values: flags.FlagValues, optional FlagValues instance to validate + against. + + Raises: + AttributeError: Raised when flag_name is not registered as a valid flag + name. + ValueError: Raised when flag_values is non-default and does not match the + FlagValues of the provided FlagHolder instance. + """ + flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values) + v = _validators_classes.SingleFlagValidator(flag_name, checker, message) + _add_validator(flag_values, v) + + +def validator(flag_name, message='Flag validation failed', + flag_values=_flagvalues.FLAGS): + """A function decorator for defining a flag validator. + + Registers the decorated function as a validator for flag_name, e.g.:: + + @flags.validator('foo') + def _CheckFoo(foo): + ... + + See :func:`register_validator` for the specification of checker function. + + Args: + flag_name: str | FlagHolder, name or holder of the flag to be checked. + Positional-only parameter. + message: str, error text to be shown to the user if checker returns False. + If checker raises flags.ValidationError, message from the raised + error will be shown. + flag_values: flags.FlagValues, optional FlagValues instance to validate + against. + Returns: + A function decorator that registers its function argument as a validator. + Raises: + AttributeError: Raised when flag_name is not registered as a valid flag + name. + """ + + def decorate(function): + register_validator(flag_name, function, + message=message, + flag_values=flag_values) + return function + return decorate + + +def register_multi_flags_validator(flag_names, + multi_flags_checker, + message='Flags validation failed', + flag_values=_flagvalues.FLAGS): + """Adds a constraint to multiple flags. + + The constraint is validated when flags are initially parsed, and after each + change of the corresponding flag's value. + + Args: + flag_names: [str | FlagHolder], a list of the flag names or holders to be + checked. Positional-only parameter. + multi_flags_checker: callable, a function to validate the flag. + + * input - dict, with keys() being flag_names, and value for each key + being the value of the corresponding flag (string, boolean, etc). + * output - bool, True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise flags.ValidationError. + + message: str, error text to be shown to the user if checker returns False. + If checker raises flags.ValidationError, message from the raised + error will be shown. + flag_values: flags.FlagValues, optional FlagValues instance to validate + against. + + Raises: + AttributeError: Raised when a flag is not registered as a valid flag name. + ValueError: Raised when multiple FlagValues are used in the same + invocation. This can occur when FlagHolders have different `_flagvalues` + or when str-type flag_names entries are present and the `flag_values` + argument does not match that of provided FlagHolder(s). + """ + flag_names, flag_values = _flagvalues.resolve_flag_refs( + flag_names, flag_values) + v = _validators_classes.MultiFlagsValidator( + flag_names, multi_flags_checker, message) + _add_validator(flag_values, v) + + +def multi_flags_validator(flag_names, + message='Flag validation failed', + flag_values=_flagvalues.FLAGS): + """A function decorator for defining a multi-flag validator. + + Registers the decorated function as a validator for flag_names, e.g.:: + + @flags.multi_flags_validator(['foo', 'bar']) + def _CheckFooBar(flags_dict): + ... + + See :func:`register_multi_flags_validator` for the specification of checker + function. + + Args: + flag_names: [str | FlagHolder], a list of the flag names or holders to be + checked. Positional-only parameter. + message: str, error text to be shown to the user if checker returns False. + If checker raises flags.ValidationError, message from the raised + error will be shown. + flag_values: flags.FlagValues, optional FlagValues instance to validate + against. + + Returns: + A function decorator that registers its function argument as a validator. + + Raises: + AttributeError: Raised when a flag is not registered as a valid flag name. + """ + + def decorate(function): + register_multi_flags_validator(flag_names, + function, + message=message, + flag_values=flag_values) + return function + + return decorate + + +def mark_flag_as_required(flag_name, flag_values=_flagvalues.FLAGS): + """Ensures that flag is not None during program execution. + + Registers a flag validator, which will follow usual validator rules. + Important note: validator will pass for any non-``None`` value, such as + ``False``, ``0`` (zero), ``''`` (empty string) and so on. + + If your module might be imported by others, and you only wish to make the flag + required when the module is directly executed, call this method like this:: + + if __name__ == '__main__': + flags.mark_flag_as_required('your_flag_name') + app.run() + + Args: + flag_name: str | FlagHolder, name or holder of the flag. + Positional-only parameter. + flag_values: flags.FlagValues, optional :class:`~absl.flags.FlagValues` + instance where the flag is defined. + Raises: + AttributeError: Raised when flag_name is not registered as a valid flag + name. + ValueError: Raised when flag_values is non-default and does not match the + FlagValues of the provided FlagHolder instance. + """ + flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values) + if flag_values[flag_name].default is not None: + warnings.warn( + 'Flag --%s has a non-None default value; therefore, ' + 'mark_flag_as_required will pass even if flag is not specified in the ' + 'command line!' % flag_name, + stacklevel=2) + register_validator( + flag_name, + lambda value: value is not None, + message=f'Flag --{flag_name} must have a value other than None.', + flag_values=flag_values, + ) + + +def mark_flags_as_required(flag_names, flag_values=_flagvalues.FLAGS): + """Ensures that flags are not None during program execution. + + If your module might be imported by others, and you only wish to make the flag + required when the module is directly executed, call this method like this:: + + if __name__ == '__main__': + flags.mark_flags_as_required(['flag1', 'flag2', 'flag3']) + app.run() + + Args: + flag_names: Sequence[str | FlagHolder], names or holders of the flags. + flag_values: flags.FlagValues, optional FlagValues instance where the flags + are defined. + Raises: + AttributeError: If any of flag name has not already been defined as a flag. + """ + for flag_name in flag_names: + mark_flag_as_required(flag_name, flag_values) + + +def mark_flags_as_mutual_exclusive(flag_names, required=False, + flag_values=_flagvalues.FLAGS): + """Ensures that only one flag among flag_names is not None. + + Important note: This validator checks if flag values are ``None``, and it does + not distinguish between default and explicit values. Therefore, this validator + does not make sense when applied to flags with default values other than None, + including other false values (e.g. ``False``, ``0``, ``''``, ``[]``). That + includes multi flags with a default value of ``[]`` instead of None. + + Args: + flag_names: [str | FlagHolder], names or holders of flags. + Positional-only parameter. + required: bool. If true, exactly one of the flags must have a value other + than None. Otherwise, at most one of the flags can have a value other + than None, and it is valid for all of the flags to be None. + flag_values: flags.FlagValues, optional FlagValues instance where the flags + are defined. + + Raises: + ValueError: Raised when multiple FlagValues are used in the same + invocation. This can occur when FlagHolders have different `_flagvalues` + or when str-type flag_names entries are present and the `flag_values` + argument does not match that of provided FlagHolder(s). + """ + flag_names, flag_values = _flagvalues.resolve_flag_refs( + flag_names, flag_values) + for flag_name in flag_names: + if flag_values[flag_name].default is not None: + warnings.warn( + 'Flag --{} has a non-None default value. That does not make sense ' + 'with mark_flags_as_mutual_exclusive, which checks whether the ' + 'listed flags have a value other than None.'.format(flag_name), + stacklevel=2) + + def validate_mutual_exclusion(flags_dict): + flag_count = sum(1 for val in flags_dict.values() if val is not None) + if flag_count == 1 or (not required and flag_count == 0): + return True + raise _exceptions.ValidationError( + '{} one of ({}) must have a value other than None.'.format( + 'Exactly' if required else 'At most', ', '.join(flag_names))) + + register_multi_flags_validator( + flag_names, validate_mutual_exclusion, flag_values=flag_values) + + +def mark_bool_flags_as_mutual_exclusive(flag_names, required=False, + flag_values=_flagvalues.FLAGS): + """Ensures that only one flag among flag_names is True. + + Args: + flag_names: [str | FlagHolder], names or holders of flags. + Positional-only parameter. + required: bool. If true, exactly one flag must be True. Otherwise, at most + one flag can be True, and it is valid for all flags to be False. + flag_values: flags.FlagValues, optional FlagValues instance where the flags + are defined. + + Raises: + ValueError: Raised when multiple FlagValues are used in the same + invocation. This can occur when FlagHolders have different `_flagvalues` + or when str-type flag_names entries are present and the `flag_values` + argument does not match that of provided FlagHolder(s). + """ + flag_names, flag_values = _flagvalues.resolve_flag_refs( + flag_names, flag_values) + for flag_name in flag_names: + if not flag_values[flag_name].boolean: + raise _exceptions.ValidationError( + 'Flag --{} is not Boolean, which is required for flags used in ' + 'mark_bool_flags_as_mutual_exclusive.'.format(flag_name)) + + def validate_boolean_mutual_exclusion(flags_dict): + flag_count = sum(bool(val) for val in flags_dict.values()) + if flag_count == 1 or (not required and flag_count == 0): + return True + raise _exceptions.ValidationError( + '{} one of ({}) must be True.'.format( + 'Exactly' if required else 'At most', ', '.join(flag_names))) + + register_multi_flags_validator( + flag_names, validate_boolean_mutual_exclusion, flag_values=flag_values) + + +def _add_validator(fv, validator_instance): + """Register new flags validator to be checked. + + Args: + fv: flags.FlagValues, the FlagValues instance to add the validator. + validator_instance: validators.Validator, the validator to add. + Raises: + KeyError: Raised when validators work with a non-existing flag. + """ + for flag_name in validator_instance.get_flags_names(): + fv[flag_name].validators.append(validator_instance) diff --git a/lib/python3.10/site-packages/absl/flags/_validators_classes.py b/lib/python3.10/site-packages/absl/flags/_validators_classes.py new file mode 100644 index 0000000000000000000000000000000000000000..cf978bfd1bf837895310ae0b5e1b3eed90fef375 --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/_validators_classes.py @@ -0,0 +1,172 @@ +# Copyright 2021 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Defines *private* classes used for flag validators. + +Do NOT import this module. DO NOT use anything from this module. They are +private APIs. +""" + +from absl.flags import _exceptions + + +class Validator: + """Base class for flags validators. + + Users should NOT overload these classes, and use flags.Register... + methods instead. + """ + + # Used to assign each validator an unique insertion_index + validators_count = 0 + + def __init__(self, checker, message): + """Constructor to create all validators. + + Args: + checker: function to verify the constraint. + Input of this method varies, see SingleFlagValidator and + multi_flags_validator for a detailed description. + message: str, error message to be shown to the user. + """ + self.checker = checker + self.message = message + Validator.validators_count += 1 + # Used to assert validators in the order they were registered. + self.insertion_index = Validator.validators_count + + def verify(self, flag_values): + """Verifies that constraint is satisfied. + + flags library calls this method to verify Validator's constraint. + + Args: + flag_values: flags.FlagValues, the FlagValues instance to get flags from. + Raises: + Error: Raised if constraint is not satisfied. + """ + param = self._get_input_to_checker_function(flag_values) + if not self.checker(param): + raise _exceptions.ValidationError(self.message) + + def get_flags_names(self): + """Returns the names of the flags checked by this validator. + + Returns: + [string], names of the flags. + """ + raise NotImplementedError('This method should be overloaded') + + def print_flags_with_values(self, flag_values): + raise NotImplementedError('This method should be overloaded') + + def _get_input_to_checker_function(self, flag_values): + """Given flag values, returns the input to be given to checker. + + Args: + flag_values: flags.FlagValues, containing all flags. + Returns: + The input to be given to checker. The return type depends on the specific + validator. + """ + raise NotImplementedError('This method should be overloaded') + + +class SingleFlagValidator(Validator): + """Validator behind register_validator() method. + + Validates that a single flag passes its checker function. The checker function + takes the flag value and returns True (if value looks fine) or, if flag value + is not valid, either returns False or raises an Exception. + """ + + def __init__(self, flag_name, checker, message): + """Constructor. + + Args: + flag_name: string, name of the flag. + checker: function to verify the validator. + input - value of the corresponding flag (string, boolean, etc). + output - bool, True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise flags.ValidationError(desired_error_message). + message: str, error message to be shown to the user if validator's + condition is not satisfied. + """ + super().__init__(checker, message) + self.flag_name = flag_name + + def get_flags_names(self): + return [self.flag_name] + + def print_flags_with_values(self, flag_values): + return 'flag --%s=%s' % (self.flag_name, flag_values[self.flag_name].value) + + def _get_input_to_checker_function(self, flag_values): + """Given flag values, returns the input to be given to checker. + + Args: + flag_values: flags.FlagValues, the FlagValues instance to get flags from. + Returns: + object, the input to be given to checker. + """ + return flag_values[self.flag_name].value + + +class MultiFlagsValidator(Validator): + """Validator behind register_multi_flags_validator method. + + Validates that flag values pass their common checker function. The checker + function takes flag values and returns True (if values look fine) or, + if values are not valid, either returns False or raises an Exception. + """ + + def __init__(self, flag_names, checker, message): + """Constructor. + + Args: + flag_names: [str], containing names of the flags used by checker. + checker: function to verify the validator. + input - dict, with keys() being flag_names, and value for each + key being the value of the corresponding flag (string, boolean, + etc). + output - bool, True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise flags.ValidationError(desired_error_message). + message: str, error message to be shown to the user if validator's + condition is not satisfied + """ + super().__init__(checker, message) + self.flag_names = flag_names + + def _get_input_to_checker_function(self, flag_values): + """Given flag values, returns the input to be given to checker. + + Args: + flag_values: flags.FlagValues, the FlagValues instance to get flags from. + Returns: + dict, with keys() being self.flag_names, and value for each key + being the value of the corresponding flag (string, boolean, etc). + """ + return {key: flag_values[key].value for key in self.flag_names} + + def print_flags_with_values(self, flag_values): + prefix = 'flags ' + flags_with_values = [] + for key in self.flag_names: + flags_with_values.append('%s=%s' % (key, flag_values[key].value)) + return prefix + ', '.join(flags_with_values) + + def get_flags_names(self): + return self.flag_names diff --git a/lib/python3.10/site-packages/absl/flags/argparse_flags.py b/lib/python3.10/site-packages/absl/flags/argparse_flags.py new file mode 100644 index 0000000000000000000000000000000000000000..9f77690f10c1e0fc9619b857ba6169636f30e6c3 --- /dev/null +++ b/lib/python3.10/site-packages/absl/flags/argparse_flags.py @@ -0,0 +1,390 @@ +# Copyright 2018 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module provides argparse integration with absl.flags. + +``argparse_flags.ArgumentParser`` is a drop-in replacement for +:class:`argparse.ArgumentParser`. It takes care of collecting and defining absl +flags in :mod:`argparse`. + +Here is a simple example:: + + # Assume the following absl.flags is defined in another module: + # + # from absl import flags + # flags.DEFINE_string('echo', None, 'The echo message.') + # + parser = argparse_flags.ArgumentParser( + description='A demo of absl.flags and argparse integration.') + parser.add_argument('--header', help='Header message to print.') + + # The parser will also accept the absl flag `--echo`. + # The `header` value is available as `args.header` just like a regular + # argparse flag. The absl flag `--echo` continues to be available via + # `absl.flags.FLAGS` if you want to access it. + args = parser.parse_args() + + # Example usages: + # ./program --echo='A message.' --header='A header' + # ./program --header 'A header' --echo 'A message.' + + +Here is another example demonstrates subparsers:: + + parser = argparse_flags.ArgumentParser(description='A subcommands demo.') + parser.add_argument('--header', help='The header message to print.') + + subparsers = parser.add_subparsers(help='The command to execute.') + + roll_dice_parser = subparsers.add_parser( + 'roll_dice', help='Roll a dice.', + # By default, absl flags can also be specified after the sub-command. + # To only allow them before sub-command, pass + # `inherited_absl_flags=None`. + inherited_absl_flags=None) + roll_dice_parser.add_argument('--num_faces', type=int, default=6) + roll_dice_parser.set_defaults(command=roll_dice) + + shuffle_parser = subparsers.add_parser('shuffle', help='Shuffle inputs.') + shuffle_parser.add_argument( + 'inputs', metavar='I', nargs='+', help='Inputs to shuffle.') + shuffle_parser.set_defaults(command=shuffle) + + args = parser.parse_args(argv[1:]) + args.command(args) + + # Example usages: + # ./program --echo='A message.' roll_dice --num_faces=6 + # ./program shuffle --echo='A message.' 1 2 3 4 + + +There are several differences between :mod:`absl.flags` and +:mod:`~absl.flags.argparse_flags`: + +1. Flags defined with absl.flags are parsed differently when using the + argparse parser. Notably: + + 1) absl.flags allows both single-dash and double-dash for any flag, and + doesn't distinguish them; argparse_flags only allows double-dash for + flag's regular name, and single-dash for flag's ``short_name``. + 2) Boolean flags in absl.flags can be specified with ``--bool``, + ``--nobool``, as well as ``--bool=true/false`` (though not recommended); + in argparse_flags, it only allows ``--bool``, ``--nobool``. + +2. Help related flag differences: + + 1) absl.flags does not define help flags, absl.app does that; argparse_flags + defines help flags unless passed with ``add_help=False``. + 2) absl.app supports ``--helpxml``; argparse_flags does not. + 3) argparse_flags supports ``-h``; absl.app does not. +""" + +import argparse +import sys + +from absl import flags + + +_BUILT_IN_FLAGS = frozenset({ + 'help', + 'helpshort', + 'helpfull', + 'helpxml', + 'flagfile', + 'undefok', +}) + + +class ArgumentParser(argparse.ArgumentParser): + """Custom ArgumentParser class to support special absl flags.""" + + def __init__(self, **kwargs): + """Initializes ArgumentParser. + + Args: + **kwargs: same as argparse.ArgumentParser, except: + 1. It also accepts `inherited_absl_flags`: the absl flags to inherit. + The default is the global absl.flags.FLAGS instance. Pass None to + ignore absl flags. + 2. The `prefix_chars` argument must be the default value '-'. + + Raises: + ValueError: Raised when prefix_chars is not '-'. + """ + prefix_chars = kwargs.get('prefix_chars', '-') + if prefix_chars != '-': + raise ValueError( + 'argparse_flags.ArgumentParser only supports "-" as the prefix ' + 'character, found "{}".'.format(prefix_chars)) + + # Remove inherited_absl_flags before calling super. + self._inherited_absl_flags = kwargs.pop('inherited_absl_flags', flags.FLAGS) + # Now call super to initialize argparse.ArgumentParser before calling + # add_argument in _define_absl_flags. + super().__init__(**kwargs) + + if self.add_help: + # -h and --help are defined in super. + # Also add the --helpshort and --helpfull flags. + self.add_argument( + # Action 'help' defines a similar flag to -h/--help. + '--helpshort', action='help', + default=argparse.SUPPRESS, help=argparse.SUPPRESS) + self.add_argument( + '--helpfull', action=_HelpFullAction, + default=argparse.SUPPRESS, help='show full help message and exit') + + if self._inherited_absl_flags is not None: + self.add_argument( + '--undefok', default=argparse.SUPPRESS, help=argparse.SUPPRESS) + self._define_absl_flags(self._inherited_absl_flags) + + def parse_known_args(self, args=None, namespace=None): + if args is None: + args = sys.argv[1:] + if self._inherited_absl_flags is not None: + # Handle --flagfile. + # Explicitly specify force_gnu=True, since argparse behaves like + # gnu_getopt: flags can be specified after positional arguments. + args = self._inherited_absl_flags.read_flags_from_files( + args, force_gnu=True) + + undefok_missing = object() + undefok = getattr(namespace, 'undefok', undefok_missing) + + namespace, args = super().parse_known_args(args, namespace) + + # For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where + # sub-parsers don't preserve existing namespace attributes. + # Restore the undefok attribute if a sub-parser dropped it. + if undefok is not undefok_missing: + namespace.undefok = undefok + + if self._inherited_absl_flags is not None: + # Handle --undefok. At this point, `args` only contains unknown flags, + # so it won't strip defined flags that are also specified with --undefok. + # For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where + # sub-parsers don't preserve existing namespace attributes. The undefok + # attribute might not exist because a subparser dropped it. + if hasattr(namespace, 'undefok'): + args = _strip_undefok_args(namespace.undefok, args) + # absl flags are not exposed in the Namespace object. See Namespace: + # https://docs.python.org/3/library/argparse.html#argparse.Namespace. + del namespace.undefok + self._inherited_absl_flags.mark_as_parsed() + try: + self._inherited_absl_flags.validate_all_flags() + except flags.IllegalFlagValueError as e: + self.error(str(e)) + + return namespace, args + + def _define_absl_flags(self, absl_flags): + """Defines flags from absl_flags.""" + key_flags = set(absl_flags.get_key_flags_for_module(sys.argv[0])) + for name in absl_flags: + if name in _BUILT_IN_FLAGS: + # Do not inherit built-in flags. + continue + flag_instance = absl_flags[name] + # Each flags with short_name appears in FLAGS twice, so only define + # when the dictionary key is equal to the regular name. + if name == flag_instance.name: + # Suppress the flag in the help short message if it's not a main + # module's key flag. + suppress = flag_instance not in key_flags + self._define_absl_flag(flag_instance, suppress) + + def _define_absl_flag(self, flag_instance, suppress): + """Defines a flag from the flag_instance.""" + flag_name = flag_instance.name + short_name = flag_instance.short_name + argument_names = ['--' + flag_name] + if short_name: + argument_names.insert(0, '-' + short_name) + if suppress: + helptext = argparse.SUPPRESS + else: + # argparse help string uses %-formatting. Escape the literal %'s. + helptext = flag_instance.help.replace('%', '%%') + if flag_instance.boolean: + # Only add the `no` form to the long name. + argument_names.append('--no' + flag_name) + self.add_argument( + *argument_names, action=_BooleanFlagAction, help=helptext, + metavar=flag_instance.name.upper(), + flag_instance=flag_instance) + else: + self.add_argument( + *argument_names, action=_FlagAction, help=helptext, + metavar=flag_instance.name.upper(), + flag_instance=flag_instance) + + +class _FlagAction(argparse.Action): + """Action class for Abseil non-boolean flags.""" + + def __init__( + self, + option_strings, + dest, + help, # pylint: disable=redefined-builtin + metavar, + flag_instance, + default=argparse.SUPPRESS): + """Initializes _FlagAction. + + Args: + option_strings: See argparse.Action. + dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. + help: See argparse.Action. + metavar: See argparse.Action. + flag_instance: absl.flags.Flag, the absl flag instance. + default: Ignored. The flag always uses dest=argparse.SUPPRESS so it + doesn't affect the parsing result. + """ + del dest + self._flag_instance = flag_instance + super().__init__( + option_strings=option_strings, + dest=argparse.SUPPRESS, + help=help, + metavar=metavar, + ) + + def __call__(self, parser, namespace, values, option_string=None): + """See https://docs.python.org/3/library/argparse.html#action-classes.""" + self._flag_instance.parse(values) + self._flag_instance.using_default_value = False + + +class _BooleanFlagAction(argparse.Action): + """Action class for Abseil boolean flags.""" + + def __init__( + self, + option_strings, + dest, + help, # pylint: disable=redefined-builtin + metavar, + flag_instance, + default=argparse.SUPPRESS): + """Initializes _BooleanFlagAction. + + Args: + option_strings: See argparse.Action. + dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. + help: See argparse.Action. + metavar: See argparse.Action. + flag_instance: absl.flags.Flag, the absl flag instance. + default: Ignored. The flag always uses dest=argparse.SUPPRESS so it + doesn't affect the parsing result. + """ + del dest, default + self._flag_instance = flag_instance + flag_names = [self._flag_instance.name] + if self._flag_instance.short_name: + flag_names.append(self._flag_instance.short_name) + self._flag_names = frozenset(flag_names) + super().__init__( + option_strings=option_strings, + dest=argparse.SUPPRESS, + nargs=0, # Does not accept values, only `--bool` or `--nobool`. + help=help, + metavar=metavar, + ) + + def __call__(self, parser, namespace, values, option_string=None): + """See https://docs.python.org/3/library/argparse.html#action-classes.""" + if not isinstance(values, list) or values: + raise ValueError('values must be an empty list.') + if option_string.startswith('--'): + option = option_string[2:] + else: + option = option_string[1:] + if option in self._flag_names: + self._flag_instance.parse('true') + else: + if not option.startswith('no') or option[2:] not in self._flag_names: + raise ValueError('invalid option_string: ' + option_string) + self._flag_instance.parse('false') + self._flag_instance.using_default_value = False + + +class _HelpFullAction(argparse.Action): + """Action class for --helpfull flag.""" + + def __init__(self, option_strings, dest, default, help): # pylint: disable=redefined-builtin + """Initializes _HelpFullAction. + + Args: + option_strings: See argparse.Action. + dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS. + default: Ignored. + help: See argparse.Action. + """ + del dest, default + super().__init__( + option_strings=option_strings, + dest=argparse.SUPPRESS, + default=argparse.SUPPRESS, + nargs=0, + help=help, + ) + + def __call__(self, parser, namespace, values, option_string=None): + """See https://docs.python.org/3/library/argparse.html#action-classes.""" + # This only prints flags when help is not argparse.SUPPRESS. + # It includes user defined argparse flags, as well as main module's + # key absl flags. Other absl flags use argparse.SUPPRESS, so they aren't + # printed here. + parser.print_help() + + absl_flags = parser._inherited_absl_flags # pylint: disable=protected-access + if absl_flags is not None: + modules = sorted(absl_flags.flags_by_module_dict()) + main_module = sys.argv[0] + if main_module in modules: + # The main module flags are already printed in parser.print_help(). + modules.remove(main_module) + print(absl_flags._get_help_for_modules( # pylint: disable=protected-access + modules, prefix='', include_special_flags=True)) + parser.exit() + + +def _strip_undefok_args(undefok, args): + """Returns a new list of args after removing flags in --undefok.""" + if undefok: + undefok_names = {name.strip() for name in undefok.split(',')} + undefok_names |= {'no' + name for name in undefok_names} + # Remove undefok flags. + args = [arg for arg in args if not _is_undefok(arg, undefok_names)] + return args + + +def _is_undefok(arg, undefok_names): + """Returns whether we can ignore arg based on a set of undefok flag names.""" + if not arg.startswith('-'): + return False + if arg.startswith('--'): + arg_without_dash = arg[2:] + else: + arg_without_dash = arg[1:] + if '=' in arg_without_dash: + name, _ = arg_without_dash.split('=', 1) + else: + name = arg_without_dash + if name in undefok_names: + return True + return False diff --git a/lib/python3.10/site-packages/absl/logging/__init__.py b/lib/python3.10/site-packages/absl/logging/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b0920d831ed10c2374cb0e1b47e8f7dc04d61ccb --- /dev/null +++ b/lib/python3.10/site-packages/absl/logging/__init__.py @@ -0,0 +1,1331 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Abseil Python logging module implemented on top of standard logging. + +Simple usage:: + + from absl import logging + + logging.info('Interesting Stuff') + logging.info('Interesting Stuff with Arguments: %d', 42) + + logging.set_verbosity(logging.INFO) + logging.log(logging.DEBUG, 'This will *not* be printed') + logging.set_verbosity(logging.DEBUG) + logging.log(logging.DEBUG, 'This will be printed') + + logging.warning('Worrying Stuff') + logging.error('Alarming Stuff') + logging.fatal('AAAAHHHHH!!!!') # Process exits. + +Usage note: Do not pre-format the strings in your program code. +Instead, let the logging module perform argument interpolation. +This saves cycles because strings that don't need to be printed +are never formatted. Note that this module does not attempt to +interpolate arguments when no arguments are given. In other words:: + + logging.info('Interesting Stuff: %s') + +does not raise an exception because logging.info() has only one +argument, the message string. + +"Lazy" evaluation for debugging +------------------------------- + +If you do something like this:: + + logging.debug('Thing: %s', thing.ExpensiveOp()) + +then the ExpensiveOp will be evaluated even if nothing +is printed to the log. To avoid this, use the level_debug() function:: + + if logging.level_debug(): + logging.debug('Thing: %s', thing.ExpensiveOp()) + +Per file level logging is supported by logging.vlog() and +logging.vlog_is_on(). For example:: + + if logging.vlog_is_on(2): + logging.vlog(2, very_expensive_debug_message()) + +Notes on Unicode +---------------- + +The log output is encoded as UTF-8. Don't pass data in other encodings in +bytes() instances -- instead pass unicode string instances when you need to +(for both the format string and arguments). + +Note on critical and fatal: +Standard logging module defines fatal as an alias to critical, but it's not +documented, and it does NOT actually terminate the program. +This module only defines fatal but not critical, and it DOES terminate the +program. + +The differences in behavior are historical and unfortunate. +""" + +import collections +from collections import abc +import getpass +import io +import inspect +import itertools +import logging +import os +import socket +import struct +import sys +import tempfile +import threading +import time +import timeit +import traceback +import types +import warnings + +from absl import flags +from absl.logging import converter + +# pylint: disable=g-import-not-at-top +try: + from typing import NoReturn +except ImportError: + pass + +# pylint: enable=g-import-not-at-top + +FLAGS = flags.FLAGS + + +# Logging levels. +FATAL = converter.ABSL_FATAL +ERROR = converter.ABSL_ERROR +WARNING = converter.ABSL_WARNING +WARN = converter.ABSL_WARNING # Deprecated name. +INFO = converter.ABSL_INFO +DEBUG = converter.ABSL_DEBUG + +# Regex to match/parse log line prefixes. +ABSL_LOGGING_PREFIX_REGEX = ( + r'^(?P[IWEF])' + r'(?P\d\d)(?P\d\d) ' + r'(?P\d\d):(?P\d\d):(?P\d\d)' + r'\.(?P\d\d\d\d\d\d) +' + r'(?P-?\d+) ' + r'(?P[a-zA-Z<][\w._<>-]+):(?P\d+)') + + +# Mask to convert integer thread ids to unsigned quantities for logging purposes +_THREAD_ID_MASK = 2 ** (struct.calcsize('L') * 8) - 1 + +# Extra property set on the LogRecord created by ABSLLogger when its level is +# CRITICAL/FATAL. +_ABSL_LOG_FATAL = '_absl_log_fatal' +# Extra prefix added to the log message when a non-absl logger logs a +# CRITICAL/FATAL message. +_CRITICAL_PREFIX = 'CRITICAL - ' + +# Used by findCaller to skip callers from */logging/__init__.py. +_LOGGING_FILE_PREFIX = os.path.join('logging', '__init__.') + +# The ABSL logger instance, initialized in _initialize(). +_absl_logger = None +# The ABSL handler instance, initialized in _initialize(). +_absl_handler = None + + +_CPP_NAME_TO_LEVELS = { + 'debug': '0', # Abseil C++ has no DEBUG level, mapping it to INFO here. + 'info': '0', + 'warning': '1', + 'warn': '1', + 'error': '2', + 'fatal': '3' +} + +_CPP_LEVEL_TO_NAMES = { + '0': 'info', + '1': 'warning', + '2': 'error', + '3': 'fatal', +} + + +class _VerbosityFlag(flags.Flag): + """Flag class for -v/--verbosity.""" + + def __init__(self, *args, **kwargs): + super().__init__( + flags.IntegerParser(), flags.ArgumentSerializer(), *args, **kwargs + ) + + @property + def value(self): + return self._value + + @value.setter + def value(self, v): + self._value = v + self._update_logging_levels() + + def _update_logging_levels(self): + """Updates absl logging levels to the current verbosity. + + Visibility: module-private + """ + if not _absl_logger: + return + + if self._value <= converter.ABSL_DEBUG: + standard_verbosity = converter.absl_to_standard(self._value) + else: + # --verbosity is set to higher than 1 for vlog. + standard_verbosity = logging.DEBUG - (self._value - 1) + + # Also update root level when absl_handler is used. + if _absl_handler in logging.root.handlers: + # Make absl logger inherit from the root logger. absl logger might have + # a non-NOTSET value if logging.set_verbosity() is called at import time. + _absl_logger.setLevel(logging.NOTSET) + logging.root.setLevel(standard_verbosity) + else: + _absl_logger.setLevel(standard_verbosity) + + +class _LoggerLevelsFlag(flags.Flag): + """Flag class for --logger_levels.""" + + def __init__(self, *args, **kwargs): + super().__init__( + _LoggerLevelsParser(), _LoggerLevelsSerializer(), *args, **kwargs + ) + + @property + def value(self): + # For lack of an immutable type, be defensive and return a copy. + # Modifications to the dict aren't supported and won't have any affect. + # While Py3 could use MappingProxyType, that isn't deepcopy friendly, so + # just return a copy. + return self._value.copy() + + @value.setter + def value(self, v): + self._value = {} if v is None else v + self._update_logger_levels() + + def _update_logger_levels(self): + # Visibility: module-private. + # This is called by absl.app.run() during initialization. + for name, level in self._value.items(): + logging.getLogger(name).setLevel(level) + + +class _LoggerLevelsParser(flags.ArgumentParser): + """Parser for --logger_levels flag.""" + + def parse(self, value): + if isinstance(value, abc.Mapping): + return value + + pairs = [pair.strip() for pair in value.split(',') if pair.strip()] + + # Preserve the order so that serialization is deterministic. + levels = collections.OrderedDict() + for name_level in pairs: + name, level = name_level.split(':', 1) + name = name.strip() + level = level.strip() + levels[name] = level + return levels + + +class _LoggerLevelsSerializer: + """Serializer for --logger_levels flag.""" + + def serialize(self, value): + if isinstance(value, str): + return value + return ','.join(f'{name}:{level}' for name, level in value.items()) + + +class _StderrthresholdFlag(flags.Flag): + """Flag class for --stderrthreshold.""" + + def __init__(self, *args, **kwargs): + super().__init__( + flags.ArgumentParser(), flags.ArgumentSerializer(), *args, **kwargs + ) + + @property + def value(self): + return self._value + + @value.setter + def value(self, v): + if v in _CPP_LEVEL_TO_NAMES: + # --stderrthreshold also accepts numeric strings whose values are + # Abseil C++ log levels. + cpp_value = int(v) + v = _CPP_LEVEL_TO_NAMES[v] # Normalize to strings. + elif v.lower() in _CPP_NAME_TO_LEVELS: + v = v.lower() + if v == 'warn': + v = 'warning' # Use 'warning' as the canonical name. + cpp_value = int(_CPP_NAME_TO_LEVELS[v]) + else: + raise ValueError( + '--stderrthreshold must be one of (case-insensitive) ' + "'debug', 'info', 'warning', 'error', 'fatal', " + "or '0', '1', '2', '3', not '%s'" % v) + + self._value = v + + +LOGTOSTDERR = flags.DEFINE_boolean( + 'logtostderr', + False, + 'Should only log to stderr?', + allow_override_cpp=True, +) +ALSOLOGTOSTDERR = flags.DEFINE_boolean( + 'alsologtostderr', + False, + 'also log to stderr?', + allow_override_cpp=True, +) +LOG_DIR = flags.DEFINE_string( + 'log_dir', + os.getenv('TEST_TMPDIR', ''), + 'directory to write logfiles into', + allow_override_cpp=True, +) +VERBOSITY = flags.DEFINE_flag( + _VerbosityFlag( + 'verbosity', + -1, + ( + 'Logging verbosity level. Messages logged at this level or lower' + ' will be included. Set to 1 for debug logging. If the flag was not' + ' set or supplied, the value will be changed from the default of -1' + ' (warning) to 0 (info) after flags are parsed.' + ), + short_name='v', + allow_hide_cpp=True, + ) +) +LOGGER_LEVELS = flags.DEFINE_flag( + _LoggerLevelsFlag( + 'logger_levels', + {}, + ( + 'Specify log level of loggers. The format is a CSV list of ' + '`name:level`. Where `name` is the logger name used with ' + '`logging.getLogger()`, and `level` is a level name (INFO, DEBUG, ' + 'etc). e.g. `myapp.foo:INFO,other.logger:DEBUG`' + ), + ) +) +STDERRTHRESHOLD = flags.DEFINE_flag( + _StderrthresholdFlag( + 'stderrthreshold', + 'fatal', + ( + 'log messages at this level, or more severe, to stderr in ' + 'addition to the logfile. Possible values are ' + "'debug', 'info', 'warning', 'error', and 'fatal'. " + 'Obsoletes --alsologtostderr. Using --alsologtostderr ' + 'cancels the effect of this flag. Please also note that ' + 'this flag is subject to --verbosity and requires logfile ' + 'not be stderr.' + ), + allow_hide_cpp=True, + ) +) +SHOWPREFIXFORINFO = flags.DEFINE_boolean( + 'showprefixforinfo', + True, + ( + 'If False, do not prepend prefix to info messages ' + "when it's logged to stderr, " + '--verbosity is set to INFO level, ' + 'and python logging is used.' + ), +) + + +def get_verbosity(): + """Returns the logging verbosity.""" + return FLAGS['verbosity'].value + + +def set_verbosity(v): + """Sets the logging verbosity. + + Causes all messages of level <= v to be logged, + and all messages of level > v to be silently discarded. + + Args: + v: int|str, the verbosity level as an integer or string. Legal string values + are those that can be coerced to an integer as well as case-insensitive + 'debug', 'info', 'warning', 'error', and 'fatal'. + """ + try: + new_level = int(v) + except ValueError: + new_level = converter.ABSL_NAMES[v.upper()] + FLAGS.verbosity = new_level + + +def set_stderrthreshold(s): + """Sets the stderr threshold to the value passed in. + + Args: + s: str|int, valid strings values are case-insensitive 'debug', + 'info', 'warning', 'error', and 'fatal'; valid integer values are + logging.DEBUG|INFO|WARNING|ERROR|FATAL. + + Raises: + ValueError: Raised when s is an invalid value. + """ + if s in converter.ABSL_LEVELS: + FLAGS.stderrthreshold = converter.ABSL_LEVELS[s] + elif isinstance(s, str) and s.upper() in converter.ABSL_NAMES: + FLAGS.stderrthreshold = s + else: + raise ValueError( + 'set_stderrthreshold only accepts integer absl logging level ' + 'from -3 to 1, or case-insensitive string values ' + "'debug', 'info', 'warning', 'error', and 'fatal'. " + 'But found "{}" ({}).'.format(s, type(s))) + + +def fatal(msg, *args, **kwargs): + # type: (Any, Any, Any) -> NoReturn + """Logs a fatal message.""" + log(FATAL, msg, *args, **kwargs) + + +def error(msg, *args, **kwargs): + """Logs an error message.""" + log(ERROR, msg, *args, **kwargs) + + +def warning(msg, *args, **kwargs): + """Logs a warning message.""" + log(WARNING, msg, *args, **kwargs) + + +def warn(msg, *args, **kwargs): + """Deprecated, use 'warning' instead.""" + warnings.warn("The 'warn' function is deprecated, use 'warning' instead", + DeprecationWarning, 2) + log(WARNING, msg, *args, **kwargs) + + +def info(msg, *args, **kwargs): + """Logs an info message.""" + log(INFO, msg, *args, **kwargs) + + +def debug(msg, *args, **kwargs): + """Logs a debug message.""" + log(DEBUG, msg, *args, **kwargs) + + +def exception(msg, *args, exc_info=True, **kwargs): + """Logs an exception, with traceback and message.""" + error(msg, *args, exc_info=exc_info, **kwargs) + + +def _fast_stack_trace(): + """A fast stack trace that gets us the minimal information we need. + + Compared to using `get_absl_logger().findCaller(stack_info=True)`, this + function is ~100x faster. + + Returns: + A tuple of tuples of (filename, line_number, last_instruction_offset). + """ + cur_stack = inspect.currentframe() + if cur_stack is None or cur_stack.f_back is None: + return tuple() + # We drop the first frame, which is this function itself. + cur_stack = cur_stack.f_back + call_stack = [] + while cur_stack.f_back: + cur_stack = cur_stack.f_back + call_stack.append( + (cur_stack.f_code.co_filename, cur_stack.f_lineno, cur_stack.f_lasti) + ) + return tuple(call_stack) + + +# Counter to keep track of number of log entries per token. +_log_counter_per_token = {} + + +def _get_next_log_count_per_token(token): + """Wrapper for _log_counter_per_token. Thread-safe. + + Args: + token: The token for which to look up the count. + + Returns: + The number of times this function has been called with + *token* as an argument (starting at 0). + """ + # Can't use a defaultdict because defaultdict isn't atomic, whereas + # setdefault is. + return next(_log_counter_per_token.setdefault(token, itertools.count())) + + +def log_every_n(level, msg, n, *args, use_call_stack=False): + """Logs ``msg % args`` at level 'level' once per 'n' times. + + Logs the 1st call, (N+1)st call, (2N+1)st call, etc. + Not threadsafe. + + Args: + level: int, the absl logging level at which to log. + msg: str, the message to be logged. + n: int, the number of times this should be called before it is logged. + *args: The args to be substituted into the msg. + use_call_stack: bool, whether to include the call stack when counting the + number of times the message is logged. + """ + caller_info = get_absl_logger().findCaller() + if use_call_stack: + # To reduce storage costs, we hash the call stack. + caller_info = (*caller_info[0:3], hash(_fast_stack_trace())) + count = _get_next_log_count_per_token(caller_info) + log_if(level, msg, not (count % n), *args) + + +# Keeps track of the last log time of the given token. +# Note: must be a dict since set/get is atomic in CPython. +# Note: entries are never released as their number is expected to be low. +_log_timer_per_token = {} + + +def _seconds_have_elapsed(token, num_seconds): + """Tests if 'num_seconds' have passed since 'token' was requested. + + Not strictly thread-safe - may log with the wrong frequency if called + concurrently from multiple threads. Accuracy depends on resolution of + 'timeit.default_timer()'. + + Always returns True on the first call for a given 'token'. + + Args: + token: The token for which to look up the count. + num_seconds: The number of seconds to test for. + + Returns: + Whether it has been >= 'num_seconds' since 'token' was last requested. + """ + now = timeit.default_timer() + then = _log_timer_per_token.get(token, None) + if then is None or (now - then) >= num_seconds: + _log_timer_per_token[token] = now + return True + else: + return False + + +def log_every_n_seconds(level, msg, n_seconds, *args, use_call_stack=False): + """Logs ``msg % args`` at level ``level`` iff ``n_seconds`` elapsed since last call. + + Logs the first call, logs subsequent calls if 'n' seconds have elapsed since + the last logging call from the same call site (file + line). Not thread-safe. + + Args: + level: int, the absl logging level at which to log. + msg: str, the message to be logged. + n_seconds: float or int, seconds which should elapse before logging again. + *args: The args to be substituted into the msg. + use_call_stack: bool, whether to include the call stack when counting the + number of times the message is logged. + """ + caller_info = get_absl_logger().findCaller() + if use_call_stack: + # To reduce storage costs, we hash the call stack. + caller_info = (*caller_info[0:3], hash(_fast_stack_trace())) + should_log = _seconds_have_elapsed(caller_info, n_seconds) + log_if(level, msg, should_log, *args) + + +def log_first_n(level, msg, n, *args, use_call_stack=False): + """Logs ``msg % args`` at level ``level`` only first ``n`` times. + + Not threadsafe. + + Args: + level: int, the absl logging level at which to log. + msg: str, the message to be logged. + n: int, the maximal number of times the message is logged. + *args: The args to be substituted into the msg. + use_call_stack: bool, whether to include the call stack when counting the + number of times the message is logged. + """ + caller_info = get_absl_logger().findCaller() + if use_call_stack: + # To reduce storage costs, we hash the call stack. + caller_info = (*caller_info[0:3], hash(_fast_stack_trace())) + count = _get_next_log_count_per_token(caller_info) + log_if(level, msg, count < n, *args) + + +def log_if(level, msg, condition, *args): + """Logs ``msg % args`` at level ``level`` only if condition is fulfilled.""" + if condition: + log(level, msg, *args) + + +def log(level, msg, *args, **kwargs): + """Logs ``msg % args`` at absl logging level ``level``. + + If no args are given just print msg, ignoring any interpolation specifiers. + + Args: + level: int, the absl logging level at which to log the message + (logging.DEBUG|INFO|WARNING|ERROR|FATAL). While some C++ verbose logging + level constants are also supported, callers should prefer explicit + logging.vlog() calls for such purpose. + + msg: str, the message to be logged. + *args: The args to be substituted into the msg. + **kwargs: May contain exc_info to add exception traceback to message. + """ + if level > converter.ABSL_DEBUG: + # Even though this function supports level that is greater than 1, users + # should use logging.vlog instead for such cases. + # Treat this as vlog, 1 is equivalent to DEBUG. + standard_level = converter.STANDARD_DEBUG - (level - 1) + else: + if level < converter.ABSL_FATAL: + level = converter.ABSL_FATAL + standard_level = converter.absl_to_standard(level) + + # Match standard logging's behavior. Before use_absl_handler() and + # logging is configured, there is no handler attached on _absl_logger nor + # logging.root. So logs go no where. + if not logging.root.handlers: + logging.basicConfig() + + _absl_logger.log(standard_level, msg, *args, **kwargs) + + +def vlog(level, msg, *args, **kwargs): + """Log ``msg % args`` at C++ vlog level ``level``. + + Args: + level: int, the C++ verbose logging level at which to log the message, + e.g. 1, 2, 3, 4... While absl level constants are also supported, + callers should prefer logging.log|debug|info|... calls for such purpose. + msg: str, the message to be logged. + *args: The args to be substituted into the msg. + **kwargs: May contain exc_info to add exception traceback to message. + """ + log(level, msg, *args, **kwargs) + + +def vlog_is_on(level): + """Checks if vlog is enabled for the given level in caller's source file. + + Args: + level: int, the C++ verbose logging level at which to log the message, + e.g. 1, 2, 3, 4... While absl level constants are also supported, + callers should prefer level_debug|level_info|... calls for + checking those. + + Returns: + True if logging is turned on for that level. + """ + + if level > converter.ABSL_DEBUG: + # Even though this function supports level that is greater than 1, users + # should use logging.vlog instead for such cases. + # Treat this as vlog, 1 is equivalent to DEBUG. + standard_level = converter.STANDARD_DEBUG - (level - 1) + else: + if level < converter.ABSL_FATAL: + level = converter.ABSL_FATAL + standard_level = converter.absl_to_standard(level) + return _absl_logger.isEnabledFor(standard_level) + + +def flush(): + """Flushes all log files.""" + get_absl_handler().flush() + + +def level_debug(): + """Returns True if debug logging is turned on.""" + return get_verbosity() >= DEBUG + + +def level_info(): + """Returns True if info logging is turned on.""" + return get_verbosity() >= INFO + + +def level_warning(): + """Returns True if warning logging is turned on.""" + return get_verbosity() >= WARNING + + +level_warn = level_warning # Deprecated function. + + +def level_error(): + """Returns True if error logging is turned on.""" + return get_verbosity() >= ERROR + + +def get_log_file_name(level=INFO): + """Returns the name of the log file. + + For Python logging, only one file is used and level is ignored. And it returns + empty string if it logs to stderr/stdout or the log stream has no `name` + attribute. + + Args: + level: int, the absl.logging level. + + Raises: + ValueError: Raised when `level` has an invalid value. + """ + if level not in converter.ABSL_LEVELS: + raise ValueError(f'Invalid absl.logging level {level}') + stream = get_absl_handler().python_handler.stream + if (stream == sys.stderr or stream == sys.stdout or + not hasattr(stream, 'name')): + return '' + else: + return stream.name + + +def find_log_dir_and_names(program_name=None, log_dir=None): + """Computes the directory and filename prefix for log file. + + Args: + program_name: str|None, the filename part of the path to the program that is + running without its extension. e.g: if your program is called + ``usr/bin/foobar.py`` this method should probably be called with + ``program_name='foobar`` However, this is just a convention, you can pass + in any string you want, and it will be used as part of the log filename. + If you don't pass in anything, the default behavior is as described in the + example. In python standard logging mode, the program_name will be + prepended with ``py_`` if it is the ``program_name`` argument is omitted. + log_dir: str|None, the desired log directory. + + Returns: + (log_dir, file_prefix, symlink_prefix) + + Raises: + FileNotFoundError: raised when it cannot find a log directory. + """ + if not program_name: + # Strip the extension (foobar.par becomes foobar, and + # fubar.py becomes fubar). We do this so that the log + # file names are similar to C++ log file names. + program_name = os.path.splitext(os.path.basename(sys.argv[0]))[0] + + # Prepend py_ to files so that python code gets a unique file, and + # so that C++ libraries do not try to write to the same log files as us. + program_name = 'py_%s' % program_name + + actual_log_dir = find_log_dir(log_dir=log_dir) + + try: + username = getpass.getuser() + except KeyError: + # This can happen, e.g. when running under docker w/o passwd file. + if hasattr(os, 'getuid'): + # Windows doesn't have os.getuid + username = str(os.getuid()) + else: + username = 'unknown' + hostname = socket.gethostname() + file_prefix = '%s.%s.%s.log' % (program_name, hostname, username) + + return actual_log_dir, file_prefix, program_name + + +def find_log_dir(log_dir=None): + """Returns the most suitable directory to put log files into. + + Args: + log_dir: str|None, if specified, the logfile(s) will be created in that + directory. Otherwise if the --log_dir command-line flag is provided, the + logfile will be created in that directory. Otherwise the logfile will be + created in a standard location. + + Raises: + FileNotFoundError: raised when it cannot find a log directory. + """ + # Get a list of possible log dirs (will try to use them in order). + # NOTE: Google's internal implementation has a special handling for Google + # machines, which uses a list of directories. Hence the following uses `dirs` + # instead of a single directory. + if log_dir: + # log_dir was explicitly specified as an arg, so use it and it alone. + dirs = [log_dir] + elif FLAGS['log_dir'].value: + # log_dir flag was provided, so use it and it alone (this mimics the + # behavior of the same flag in logging.cc). + dirs = [FLAGS['log_dir'].value] + else: + dirs = [tempfile.gettempdir()] + + # Find the first usable log dir. + for d in dirs: + if os.path.isdir(d) and os.access(d, os.W_OK): + return d + raise FileNotFoundError( + "Can't find a writable directory for logs, tried %s" % dirs) + + +def get_absl_log_prefix(record): + """Returns the absl log prefix for the log record. + + Args: + record: logging.LogRecord, the record to get prefix for. + """ + created_tuple = time.localtime(record.created) + created_microsecond = int(record.created % 1.0 * 1e6) + + critical_prefix = '' + level = record.levelno + if _is_non_absl_fatal_record(record): + # When the level is FATAL, but not logged from absl, lower the level so + # it's treated as ERROR. + level = logging.ERROR + critical_prefix = _CRITICAL_PREFIX + severity = converter.get_initial_for_level(level) + + return '%c%02d%02d %02d:%02d:%02d.%06d %5d %s:%d] %s' % ( + severity, + created_tuple.tm_mon, + created_tuple.tm_mday, + created_tuple.tm_hour, + created_tuple.tm_min, + created_tuple.tm_sec, + created_microsecond, + _get_thread_id(), + record.filename, + record.lineno, + critical_prefix) + + +def skip_log_prefix(func): + """Skips reporting the prefix of a given function or name by :class:`~absl.logging.ABSLLogger`. + + This is a convenience wrapper function / decorator for + :meth:`~absl.logging.ABSLLogger.register_frame_to_skip`. + + If a callable function is provided, only that function will be skipped. + If a function name is provided, all functions with the same name in the + file that this is called in will be skipped. + + This can be used as a decorator of the intended function to be skipped. + + Args: + func: Callable function or its name as a string. + + Returns: + func (the input, unchanged). + + Raises: + ValueError: The input is callable but does not have a function code object. + TypeError: The input is neither callable nor a string. + """ + if callable(func): + func_code = getattr(func, '__code__', None) + if func_code is None: + raise ValueError('Input callable does not have a function code object.') + file_name = func_code.co_filename + func_name = func_code.co_name + func_lineno = func_code.co_firstlineno + elif isinstance(func, str): + file_name = get_absl_logger().findCaller()[0] + func_name = func + func_lineno = None + else: + raise TypeError('Input is neither callable nor a string.') + ABSLLogger.register_frame_to_skip(file_name, func_name, func_lineno) + return func + + +def _is_non_absl_fatal_record(log_record): + return (log_record.levelno >= logging.FATAL and + not log_record.__dict__.get(_ABSL_LOG_FATAL, False)) + + +def _is_absl_fatal_record(log_record): + return (log_record.levelno >= logging.FATAL and + log_record.__dict__.get(_ABSL_LOG_FATAL, False)) + + +# Indicates if we still need to warn about pre-init logs going to stderr. +_warn_preinit_stderr = True + + +class PythonHandler(logging.StreamHandler): + """The handler class used by Abseil Python logging implementation.""" + + def __init__(self, stream=None, formatter=None): + super().__init__(stream) + self.setFormatter(formatter or PythonFormatter()) + + def start_logging_to_file(self, program_name=None, log_dir=None): + """Starts logging messages to files instead of standard error.""" + FLAGS.logtostderr = False + + actual_log_dir, file_prefix, symlink_prefix = find_log_dir_and_names( + program_name=program_name, log_dir=log_dir) + + basename = '%s.INFO.%s.%d' % ( + file_prefix, + time.strftime('%Y%m%d-%H%M%S', time.localtime(time.time())), + os.getpid()) + filename = os.path.join(actual_log_dir, basename) + + self.stream = open(filename, 'a', encoding='utf-8') + + # os.symlink is not available on Windows Python 2. + if getattr(os, 'symlink', None): + # Create a symlink to the log file with a canonical name. + symlink = os.path.join(actual_log_dir, symlink_prefix + '.INFO') + try: + if os.path.islink(symlink): + os.unlink(symlink) + os.symlink(os.path.basename(filename), symlink) + except OSError: + # If it fails, we're sad but it's no error. Commonly, this + # fails because the symlink was created by another user and so + # we can't modify it + pass + + def use_absl_log_file(self, program_name=None, log_dir=None): + """Conditionally logs to files, based on --logtostderr.""" + if FLAGS['logtostderr'].value: + self.stream = sys.stderr + else: + self.start_logging_to_file(program_name=program_name, log_dir=log_dir) + + def flush(self): + """Flushes all log files.""" + self.acquire() + try: + if self.stream and hasattr(self.stream, 'flush'): + self.stream.flush() + except (OSError, ValueError): + # A ValueError is thrown if we try to flush a closed file. + pass + finally: + self.release() + + def _log_to_stderr(self, record): + """Emits the record to stderr. + + This temporarily sets the handler stream to stderr, calls + StreamHandler.emit, then reverts the stream back. + + Args: + record: logging.LogRecord, the record to log. + """ + # emit() is protected by a lock in logging.Handler, so we don't need to + # protect here again. + old_stream = self.stream + self.stream = sys.stderr + try: + super().emit(record) + finally: + self.stream = old_stream + + def emit(self, record): + """Prints a record out to some streams. + + 1. If ``FLAGS.logtostderr`` is set, it will print to ``sys.stderr`` ONLY. + 2. If ``FLAGS.alsologtostderr`` is set, it will print to ``sys.stderr``. + 3. If ``FLAGS.logtostderr`` is not set, it will log to the stream + associated with the current thread. + + Args: + record: :class:`logging.LogRecord`, the record to emit. + """ + # People occasionally call logging functions at import time before + # our flags may have even been defined yet, let alone even parsed, as we + # rely on the C++ side to define some flags for us and app init to + # deal with parsing. Match the C++ library behavior of notify and emit + # such messages to stderr. It encourages people to clean-up and does + # not hide the message. + level = record.levelno + if not FLAGS.is_parsed(): # Also implies "before flag has been defined". + global _warn_preinit_stderr + if _warn_preinit_stderr: + sys.stderr.write( + 'WARNING: Logging before flag parsing goes to stderr.\n') + _warn_preinit_stderr = False + self._log_to_stderr(record) + elif FLAGS['logtostderr'].value: + self._log_to_stderr(record) + else: + super().emit(record) + stderr_threshold = converter.string_to_standard( + FLAGS['stderrthreshold'].value) + if ((FLAGS['alsologtostderr'].value or level >= stderr_threshold) and + self.stream != sys.stderr): + self._log_to_stderr(record) + # Die when the record is created from ABSLLogger and level is FATAL. + if _is_absl_fatal_record(record): + self.flush() # Flush the log before dying. + + # In threaded python, sys.exit() from a non-main thread only + # exits the thread in question. + os.abort() + + def close(self): + """Closes the stream to which we are writing.""" + self.acquire() + try: + self.flush() + try: + # Do not close the stream if it's sys.stderr|stdout. They may be + # redirected or overridden to files, which should be managed by users + # explicitly. + user_managed = sys.stderr, sys.stdout, sys.__stderr__, sys.__stdout__ + if self.stream not in user_managed and ( + not hasattr(self.stream, 'isatty') or not self.stream.isatty()): + self.stream.close() + except ValueError: + # A ValueError is thrown if we try to run isatty() on a closed file. + pass + super().close() + finally: + self.release() + + +class ABSLHandler(logging.Handler): + """Abseil Python logging module's log handler.""" + + def __init__(self, python_logging_formatter): + super().__init__() + + self._python_handler = PythonHandler(formatter=python_logging_formatter) + self.activate_python_handler() + + def format(self, record): + return self._current_handler.format(record) + + def setFormatter(self, fmt): + self._current_handler.setFormatter(fmt) + + def emit(self, record): + self._current_handler.emit(record) + + def flush(self): + self._current_handler.flush() + + def close(self): + super().close() + self._current_handler.close() + + def handle(self, record): + rv = self.filter(record) + if rv: + return self._current_handler.handle(record) + return rv + + @property + def python_handler(self): + return self._python_handler + + def activate_python_handler(self): + """Uses the Python logging handler as the current logging handler.""" + self._current_handler = self._python_handler + + def use_absl_log_file(self, program_name=None, log_dir=None): + self._current_handler.use_absl_log_file(program_name, log_dir) + + def start_logging_to_file(self, program_name=None, log_dir=None): + self._current_handler.start_logging_to_file(program_name, log_dir) + + +class PythonFormatter(logging.Formatter): + """Formatter class used by :class:`~absl.logging.PythonHandler`.""" + + def format(self, record): + """Appends the message from the record to the results of the prefix. + + Args: + record: logging.LogRecord, the record to be formatted. + + Returns: + The formatted string representing the record. + """ + if (not FLAGS['showprefixforinfo'].value and + FLAGS['verbosity'].value == converter.ABSL_INFO and + record.levelno == logging.INFO and + _absl_handler.python_handler.stream == sys.stderr): + prefix = '' + else: + prefix = get_absl_log_prefix(record) + return prefix + super().format(record) + + +class ABSLLogger(logging.getLoggerClass()): + """A logger that will create LogRecords while skipping some stack frames. + + This class maintains an internal list of filenames and method names + for use when determining who called the currently executing stack + frame. Any method names from specific source files are skipped when + walking backwards through the stack. + + Client code should use the register_frame_to_skip method to let the + ABSLLogger know which method from which file should be + excluded from the walk backwards through the stack. + """ + _frames_to_skip = set() + + def findCaller(self, stack_info=False, stacklevel=1): + """Finds the frame of the calling method on the stack. + + This method skips any frames registered with the + ABSLLogger and any methods from this file, and whatever + method is currently being used to generate the prefix for the log + line. Then it returns the file name, line number, and method name + of the calling method. An optional fourth item may be returned, + callers who only need things from the first three are advised to + always slice or index the result rather than using direct unpacking + assignment. + + Args: + stack_info: bool, when True, include the stack trace as a fourth item + returned. On Python 3 there are always four items returned - the fourth + will be None when this is False. On Python 2 the stdlib base class API + only returns three items. We do the same when this new parameter is + unspecified or False for compatibility. + stacklevel: int, if greater than 1, that number of frames will be skipped. + + Returns: + (filename, lineno, methodname[, sinfo]) of the calling method. + """ + f_to_skip = ABSLLogger._frames_to_skip + # Use sys._getframe(2) instead of logging.currentframe(), it's slightly + # faster because there is one less frame to traverse. + frame = sys._getframe(2) # pylint: disable=protected-access + frame_to_return = None + + while frame: + code = frame.f_code + if (_LOGGING_FILE_PREFIX not in code.co_filename and + (code.co_filename, code.co_name, + code.co_firstlineno) not in f_to_skip and + (code.co_filename, code.co_name) not in f_to_skip): + frame_to_return = frame + stacklevel -= 1 + if stacklevel <= 0: + break + frame = frame.f_back + + if frame_to_return is not None: + sinfo = None + if stack_info: + out = io.StringIO() + out.write('Stack (most recent call last):\n') + traceback.print_stack(frame, file=out) + sinfo = out.getvalue().rstrip('\n') + return ( + frame_to_return.f_code.co_filename, + frame_to_return.f_lineno, + frame_to_return.f_code.co_name, + sinfo, + ) + + return None + + def critical(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``CRITICAL``.""" + self.log(logging.CRITICAL, msg, *args, **kwargs) + + def fatal(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``FATAL``.""" + self.log(logging.FATAL, msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``ERROR``.""" + self.log(logging.ERROR, msg, *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``WARN``.""" + warnings.warn("The 'warn' method is deprecated, use 'warning' instead", + DeprecationWarning, 2) + self.log(logging.WARN, msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``WARNING``.""" + self.log(logging.WARNING, msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``INFO``.""" + self.log(logging.INFO, msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + """Logs ``msg % args`` with severity ``DEBUG``.""" + self.log(logging.DEBUG, msg, *args, **kwargs) + + def log(self, level, msg, *args, **kwargs): + """Logs a message at a cetain level substituting in the supplied arguments. + + This method behaves differently in python and c++ modes. + + Args: + level: int, the standard logging level at which to log the message. + msg: str, the text of the message to log. + *args: The arguments to substitute in the message. + **kwargs: The keyword arguments to substitute in the message. + """ + if level >= logging.FATAL: + # Add property to the LogRecord created by this logger. + # This will be used by the ABSLHandler to determine whether it should + # treat CRITICAL/FATAL logs as really FATAL. + extra = kwargs.setdefault('extra', {}) + extra[_ABSL_LOG_FATAL] = True + super().log(level, msg, *args, **kwargs) + + def handle(self, record): + """Calls handlers without checking ``Logger.disabled``. + + Non-root loggers are set to disabled after setup with :func:`logging.config` + if it's not explicitly specified. Historically, absl logging will not be + disabled by that. To maintaining this behavior, this function skips + checking the ``Logger.disabled`` bit. + + This logger can still be disabled by adding a filter that filters out + everything. + + Args: + record: logging.LogRecord, the record to handle. + """ + if self.filter(record): + self.callHandlers(record) + + @classmethod + def register_frame_to_skip(cls, file_name, function_name, line_number=None): + """Registers a function name to skip when walking the stack. + + The :class:`~absl.logging.ABSLLogger` sometimes skips method calls on the + stack to make the log messages meaningful in their appropriate context. + This method registers a function from a particular file as one + which should be skipped. + + Args: + file_name: str, the name of the file that contains the function. + function_name: str, the name of the function to skip. + line_number: int, if provided, only the function with this starting line + number will be skipped. Otherwise, all functions with the same name + in the file will be skipped. + """ + if line_number is not None: + cls._frames_to_skip.add((file_name, function_name, line_number)) + else: + cls._frames_to_skip.add((file_name, function_name)) + + +def _get_thread_id(): + """Gets id of current thread, suitable for logging as an unsigned quantity. + + If pywrapbase is linked, returns GetTID() for the thread ID to be + consistent with C++ logging. Otherwise, returns the numeric thread id. + The quantities are made unsigned by masking with 2*sys.maxint + 1. + + Returns: + Thread ID unique to this process (unsigned) + """ + thread_id = threading.get_ident() + return thread_id & _THREAD_ID_MASK + + +def get_absl_logger(): + """Returns the absl logger instance.""" + assert _absl_logger is not None + return _absl_logger + + +def get_absl_handler(): + """Returns the absl handler instance.""" + assert _absl_handler is not None + return _absl_handler + + +def use_python_logging(quiet=False): + """Uses the python implementation of the logging code. + + Args: + quiet: No logging message about switching logging type. + """ + get_absl_handler().activate_python_handler() + if not quiet: + info('Restoring pure python logging') + + +_attempted_to_remove_stderr_stream_handlers = False + + +def use_absl_handler(): + """Uses the ABSL logging handler for logging. + + This method is called in :func:`app.run()` so the absl handler + is used in absl apps. + """ + global _attempted_to_remove_stderr_stream_handlers + if not _attempted_to_remove_stderr_stream_handlers: + # The absl handler logs to stderr by default. To prevent double logging to + # stderr, the following code tries its best to remove other handlers that + # emit to stderr. Those handlers are most commonly added when + # logging.info/debug is called before calling use_absl_handler(). + handlers = [ + h for h in logging.root.handlers + if isinstance(h, logging.StreamHandler) and h.stream == sys.stderr] + for h in handlers: + logging.root.removeHandler(h) + _attempted_to_remove_stderr_stream_handlers = True + + absl_handler = get_absl_handler() + if absl_handler not in logging.root.handlers: + logging.root.addHandler(absl_handler) + FLAGS['verbosity']._update_logging_levels() # pylint: disable=protected-access + FLAGS['logger_levels']._update_logger_levels() # pylint: disable=protected-access + + +def _initialize(): + """Initializes loggers and handlers.""" + global _absl_logger, _absl_handler + + if _absl_logger: + return + + original_logger_class = logging.getLoggerClass() + logging.setLoggerClass(ABSLLogger) + _absl_logger = logging.getLogger('absl') + logging.setLoggerClass(original_logger_class) + + python_logging_formatter = PythonFormatter() + _absl_handler = ABSLHandler(python_logging_formatter) + + +_initialize() diff --git a/lib/python3.10/site-packages/absl/logging/__init__.pyi b/lib/python3.10/site-packages/absl/logging/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..8b625a7f8be71d4c396760f2b74362f8a2f39121 --- /dev/null +++ b/lib/python3.10/site-packages/absl/logging/__init__.pyi @@ -0,0 +1,261 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Any, Callable, Dict, IO, NoReturn, Optional, Tuple, TypeVar, Union + +from absl import flags + +# Logging levels. +FATAL: int +ERROR: int +WARNING: int +WARN: int # Deprecated name. +INFO: int +DEBUG: int + +ABSL_LOGGING_PREFIX_REGEX: str + +LOGTOSTDERR: flags.FlagHolder[bool] +ALSOLOGTOSTDERR: flags.FlagHolder[bool] +LOG_DIR: flags.FlagHolder[str] +VERBOSITY: flags.FlagHolder[int] +LOGGER_LEVELS: flags.FlagHolder[Dict[str, str]] +STDERRTHRESHOLD: flags.FlagHolder[str] +SHOWPREFIXFORINFO: flags.FlagHolder[bool] + +def get_verbosity() -> int: + ... + +def set_verbosity(v: Union[int, str]) -> None: + ... + +def set_stderrthreshold(s: Union[int, str]) -> None: + ... + +# TODO(b/277607978): Provide actual args+kwargs shadowing stdlib's logging functions. +def fatal(msg: Any, *args: Any, **kwargs: Any) -> NoReturn: + ... + +def error(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def warning(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def warn(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def info(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def debug(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def exception(msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def log_every_n( + level: int, msg: Any, n: int, *args: Any, use_call_stack: bool = ... +) -> None: + ... + +def log_every_n_seconds( + level: int, + msg: Any, + n_seconds: float, + *args: Any, + use_call_stack: bool = ... +) -> None: + ... + +def log_first_n( + level: int, msg: Any, n: int, *args: Any, use_call_stack: bool = ... +) -> None: + ... + +def log_if(level: int, msg: Any, condition: Any, *args: Any) -> None: + ... + +def log(level: int, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def vlog(level: int, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + +def vlog_is_on(level: int) -> bool: + ... + +def flush() -> None: + ... + +def level_debug() -> bool: + ... + +def level_info() -> bool: + ... + +def level_warning() -> bool: + ... + +level_warn = level_warning # Deprecated function. + +def level_error() -> bool: + ... + +def get_log_file_name(level: int = ...) -> str: + ... + +def find_log_dir_and_names( + program_name: Optional[str] = ..., log_dir: Optional[str] = ... +) -> Tuple[str, str, str]: + ... + +def find_log_dir(log_dir: Optional[str] = ...) -> str: + ... + +def get_absl_log_prefix(record: logging.LogRecord) -> str: + ... + +_SkipLogT = TypeVar('_SkipLogT', str, Callable[..., Any]) + +def skip_log_prefix(func: _SkipLogT) -> _SkipLogT: + ... + +_StreamT = TypeVar('_StreamT') + +class PythonHandler(logging.StreamHandler[_StreamT]): # type: ignore[type-var] + + def __init__( + self, + stream: Optional[_StreamT] = ..., + formatter: Optional[logging.Formatter] = ..., + ) -> None: + ... + + def start_logging_to_file( + self, program_name: Optional[str] = ..., log_dir: Optional[str] = ... + ) -> None: + ... + + def use_absl_log_file( + self, program_name: Optional[str] = ..., log_dir: Optional[str] = ... + ) -> None: + ... + + def flush(self) -> None: + ... + + def emit(self, record: logging.LogRecord) -> None: + ... + + def close(self) -> None: + ... + +class ABSLHandler(logging.Handler): + + def __init__(self, python_logging_formatter: PythonFormatter) -> None: + ... + + def format(self, record: logging.LogRecord) -> str: + ... + + def setFormatter(self, fmt) -> None: + ... + + def emit(self, record: logging.LogRecord) -> None: + ... + + def flush(self) -> None: + ... + + def close(self) -> None: + ... + + def handle(self, record: logging.LogRecord) -> bool: + ... + + @property + def python_handler(self) -> PythonHandler: + ... + + def activate_python_handler(self) -> None: + ... + + def use_absl_log_file( + self, program_name: Optional[str] = ..., log_dir: Optional[str] = ... + ) -> None: + ... + + def start_logging_to_file(self, program_name=None, log_dir=None) -> None: + ... + +class PythonFormatter(logging.Formatter): + + def format(self, record: logging.LogRecord) -> str: + ... + +class ABSLLogger(logging.Logger): + + def findCaller( + self, stack_info: bool = ..., stacklevel: int = ... + ) -> Tuple[str, int, str, Optional[str]]: + ... + + def critical(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def fatal(self, msg: Any, *args: Any, **kwargs: Any) -> NoReturn: # type: ignore[override] + ... + + def error(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def warn(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def warning(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def info(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def debug(self, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: + ... + + def handle(self, record: logging.LogRecord) -> None: + ... + + @classmethod + def register_frame_to_skip( + cls, file_name: str, function_name: str, line_number: Optional[int] = ... + ) -> None: + ... + +# NOTE: Returns None before _initialize called but shouldn't occur after import. +def get_absl_logger() -> ABSLLogger: + ... + +# NOTE: Returns None before _initialize called but shouldn't occur after import. +def get_absl_handler() -> ABSLHandler: + ... + +def use_python_logging(quiet: bool = ...) -> None: + ... + +def use_absl_handler() -> None: + ... diff --git a/lib/python3.10/site-packages/absl/logging/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/absl/logging/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..273f0033149bb09a7499cf41b472a686acbb78ef Binary files /dev/null and b/lib/python3.10/site-packages/absl/logging/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/logging/__pycache__/converter.cpython-310.pyc b/lib/python3.10/site-packages/absl/logging/__pycache__/converter.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2d5903523a59ad1003c244556dcd7eb725b929c Binary files /dev/null and b/lib/python3.10/site-packages/absl/logging/__pycache__/converter.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/logging/converter.py b/lib/python3.10/site-packages/absl/logging/converter.py new file mode 100644 index 0000000000000000000000000000000000000000..ad3fcd5045edc665cc3118a9caa4f44fc1607be2 --- /dev/null +++ b/lib/python3.10/site-packages/absl/logging/converter.py @@ -0,0 +1,214 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module to convert log levels between Abseil Python, C++, and Python standard. + +This converter has to convert (best effort) between three different +logging level schemes: + + * **cpp**: The C++ logging level scheme used in Abseil C++. + * **absl**: The absl.logging level scheme used in Abseil Python. + * **standard**: The python standard library logging level scheme. + +Here is a handy ascii chart for easy mental mapping:: + + LEVEL | cpp | absl | standard | + ---------+-----+--------+----------+ + DEBUG | 0 | 1 | 10 | + INFO | 0 | 0 | 20 | + WARNING | 1 | -1 | 30 | + ERROR | 2 | -2 | 40 | + CRITICAL | 3 | -3 | 50 | + FATAL | 3 | -3 | 50 | + +Note: standard logging ``CRITICAL`` is mapped to absl/cpp ``FATAL``. +However, only ``CRITICAL`` logs from the absl logger (or absl.logging.fatal) +will terminate the program. ``CRITICAL`` logs from non-absl loggers are treated +as error logs with a message prefix ``"CRITICAL - "``. + +Converting from standard to absl or cpp is a lossy conversion. +Converting back to standard will lose granularity. For this reason, +users should always try to convert to standard, the richest +representation, before manipulating the levels, and then only to cpp +or absl if those level schemes are absolutely necessary. +""" + +import logging + +STANDARD_CRITICAL = logging.CRITICAL +STANDARD_ERROR = logging.ERROR +STANDARD_WARNING = logging.WARNING +STANDARD_INFO = logging.INFO +STANDARD_DEBUG = logging.DEBUG + +# These levels are also used to define the constants +# FATAL, ERROR, WARNING, INFO, and DEBUG in the +# absl.logging module. +ABSL_FATAL = -3 +ABSL_ERROR = -2 +ABSL_WARNING = -1 +ABSL_WARN = -1 # Deprecated name. +ABSL_INFO = 0 +ABSL_DEBUG = 1 + +ABSL_LEVELS = {ABSL_FATAL: 'FATAL', + ABSL_ERROR: 'ERROR', + ABSL_WARNING: 'WARNING', + ABSL_INFO: 'INFO', + ABSL_DEBUG: 'DEBUG'} + +# Inverts the ABSL_LEVELS dictionary +ABSL_NAMES = {'FATAL': ABSL_FATAL, + 'ERROR': ABSL_ERROR, + 'WARNING': ABSL_WARNING, + 'WARN': ABSL_WARNING, # Deprecated name. + 'INFO': ABSL_INFO, + 'DEBUG': ABSL_DEBUG} + +ABSL_TO_STANDARD = {ABSL_FATAL: STANDARD_CRITICAL, + ABSL_ERROR: STANDARD_ERROR, + ABSL_WARNING: STANDARD_WARNING, + ABSL_INFO: STANDARD_INFO, + ABSL_DEBUG: STANDARD_DEBUG} + +# Inverts the ABSL_TO_STANDARD +STANDARD_TO_ABSL = {v: k for (k, v) in ABSL_TO_STANDARD.items()} + + +def get_initial_for_level(level): + """Gets the initial that should start the log line for the given level. + + It returns: + + * ``'I'`` when: ``level < STANDARD_WARNING``. + * ``'W'`` when: ``STANDARD_WARNING <= level < STANDARD_ERROR``. + * ``'E'`` when: ``STANDARD_ERROR <= level < STANDARD_CRITICAL``. + * ``'F'`` when: ``level >= STANDARD_CRITICAL``. + + Args: + level: int, a Python standard logging level. + + Returns: + The first initial as it would be logged by the C++ logging module. + """ + if level < STANDARD_WARNING: + return 'I' + elif level < STANDARD_ERROR: + return 'W' + elif level < STANDARD_CRITICAL: + return 'E' + else: + return 'F' + + +def absl_to_cpp(level): + """Converts an absl log level to a cpp log level. + + Args: + level: int, an absl.logging level. + + Raises: + TypeError: Raised when level is not an integer. + + Returns: + The corresponding integer level for use in Abseil C++. + """ + if not isinstance(level, int): + raise TypeError(f'Expect an int level, found {type(level)}') + if level >= 0: + # C++ log levels must be >= 0 + return 0 + else: + return -level + + +def absl_to_standard(level): + """Converts an integer level from the absl value to the standard value. + + Args: + level: int, an absl.logging level. + + Raises: + TypeError: Raised when level is not an integer. + + Returns: + The corresponding integer level for use in standard logging. + """ + if not isinstance(level, int): + raise TypeError(f'Expect an int level, found {type(level)}') + if level < ABSL_FATAL: + level = ABSL_FATAL + if level <= ABSL_DEBUG: + return ABSL_TO_STANDARD[level] + # Maps to vlog levels. + return STANDARD_DEBUG - level + 1 + + +def string_to_standard(level): + """Converts a string level to standard logging level value. + + Args: + level: str, case-insensitive ``'debug'``, ``'info'``, ``'warning'``, + ``'error'``, ``'fatal'``. + + Returns: + The corresponding integer level for use in standard logging. + """ + return absl_to_standard(ABSL_NAMES.get(level.upper())) + + +def standard_to_absl(level): + """Converts an integer level from the standard value to the absl value. + + Args: + level: int, a Python standard logging level. + + Raises: + TypeError: Raised when level is not an integer. + + Returns: + The corresponding integer level for use in absl logging. + """ + if not isinstance(level, int): + raise TypeError(f'Expect an int level, found {type(level)}') + if level < 0: + level = 0 + if level < STANDARD_DEBUG: + # Maps to vlog levels. + return STANDARD_DEBUG - level + 1 + elif level < STANDARD_INFO: + return ABSL_DEBUG + elif level < STANDARD_WARNING: + return ABSL_INFO + elif level < STANDARD_ERROR: + return ABSL_WARNING + elif level < STANDARD_CRITICAL: + return ABSL_ERROR + else: + return ABSL_FATAL + + +def standard_to_cpp(level): + """Converts an integer level from the standard value to the cpp value. + + Args: + level: int, a Python standard logging level. + + Raises: + TypeError: Raised when level is not an integer. + + Returns: + The corresponding integer level for use in cpp logging. + """ + return absl_to_cpp(standard_to_absl(level)) diff --git a/lib/python3.10/site-packages/absl/py.typed b/lib/python3.10/site-packages/absl/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/absl/testing/__init__.py b/lib/python3.10/site-packages/absl/testing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a3bd1cd51810385ca0e5e9fed3fb9a804febf27e --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d49939d341aa25c3150e8058b40063e3350bf6c Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..459e91efd4dfaf9108fee4f8ef4c5a84ae52d17c Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..699086fa1b22a12a4d944fd63bde1def6d3127fc Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/absltest.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/absltest.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5aa05177bbea0ec49a3d287d94512d1beda6249d Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/absltest.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/flagsaver.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/flagsaver.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2f6a1da2469400c6de93c6bfba8cb01af662700 Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/flagsaver.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/parameterized.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/parameterized.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd041f15f11d60435d013d37e5a06fd2166c76f7 Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/parameterized.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/__pycache__/xml_reporter.cpython-310.pyc b/lib/python3.10/site-packages/absl/testing/__pycache__/xml_reporter.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b086fe25c13ac20c7ec121db60207e72bb33785 Binary files /dev/null and b/lib/python3.10/site-packages/absl/testing/__pycache__/xml_reporter.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/absl/testing/_bazelize_command.py b/lib/python3.10/site-packages/absl/testing/_bazelize_command.py new file mode 100644 index 0000000000000000000000000000000000000000..acde3d2bd674a148ab3ec2a50984219c42b58a34 --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/_bazelize_command.py @@ -0,0 +1,68 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal helper for running tests on Windows Bazel.""" + +import os + +from absl import flags + +FLAGS = flags.FLAGS + + +def get_executable_path(py_binary_name): + """Returns the executable path of a py_binary. + + This returns the executable path of a py_binary that is in another Bazel + target's data dependencies. + + On Linux/macOS, the path and __file__ has the same root directory. + On Windows, bazel builds an .exe file and we need to use the MANIFEST file + the location the actual binary. + + Args: + py_binary_name: string, the name of a py_binary that is in another Bazel + target's data dependencies. + + Raises: + RuntimeError: Raised when it cannot locate the executable path. + """ + + if os.name == 'nt': + py_binary_name += '.exe' + manifest_file = os.path.join(FLAGS.test_srcdir, 'MANIFEST') + workspace_name = os.environ['TEST_WORKSPACE'] + manifest_entry = f'{workspace_name}/{py_binary_name}' + with open(manifest_file) as manifest_fd: + for line in manifest_fd: + tokens = line.strip().split(' ') + if len(tokens) != 2: + continue + if manifest_entry == tokens[0]: + return tokens[1] + raise RuntimeError( + 'Cannot locate executable path for {}, MANIFEST file: {}.'.format( + py_binary_name, manifest_file)) + else: + # NOTE: __file__ may be .py or .pyc, depending on how the module was + # loaded and executed. + path = __file__ + + # Use the package name to find the root directory: every dot is + # a directory, plus one for ourselves. + for _ in range(__name__.count('.') + 1): + path = os.path.dirname(path) + + root_directory = path + return os.path.join(root_directory, py_binary_name) diff --git a/lib/python3.10/site-packages/absl/testing/_pretty_print_reporter.py b/lib/python3.10/site-packages/absl/testing/_pretty_print_reporter.py new file mode 100644 index 0000000000000000000000000000000000000000..af2bedbc6558b53c0d4bbd54a71859138cddb0c5 --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/_pretty_print_reporter.py @@ -0,0 +1,92 @@ +# Copyright 2018 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""TestResult implementing default output for test execution status.""" + +import unittest + + +class TextTestResult(unittest.TextTestResult): + """TestResult class that provides the default text result formatting.""" + + def __init__(self, stream, descriptions, verbosity): + # Disable the verbose per-test output from the superclass, since it would + # conflict with our customized output. + super().__init__(stream, descriptions, 0) + self._per_test_output = verbosity > 0 + + def _print_status(self, tag, test, reason=None): + if self._per_test_output: + test_id = test.id() + if test_id.startswith('__main__.'): + test_id = test_id[len('__main__.'):] + if reason: + print('[%s] %s - %s' % (tag, test_id, reason), file=self.stream) + else: + print('[%s] %s' % (tag, test_id), file=self.stream) + self.stream.flush() + + def startTest(self, test): + super().startTest(test) + self._print_status(' RUN ', test) + + def addSuccess(self, test): + super().addSuccess(test) + self._print_status(' OK ', test) + + def addError(self, test, err): + super().addError(test, err) + self._print_status(' FAILED ', test) + + def addFailure(self, test, err): + super().addFailure(test, err) + self._print_status(' FAILED ', test) + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self._print_status(' SKIPPED ', test, reason) + + def addExpectedFailure(self, test, err): + super().addExpectedFailure(test, err) + self._print_status(' OK ', test) + + def addUnexpectedSuccess(self, test): + super().addUnexpectedSuccess(test) + self._print_status(' FAILED ', test) + + +class TextTestRunner(unittest.TextTestRunner): + """A test runner that produces formatted text results.""" + + _TEST_RESULT_CLASS = TextTestResult + + # Set this to true at the class or instance level to run tests using a + # debug-friendly method (e.g, one that doesn't catch exceptions and interacts + # better with debuggers). + # Usually this is set using --pdb_post_mortem. + run_for_debugging = False + + def run(self, test) -> unittest.TextTestResult: + if self.run_for_debugging: + return self._run_debug(test) + else: + return super().run(test) + + def _run_debug(self, test) -> unittest.TextTestResult: + test.debug() + # Return an empty result to indicate success. + return self._makeResult() + + def _makeResult(self): + return TextTestResult(self.stream, self.descriptions, self.verbosity) diff --git a/lib/python3.10/site-packages/absl/testing/absltest.py b/lib/python3.10/site-packages/absl/testing/absltest.py new file mode 100644 index 0000000000000000000000000000000000000000..8831b69be65a5bda9e20e5924e01dcefa72bd945 --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/absltest.py @@ -0,0 +1,2879 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base functionality for Abseil Python tests. + +This module contains base classes and high-level functions for Abseil-style +tests. +""" + +import collections +from collections import abc +import contextlib +import dataclasses +import difflib +import enum +import errno +import faulthandler +import functools +import getpass +import inspect +import io +import itertools +import json +import numbers +import os +import random +import re +import shlex +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import textwrap +import typing +from typing import Any, AnyStr, BinaryIO, Callable, ContextManager, IO, Iterator, List, Mapping, MutableMapping, MutableSequence, NoReturn, Optional, Sequence, TextIO, Tuple, Type, Union +import unittest +from unittest import mock # pylint: disable=unused-import Allow absltest.mock. +import unittest.case +from urllib import parse + +from absl import app # pylint: disable=g-import-not-at-top +from absl import flags +from absl import logging +from absl.testing import _pretty_print_reporter +from absl.testing import xml_reporter + + +# Re-export a bunch of unittest functions we support so that people don't +# have to import unittest to get them +# pylint: disable=invalid-name +skip = unittest.skip +skipIf = unittest.skipIf +skipUnless = unittest.skipUnless +SkipTest = unittest.SkipTest +expectedFailure = unittest.expectedFailure +# pylint: enable=invalid-name + +# End unittest re-exports + +FLAGS = flags.FLAGS + +# Private typing symbols. +_T = typing.TypeVar('_T') # Unbounded TypeVar for general usage +_TEXT_OR_BINARY_TYPES = (str, bytes) + +# Suppress surplus entries in AssertionError stack traces. +__unittest = True # pylint: disable=invalid-name + + +def expectedFailureIf(condition, reason): # pylint: disable=invalid-name + """Expects the test to fail if the run condition is True. + + Example usage:: + + @expectedFailureIf(sys.version.major == 2, "Not yet working in py2") + def test_foo(self): + ... + + Args: + condition: bool, whether to expect failure or not. + reason: str, the reason to expect failure. + + Returns: + Decorator function + """ + del reason # Unused + if condition: + return unittest.expectedFailure + else: + return lambda f: f + + +class TempFileCleanup(enum.Enum): + # Always cleanup temp files when the test completes. + ALWAYS = 'always' + # Only cleanup temp file if the test passes. This allows easier inspection + # of tempfile contents on test failure. absltest.TEST_TMPDIR.value determines + # where tempfiles are created. + SUCCESS = 'success' + # Never cleanup temp files. + OFF = 'never' + + +# Many of the methods in this module have names like assertSameElements. +# This kind of name does not comply with PEP8 style, +# but it is consistent with the naming of methods in unittest.py. +# pylint: disable=invalid-name + + +def _get_default_test_random_seed() -> int: + random_seed = 301 + value = os.environ.get('TEST_RANDOM_SEED', '') + try: + random_seed = int(value) + except ValueError: + pass + return random_seed + + +def get_default_test_srcdir() -> str: + """Returns default test source dir.""" + return os.environ.get('TEST_SRCDIR', '') + + +def get_default_test_tmpdir() -> str: + """Returns default test temp dir.""" + tmpdir = os.environ.get('TEST_TMPDIR', '') + if not tmpdir: + tmpdir = os.path.join(tempfile.gettempdir(), 'absl_testing') + + return tmpdir + + +def _get_default_randomize_ordering_seed() -> int: + """Returns default seed to use for randomizing test order. + + This function first checks the --test_randomize_ordering_seed flag, and then + the TEST_RANDOMIZE_ORDERING_SEED environment variable. If the first value + we find is: + * (not set): disable test randomization + * 0: disable test randomization + * 'random': choose a random seed in [1, 4294967295] for test order + randomization + * positive integer: use this seed for test order randomization + + (The values used are patterned after + https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED). + + In principle, it would be simpler to return None if no override is provided; + however, the python random module has no `get_seed()`, only `getstate()`, + which returns far more data than we want to pass via an environment variable + or flag. + + Returns: + A default value for test case randomization (int). 0 means do not randomize. + + Raises: + ValueError: Raised when the flag or env value is not one of the options + above. + """ + if FLAGS['test_randomize_ordering_seed'].present: + randomize = FLAGS.test_randomize_ordering_seed + elif 'TEST_RANDOMIZE_ORDERING_SEED' in os.environ: + randomize = os.environ['TEST_RANDOMIZE_ORDERING_SEED'] + else: + randomize = '' + if not randomize: + return 0 + if randomize == 'random': + return random.Random().randint(1, 4294967295) + if randomize == '0': + return 0 + try: + seed = int(randomize) + if seed > 0: + return seed + except ValueError: + pass + raise ValueError(f'Unknown test randomization seed value: {randomize}') + + +TEST_SRCDIR = flags.DEFINE_string( + 'test_srcdir', + get_default_test_srcdir(), + 'Root of directory tree where source files live', + allow_override_cpp=True) +TEST_TMPDIR = flags.DEFINE_string( + 'test_tmpdir', + get_default_test_tmpdir(), + 'Directory for temporary testing files', + allow_override_cpp=True) + +flags.DEFINE_integer( + 'test_random_seed', + _get_default_test_random_seed(), + 'Random seed for testing. Some test frameworks may ' + 'change the default value of this flag between runs, so ' + 'it is not appropriate for seeding probabilistic tests.', + allow_override_cpp=True) +flags.DEFINE_string( + 'test_randomize_ordering_seed', + '', + 'If positive, use this as a seed to randomize the ' + 'execution order for test cases. If "random", pick a ' + 'random seed to use. If 0 or not set, do not randomize ' + 'test case execution order. This flag also overrides ' + 'the TEST_RANDOMIZE_ORDERING_SEED environment variable.', + allow_override_cpp=True) +flags.DEFINE_string('xml_output_file', '', 'File to store XML test results') + + +# We might need to monkey-patch TestResult so that it stops considering an +# unexpected pass as a as a "successful result". For details, see +# http://bugs.python.org/issue20165 +def _monkey_patch_test_result_for_unexpected_passes() -> None: + """Workaround for .""" + + def wasSuccessful(self) -> bool: + """Tells whether or not this result was a success. + + Any unexpected pass is to be counted as a non-success. + + Args: + self: The TestResult instance. + + Returns: + Whether or not this result was a success. + """ + return (len(self.failures) == len(self.errors) == + len(self.unexpectedSuccesses) == 0) + + test_result = unittest.TestResult() + test_result.addUnexpectedSuccess(unittest.FunctionTestCase(lambda: None)) + if test_result.wasSuccessful(): # The bug is present. + unittest.TestResult.wasSuccessful = wasSuccessful # type: ignore[method-assign] + if test_result.wasSuccessful(): # Warn the user if our hot-fix failed. + sys.stderr.write('unittest.result.TestResult monkey patch to report' + ' unexpected passes as failures did not work.\n') + + +_monkey_patch_test_result_for_unexpected_passes() + + +def _open( + filepath: str, mode: str, _open_func: Callable[..., IO[AnyStr]] = open +) -> IO[AnyStr]: + """Opens a file. + + Like open(), but ensure that we can open real files even if tests stub out + open(). + + Args: + filepath: A filepath. + mode: A mode. + _open_func: A built-in open() function. + + Returns: + The opened file object. + """ + return _open_func(filepath, mode, encoding='utf-8') + + +class _TempDir: + """Represents a temporary directory for tests. + + Creation of this class is internal. Using its public methods is OK. + + This class implements the `os.PathLike[str]` interface. This means it can + be directly passed to e.g. `os.path.join()`. + """ + + def __init__(self, path: str) -> None: + """Module-private: do not instantiate outside module.""" + self._path = path + + @property + def full_path(self) -> str: + """Returns the path, as a string, for the directory. + + TIP: Instead of e.g. `os.path.join(temp_dir.full_path, some_file_name)`, + you can simply do `os.path.join(temp_dir, some_file_name)` because + `__fspath__()` is implemented. + """ + return self._path + + def __fspath__(self) -> str: + """See os.PathLike.""" + return self.full_path + + def create_file( + self, + file_path: Optional[str] = None, + content: Optional[AnyStr] = None, + mode: str = 'w', + encoding: str = 'utf8', + errors: str = 'strict', + ) -> '_TempFile': + """Create a file in the directory. + + NOTE: If the file already exists, it will be made writable and overwritten. + + Args: + file_path: Optional file path for the temp file. If not given, a unique + file name will be generated and used. Slashes are allowed in the name; + any missing intermediate directories will be created. NOTE: This path + is the path that will be cleaned up, including any directories in the + path, e.g., 'foo/bar/baz.txt' will `rm -r foo` + content: Optional string or bytes to initially write to the file. If not + specified, then an empty file is created. + mode: Mode string to use when writing content. Only used if `content` is + non-empty. + encoding: Encoding to use when writing string content. Only used if + `content` is text. + errors: How to handle text to bytes encoding errors. Only used if + `content` is text. + + Returns: + A _TempFile representing the created file. + """ + tf, _ = _TempFile._create(self._path, file_path, content, mode, encoding, + errors) + return tf + + def mkdir(self, dir_path: Optional[str] = None) -> '_TempDir': + """Create a directory in the directory. + + Args: + dir_path: Optional path to the directory to create. If not given, + a unique name will be generated and used. + + Returns: + A _TempDir representing the created directory. + """ + if dir_path: + path = os.path.join(self._path, dir_path) + else: + path = tempfile.mkdtemp(dir=self._path) + + # Note: there's no need to clear the directory since the containing + # dir was cleared by the tempdir() function. + os.makedirs(path, exist_ok=True) + return _TempDir(path) + + +class _TempFile: + """Represents a tempfile for tests. + + Creation of this class is internal. Using its public methods is OK. + + This class implements the `os.PathLike[str]` interface. This means it can + be directly passed to e.g. `os.path.join()`. + """ + + def __init__(self, path: str) -> None: + """Private: use _create instead.""" + self._path = path + + @classmethod + def _create( + cls, + base_path: str, + file_path: Optional[str], + content: Optional[AnyStr], + mode: str, + encoding: str, + errors: str, + ) -> Tuple['_TempFile', str]: + """Module-private: create a tempfile instance.""" + if file_path: + cleanup_path = os.path.join(base_path, _get_first_part(file_path)) + path = os.path.join(base_path, file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + # The file may already exist, in which case, ensure it's writable so that + # it can be truncated. + if os.path.exists(path) and not os.access(path, os.W_OK): + stat_info = os.stat(path) + os.chmod(path, stat_info.st_mode | stat.S_IWUSR) + else: + os.makedirs(base_path, exist_ok=True) + fd, path = tempfile.mkstemp(dir=str(base_path)) + os.close(fd) + cleanup_path = path + + tf = cls(path) + + if content: + if isinstance(content, str): + tf.write_text(content, mode=mode, encoding=encoding, errors=errors) + else: + tf.write_bytes(content, mode) + + else: + tf.write_bytes(b'') + + return tf, cleanup_path + + @property + def full_path(self) -> str: + """Returns the path, as a string, for the file. + + TIP: Instead of e.g. `os.path.join(temp_file.full_path, some_file_name)`, + you can simply do `os.path.join(temp_file, some_file_name)` because + `__fspath__()` is implemented. + """ + return self._path + + def __fspath__(self) -> str: + """See os.PathLike.""" + return self.full_path + + def read_text(self, encoding: str = 'utf8', errors: str = 'strict') -> str: + """Return the contents of the file as text.""" + with self.open_text(encoding=encoding, errors=errors) as fp: + return fp.read() + + def read_bytes(self) -> bytes: + """Return the content of the file as bytes.""" + with self.open_bytes() as fp: + return fp.read() + + def write_text( + self, + text: str, + mode: str = 'w', + encoding: str = 'utf8', + errors: str = 'strict', + ) -> None: + """Write text to the file. + + Args: + text: Text to write. + mode: The mode to open the file for writing. + encoding: The encoding to use when writing the text to the file. + errors: The error handling strategy to use when converting text to bytes. + """ + with self.open_text(mode, encoding=encoding, errors=errors) as fp: + fp.write(text) + + def write_bytes(self, data: bytes, mode: str = 'wb') -> None: + """Write bytes to the file. + + Args: + data: bytes to write. + mode: Mode to open the file for writing. The "b" flag is implicit if + not already present. It must not have the "t" flag. + """ + with self.open_bytes(mode) as fp: + fp.write(data) + + def open_text( + self, mode: str = 'rt', encoding: str = 'utf8', errors: str = 'strict' + ) -> ContextManager[TextIO]: + """Return a context manager for opening the file in text mode. + + Args: + mode: The mode to open the file in. The "t" flag is implicit if not + already present. It must not have the "b" flag. + encoding: The encoding to use when opening the file. + errors: How to handle decoding errors. + + Returns: + Context manager that yields an open file. + + Raises: + ValueError: if invalid inputs are provided. + """ + if 'b' in mode: + raise ValueError('Invalid mode {!r}: "b" flag not allowed when opening ' + 'file in text mode'.format(mode)) + if 't' not in mode: + mode += 't' + cm = self._open(mode, encoding, errors) + return cm + + def open_bytes(self, mode: str = 'rb') -> ContextManager[BinaryIO]: + """Return a context manager for opening the file in binary mode. + + Args: + mode: The mode to open the file in. The "b" mode is implicit if not + already present. It must not have the "t" flag. + + Returns: + Context manager that yields an open file. + + Raises: + ValueError: if invalid inputs are provided. + """ + if 't' in mode: + raise ValueError('Invalid mode {!r}: "t" flag not allowed when opening ' + 'file in binary mode'.format(mode)) + if 'b' not in mode: + mode += 'b' + cm = self._open(mode, encoding=None, errors=None) + return cm + + # TODO(b/123775699): Once pytype supports typing.Literal, use overload and + # Literal to express more precise return types. The contained type is + # currently `Any` to avoid [bad-return-type] errors in the open_* methods. + @contextlib.contextmanager + def _open( + self, + mode: str, + encoding: Optional[str] = 'utf8', + errors: Optional[str] = 'strict', + ) -> Iterator[Any]: + with open( + self.full_path, mode=mode, encoding=encoding, errors=errors + ) as fp: + yield fp + + +class _method: + """A decorator that supports both instance and classmethod invocations. + + Using similar semantics to the @property builtin, this decorator can augment + an instance method to support conditional logic when invoked on a class + object. This breaks support for invoking an instance method via the class + (e.g. Cls.method(self, ...)) but is still situationally useful. + """ + + _finstancemethod: Any + _fclassmethod: Optional[Any] + + def __init__(self, finstancemethod: Callable[..., Any]) -> None: + self._finstancemethod = finstancemethod + self._fclassmethod = None + + def classmethod(self, fclassmethod: Callable[..., Any]) -> '_method': + if isinstance(fclassmethod, classmethod): + self._fclassmethod = fclassmethod + else: + self._fclassmethod = classmethod(fclassmethod) + return self + + def __doc__(self) -> str: # type: ignore[override] + return ( + getattr(self._finstancemethod, '__doc__') + or getattr(self._fclassmethod, '__doc__') + or '' + ) + + def __get__( + self, obj: Optional[Any], type_: Optional[Type[Any]] + ) -> Callable[..., Any]: + func = self._fclassmethod if obj is None else self._finstancemethod + return func.__get__(obj, type_) # type: ignore[attribute-error, union-attr] + + +class TestCase(unittest.TestCase): + """Extension of unittest.TestCase providing more power.""" + + # When to cleanup files/directories created by our `create_tempfile()` and + # `create_tempdir()` methods after each test case completes. This does *not* + # affect e.g., files created outside of those methods, e.g., using the stdlib + # tempfile module. This can be overridden at the class level, instance level, + # or with the `cleanup` arg of `create_tempfile()` and `create_tempdir()`. See + # `TempFileCleanup` for details on the different values. + tempfile_cleanup: TempFileCleanup = TempFileCleanup.ALWAYS + + maxDiff = 80 * 20 + longMessage = True + + # Exit stacks for per-test and per-class scopes. + if sys.version_info < (3, 11): + _exit_stack = None + _cls_exit_stack = None + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + # This is to work around missing type stubs in unittest.pyi + self._outcome: Optional[Any] = getattr(self, '_outcome') + + def setUp(self): + super().setUp() + # NOTE: Only Python 3 contextlib has ExitStack and + # Python 3.11+ already has enterContext. + if hasattr(contextlib, 'ExitStack') and sys.version_info < (3, 11): + self._exit_stack = contextlib.ExitStack() + self.addCleanup(self._exit_stack.close) + + @classmethod + def setUpClass(cls): + super().setUpClass() + # NOTE: Only Python 3 contextlib has ExitStack, only Python 3.8+ has + # addClassCleanup and Python 3.11+ already has enterClassContext. + if ( + hasattr(contextlib, 'ExitStack') + and hasattr(cls, 'addClassCleanup') + and sys.version_info < (3, 11) + ): + cls._cls_exit_stack = contextlib.ExitStack() + cls.addClassCleanup(cls._cls_exit_stack.close) + + def create_tempdir( + self, + name: Optional[str] = None, + cleanup: Optional[TempFileCleanup] = None, + ) -> _TempDir: + """Create a temporary directory specific to the test. + + NOTE: The directory and its contents will be recursively cleared before + creation. This ensures that there is no pre-existing state. + + This creates a named directory on disk that is isolated to this test, and + will be properly cleaned up by the test. This avoids several pitfalls of + creating temporary directories for test purposes, as well as makes it easier + to setup directories and verify their contents. For example:: + + def test_foo(self): + out_dir = self.create_tempdir() + out_log = out_dir.create_file('output.log') + expected_outputs = [ + os.path.join(out_dir, 'data-0.txt'), + os.path.join(out_dir, 'data-1.txt'), + ] + code_under_test(out_dir) + self.assertTrue(os.path.exists(expected_paths[0])) + self.assertTrue(os.path.exists(expected_paths[1])) + self.assertEqual('foo', out_log.read_text()) + + See also: :meth:`create_tempfile` for creating temporary files. + + Args: + name: Optional name of the directory. If not given, a unique + name will be generated and used. + cleanup: Optional cleanup policy on when/if to remove the directory (and + all its contents) at the end of the test. If None, then uses + :attr:`tempfile_cleanup`. + + Returns: + A _TempDir representing the created directory; see _TempDir class docs + for usage. + """ + test_path = self._get_tempdir_path_test() + + if name: + path = os.path.join(test_path, name) + cleanup_path = os.path.join(test_path, _get_first_part(name)) + else: + os.makedirs(test_path, exist_ok=True) + path = tempfile.mkdtemp(dir=test_path) + cleanup_path = path + + _rmtree_ignore_errors(cleanup_path) + os.makedirs(path, exist_ok=True) + + self._maybe_add_temp_path_cleanup(cleanup_path, cleanup) + + return _TempDir(path) + + def create_tempfile( + self, + file_path: Optional[str] = None, + content: Optional[AnyStr] = None, + mode: str = 'w', + encoding: str = 'utf8', + errors: str = 'strict', + cleanup: Optional[TempFileCleanup] = None, + ) -> _TempFile: + """Create a temporary file specific to the test. + + This creates a named file on disk that is isolated to this test, and will + be properly cleaned up by the test. This avoids several pitfalls of + creating temporary files for test purposes, as well as makes it easier + to setup files, their data, read them back, and inspect them when + a test fails. For example:: + + def test_foo(self): + output = self.create_tempfile() + code_under_test(output) + self.assertGreater(os.path.getsize(output), 0) + self.assertEqual('foo', output.read_text()) + + NOTE: This will zero-out the file. This ensures there is no pre-existing + state. + NOTE: If the file already exists, it will be made writable and overwritten. + + See also: :meth:`create_tempdir` for creating temporary directories, and + ``_TempDir.create_file`` for creating files within a temporary directory. + + Args: + file_path: Optional file path for the temp file. If not given, a unique + file name will be generated and used. Slashes are allowed in the name; + any missing intermediate directories will be created. NOTE: This path is + the path that will be cleaned up, including any directories in the path, + e.g., ``'foo/bar/baz.txt'`` will ``rm -r foo``. + content: Optional string or + bytes to initially write to the file. If not + specified, then an empty file is created. + mode: Mode string to use when writing content. Only used if `content` is + non-empty. + encoding: Encoding to use when writing string content. Only used if + `content` is text. + errors: How to handle text to bytes encoding errors. Only used if + `content` is text. + cleanup: Optional cleanup policy on when/if to remove the directory (and + all its contents) at the end of the test. If None, then uses + :attr:`tempfile_cleanup`. + + Returns: + A _TempFile representing the created file; see _TempFile class docs for + usage. + """ + test_path = self._get_tempdir_path_test() + tf, cleanup_path = _TempFile._create(test_path, file_path, content=content, + mode=mode, encoding=encoding, + errors=errors) + self._maybe_add_temp_path_cleanup(cleanup_path, cleanup) + return tf + + @_method + def enter_context(self, manager: ContextManager[_T]) -> _T: + """Returns the CM's value after registering it with the exit stack. + + Entering a context pushes it onto a stack of contexts. When `enter_context` + is called on the test instance (e.g. `self.enter_context`), the context is + exited after the test case's tearDown call. When called on the test class + (e.g. `TestCase.enter_context`), the context is exited after the test + class's tearDownClass call. + + Contexts are exited in the reverse order of entering. They will always + be exited, regardless of test failure/success. + + This is useful to eliminate per-test boilerplate when context managers + are used. For example, instead of decorating every test with `@mock.patch`, + simply do `self.foo = self.enter_context(mock.patch(...))' in `setUp()`. + + NOTE: The context managers will always be exited without any error + information. This is an unfortunate implementation detail due to some + internals of how unittest runs tests. + + Args: + manager: The context manager to enter. + """ + if sys.version_info >= (3, 11): + return self.enterContext(manager) + + if not self._exit_stack: + raise AssertionError( + 'self._exit_stack is not set: enter_context is Py3-only; also make ' + 'sure that AbslTest.setUp() is called.') + return self._exit_stack.enter_context(manager) + + @enter_context.classmethod + @classmethod + def _enter_context_cls(cls, manager: ContextManager[_T]) -> _T: + if sys.version_info >= (3, 11): + return cls.enterClassContext(manager) + + if not cls._cls_exit_stack: + raise AssertionError( + 'cls._cls_exit_stack is not set: cls.enter_context requires ' + 'Python 3.8+; also make sure that AbslTest.setUpClass() is called.') + return cls._cls_exit_stack.enter_context(manager) + + @classmethod + def _get_tempdir_path_cls(cls) -> str: + return os.path.join(TEST_TMPDIR.value, + cls.__qualname__.replace('__main__.', '')) + + def _get_tempdir_path_test(self) -> str: + return os.path.join(self._get_tempdir_path_cls(), self._testMethodName) + + def _get_tempfile_cleanup( + self, override: Optional[TempFileCleanup] + ) -> TempFileCleanup: + if override is not None: + return override + return self.tempfile_cleanup + + def _maybe_add_temp_path_cleanup( + self, path: str, cleanup: Optional[TempFileCleanup] + ) -> None: + cleanup = self._get_tempfile_cleanup(cleanup) + if cleanup == TempFileCleanup.OFF: + return + elif cleanup == TempFileCleanup.ALWAYS: + self.addCleanup(_rmtree_ignore_errors, path) + elif cleanup == TempFileCleanup.SUCCESS: + self._internal_add_cleanup_on_success(_rmtree_ignore_errors, path) + else: + raise AssertionError(f'Unexpected cleanup value: {cleanup}') + + def _internal_add_cleanup_on_success( + self, + function: Callable[..., Any], + *args: Any, + **kwargs: Any, + ) -> None: + """Adds `function` as cleanup when the test case succeeds.""" + outcome = self._outcome + assert outcome is not None + previous_failure_count = ( + len(outcome.result.failures) + + len(outcome.result.errors) + + len(outcome.result.unexpectedSuccesses) + ) + def _call_cleaner_on_success(*args, **kwargs): + if not self._internal_ran_and_passed_when_called_during_cleanup( + previous_failure_count): + return + function(*args, **kwargs) + self.addCleanup(_call_cleaner_on_success, *args, **kwargs) + + def _internal_ran_and_passed_when_called_during_cleanup( + self, + previous_failure_count: int, + ) -> bool: + """Returns whether test is passed. Expected to be called during cleanup.""" + outcome = self._outcome + if sys.version_info[:2] >= (3, 11): + assert outcome is not None + current_failure_count = ( + len(outcome.result.failures) + + len(outcome.result.errors) + + len(outcome.result.unexpectedSuccesses) + ) + return current_failure_count == previous_failure_count + else: + # Before Python 3.11 https://github.com/python/cpython/pull/28180, errors + # were bufferred in _Outcome before calling cleanup. + result = self.defaultTestResult() + self._feedErrorsToResult(result, outcome.errors) # pytype: disable=attribute-error + return result.wasSuccessful() + + def shortDescription(self) -> str: + """Formats both the test method name and the first line of its docstring. + + If no docstring is given, only returns the method name. + + This method overrides unittest.TestCase.shortDescription(), which + only returns the first line of the docstring, obscuring the name + of the test upon failure. + + Returns: + desc: A short description of a test method. + """ + desc = self.id() + + # Omit the main name so that test name can be directly copy/pasted to + # the command line. + if desc.startswith('__main__.'): + desc = desc[len('__main__.'):] + + # NOTE: super() is used here instead of directly invoking + # unittest.TestCase.shortDescription(self), because of the + # following line that occurs later on: + # unittest.TestCase = TestCase + # Because of this, direct invocation of what we think is the + # superclass will actually cause infinite recursion. + doc_first_line = super().shortDescription() + if doc_first_line is not None: + desc = '\n'.join((desc, doc_first_line)) + return desc + + def assertStartsWith(self, actual, expected_start, msg=None): + """Asserts that actual.startswith(expected_start) is True. + + Args: + actual: str + expected_start: str + msg: Optional message to report on failure. + """ + if not actual.startswith(expected_start): + self.fail('%r does not start with %r' % (actual, expected_start), msg) + + def assertNotStartsWith(self, actual, unexpected_start, msg=None): + """Asserts that actual.startswith(unexpected_start) is False. + + Args: + actual: str + unexpected_start: str + msg: Optional message to report on failure. + """ + if actual.startswith(unexpected_start): + self.fail('%r does start with %r' % (actual, unexpected_start), msg) + + def assertEndsWith(self, actual, expected_end, msg=None): + """Asserts that actual.endswith(expected_end) is True. + + Args: + actual: str + expected_end: str + msg: Optional message to report on failure. + """ + if not actual.endswith(expected_end): + self.fail('%r does not end with %r' % (actual, expected_end), msg) + + def assertNotEndsWith(self, actual, unexpected_end, msg=None): + """Asserts that actual.endswith(unexpected_end) is False. + + Args: + actual: str + unexpected_end: str + msg: Optional message to report on failure. + """ + if actual.endswith(unexpected_end): + self.fail('%r does end with %r' % (actual, unexpected_end), msg) + + def assertSequenceStartsWith(self, prefix, whole, msg=None): + """An equality assertion for the beginning of ordered sequences. + + If prefix is an empty sequence, it will raise an error unless whole is also + an empty sequence. + + If prefix is not a sequence, it will raise an error if the first element of + whole does not match. + + Args: + prefix: A sequence expected at the beginning of the whole parameter. + whole: The sequence in which to look for prefix. + msg: Optional message to report on failure. + """ + try: + prefix_len = len(prefix) + except (TypeError, NotImplementedError): + prefix = [prefix] + prefix_len = 1 + + if isinstance(whole, abc.Mapping) or isinstance(whole, abc.Set): + self.fail( + 'For whole: Mapping or Set objects are not supported, found type: %s' + % type(whole), + msg, + ) + try: + whole_len = len(whole) + except (TypeError, NotImplementedError): + self.fail('For whole: len(%s) is not supported, it appears to be type: ' + '%s' % (whole, type(whole)), msg) + + assert prefix_len <= whole_len, self._formatMessage( + msg, + 'Prefix length (%d) is longer than whole length (%d).' % + (prefix_len, whole_len) + ) + + if not prefix_len and whole_len: + self.fail('Prefix length is 0 but whole length is %d: %s' % + (len(whole), whole), msg) + + try: + self.assertSequenceEqual(prefix, whole[:prefix_len], msg) + except AssertionError: + self.fail('prefix: %s not found at start of whole: %s.' % + (prefix, whole), msg) + + def assertEmpty(self, container, msg=None): + """Asserts that an object has zero length. + + Args: + container: Anything that implements the collections.abc.Sized interface. + msg: Optional message to report on failure. + """ + if not isinstance(container, abc.Sized): + self.fail('Expected a Sized object, got: ' + '{!r}'.format(type(container).__name__), msg) + + # explicitly check the length since some Sized objects (e.g. numpy.ndarray) + # have strange __nonzero__/__bool__ behavior. + if len(container): # pylint: disable=g-explicit-length-test + self.fail(f'{container!r} has length of {len(container)}.', msg) + + def assertNotEmpty(self, container, msg=None): + """Asserts that an object has non-zero length. + + Args: + container: Anything that implements the collections.abc.Sized interface. + msg: Optional message to report on failure. + """ + if not isinstance(container, abc.Sized): + self.fail('Expected a Sized object, got: ' + '{!r}'.format(type(container).__name__), msg) + + # explicitly check the length since some Sized objects (e.g. numpy.ndarray) + # have strange __nonzero__/__bool__ behavior. + if not len(container): # pylint: disable=g-explicit-length-test + self.fail(f'{container!r} has length of 0.', msg) + + def assertLen(self, container, expected_len, msg=None): + """Asserts that an object has the expected length. + + Args: + container: Anything that implements the collections.abc.Sized interface. + expected_len: The expected length of the container. + msg: Optional message to report on failure. + """ + if not isinstance(container, abc.Sized): + self.fail('Expected a Sized object, got: ' + '{!r}'.format(type(container).__name__), msg) + if len(container) != expected_len: + container_repr = unittest.util.safe_repr(container) # pytype: disable=module-attr + self.fail('{} has length of {}, expected {}.'.format( + container_repr, len(container), expected_len), msg) + + def assertSequenceAlmostEqual(self, expected_seq, actual_seq, places=None, + msg=None, delta=None): + """An approximate equality assertion for ordered sequences. + + Fail if the two sequences are unequal as determined by their value + differences rounded to the given number of decimal places (default 7) and + comparing to zero, or by comparing that the difference between each value + in the two sequences is more than the given delta. + + Note that decimal places (from zero) are usually not the same as significant + digits (measured from the most significant digit). + + If the two sequences compare equal then they will automatically compare + almost equal. + + Args: + expected_seq: A sequence containing elements we are expecting. + actual_seq: The sequence that we are testing. + places: The number of decimal places to compare. + msg: The message to be printed if the test fails. + delta: The OK difference between compared values. + """ + if len(expected_seq) != len(actual_seq): + self.fail('Sequence size mismatch: {} vs {}'.format( + len(expected_seq), len(actual_seq)), msg) + + err_list = [] + for idx, (exp_elem, act_elem) in enumerate(zip(expected_seq, actual_seq)): + try: + # assertAlmostEqual should be called with at most one of `places` and + # `delta`. However, it's okay for assertSequenceAlmostEqual to pass + # both because we want the latter to fail if the former does. + # pytype: disable=wrong-keyword-args + self.assertAlmostEqual(exp_elem, act_elem, places=places, msg=msg, + delta=delta) + # pytype: enable=wrong-keyword-args + except self.failureException as err: + err_list.append(f'At index {idx}: {err}') + + if err_list: + if len(err_list) > 30: + err_list = err_list[:30] + ['...'] + msg = self._formatMessage(msg, '\n'.join(err_list)) + self.fail(msg) + + def assertContainsSubset(self, expected_subset, actual_set, msg=None): + """Checks whether actual iterable is a superset of expected iterable.""" + missing = set(expected_subset) - set(actual_set) + if not missing: + return + + self.fail('Missing elements %s\nExpected: %s\nActual: %s' % ( + missing, expected_subset, actual_set), msg) + + def assertNoCommonElements(self, expected_seq, actual_seq, msg=None): + """Checks whether actual iterable and expected iterable are disjoint.""" + common = set(expected_seq) & set(actual_seq) + if not common: + return + + self.fail('Common elements %s\nExpected: %s\nActual: %s' % ( + common, expected_seq, actual_seq), msg) + + def assertItemsEqual(self, expected_seq, actual_seq, msg=None): + """Deprecated, please use assertCountEqual instead. + + This is equivalent to assertCountEqual. + + Args: + expected_seq: A sequence containing elements we are expecting. + actual_seq: The sequence that we are testing. + msg: The message to be printed if the test fails. + """ + super().assertCountEqual(expected_seq, actual_seq, msg) + + def assertSameElements(self, expected_seq, actual_seq, msg=None): + """Asserts that two sequences have the same elements (in any order). + + This method, unlike assertCountEqual, doesn't care about any + duplicates in the expected and actual sequences:: + + # Doesn't raise an AssertionError + assertSameElements([1, 1, 1, 0, 0, 0], [0, 1]) + + If possible, you should use assertCountEqual instead of + assertSameElements. + + Args: + expected_seq: A sequence containing elements we are expecting. + actual_seq: The sequence that we are testing. + msg: The message to be printed if the test fails. + """ + # `unittest2.TestCase` used to have assertSameElements, but it was + # removed in favor of assertItemsEqual. As there's a unit test + # that explicitly checks this behavior, I am leaving this method + # alone. + # Fail on strings: empirically, passing strings to this test method + # is almost always a bug. If comparing the character sets of two strings + # is desired, cast the inputs to sets or lists explicitly. + if (isinstance(expected_seq, _TEXT_OR_BINARY_TYPES) or + isinstance(actual_seq, _TEXT_OR_BINARY_TYPES)): + self.fail('Passing string/bytes to assertSameElements is usually a bug. ' + 'Did you mean to use assertEqual?\n' + 'Expected: %s\nActual: %s' % (expected_seq, actual_seq)) + try: + expected = {element: None for element in expected_seq} + actual = {element: None for element in actual_seq} + missing = [element for element in expected if element not in actual] + unexpected = [element for element in actual if element not in expected] + missing.sort() + unexpected.sort() + except TypeError: + # Fall back to slower list-compare if any of the objects are + # not hashable. + expected = list(expected_seq) + actual = list(actual_seq) + expected.sort() + actual.sort() + missing, unexpected = _sorted_list_difference(expected, actual) + errors = [] + if msg: + errors.extend((msg, ':\n')) + if missing: + errors.append('Expected, but missing:\n %r\n' % missing) + if unexpected: + errors.append('Unexpected, but present:\n %r\n' % unexpected) + if missing or unexpected: + self.fail(''.join(errors)) + + # unittest.TestCase.assertMultiLineEqual works very similarly, but it + # has a different error format. However, I find this slightly more readable. + def assertMultiLineEqual(self, first, second, msg=None, **kwargs): + """Asserts that two multi-line strings are equal.""" + assert isinstance(first, + str), ('First argument is not a string: %r' % (first,)) + assert isinstance(second, + str), ('Second argument is not a string: %r' % (second,)) + line_limit = kwargs.pop('line_limit', 0) + if kwargs: + raise TypeError(f'Unexpected keyword args {tuple(kwargs)}') + + if first == second: + return + if msg: + failure_message = [msg + ':\n'] + else: + failure_message = ['\n'] + if line_limit: + line_limit += len(failure_message) + for line in difflib.ndiff(first.splitlines(True), second.splitlines(True)): + failure_message.append(line) + if not line.endswith('\n'): + failure_message.append('\n') + if line_limit and len(failure_message) > line_limit: + n_omitted = len(failure_message) - line_limit + failure_message = failure_message[:line_limit] + failure_message.append( + '(... and {} more delta lines omitted for brevity.)\n'.format( + n_omitted)) + + raise self.failureException(''.join(failure_message)) + + def assertBetween(self, value, minv, maxv, msg=None): + """Asserts that value is between minv and maxv (inclusive).""" + msg = self._formatMessage(msg, + '"%r" unexpectedly not between "%r" and "%r"' % + (value, minv, maxv)) + self.assertTrue(minv <= value, msg) + self.assertTrue(maxv >= value, msg) + + def assertRegexMatch(self, actual_str, regexes, message=None): + r"""Asserts that at least one regex in regexes matches str. + + If possible you should use `assertRegex`, which is a simpler + version of this method. `assertRegex` takes a single regular + expression (a string or re compiled object) instead of a list. + + Notes: + + 1. This function uses substring matching, i.e. the matching + succeeds if *any* substring of the error message matches *any* + regex in the list. This is more convenient for the user than + full-string matching. + + 2. If regexes is the empty list, the matching will always fail. + + 3. Use regexes=[''] for a regex that will always pass. + + 4. '.' matches any single character *except* the newline. To + match any character, use '(.|\n)'. + + 5. '^' matches the beginning of each line, not just the beginning + of the string. Similarly, '$' matches the end of each line. + + 6. An exception will be thrown if regexes contains an invalid + regex. + + Args: + actual_str: The string we try to match with the items in regexes. + regexes: The regular expressions we want to match against str. + See "Notes" above for detailed notes on how this is interpreted. + message: The message to be printed if the test fails. + """ + if isinstance(regexes, _TEXT_OR_BINARY_TYPES): + self.fail('regexes is string or bytes; use assertRegex instead.', + message) + if not regexes: + self.fail('No regexes specified.', message) + + regex_type = type(regexes[0]) + for regex in regexes[1:]: + if type(regex) is not regex_type: # pylint: disable=unidiomatic-typecheck + self.fail('regexes list must all be the same type.', message) + + if regex_type is bytes and isinstance(actual_str, str): + regexes = [regex.decode('utf-8') for regex in regexes] + regex_type = str + elif regex_type is str and isinstance(actual_str, bytes): + regexes = [regex.encode('utf-8') for regex in regexes] + regex_type = bytes + + if regex_type is str: + regex = '(?:%s)' % ')|(?:'.join(regexes) + elif regex_type is bytes: + regex = b'(?:' + (b')|(?:'.join(regexes)) + b')' + else: + self.fail('Only know how to deal with unicode str or bytes regexes.', + message) + + if not re.search(regex, actual_str, re.MULTILINE): + self.fail('"%s" does not contain any of these regexes: %s.' % + (actual_str, regexes), message) + + def assertCommandSucceeds(self, command, regexes=(b'',), env=None, + close_fds=True, msg=None): + """Asserts that a shell command succeeds (i.e. exits with code 0). + + Args: + command: List or string representing the command to run. + regexes: List of regular expression byte strings that match success. + env: Dictionary of environment variable settings. If None, no environment + variables will be set for the child process. This is to make tests + more hermetic. NOTE: this behavior is different than the standard + subprocess module. + close_fds: Whether or not to close all open fd's in the child after + forking. + msg: Optional message to report on failure. + """ + (ret_code, err) = get_command_stderr(command, env, close_fds) + + # We need bytes regexes here because `err` is bytes. + # Accommodate code which listed their output regexes w/o the b'' prefix by + # converting them to bytes for the user. + if isinstance(regexes[0], str): + regexes = [regex.encode('utf-8') for regex in regexes] + + command_string = get_command_string(command) + self.assertEqual( + ret_code, 0, + self._formatMessage(msg, + 'Running command\n' + '%s failed with error code %s and message\n' + '%s' % (_quote_long_string(command_string), + ret_code, + _quote_long_string(err))) + ) + self.assertRegexMatch( + err, + regexes, + message=self._formatMessage( + msg, + 'Running command\n' + '%s failed with error code %s and message\n' + '%s which matches no regex in %s' % ( + _quote_long_string(command_string), + ret_code, + _quote_long_string(err), + regexes))) + + def assertCommandFails(self, command, regexes, env=None, close_fds=True, + msg=None): + """Asserts a shell command fails and the error matches a regex in a list. + + Args: + command: List or string representing the command to run. + regexes: the list of regular expression strings. + env: Dictionary of environment variable settings. If None, no environment + variables will be set for the child process. This is to make tests + more hermetic. NOTE: this behavior is different than the standard + subprocess module. + close_fds: Whether or not to close all open fd's in the child after + forking. + msg: Optional message to report on failure. + """ + (ret_code, err) = get_command_stderr(command, env, close_fds) + + # We need bytes regexes here because `err` is bytes. + # Accommodate code which listed their output regexes w/o the b'' prefix by + # converting them to bytes for the user. + if isinstance(regexes[0], str): + regexes = [regex.encode('utf-8') for regex in regexes] + + command_string = get_command_string(command) + self.assertNotEqual( + ret_code, 0, + self._formatMessage(msg, 'The following command succeeded ' + 'while expected to fail:\n%s' % + _quote_long_string(command_string))) + self.assertRegexMatch( + err, + regexes, + message=self._formatMessage( + msg, + 'Running command\n' + '%s failed with error code %s and message\n' + '%s which matches no regex in %s' % ( + _quote_long_string(command_string), + ret_code, + _quote_long_string(err), + regexes))) + + class _AssertRaisesContext: + + def __init__(self, expected_exception, test_case, test_func, msg=None): + self.expected_exception = expected_exception + self.test_case = test_case + self.test_func = test_func + self.msg = msg + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + if exc_type is None: + self.test_case.fail(self.expected_exception.__name__ + ' not raised', + self.msg) + if not issubclass(exc_type, self.expected_exception): + return False + self.test_func(exc_value) + if exc_value: + self.exception = exc_value.with_traceback(None) + return True + + @typing.overload + def assertRaisesWithPredicateMatch( + self, expected_exception, predicate) -> _AssertRaisesContext: + # The purpose of this return statement is to work around + # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored. + return self._AssertRaisesContext(None, None, None) + + @typing.overload + def assertRaisesWithPredicateMatch( + self, expected_exception, predicate, callable_obj: Callable[..., Any], + *args, **kwargs) -> None: + # The purpose of this return statement is to work around + # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored. + return self._AssertRaisesContext(None, None, None) # type: ignore[return-value] + + def assertRaisesWithPredicateMatch(self, expected_exception, predicate, + callable_obj=None, *args, **kwargs): + """Asserts that exception is thrown and predicate(exception) is true. + + Args: + expected_exception: Exception class expected to be raised. + predicate: Function of one argument that inspects the passed-in exception + and returns True (success) or False (please fail the test). + callable_obj: Function to be called. + *args: Extra args. + **kwargs: Extra keyword args. + + Returns: + A context manager if callable_obj is None. Otherwise, None. + + Raises: + self.failureException if callable_obj does not raise a matching exception. + """ + def Check(err): + self.assertTrue(predicate(err), + '%r does not match predicate %r' % (err, predicate)) + + context = self._AssertRaisesContext(expected_exception, self, Check) + if callable_obj is None: + return context + with context: + callable_obj(*args, **kwargs) + + @typing.overload + def assertRaisesWithLiteralMatch( + self, expected_exception, expected_exception_message + ) -> _AssertRaisesContext: + # The purpose of this return statement is to work around + # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored. + return self._AssertRaisesContext(None, None, None) + + @typing.overload + def assertRaisesWithLiteralMatch( + self, expected_exception, expected_exception_message, + callable_obj: Callable[..., Any], *args, **kwargs) -> None: + # The purpose of this return statement is to work around + # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored. + return self._AssertRaisesContext(None, None, None) # type: ignore[return-value] + + def assertRaisesWithLiteralMatch(self, expected_exception, + expected_exception_message, + callable_obj=None, *args, **kwargs): + """Asserts that the message in a raised exception equals the given string. + + Unlike assertRaisesRegex, this method takes a literal string, not + a regular expression. + + with self.assertRaisesWithLiteralMatch(ExType, 'message'): + DoSomething() + + Args: + expected_exception: Exception class expected to be raised. + expected_exception_message: String message expected in the raised + exception. For a raise exception e, expected_exception_message must + equal str(e). + callable_obj: Function to be called, or None to return a context. + *args: Extra args. + **kwargs: Extra kwargs. + + Returns: + A context manager if callable_obj is None. Otherwise, None. + + Raises: + self.failureException if callable_obj does not raise a matching exception. + """ + def Check(err): + actual_exception_message = str(err) + self.assertTrue(expected_exception_message == actual_exception_message, + 'Exception message does not match.\n' + 'Expected: %r\n' + 'Actual: %r' % (expected_exception_message, + actual_exception_message)) + + context = self._AssertRaisesContext(expected_exception, self, Check) + if callable_obj is None: + return context + with context: + callable_obj(*args, **kwargs) + + def assertContainsInOrder(self, strings, target, msg=None): + """Asserts that the strings provided are found in the target in order. + + This may be useful for checking HTML output. + + Args: + strings: A list of strings, such as [ 'fox', 'dog' ] + target: A target string in which to look for the strings, such as + 'The quick brown fox jumped over the lazy dog'. + msg: Optional message to report on failure. + """ + if isinstance(strings, (bytes, unicode if str is bytes else str)): + strings = (strings,) + + current_index = 0 + last_string = None + for string in strings: + index = target.find(str(string), current_index) + if index == -1 and current_index == 0: + self.fail("Did not find '%s' in '%s'" % + (string, target), msg) + elif index == -1: + self.fail("Did not find '%s' after '%s' in '%s'" % + (string, last_string, target), msg) + last_string = string + current_index = index + + def assertContainsSubsequence(self, container, subsequence, msg=None): + """Asserts that "container" contains "subsequence" as a subsequence. + + Asserts that "container" contains all the elements of "subsequence", in + order, but possibly with other elements interspersed. For example, [1, 2, 3] + is a subsequence of [0, 0, 1, 2, 0, 3, 0] but not of [0, 0, 1, 3, 0, 2, 0]. + + Args: + container: the list we're testing for subsequence inclusion. + subsequence: the list we hope will be a subsequence of container. + msg: Optional message to report on failure. + """ + first_nonmatching = None + reversed_container = list(reversed(container)) + subsequence = list(subsequence) + + for e in subsequence: + if e not in reversed_container: + first_nonmatching = e + break + while e != reversed_container.pop(): + pass + + if first_nonmatching is not None: + self.fail('%s not a subsequence of %s. First non-matching element: %s' % + (subsequence, container, first_nonmatching), msg) + + def assertContainsExactSubsequence(self, container, subsequence, msg=None): + """Asserts that "container" contains "subsequence" as an exact subsequence. + + Asserts that "container" contains all the elements of "subsequence", in + order, and without other elements interspersed. For example, [1, 2, 3] is an + exact subsequence of [0, 0, 1, 2, 3, 0] but not of [0, 0, 1, 2, 0, 3, 0]. + + Args: + container: the list we're testing for subsequence inclusion. + subsequence: the list we hope will be an exact subsequence of container. + msg: Optional message to report on failure. + """ + container = list(container) + subsequence = list(subsequence) + longest_match = 0 + + for start in range(1 + len(container) - len(subsequence)): + if longest_match == len(subsequence): + break + index = 0 + while (index < len(subsequence) and + subsequence[index] == container[start + index]): + index += 1 + longest_match = max(longest_match, index) + + if longest_match < len(subsequence): + self.fail('%s not an exact subsequence of %s. ' + 'Longest matching prefix: %s' % + (subsequence, container, subsequence[:longest_match]), msg) + + def assertTotallyOrdered(self, *groups, **kwargs): + """Asserts that total ordering has been implemented correctly. + + For example, say you have a class A that compares only on its attribute x. + Comparators other than ``__lt__`` are omitted for brevity:: + + class A(object): + def __init__(self, x, y): + self.x = x + self.y = y + + def __hash__(self): + return hash(self.x) + + def __lt__(self, other): + try: + return self.x < other.x + except AttributeError: + return NotImplemented + + assertTotallyOrdered will check that instances can be ordered correctly. + For example:: + + self.assertTotallyOrdered( + [None], # None should come before everything else. + [1], # Integers sort earlier. + [A(1, 'a')], + [A(2, 'b')], # 2 is after 1. + [A(3, 'c'), A(3, 'd')], # The second argument is irrelevant. + [A(4, 'z')], + ['foo']) # Strings sort last. + + Args: + *groups: A list of groups of elements. Each group of elements is a list + of objects that are equal. The elements in each group must be less + than the elements in the group after it. For example, these groups are + totally ordered: ``[None]``, ``[1]``, ``[2, 2]``, ``[3]``. + **kwargs: optional msg keyword argument can be passed. + """ + + def CheckOrder(small, big): + """Ensures small is ordered before big.""" + self.assertFalse(small == big, + self._formatMessage(msg, '%r unexpectedly equals %r' % + (small, big))) + self.assertTrue(small != big, + self._formatMessage(msg, '%r unexpectedly equals %r' % + (small, big))) + self.assertLess(small, big, msg) + self.assertFalse(big < small, + self._formatMessage(msg, + '%r unexpectedly less than %r' % + (big, small))) + self.assertLessEqual(small, big, msg) + self.assertFalse(big <= small, self._formatMessage( + '%r unexpectedly less than or equal to %r' % (big, small), msg + )) + self.assertGreater(big, small, msg) + self.assertFalse(small > big, + self._formatMessage(msg, + '%r unexpectedly greater than %r' % + (small, big))) + self.assertGreaterEqual(big, small) + self.assertFalse(small >= big, self._formatMessage( + msg, + '%r unexpectedly greater than or equal to %r' % (small, big))) + + def CheckEqual(a, b): + """Ensures that a and b are equal.""" + self.assertEqual(a, b, msg) + self.assertFalse(a != b, + self._formatMessage(msg, '%r unexpectedly unequals %r' % + (a, b))) + + # Objects that compare equal must hash to the same value, but this only + # applies if both objects are hashable. + if (isinstance(a, abc.Hashable) and + isinstance(b, abc.Hashable)): + self.assertEqual( + hash(a), hash(b), + self._formatMessage( + msg, 'hash %d of %r unexpectedly not equal to hash %d of %r' % + (hash(a), a, hash(b), b))) + + self.assertFalse(a < b, + self._formatMessage(msg, + '%r unexpectedly less than %r' % + (a, b))) + self.assertFalse(b < a, + self._formatMessage(msg, + '%r unexpectedly less than %r' % + (b, a))) + self.assertLessEqual(a, b, msg) + self.assertLessEqual(b, a, msg) # pylint: disable=arguments-out-of-order + self.assertFalse(a > b, + self._formatMessage(msg, + '%r unexpectedly greater than %r' % + (a, b))) + self.assertFalse(b > a, + self._formatMessage(msg, + '%r unexpectedly greater than %r' % + (b, a))) + self.assertGreaterEqual(a, b, msg) + self.assertGreaterEqual(b, a, msg) # pylint: disable=arguments-out-of-order + + msg = kwargs.get('msg') + + # For every combination of elements, check the order of every pair of + # elements. + for elements in itertools.product(*groups): + elements = list(elements) + for index, small in enumerate(elements[:-1]): + for big in elements[index + 1:]: + CheckOrder(small, big) + + # Check that every element in each group is equal. + for group in groups: + for a in group: + CheckEqual(a, a) + for a, b in itertools.product(group, group): + CheckEqual(a, b) + + def assertDictContainsSubset( + self, subset: Mapping[Any, Any], dictionary: Mapping[Any, Any], msg=None + ): + """Raises AssertionError if "dictionary" is not a superset of "subset". + + Args: + subset: A dict, the expected subset of the "dictionary". + dictionary: A dict, the actual value. + msg: An optional str, the associated message. + + Raises: + AssertionError: if "dictionary" is not a superset of "subset". + """ + self.assertDictEqual({**dictionary}, {**dictionary, **subset}, msg) + + def assertDictEqual(self, a, b, msg=None): + """Raises AssertionError if a and b are not equal dictionaries. + + Args: + a: A dict, the expected value. + b: A dict, the actual value. + msg: An optional str, the associated message. + + Raises: + AssertionError: if the dictionaries are not equal. + """ + self.assertMappingEqual(a, b, msg, mapping_type=dict) + + def assertDictAlmostEqual( + self, + a, + b, + places=None, + msg=None, + delta=None, + ): + """Raises AssertionError if a and b are not equal or almost equal dicts. + + This is like assertDictEqual, except for float values which are compared + using assertAlmostEqual. Almost equality is determined for float values by: + - have numeric difference less than the given delta, + or + - equal if rounded to the given number of decimal places after the decimal + point (default 7). + + Args: + a: A dict, the expected value. + b: A dict, the actual value. + places: The number of decimal places to compare for floats. + msg: An optional str, the associated message. + delta: The OK difference between compared values for floats. + + Raises: + AssertionError: if the dictionaries are not equal or almost equal. + ValueError: if both places and delta are specified. + """ + + # Almost equality with preset places and delta. + def almost_equal_compare(a_value, b_value): + if isinstance(a_value, numbers.Number) and isinstance( + b_value, numbers.Number + ): + try: + # assertAlmostEqual should be called with at most one of `places` + # and `delta`. However, it's okay for assertMappingEqual to pass + # both because we want the latter to fail if the former does. + # pytype: disable=wrong-keyword-args + self.assertAlmostEqual( + a_value, + b_value, + places=places, + delta=delta, + ) + # pytype: enable=wrong-keyword-args + except self.failureException as err: + return False, err + return True, None + else: + # Fall back to regular equality check if the values are not numbers. + try: + self.assertEqual(a_value, b_value) + except self.failureException as err: + return False, err + return True, None + + if delta is not None and places is not None: + raise ValueError('specify delta or places not both\n') + + self.assertMappingEqual( + a, + b, + msg, + mapping_type=dict, + check_values_equality=almost_equal_compare, + ) + + def assertMappingEqual( + self, + a, + b, + msg=None, + mapping_type=collections.abc.Mapping, + check_values_equality=lambda x, y: (x == y, None), + ): + """Raises AssertionError if a and b differ in keys or values. + + Key sets must be exectly the same, the corresponding values should satisfy + the provided equality function. + + Args: + a: A mapping, the expected value. + b: A mapping, the actual value. + msg: An optional str, the associated message. + mapping_type: The expected type of the mappings. + check_values_equality: A function that takes two values and returns a + tuple of (bool, BaseException), where the bool is True if the values are + equal and the BaseException is an optional exception occured during the + equality check. + + Raises: + AssertionError: if the dictionaries are not equal. + """ + + if not isinstance(a, mapping_type): + self.fail( + f'a should be a {mapping_type.__name__}, found type:' + f' {type(a).__name__}', + msg, + ) + if not isinstance(b, mapping_type): + self.fail( + f'b should be a {mapping_type.__name__}, found type:' + f' {type(b).__name__}', + msg, + ) + if a == b: + return + + def Sorted(list_of_items): + try: + return sorted(list_of_items) # In 3.3, unordered are possible. + except TypeError: + return list_of_items + + a_items = Sorted(list(a.items())) + b_items = Sorted(list(b.items())) + + unexpected = [] + missing = [] + different = [] + + # The standard library default output confounds lexical difference with + # value difference; treat them separately. + for a_key, a_value in a_items: + if a_key not in b: + missing.append((a_key, a_value)) + continue + b_value = b[a_key] + is_equal, err = check_values_equality(a_value, b_value) + if not is_equal: + different.append((a_key, a_value, b_value, err)) + + for b_key, b_value in b_items: + if b_key not in a: + unexpected.append((b_key, b_value)) + + # If all difference buckets are empty, then mappings are considered equal. + if not unexpected and not different and not missing: + return + + safe_repr = unittest.util.safe_repr # pytype: disable=module-attr + + def Repr(dikt): + """Deterministic repr for dict.""" + # Sort the entries based on their repr, not based on their sort order, + # which will be non-deterministic across executions, for many types. + entries = sorted((safe_repr(k), safe_repr(v)) for k, v in dikt.items()) + return '{' + ', '.join(f'{k}: {v}' for k, v in entries) + '}' + + message = [f'{Repr(a)} != {Repr(b)}{"("+msg+")" if msg else ""}'] + + if unexpected: + message.append( + 'Unexpected, but present entries:\n' + + ''.join(f'{safe_repr(k)}: {safe_repr(v)}\n' for k, v in unexpected) + ) + + if different: + message.append( + 'repr() of differing entries:\n' + + ''.join( + f'{safe_repr(k)}: ' + f'{err if err else f"{safe_repr(a_value)} != {safe_repr(b_value)}"}\n' + for k, a_value, b_value, err in different + ) + ) + + if missing: + message.append( + 'Missing entries:\n' + + ''.join(f'{safe_repr(k)}: {safe_repr(v)}\n' for k, v in missing) + ) + + raise self.failureException('\n'.join(message)) + + def assertDataclassEqual(self, first, second, msg=None): + """Asserts two dataclasses are equal with more informative errors. + + Arguments must both be dataclasses. This compares equality of individual + fields and takes care to not compare fields that are marked as + non-comparable. It gives per field differences, which are easier to parse + than the comparison of the string representations from assertEqual. + + In cases where the dataclass has a custom __eq__, and it is defined in a + way that is inconsistent with equality of comparable fields, we raise an + exception without further trying to figure out how they are different. + + Args: + first: A dataclass, the first value. + second: A dataclass, the second value. + msg: An optional str, the associated message. + + Raises: + AssertionError: if the dataclasses are not equal. + """ + + if not dataclasses.is_dataclass(first) or isinstance(first, type): + raise self.failureException('First argument is not a dataclass instance.') + if not dataclasses.is_dataclass(second) or isinstance(second, type): + raise self.failureException( + 'Second argument is not a dataclass instance.' + ) + + if first == second: + return + + if type(first) is not type(second): + self.fail( + 'Found different dataclass types: %s != %s' + % (type(first), type(second)), + msg, + ) + + # Make sure to skip fields that are marked compare=False. + different = [ + (f.name, getattr(first, f.name), getattr(second, f.name)) + for f in dataclasses.fields(first) + if f.compare and getattr(first, f.name) != getattr(second, f.name) + ] + + safe_repr = unittest.util.safe_repr # pytype: disable=module-attr + message = ['%s != %s' % (safe_repr(first), safe_repr(second))] + if different: + message.append('Fields that differ:') + message.extend( + '%s: %s != %s' % (k, safe_repr(first_v), safe_repr(second_v)) + for k, first_v, second_v in different + ) + else: + message.append( + 'Cannot detect difference by examining the fields of the dataclass.' + ) + + self.fail('\n'.join(message), msg) + + def assertUrlEqual(self, a, b, msg=None): + """Asserts that urls are equal, ignoring ordering of query params.""" + parsed_a = parse.urlparse(a) + parsed_b = parse.urlparse(b) + self.assertEqual(parsed_a.scheme, parsed_b.scheme, msg) + self.assertEqual(parsed_a.netloc, parsed_b.netloc, msg) + self.assertEqual(parsed_a.path, parsed_b.path, msg) + self.assertEqual(parsed_a.fragment, parsed_b.fragment, msg) + self.assertEqual(sorted(parsed_a.params.split(';')), + sorted(parsed_b.params.split(';')), msg) + self.assertDictEqual( + parse.parse_qs(parsed_a.query, keep_blank_values=True), + parse.parse_qs(parsed_b.query, keep_blank_values=True), msg) + + def assertSameStructure(self, a, b, aname='a', bname='b', msg=None): + """Asserts that two values contain the same structural content. + + The two arguments should be data trees consisting of trees of dicts and + lists. They will be deeply compared by walking into the contents of dicts + and lists; other items will be compared using the == operator. + If the two structures differ in content, the failure message will indicate + the location within the structures where the first difference is found. + This may be helpful when comparing large structures. + + Mixed Sequence and Set types are supported. Mixed Mapping types are + supported, but the order of the keys will not be considered in the + comparison. + + Args: + a: The first structure to compare. + b: The second structure to compare. + aname: Variable name to use for the first structure in assertion messages. + bname: Variable name to use for the second structure. + msg: Additional text to include in the failure message. + """ + + # Accumulate all the problems found so we can report all of them at once + # rather than just stopping at the first + problems = [] + + _walk_structure_for_problems(a, b, aname, bname, problems, + self.assertEqual, self.failureException) + + # Avoid spamming the user toooo much + if self.maxDiff is not None: + max_problems_to_show = self.maxDiff // 80 + if len(problems) > max_problems_to_show: + problems = problems[0:max_problems_to_show-1] + ['...'] + + if problems: + self.fail('; '.join(problems), msg) + + def assertJsonEqual(self, first, second, msg=None): + """Asserts that the JSON objects defined in two strings are equal. + + A summary of the differences will be included in the failure message + using assertSameStructure. + + Args: + first: A string containing JSON to decode and compare to second. + second: A string containing JSON to decode and compare to first. + msg: Additional text to include in the failure message. + """ + try: + first_structured = json.loads(first) + except ValueError as e: + raise ValueError(self._formatMessage( + msg, + 'could not decode first JSON value %s: %s' % (first, e))) + + try: + second_structured = json.loads(second) + except ValueError as e: + raise ValueError(self._formatMessage( + msg, + 'could not decode second JSON value %s: %s' % (second, e))) + + self.assertSameStructure(first_structured, second_structured, + aname='first', bname='second', msg=msg) + + def _getAssertEqualityFunc( + self, first: Any, second: Any + ) -> Callable[..., None]: + try: + return super()._getAssertEqualityFunc(first, second) + except AttributeError: + # This is a workaround if unittest.TestCase.__init__ was never run. + # It usually means that somebody created a subclass just for the + # assertions and has overridden __init__. "assertTrue" is a safe + # value that will not make __init__ raise a ValueError. + test_method = getattr(self, '_testMethodName', 'assertTrue') + super().__init__(test_method) + + return super()._getAssertEqualityFunc(first, second) + + def fail(self, msg=None, user_msg=None) -> NoReturn: + """Fail immediately with the given standard message and user message.""" + super().fail(self._formatMessage(user_msg, msg)) + + +def _sorted_list_difference( + expected: List[_T], actual: List[_T] +) -> Tuple[List[_T], List[_T]]: + """Finds elements in only one or the other of two, sorted input lists. + + Returns a two-element tuple of lists. The first list contains those + elements in the "expected" list but not in the "actual" list, and the + second contains those elements in the "actual" list but not in the + "expected" list. Duplicate elements in either input list are ignored. + + Args: + expected: The list we expected. + actual: The list we actually got. + Returns: + (missing, unexpected) + missing: items in expected that are not in actual. + unexpected: items in actual that are not in expected. + """ + i = j = 0 + missing = [] + unexpected = [] + while True: + try: + e = expected[i] + a = actual[j] + if e < a: # type: ignore[operator] + missing.append(e) + i += 1 + while expected[i] == e: + i += 1 + elif e > a: # type: ignore[operator] + unexpected.append(a) + j += 1 + while actual[j] == a: + j += 1 + else: + i += 1 + try: + while expected[i] == e: + i += 1 + finally: + j += 1 + while actual[j] == a: + j += 1 + except IndexError: + missing.extend(expected[i:]) + unexpected.extend(actual[j:]) + break + return missing, unexpected + + +def _are_both_of_integer_type(a: object, b: object) -> bool: + return isinstance(a, int) and isinstance(b, int) + + +def _are_both_of_sequence_type(a: object, b: object) -> bool: + return isinstance(a, abc.Sequence) and isinstance( + b, abc.Sequence) and not isinstance( + a, _TEXT_OR_BINARY_TYPES) and not isinstance(b, _TEXT_OR_BINARY_TYPES) + + +def _are_both_of_set_type(a: object, b: object) -> bool: + return isinstance(a, abc.Set) and isinstance(b, abc.Set) + + +def _are_both_of_mapping_type(a: object, b: object) -> bool: + return isinstance(a, abc.Mapping) and isinstance( + b, abc.Mapping) + + +def _walk_structure_for_problems( + a, b, aname, bname, problem_list, leaf_assert_equal_func, failure_exception +): + """The recursive comparison behind assertSameStructure.""" + if type(a) != type(b) and not ( # pylint: disable=unidiomatic-typecheck + _are_both_of_integer_type(a, b) or _are_both_of_sequence_type(a, b) or + _are_both_of_set_type(a, b) or _are_both_of_mapping_type(a, b)): + # We do not distinguish between int and long types as 99.99% of Python 2 + # code should never care. They collapse into a single type in Python 3. + problem_list.append('%s is a %r but %s is a %r' % + (aname, type(a), bname, type(b))) + # If they have different types there's no point continuing + return + + if isinstance(a, abc.Set): + for k in a: + if k not in b: + problem_list.append( + '%s has %r but %s does not' % (aname, k, bname)) + for k in b: + if k not in a: + problem_list.append('%s lacks %r but %s has it' % (aname, k, bname)) + + # NOTE: a or b could be a defaultdict, so we must take care that the traversal + # doesn't modify the data. + elif isinstance(a, abc.Mapping): + for k in a: + if k in b: + _walk_structure_for_problems( + a[k], b[k], '%s[%r]' % (aname, k), '%s[%r]' % (bname, k), + problem_list, leaf_assert_equal_func, failure_exception) + else: + problem_list.append( + "%s has [%r] with value %r but it's missing in %s" % + (aname, k, a[k], bname)) + for k in b: + if k not in a: + problem_list.append( + '%s lacks [%r] but %s has it with value %r' % + (aname, k, bname, b[k])) + + # Strings/bytes are Sequences but we'll just do those with regular != + elif (isinstance(a, abc.Sequence) and + not isinstance(a, _TEXT_OR_BINARY_TYPES)): + minlen = min(len(a), len(b)) + for i in range(minlen): + _walk_structure_for_problems( + a[i], b[i], '%s[%d]' % (aname, i), '%s[%d]' % (bname, i), + problem_list, leaf_assert_equal_func, failure_exception) + for i in range(minlen, len(a)): + problem_list.append('%s has [%i] with value %r but %s does not' % + (aname, i, a[i], bname)) + for i in range(minlen, len(b)): + problem_list.append('%s lacks [%i] but %s has it with value %r' % + (aname, i, bname, b[i])) + + else: + try: + leaf_assert_equal_func(a, b) + except failure_exception: + problem_list.append('%s is %r but %s is %r' % (aname, a, bname, b)) + + +def get_command_string(command): + """Returns an escaped string that can be used as a shell command. + + Args: + command: List or string representing the command to run. + Returns: + A string suitable for use as a shell command. + """ + if isinstance(command, str): + return command + else: + if os.name == 'nt': + return ' '.join(command) + else: + # The following is identical to Python 3's shlex.quote function. + command_string = '' + for word in command: + # Single quote word, and replace each ' in word with '"'"' + command_string += "'" + word.replace("'", "'\"'\"'") + "' " + return command_string[:-1] + + +def get_command_stderr(command, env=None, close_fds=True): + """Runs the given shell command and returns a tuple. + + Args: + command: List or string representing the command to run. + env: Dictionary of environment variable settings. If None, no environment + variables will be set for the child process. This is to make tests + more hermetic. NOTE: this behavior is different than the standard + subprocess module. + close_fds: Whether or not to close all open fd's in the child after forking. + On Windows, this is ignored and close_fds is always False. + + Returns: + Tuple of (exit status, text printed to stdout and stderr by the command). + """ + if env is None: env = {} + if os.name == 'nt': + # Windows does not support setting close_fds to True while also redirecting + # standard handles. + close_fds = False + + use_shell = isinstance(command, str) + # Pass the shell command as stdin to /bin/sh rather than using Python's + # behavior of passing it in as a command line argument. That can save us when + # the shell command exceeds the maximum command line length but the actual + # individual process invocations within it don't. + if os.name != 'nt' and use_shell: + stdin_input = command.encode() + command = ['/bin/sh'] + use_shell = False + else: + stdin_input = None + + result = subprocess.run( + command, + close_fds=close_fds, + env=env, + shell=use_shell, + input=stdin_input, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + check=False, + ) + return (result.returncode, result.stdout) + + +def _quote_long_string(s: Union[str, bytes, bytearray]) -> str: + """Quotes a potentially multi-line string to make the start and end obvious. + + Args: + s: A string. + + Returns: + The quoted string. + """ + if isinstance(s, (bytes, bytearray)): + try: + s = s.decode('utf-8') + except UnicodeDecodeError: + s = str(s) + return ('8<-----------\n' + + s + '\n' + + '----------->8\n') + + +def print_python_version() -> None: + # Having this in the test output logs by default helps debugging when all + # you've got is the log and no other idea of which Python was used. + sys.stderr.write('Running tests under Python {0[0]}.{0[1]}.{0[2]}: ' + '{1}\n'.format( + sys.version_info, + sys.executable if sys.executable else 'embedded.')) + + +def main(*args: str, **kwargs: Any) -> None: + """Executes a set of Python unit tests. + + Usually this function is called without arguments, so the + unittest.TestProgram instance will get created with the default settings, + so it will run all test methods of all TestCase classes in the ``__main__`` + module. + + Args: + *args: Positional arguments passed through to + ``unittest.TestProgram.__init__``. + **kwargs: Keyword arguments passed through to + ``unittest.TestProgram.__init__``. + """ + print_python_version() + _run_in_app(run_tests, args, kwargs) + + +def _is_in_app_main() -> bool: + """Returns True iff app.run is active.""" + f = sys._getframe().f_back # pylint: disable=protected-access + while f: + if f.f_code == app.run.__code__: + return True + f = f.f_back + return False + + +def _register_sigterm_with_faulthandler() -> None: + """Have faulthandler dump stacks on SIGTERM. Useful to diagnose timeouts.""" + if getattr(faulthandler, 'register', None): + # faulthandler.register is not available on Windows. + # faulthandler.enable() is already called by app.run. + try: + faulthandler.register(signal.SIGTERM, chain=True) # pytype: disable=module-attr + except Exception as e: # pylint: disable=broad-except + sys.stderr.write('faulthandler.register(SIGTERM) failed ' + '%r; ignoring.\n' % e) + + +def _run_in_app( + function: Callable[..., None], + args: Sequence[str], + kwargs: Mapping[str, Any], +) -> None: + """Executes a set of Python unit tests, ensuring app.run. + + This is a private function, users should call absltest.main(). + + _run_in_app calculates argv to be the command-line arguments of this program + (without the flags), sets the default of FLAGS.alsologtostderr to True, + then it calls function(argv, args, kwargs), making sure that `function' + will get called within app.run(). _run_in_app does this by checking whether + it is called by app.run(), or by calling app.run() explicitly. + + The reason why app.run has to be ensured is to make sure that + flags are parsed and stripped properly, and other initializations done by + the app module are also carried out, no matter if absltest.run() is called + from within or outside app.run(). + + If _run_in_app is called from within app.run(), then it will reparse + sys.argv and pass the result without command-line flags into the argv + argument of `function'. The reason why this parsing is needed is that + __main__.main() calls absltest.main() without passing its argv. So the + only way _run_in_app could get to know the argv without the flags is that + it reparses sys.argv. + + _run_in_app changes the default of FLAGS.alsologtostderr to True so that the + test program's stderr will contain all the log messages unless otherwise + specified on the command-line. This overrides any explicit assignment to + FLAGS.alsologtostderr by the test program prior to the call to _run_in_app() + (e.g. in __main__.main). + + Please note that _run_in_app (and the function it calls) is allowed to make + changes to kwargs. + + Args: + function: absltest.run_tests or a similar function. It will be called as + function(argv, args, kwargs) where argv is a list containing the + elements of sys.argv without the command-line flags. + args: Positional arguments passed through to unittest.TestProgram.__init__. + kwargs: Keyword arguments passed through to unittest.TestProgram.__init__. + """ + if _is_in_app_main(): + _register_sigterm_with_faulthandler() + + # Change the default of alsologtostderr from False to True, so the test + # programs's stderr will contain all the log messages. + # If --alsologtostderr=false is specified in the command-line, or user + # has called FLAGS.alsologtostderr = False before, then the value is kept + # False. + FLAGS.set_default('alsologtostderr', True) + + # Here we only want to get the `argv` without the flags. To avoid any + # side effects of parsing flags, we temporarily stub out the `parse` method + stored_parse_methods = {} + noop_parse = lambda _: None + for name in FLAGS: + # Avoid any side effects of parsing flags. + stored_parse_methods[name] = FLAGS[name].parse + # This must be a separate loop since multiple flag names (short_name=) can + # point to the same flag object. + for name in FLAGS: + FLAGS[name].parse = noop_parse # type: ignore[method-assign] + try: + argv = FLAGS(sys.argv) + finally: + for name in FLAGS: + FLAGS[name].parse = stored_parse_methods[name] # type: ignore[method-assign] + sys.stdout.flush() + + function(argv, args, kwargs) + else: + # Send logging to stderr. Use --alsologtostderr instead of --logtostderr + # in case tests are reading their own logs. + FLAGS.set_default('alsologtostderr', True) + + def main_function(argv): + _register_sigterm_with_faulthandler() + function(argv, args, kwargs) + + app.run(main=main_function) + + +def _is_suspicious_attribute( + testCaseClass: Type[unittest.TestCase], name: str +) -> bool: + """Returns True if an attribute is a method named like a test method.""" + if name.startswith('Test') and len(name) > 4 and name[4].isupper(): + attr = getattr(testCaseClass, name) + if inspect.isfunction(attr) or inspect.ismethod(attr): + args = inspect.getfullargspec(attr) + return (len(args.args) == 1 and args.args[0] == 'self' and + args.varargs is None and args.varkw is None and + not args.kwonlyargs) + return False + + +def skipThisClass(reason: str) -> Callable[[_T], _T]: + """Skip tests in the decorated TestCase, but not any of its subclasses. + + This decorator indicates that this class should skip all its tests, but not + any of its subclasses. Useful for if you want to share testMethod or setUp + implementations between a number of concrete testcase classes. + + Example usage, showing how you can share some common test methods between + subclasses. In this example, only ``BaseTest`` will be marked as skipped, and + not RealTest or SecondRealTest:: + + @absltest.skipThisClass("Shared functionality") + class BaseTest(absltest.TestCase): + def test_simple_functionality(self): + self.assertEqual(self.system_under_test.method(), 1) + + class RealTest(BaseTest): + def setUp(self): + super().setUp() + self.system_under_test = MakeSystem(argument) + + def test_specific_behavior(self): + ... + + class SecondRealTest(BaseTest): + def setUp(self): + super().setUp() + self.system_under_test = MakeSystem(other_arguments) + + def test_other_behavior(self): + ... + + Args: + reason: The reason we have a skip in place. For instance: 'shared test + methods' or 'shared assertion methods'. + + Returns: + Decorator function that will cause a class to be skipped. + """ + if isinstance(reason, type): + raise TypeError(f'Got {reason!r}, expected reason as string') + + def _skip_class(test_case_class): + if not issubclass(test_case_class, unittest.TestCase): + raise TypeError( + f'Decorating {test_case_class!r}, expected TestCase subclass' + ) + + # Only shadow the setUpClass method if it is directly defined. If it is + # in the parent class we invoke it via a super() call instead of holding + # a reference to it. + shadowed_setupclass = test_case_class.__dict__.get('setUpClass', None) + + @classmethod + def replacement_setupclass(cls, *args, **kwargs): + # Skip this class if it is the one that was decorated with @skipThisClass + if cls is test_case_class: + raise SkipTest(reason) + if shadowed_setupclass: + # Pass along `cls` so the MRO chain doesn't break. + # The original method is a `classmethod` descriptor, which can't + # be directly called, but `__func__` has the underlying function. + return shadowed_setupclass.__func__(cls, *args, **kwargs) + else: + # Because there's no setUpClass() defined directly on test_case_class, + # we call super() ourselves to continue execution of the inheritance + # chain. + return super(test_case_class, cls).setUpClass(*args, **kwargs) + + test_case_class.setUpClass = replacement_setupclass + return test_case_class + + return _skip_class + + +class TestLoader(unittest.TestLoader): + """A test loader which supports common test features. + + Supported features include: + * Banning untested methods with test-like names: methods attached to this + testCase with names starting with `Test` are ignored by the test runner, + and often represent mistakenly-omitted test cases. This loader will raise + a TypeError when attempting to load a TestCase with such methods. + * Randomization of test case execution order (optional). + """ + + _ERROR_MSG = textwrap.dedent("""Method '%s' is named like a test case but + is not one. This is often a bug. If you want it to be a test method, + name it with 'test' in lowercase. If not, rename the method to not begin + with 'Test'.""") + + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + seed = _get_default_randomize_ordering_seed() + if seed: + self._randomize_ordering_seed = seed + self._random = random.Random(self._randomize_ordering_seed) + else: + self._randomize_ordering_seed = None + self._random = None + + def getTestCaseNames(self, testCaseClass): # pylint:disable=invalid-name + """Validates and returns a (possibly randomized) list of test case names.""" + for name in dir(testCaseClass): + if _is_suspicious_attribute(testCaseClass, name): + raise TypeError(TestLoader._ERROR_MSG % name) + names = list(super().getTestCaseNames(testCaseClass)) + if self._randomize_ordering_seed is not None: + logging.info( + 'Randomizing test order with seed: %d', self._randomize_ordering_seed) + logging.info( + 'To reproduce this order, re-run with ' + '--test_randomize_ordering_seed=%d', self._randomize_ordering_seed) + self._random.shuffle(names) + return names + + def shardTestCaseNames( + self, + iterator: Iterator[Any], + ordered_names: Sequence[str], + shard_index: int, + ) -> Sequence[str]: + """Filters and returns test case names for a specific shard. + + This method is intended to be used in conjunction with test sharding + (e.g., when running tests on a distributed system or when running tests + with bazel's test sharding feature). It will return a subset of the + input test case names, based on the shard index and total shard count. + + Args: + names: A sequence of test case names. + shard_index: The index of the current shard. + total_shards: The total number of shards. + + Returns: + A sequence of test case names for the current shard. + """ + filtered_names = [] + # We need to sort the list of tests in order to determine which tests this + # shard is responsible for; however, it's important to preserve the order + # returned by the base loader, e.g. in the case of randomized test ordering. + for testcase in sorted(ordered_names): + bucket = next(iterator) + if bucket == shard_index: + filtered_names.append(testcase) + return [x for x in ordered_names if x in filtered_names] + + +def get_default_xml_output_filename() -> Optional[str]: + if os.environ.get('XML_OUTPUT_FILE'): + return os.environ['XML_OUTPUT_FILE'] + elif os.environ.get('RUNNING_UNDER_TEST_DAEMON'): + return os.path.join(os.path.dirname(TEST_TMPDIR.value), 'test_detail.xml') + elif os.environ.get('TEST_XMLOUTPUTDIR'): + return os.path.join( + os.environ['TEST_XMLOUTPUTDIR'], + os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.xml') + return None + + +def _setup_filtering(argv: MutableSequence[str]) -> bool: + """Implements the bazel test filtering protocol. + + The following environment variable is used in this method: + + TESTBRIDGE_TEST_ONLY: string, if set, is forwarded to the unittest + framework to use as a test filter. Its value is split with shlex, then: + 1. On Python 3.6 and before, split values are passed as positional + arguments on argv. + 2. On Python 3.7+, split values are passed to unittest's `-k` flag. Tests + are matched by glob patterns or substring. See + https://docs.python.org/3/library/unittest.html#cmdoption-unittest-k + + Args: + argv: the argv to mutate in-place. + + Returns: + Whether test filtering is requested. + """ + test_filter = os.environ.get('TESTBRIDGE_TEST_ONLY') + if argv is None or not test_filter: + return False + + filters = ['-k=' + test_filter for test_filter in shlex.split(test_filter)] + + argv[1:1] = filters + return True + + +def _setup_test_runner_fail_fast(argv: MutableSequence[str]) -> None: + """Implements the bazel test fail fast protocol. + + The following environment variable is used in this method: + + TESTBRIDGE_TEST_RUNNER_FAIL_FAST=<1|0> + + If set to 1, --failfast is passed to the unittest framework to return upon + first failure. + + Args: + argv: the argv to mutate in-place. + """ + + if argv is None: + return + + if os.environ.get('TESTBRIDGE_TEST_RUNNER_FAIL_FAST') != '1': + return + + argv[1:1] = ['--failfast'] + + +def _setup_sharding( + custom_loader: Optional[unittest.TestLoader] = None, +) -> Tuple[unittest.TestLoader, Optional[int]]: + """Implements the bazel sharding protocol. + + The following environment variables are used in this method: + + TEST_SHARD_STATUS_FILE: string, if set, points to a file. We write a blank + file to tell the test runner that this test implements the test sharding + protocol. + + TEST_TOTAL_SHARDS: int, if set, sharding is requested. + + TEST_SHARD_INDEX: int, must be set if TEST_TOTAL_SHARDS is set. Specifies + the shard index for this instance of the test process. Must satisfy: + 0 <= TEST_SHARD_INDEX < TEST_TOTAL_SHARDS. + + Args: + custom_loader: A TestLoader to be made sharded. + + Returns: + A tuple of ``(test_loader, shard_index)``. ``test_loader`` is for + shard-filtering or the standard test loader depending on the sharding + environment variables. ``shard_index`` is the shard index, or ``None`` when + sharding is not used. + """ + + # It may be useful to write the shard file even if the other sharding + # environment variables are not set. Test runners may use this functionality + # to query whether a test binary implements the test sharding protocol. + if 'TEST_SHARD_STATUS_FILE' in os.environ: + try: + with open(os.environ['TEST_SHARD_STATUS_FILE'], 'w') as f: + f.write('') + except OSError: + sys.stderr.write('Error opening TEST_SHARD_STATUS_FILE (%s). Exiting.' + % os.environ['TEST_SHARD_STATUS_FILE']) + sys.exit(1) + + base_loader = custom_loader or TestLoader() + if 'TEST_TOTAL_SHARDS' not in os.environ: + # Not using sharding, use the expected test loader. + return base_loader, None + + total_shards = int(os.environ['TEST_TOTAL_SHARDS']) + shard_index = int(os.environ['TEST_SHARD_INDEX']) + + if shard_index < 0 or shard_index >= total_shards: + sys.stderr.write('ERROR: Bad sharding values. index=%d, total=%d\n' % + (shard_index, total_shards)) + sys.exit(1) + + # Replace the original getTestCaseNames with one that returns + # the test case names for this shard. + delegate_get_names = base_loader.getTestCaseNames + + bucket_iterator = itertools.cycle(range(total_shards)) + + def getSharedTestCaseNames(testCaseClass): + has_shard_test_case_names = hasattr(base_loader, 'shardTestCaseNames') + if has_shard_test_case_names: + sharder = getattr(base_loader, 'shardTestCaseNames') + else: + sharder = lambda *args: TestLoader.shardTestCaseNames(base_loader, *args) + + names = sharder( + bucket_iterator, delegate_get_names(testCaseClass), shard_index + ) + return names + + base_loader.getTestCaseNames = getSharedTestCaseNames # type: ignore[method-assign] + return base_loader, shard_index + + +def _run_and_get_tests_result( + argv: MutableSequence[str], + args: Sequence[Any], + kwargs: MutableMapping[str, Any], + xml_test_runner_class: Type[unittest.TextTestRunner], +) -> Tuple[unittest.TestResult, bool]: + """Same as run_tests, but it doesn't exit. + + Args: + argv: sys.argv with the command-line flags removed from the front, i.e. the + argv with which :func:`app.run()` has called + ``__main__.main``. It is passed to + ``unittest.TestProgram.__init__(argv=)``, which does its own flag parsing. + It is ignored if kwargs contains an argv entry. + args: Positional arguments passed through to + ``unittest.TestProgram.__init__``. + kwargs: Keyword arguments passed through to + ``unittest.TestProgram.__init__``. + xml_test_runner_class: The type of the test runner class. + + Returns: + A tuple of ``(test_result, fail_when_no_tests_ran)``. + ``fail_when_no_tests_ran`` indicates whether the test should fail when + no tests ran. + """ + + # The entry from kwargs overrides argv. + argv = kwargs.pop('argv', argv) + + if sys.version_info[:2] >= (3, 12): + # Python 3.12 unittest changed the behavior from PASS to FAIL in + # https://github.com/python/cpython/pull/102051. absltest follows this. + fail_when_no_tests_ran = True + else: + # Historically, absltest and unittest before Python 3.12 passes if no tests + # ran. + fail_when_no_tests_ran = False + + # Set up test filtering if requested in environment. + if _setup_filtering(argv): + # When test filtering is requested, ideally we also want to fail when no + # tests ran. However, the test filters are usually done when running bazel. + # When you run multiple targets, e.g. `bazel test //my_dir/... + # --test_filter=MyTest`, you don't necessarily want individual tests to fail + # because no tests match in that particular target. + # Due to this use case, we don't fail when test filtering is requested via + # the environment variable from bazel. + fail_when_no_tests_ran = False + + # Set up --failfast as requested in environment + _setup_test_runner_fail_fast(argv) + + # Shard the (default or custom) loader if sharding is turned on. + kwargs['testLoader'], shard_index = _setup_sharding( + kwargs.get('testLoader', None) + ) + if shard_index is not None and shard_index > 0: + # When sharding is requested, all the shards except the first one shall not + # fail when no tests ran. This happens when the shard count is greater than + # the test case count. + fail_when_no_tests_ran = False + + # XML file name is based upon (sorted by priority): + # --xml_output_file flag, XML_OUTPUT_FILE variable, + # TEST_XMLOUTPUTDIR variable or RUNNING_UNDER_TEST_DAEMON variable. + if FLAGS.xml_output_file: + xml_output_file = FLAGS.xml_output_file + else: + xml_output_file = get_default_xml_output_filename() + if xml_output_file: + FLAGS.xml_output_file = xml_output_file # type: ignore[assignment] + + xml_buffer = None + if xml_output_file: + xml_output_dir = os.path.dirname(xml_output_file) + if xml_output_dir and not os.path.isdir(xml_output_dir): + try: + os.makedirs(xml_output_dir) + except OSError as e: + # File exists error can occur with concurrent tests + if e.errno != errno.EEXIST: + raise + # Fail early if we can't write to the XML output file. This is so that we + # don't waste people's time running tests that will just fail anyways. + with _open(xml_output_file, 'w'): + pass + + # We can reuse testRunner if it supports XML output (e. g. by inheriting + # from xml_reporter.TextAndXMLTestRunner). Otherwise we need to use + # xml_reporter.TextAndXMLTestRunner. + if (kwargs.get('testRunner') is not None + and not hasattr(kwargs['testRunner'], 'set_default_xml_stream')): + sys.stderr.write('WARNING: XML_OUTPUT_FILE or --xml_output_file setting ' + 'overrides testRunner=%r setting (possibly from --pdb)' + % (kwargs['testRunner'])) + # Passing a class object here allows TestProgram to initialize + # instances based on its kwargs and/or parsed command-line args. + kwargs['testRunner'] = xml_test_runner_class + if kwargs.get('testRunner') is None: + kwargs['testRunner'] = xml_test_runner_class + # Use an in-memory buffer (not backed by the actual file) to store the XML + # report, because some tools modify the file (e.g., create a placeholder + # with partial information, in case the test process crashes). + xml_buffer = io.StringIO() + kwargs['testRunner'].set_default_xml_stream(xml_buffer) # pytype: disable=attribute-error + + # If we've used a seed to randomize test case ordering, we want to record it + # as a top-level attribute in the `testsuites` section of the XML output. + randomize_ordering_seed = getattr( + kwargs['testLoader'], '_randomize_ordering_seed', None) + setter = getattr(kwargs['testRunner'], 'set_testsuites_property', None) + if randomize_ordering_seed and setter: + setter('test_randomize_ordering_seed', randomize_ordering_seed) + elif kwargs.get('testRunner') is None: + kwargs['testRunner'] = _pretty_print_reporter.TextTestRunner + + if FLAGS.pdb_post_mortem: + runner = kwargs['testRunner'] + # testRunner can be a class or an instance, which must be tested for + # differently. + # Overriding testRunner isn't uncommon, so only enable the debugging + # integration if the runner claims it does; we don't want to accidentally + # clobber something on the runner. + if ((isinstance(runner, type) and + issubclass(runner, _pretty_print_reporter.TextTestRunner)) or + isinstance(runner, _pretty_print_reporter.TextTestRunner)): + runner.run_for_debugging = True + + # Make sure tmpdir exists. + if not os.path.isdir(TEST_TMPDIR.value): + try: + os.makedirs(TEST_TMPDIR.value) + except OSError as e: + # Concurrent test might have created the directory. + if e.errno != errno.EEXIST: + raise + + # Let unittest.TestProgram.__init__ do its own argv parsing, e.g. for '-v', + # on argv, which is sys.argv without the command-line flags. + kwargs['argv'] = argv + + # Request unittest.TestProgram to not exit. The exit will be handled by + # `absltest.run_tests`. + kwargs['exit'] = False + + try: + test_program = unittest.TestProgram(*args, **kwargs) + return test_program.result, fail_when_no_tests_ran + finally: + if xml_buffer: + try: + with _open(xml_output_file, 'w') as f: + f.write(xml_buffer.getvalue()) + finally: + xml_buffer.close() + + +def run_tests( + argv: MutableSequence[str], + args: Sequence[Any], + kwargs: MutableMapping[str, Any], +) -> None: + """Executes a set of Python unit tests. + + Most users should call absltest.main() instead of run_tests. + + Please note that run_tests should be called from app.run. + Calling absltest.main() would ensure that. + + Please note that run_tests is allowed to make changes to kwargs. + + Args: + argv: sys.argv with the command-line flags removed from the front, i.e. the + argv with which :func:`app.run()` has called + ``__main__.main``. It is passed to + ``unittest.TestProgram.__init__(argv=)``, which does its own flag parsing. + It is ignored if kwargs contains an argv entry. + args: Positional arguments passed through to + ``unittest.TestProgram.__init__``. + kwargs: Keyword arguments passed through to + ``unittest.TestProgram.__init__``. + """ + result, fail_when_no_tests_ran = _run_and_get_tests_result( + argv, args, kwargs, xml_reporter.TextAndXMLTestRunner + ) + if fail_when_no_tests_ran and result.testsRun == 0 and not result.skipped: + # Python 3.12 unittest exits with 5 when no tests ran. The exit code 5 comes + # from pytest which does the same thing. + sys.exit(5) + sys.exit(not result.wasSuccessful()) + + +def _rmtree_ignore_errors(path: str) -> None: + if os.path.isfile(path): + try: + os.unlink(path) + except OSError: + pass + else: + shutil.rmtree(path, ignore_errors=True) + + +def _get_first_part(path: str) -> str: + parts = path.split(os.sep, 1) + return parts[0] diff --git a/lib/python3.10/site-packages/absl/testing/flagsaver.py b/lib/python3.10/site-packages/absl/testing/flagsaver.py new file mode 100644 index 0000000000000000000000000000000000000000..cb28aee5ba8b7ba924e38e39b4404c8c50604137 --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/flagsaver.py @@ -0,0 +1,393 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Decorator and context manager for saving and restoring flag values. + +There are many ways to save and restore. Always use the most convenient method +for a given use case. + +Here are examples of each method. They all call ``do_stuff()`` while +``FLAGS.someflag`` is temporarily set to ``'foo'``:: + + from absl.testing import flagsaver + + # Use a decorator which can optionally override flags via arguments. + @flagsaver.flagsaver(someflag='foo') + def some_func(): + do_stuff() + + # Use a decorator which can optionally override flags with flagholders. + @flagsaver.flagsaver((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, 23)) + def some_func(): + do_stuff() + + # Use a decorator which does not override flags itself. + @flagsaver.flagsaver + def some_func(): + FLAGS.someflag = 'foo' + do_stuff() + + # Use a context manager which can optionally override flags via arguments. + with flagsaver.flagsaver(someflag='foo'): + do_stuff() + + # Save and restore the flag values yourself. + saved_flag_values = flagsaver.save_flag_values() + try: + FLAGS.someflag = 'foo' + do_stuff() + finally: + flagsaver.restore_flag_values(saved_flag_values) + + # Use the parsing version to emulate users providing the flags. + # Note that all flags must be provided as strings (unparsed). + @flagsaver.as_parsed(some_int_flag='123') + def some_func(): + # Because the flag was parsed it is considered "present". + assert FLAGS.some_int_flag.present + do_stuff() + + # flagsaver.as_parsed() can also be used as a context manager just like + # flagsaver.flagsaver() + with flagsaver.as_parsed(some_int_flag='123'): + do_stuff() + + # The flagsaver.as_parsed() interface also supports FlagHolder objects. + @flagsaver.as_parsed((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, '23')) + def some_func(): + do_stuff() + + # Using as_parsed with a multi_X flag requires a sequence of strings. + @flagsaver.as_parsed(some_multi_int_flag=['123', '456']) + def some_func(): + assert FLAGS.some_multi_int_flag.present + do_stuff() + + # If a flag name includes non-identifier characters it can be specified like + # so: + @flagsaver.as_parsed(**{'i-like-dashes': 'true'}) + def some_func(): + do_stuff() + +We save and restore a shallow copy of each Flag object's ``__dict__`` attribute. +This preserves all attributes of the flag, such as whether or not it was +overridden from its default value. + +WARNING: Currently a flag that is saved and then deleted cannot be restored. An +exception will be raised. However if you *add* a flag after saving flag values, +and then restore flag values, the added flag will be deleted with no errors. +""" + +import collections +import functools +import inspect +from typing import Any, Callable, Dict, Mapping, Sequence, Tuple, Type, TypeVar, Union, overload + +from absl import flags + +FLAGS = flags.FLAGS + + +# The type of pre/post wrapped functions. +_CallableT = TypeVar('_CallableT', bound=Callable) + + +@overload +def flagsaver(func: _CallableT) -> _CallableT: + ... + + +@overload +def flagsaver( + *args: Tuple[flags.FlagHolder, Any], **kwargs: Any +) -> '_FlagOverrider': + ... + + +def flagsaver(*args, **kwargs): + """The main flagsaver interface. See module doc for usage.""" + return _construct_overrider(_FlagOverrider, *args, **kwargs) # type: ignore[bad-return-type] + + +@overload +def as_parsed(*args: Tuple[flags.FlagHolder, Union[str, Sequence[str]]], + **kwargs: Union[str, Sequence[str]]) -> '_ParsingFlagOverrider': + ... + + +@overload +def as_parsed(func: _CallableT) -> _CallableT: + ... + + +def as_parsed(*args, **kwargs): + """Overrides flags by parsing strings, saves flag state similar to flagsaver. + + This function can be used as either a decorator or context manager similar to + flagsaver.flagsaver(). However, where flagsaver.flagsaver() directly sets the + flags to new values, this function will parse the provided arguments as if + they were provided on the command line. Among other things, this will cause + `FLAGS['flag_name'].present == True`. + + A note on unparsed input: For many flag types, the unparsed version will be + a single string. However for multi_x (multi_string, multi_integer, multi_enum) + the unparsed version will be a Sequence of strings. + + Args: + *args: Tuples of FlagHolders and their unparsed value. + **kwargs: The keyword args are flag names, and the values are unparsed + values. + + Returns: + _ParsingFlagOverrider that serves as a context manager or decorator. Will + save previous flag state and parse new flags, then on cleanup it will + restore the previous flag state. + """ + return _construct_overrider(_ParsingFlagOverrider, *args, **kwargs) + + +# NOTE: the order of these overload declarations matters. The type checker will +# pick the first match which could be incorrect. +@overload +def _construct_overrider( + flag_overrider_cls: Type['_ParsingFlagOverrider'], + *args: Tuple[flags.FlagHolder, Union[str, Sequence[str]]], + **kwargs: Union[str, Sequence[str]]) -> '_ParsingFlagOverrider': + ... + + +@overload +def _construct_overrider( + flag_overrider_cls: Type['_FlagOverrider'], func: _CallableT +) -> _CallableT: + ... + + +@overload +def _construct_overrider( + flag_overrider_cls: Type['_FlagOverrider'], + *args: Tuple[flags.FlagHolder, Any], + **kwargs: Any, +) -> '_FlagOverrider': + ... + + +def _construct_overrider(flag_overrider_cls, *args, **kwargs): + """Handles the args/kwargs returning an instance of flag_overrider_cls. + + If flag_overrider_cls is _FlagOverrider then values should be native python + types matching the python types. Otherwise if flag_overrider_cls is + _ParsingFlagOverrider the values should be strings or sequences of strings. + + Args: + flag_overrider_cls: The class that will do the overriding. + *args: Tuples of FlagHolder and the new flag value. + **kwargs: Keword args mapping flag name to new flag value. + + Returns: + A _FlagOverrider to be used as a decorator or context manager. + """ + if not args: + return flag_overrider_cls(**kwargs) + # args can be [func] if used as `@flagsaver` instead of `@flagsaver(...)` + if len(args) == 1 and callable(args[0]): + if kwargs: + raise ValueError( + "It's invalid to specify both positional and keyword parameters.") + func = args[0] + if inspect.isclass(func): + raise TypeError('@flagsaver.flagsaver cannot be applied to a class.') + return _wrap(flag_overrider_cls, func, {}) + # args can be a list of (FlagHolder, value) pairs. + # In which case they augment any specified kwargs. + for arg in args: + if not isinstance(arg, tuple) or len(arg) != 2: + raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,)) + holder, value = arg + if not isinstance(holder, flags.FlagHolder): + raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,)) + if holder.name in kwargs: + raise ValueError('Cannot set --%s multiple times' % holder.name) + kwargs[holder.name] = value + return flag_overrider_cls(**kwargs) + + +def save_flag_values( + flag_values: flags.FlagValues = FLAGS, +) -> Dict[str, Dict[str, Any]]: + """Returns copy of flag values as a dict. + + Args: + flag_values: FlagValues, the FlagValues instance with which the flag will be + saved. This should almost never need to be overridden. + + Returns: + Dictionary mapping keys to values. Keys are flag names, values are + corresponding ``__dict__`` members. E.g. ``{'key': value_dict, ...}``. + """ + return {name: _copy_flag_dict(flag_values[name]) for name in flag_values} + + +def restore_flag_values( + saved_flag_values: Mapping[str, Dict[str, Any]], + flag_values: flags.FlagValues = FLAGS, +) -> None: + """Restores flag values based on the dictionary of flag values. + + Args: + saved_flag_values: {'flag_name': value_dict, ...} + flag_values: FlagValues, the FlagValues instance from which the flag will be + restored. This should almost never need to be overridden. + """ + new_flag_names = list(flag_values) + for name in new_flag_names: + saved = saved_flag_values.get(name) + if saved is None: + # If __dict__ was not saved delete "new" flag. + delattr(flag_values, name) + else: + if flag_values[name].value != saved['_value']: + flag_values[name].value = saved['_value'] # Ensure C++ value is set. + flag_values[name].__dict__ = saved + + +@overload +def _wrap(flag_overrider_cls: Type['_FlagOverrider'], func: _CallableT, + overrides: Mapping[str, Any]) -> _CallableT: + ... + + +@overload +def _wrap(flag_overrider_cls: Type['_ParsingFlagOverrider'], func: _CallableT, + overrides: Mapping[str, Union[str, Sequence[str]]]) -> _CallableT: + ... + + +def _wrap(flag_overrider_cls, func, overrides): + """Creates a wrapper function that saves/restores flag values. + + Args: + flag_overrider_cls: The class that will be used as a context manager. + func: This will be called between saving flags and restoring flags. + overrides: Flag names mapped to their values. These flags will be set after + saving the original flag state. The type of the values depends on if + _FlagOverrider or _ParsingFlagOverrider was specified. + + Returns: + A wrapped version of func. + """ + + @functools.wraps(func) + def _flagsaver_wrapper(*args, **kwargs): + """Wrapper function that saves and restores flags.""" + with flag_overrider_cls(**overrides): + return func(*args, **kwargs) + + return _flagsaver_wrapper + + +class _FlagOverrider: + """Overrides flags for the duration of the decorated function call. + + It also restores all original values of flags after decorated method + completes. + """ + + def __init__(self, **overrides: Any): + self._overrides = overrides + self._saved_flag_values = None + + def __call__(self, func: _CallableT) -> _CallableT: + if inspect.isclass(func): + raise TypeError('flagsaver cannot be applied to a class.') + return _wrap(self.__class__, func, self._overrides) + + def __enter__(self): + self._saved_flag_values = save_flag_values(FLAGS) + try: + FLAGS._set_attributes(**self._overrides) + except: + # It may fail because of flag validators. + restore_flag_values(self._saved_flag_values, FLAGS) + raise + + def __exit__(self, exc_type, exc_value, traceback): + restore_flag_values(self._saved_flag_values, FLAGS) + + +class _ParsingFlagOverrider(_FlagOverrider): + """Context manager for overriding flags. + + Simulates command line parsing. + + This is simlar to _FlagOverrider except that all **overrides should be + strings or sequences of strings, and when context is entered this class calls + .parse(value) + + This results in the flags having .present set properly. + """ + + def __init__(self, **overrides: Union[str, Sequence[str]]): + for flag_name, new_value in overrides.items(): + if isinstance(new_value, str): + continue + if (isinstance(new_value, collections.abc.Sequence) and + all(isinstance(single_value, str) for single_value in new_value)): + continue + raise TypeError( + f'flagsaver.as_parsed() cannot parse {flag_name}. Expected a single ' + f'string or sequence of strings but {type(new_value)} was provided.') + super().__init__(**overrides) + + def __enter__(self): + self._saved_flag_values = save_flag_values(FLAGS) + try: + for flag_name, unparsed_value in self._overrides.items(): + # LINT.IfChange(flag_override_parsing) + FLAGS[flag_name].parse(unparsed_value) + FLAGS[flag_name].using_default_value = False + # LINT.ThenChange() + + # Perform the validation on all modified flags. This is something that + # FLAGS._set_attributes() does for you in _FlagOverrider. + for flag_name in self._overrides: + FLAGS._assert_validators(FLAGS[flag_name].validators) + + except KeyError as e: + # If a flag doesn't exist, an UnrecognizedFlagError is more specific. + restore_flag_values(self._saved_flag_values, FLAGS) + raise flags.UnrecognizedFlagError('Unknown command line flag.') from e + + except: + # It may fail because of flag validators or general parsing issues. + restore_flag_values(self._saved_flag_values, FLAGS) + raise + + +def _copy_flag_dict(flag: flags.Flag) -> Dict[str, Any]: + """Returns a copy of the flag object's ``__dict__``. + + It's mostly a shallow copy of the ``__dict__``, except it also does a shallow + copy of the validator list. + + Args: + flag: flags.Flag, the flag to copy. + + Returns: + A copy of the flag object's ``__dict__``. + """ + copy = flag.__dict__.copy() + copy['_value'] = flag.value # Ensure correct restore for C++ flags. + copy['validators'] = list(flag.validators) + return copy diff --git a/lib/python3.10/site-packages/absl/testing/parameterized.py b/lib/python3.10/site-packages/absl/testing/parameterized.py new file mode 100644 index 0000000000000000000000000000000000000000..1ee91258a702aa6db489edc0d276492f3afd5386 --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/parameterized.py @@ -0,0 +1,726 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Adds support for parameterized tests to Python's unittest TestCase class. + +A parameterized test is a method in a test case that is invoked with different +argument tuples. + +A simple example:: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + (1, 2, 3), + (4, 5, 9), + (1, 1, 3)) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +Each invocation is a separate test case and properly isolated just +like a normal test method, with its own setUp/tearDown cycle. In the +example above, there are three separate testcases, one of which will +fail due to an assertion error (1 + 1 != 3). + +Parameters for individual test cases can be tuples (with positional parameters) +or dictionaries (with named parameters):: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + {'op1': 1, 'op2': 2, 'result': 3}, + {'op1': 4, 'op2': 5, 'result': 9}, + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + +If a parameterized test fails, the error message will show the +original test name and the parameters for that test. + +The id method of the test, used internally by the unittest framework, is also +modified to show the arguments (but note that the name reported by `id()` +doesn't match the actual test name, see below). To make sure that test names +stay the same across several invocations, object representations like:: + + >>> class Foo(object): + ... pass + >>> repr(Foo()) + '<__main__.Foo object at 0x23d8610>' + +are turned into ``__main__.Foo``. When selecting a subset of test cases to run +on the command-line, the test cases contain an index suffix for each argument +in the order they were passed to :func:`parameters` (eg. testAddition0, +testAddition1, etc.) This naming scheme is subject to change; for more reliable +and stable names, especially in test logs, use :func:`named_parameters` instead. + +Tests using :func:`named_parameters` are similar to :func:`parameters`, except +only tuples or dicts of args are supported. For tuples, the first parameter arg +has to be a string (or an object that returns an apt name when converted via +``str()``). For dicts, a value for the key ``testcase_name`` must be present and +must be a string (or an object that returns an apt name when converted via +``str()``):: + + class NamedExample(parameterized.TestCase): + @parameterized.named_parameters( + ('Normal', 'aa', 'aaa', True), + ('EmptyPrefix', '', 'abc', True), + ('BothEmpty', '', '', True)) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, string.startswith(prefix)) + + class NamedExample(parameterized.TestCase): + @parameterized.named_parameters( + {'testcase_name': 'Normal', + 'result': True, 'string': 'aaa', 'prefix': 'aa'}, + {'testcase_name': 'EmptyPrefix', + 'result': True, 'string': 'abc', 'prefix': ''}, + {'testcase_name': 'BothEmpty', + 'result': True, 'string': '', 'prefix': ''}) + def testStartsWith(self, prefix, string, result): + self.assertEqual(result, string.startswith(prefix)) + +Named tests also have the benefit that they can be run individually +from the command line:: + + $ testmodule.py NamedExample.testStartsWithNormal + . + -------------------------------------------------------------------- + Ran 1 test in 0.000s + + OK + +Parameterized Classes +===================== + +If invocation arguments are shared across test methods in a single +TestCase class, instead of decorating all test methods +individually, the class itself can be decorated:: + + @parameterized.parameters( + (1, 2, 3), + (4, 5, 9)) + class ArithmeticTest(parameterized.TestCase): + def testAdd(self, arg1, arg2, result): + self.assertEqual(arg1 + arg2, result) + + def testSubtract(self, arg1, arg2, result): + self.assertEqual(result - arg1, arg2) + +Inputs from Iterables +===================== + +If parameters should be shared across several test cases, or are dynamically +created from other sources, a single non-tuple iterable can be passed into +the decorator. This iterable will be used to obtain the test cases:: + + class AdditionExample(parameterized.TestCase): + @parameterized.parameters( + c.op1, c.op2, c.result for c in testcases + ) + def testAddition(self, op1, op2, result): + self.assertEqual(result, op1 + op2) + + +Single-Argument Test Methods +============================ + +If a test method takes only one argument, the single arguments must not be +wrapped into a tuple:: + + class NegativeNumberExample(parameterized.TestCase): + @parameterized.parameters( + -1, -3, -4, -5 + ) + def testIsNegative(self, arg): + self.assertTrue(IsNegative(arg)) + + +List/tuple as a Single Argument +=============================== + +If a test method takes a single argument of a list/tuple, it must be wrapped +inside a tuple:: + + class ZeroSumExample(parameterized.TestCase): + @parameterized.parameters( + ([-1, 0, 1], ), + ([-2, 0, 2], ), + ) + def testSumIsZero(self, arg): + self.assertEqual(0, sum(arg)) + + +Cartesian product of Parameter Values as Parameterized Test Cases +================================================================= + +If required to test method over a cartesian product of parameters, +`parameterized.product` may be used to facilitate generation of parameters +test combinations:: + + class TestModuloExample(parameterized.TestCase): + @parameterized.product( + num=[0, 20, 80], + modulo=[2, 4], + expected=[0] + ) + def testModuloResult(self, num, modulo, expected): + self.assertEqual(expected, num % modulo) + +This results in 6 test cases being created - one for each combination of the +parameters. It is also possible to supply sequences of keyword argument dicts +as elements of the cartesian product:: + + @parameterized.product( + (dict(num=5, modulo=3, expected=2), + dict(num=7, modulo=4, expected=3)), + dtype=(int, float) + ) + def testModuloResult(self, num, modulo, expected, dtype): + self.assertEqual(expected, dtype(num) % modulo) + +This results in 4 test cases being created - for each of the two sets of test +data (supplied as kwarg dicts) and for each of the two data types (supplied as +a named parameter). Multiple keyword argument dicts may be supplied if required. + +Async Support +============= + +If a test needs to call async functions, it can inherit from both +parameterized.TestCase and another TestCase that supports async calls, such +as [asynctest](https://github.com/Martiusweb/asynctest):: + + import asynctest + + class AsyncExample(parameterized.TestCase, asynctest.TestCase): + @parameterized.parameters( + ('a', 1), + ('b', 2), + ) + async def testSomeAsyncFunction(self, arg, expected): + actual = await someAsyncFunction(arg) + self.assertEqual(actual, expected) +""" + +from collections import abc +import functools +import inspect +import itertools +import re +import types +import unittest +import warnings + +from absl.testing import absltest + + +_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>') +_NAMED = object() +_ARGUMENT_REPR = object() +_NAMED_DICT_KEY = 'testcase_name' + + +class NoTestsError(Exception): + """Raised when parameterized decorators do not generate any tests.""" + + +class DuplicateTestNameError(Exception): + """Raised when a parameterized test has the same test name multiple times.""" + + def __init__(self, test_class_name, new_test_name, original_test_name): + super().__init__( + 'Duplicate parameterized test name in {}: generated test name {!r} ' + '(generated from {!r}) already exists. Consider using ' + 'named_parameters() to give your tests unique names and/or renaming ' + 'the conflicting test method.'.format( + test_class_name, new_test_name, original_test_name + ) + ) + + +def _clean_repr(obj): + return _ADDR_RE.sub(r'<\1>', repr(obj)) + + +def _non_string_or_bytes_iterable(obj): + return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and + not isinstance(obj, bytes)) + + +def _format_parameter_list(testcase_params): + if isinstance(testcase_params, abc.Mapping): + return ', '.join('%s=%s' % (argname, _clean_repr(value)) + for argname, value in testcase_params.items()) + elif _non_string_or_bytes_iterable(testcase_params): + return ', '.join(map(_clean_repr, testcase_params)) + else: + return _format_parameter_list((testcase_params,)) + + +def _async_wrapped(func): + @functools.wraps(func) + async def wrapper(*args, **kwargs): + return await func(*args, **kwargs) + return wrapper + + +class _ParameterizedTestIter: + """Callable and iterable class for producing new test cases.""" + + def __init__(self, test_method, testcases, naming_type, original_name=None): + """Returns concrete test functions for a test and a list of parameters. + + The naming_type is used to determine the name of the concrete + functions as reported by the unittest framework. If naming_type is + _FIRST_ARG, the testcases must be tuples, and the first element must + have a string representation that is a valid Python identifier. + + Args: + test_method: The decorated test method. + testcases: (list of tuple/dict) A list of parameter tuples/dicts for + individual test invocations. + naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR. + original_name: The original test method name. When decorated on a test + method, None is passed to __init__ and test_method.__name__ is used. + Note test_method.__name__ might be different than the original defined + test method because of the use of other decorators. A more accurate + value is set by TestGeneratorMetaclass.__new__ later. + """ + self._test_method = test_method + self.testcases = testcases + self._naming_type = naming_type + if original_name is None: + original_name = test_method.__name__ + self._original_name = original_name + self.__name__ = _ParameterizedTestIter.__name__ + + def __call__(self, *args, **kwargs): + raise RuntimeError('You appear to be running a parameterized test case ' + 'without having inherited from parameterized.' + 'TestCase. This is bad because none of ' + 'your test cases are actually being run. You may also ' + 'be using another decorator before the parameterized ' + 'one, in which case you should reverse the order.') + + def __iter__(self): + test_method = self._test_method + naming_type = self._naming_type + + def make_bound_param_test(testcase_params): + @functools.wraps(test_method) + def bound_param_test(self): + if isinstance(testcase_params, abc.Mapping): + return test_method(self, **testcase_params) + elif _non_string_or_bytes_iterable(testcase_params): + return test_method(self, *testcase_params) + else: + return test_method(self, testcase_params) + + if naming_type is _NAMED: + # Signal the metaclass that the name of the test function is unique + # and descriptive. + bound_param_test.__x_use_name__ = True + + testcase_name = None + if isinstance(testcase_params, abc.Mapping): + if _NAMED_DICT_KEY not in testcase_params: + raise RuntimeError( + 'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY) + # Create a new dict to avoid modifying the supplied testcase_params. + testcase_name = testcase_params[_NAMED_DICT_KEY] + testcase_params = { + k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY + } + elif _non_string_or_bytes_iterable(testcase_params): + if not isinstance(testcase_params[0], str): + raise RuntimeError( + 'The first element of named test parameters is the test name ' + 'suffix and must be a string') + testcase_name = testcase_params[0] + testcase_params = testcase_params[1:] + else: + raise RuntimeError( + 'Named tests must be passed a dict or non-string iterable.') + + test_method_name = self._original_name + # Support PEP-8 underscore style for test naming if used. + if (test_method_name.startswith('test_') + and testcase_name + and not testcase_name.startswith('_')): + test_method_name += '_' + + bound_param_test.__name__ = test_method_name + str(testcase_name) + elif naming_type is _ARGUMENT_REPR: + # If it's a generator, convert it to a tuple and treat them as + # parameters. + if isinstance(testcase_params, types.GeneratorType): + testcase_params = tuple(testcase_params) + # The metaclass creates a unique, but non-descriptive method name for + # _ARGUMENT_REPR tests using an indexed suffix. + # To keep test names descriptive, only the original method name is used. + # To make sure test names are unique, we add a unique descriptive suffix + # __x_params_repr__ for every test. + params_repr = '(%s)' % (_format_parameter_list(testcase_params),) + bound_param_test.__x_params_repr__ = params_repr + else: + raise RuntimeError('%s is not a valid naming type.' % (naming_type,)) + + bound_param_test.__doc__ = '%s(%s)' % ( + bound_param_test.__name__, _format_parameter_list(testcase_params)) + if test_method.__doc__: + bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,) + if inspect.iscoroutinefunction(test_method): + return _async_wrapped(bound_param_test) + return bound_param_test + + return (make_bound_param_test(c) for c in self.testcases) + + +def _modify_class(class_object, testcases, naming_type): + assert not getattr(class_object, '_test_params_reprs', None), ( + 'Cannot add parameters to %s. Either it already has parameterized ' + 'methods, or its super class is also a parameterized class.' % ( + class_object,)) + # NOTE: _test_params_repr is private to parameterized.TestCase and it's + # metaclass; do not use it outside of those classes. + class_object._test_params_reprs = test_params_reprs = {} + for name, obj in class_object.__dict__.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) + and isinstance(obj, types.FunctionType)): + delattr(class_object, name) + methods = {} + _update_class_dict_for_param_test_case( + class_object.__name__, methods, test_params_reprs, name, + _ParameterizedTestIter(obj, testcases, naming_type, name)) + for meth_name, meth in methods.items(): + setattr(class_object, meth_name, meth) + + +def _parameter_decorator(naming_type, testcases): + """Implementation of the parameterization decorators. + + Args: + naming_type: The naming type. + testcases: Testcase parameters. + + Raises: + NoTestsError: Raised when the decorator generates no tests. + + Returns: + A function for modifying the decorated object. + """ + def _apply(obj): + if isinstance(obj, type): + _modify_class(obj, testcases, naming_type) + return obj + else: + return _ParameterizedTestIter(obj, testcases, naming_type) + + if (len(testcases) == 1 and + not isinstance(testcases[0], tuple) and + not isinstance(testcases[0], abc.Mapping)): + # Support using a single non-tuple parameter as a list of test cases. + # Note that the single non-tuple parameter can't be Mapping either, which + # means a single dict parameter case. + assert _non_string_or_bytes_iterable(testcases[0]), ( + 'Single parameter argument must be a non-string non-Mapping iterable') + testcases = testcases[0] + + if not isinstance(testcases, abc.Sequence): + testcases = list(testcases) + if not testcases: + raise NoTestsError( + 'parameterized test decorators did not generate any tests. ' + 'Make sure you specify non-empty parameters, ' + 'and do not reuse generators more than once.') + + return _apply + + +def parameters(*testcases): + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. + + Args: + *testcases: Parameters for the decorated method, either a single + iterable, or a list of tuples/dicts/objects (for tests with only one + argument). + + Raises: + NoTestsError: Raised when the decorator generates no tests. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _parameter_decorator(_ARGUMENT_REPR, testcases) + + +def named_parameters(*testcases): + """A decorator for creating parameterized tests. + + See the module docstring for a usage example. For every parameter tuple + passed, the first element of the tuple should be a string and will be appended + to the name of the test method. Each parameter dict passed must have a value + for the key "testcase_name", the string representation of that value will be + appended to the name of the test method. + + Args: + *testcases: Parameters for the decorated method, either a single iterable, + or a list of tuples or dicts. + + Raises: + NoTestsError: Raised when the decorator generates no tests. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + return _parameter_decorator(_NAMED, testcases) + + +def product(*kwargs_seqs, **testgrid): + """A decorator for running tests over cartesian product of parameters values. + + See the module docstring for a usage example. The test will be run for every + possible combination of the parameters. + + Args: + *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts; + every test case generated will include exactly one kwargs dict from each + positional parameter; these will then be merged to form an overall list + of arguments for the test case. + **testgrid: A mapping of parameter names and their possible values. Possible + values should given as either a list or a tuple. + + Raises: + NoTestsError: Raised when the decorator generates no tests. + + Returns: + A test generator to be handled by TestGeneratorMetaclass. + """ + + for name, values in testgrid.items(): + assert isinstance(values, (list, tuple)), ( + 'Values of {} must be given as list or tuple, found {}'.format( + name, type(values))) + + prior_arg_names = set() + for kwargs_seq in kwargs_seqs: + assert ((isinstance(kwargs_seq, (list, tuple))) and + all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), ( + 'Positional parameters must be a sequence of keyword arg' + 'dicts, found {}' + .format(kwargs_seq)) + if kwargs_seq: + arg_names = set(kwargs_seq[0]) + assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), ( + 'Keyword argument dicts within a single parameter must all have the ' + 'same keys, found {}'.format(kwargs_seq)) + assert not (arg_names & prior_arg_names), ( + 'Keyword argument dict sequences must all have distinct argument ' + 'names, found duplicate(s) {}' + .format(sorted(arg_names & prior_arg_names))) + prior_arg_names |= arg_names + + assert not (prior_arg_names & set(testgrid)), ( + 'Arguments supplied in kwargs dicts in positional parameters must not ' + 'overlap with arguments supplied as named parameters; found duplicate ' + 'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid)))) + + # Convert testgrid into a sequence of sequences of kwargs dicts and combine + # with the positional parameters. + # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]] + testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items()) + testgrid = tuple(kwargs_seqs) + tuple(testgrid) + + # Create all possible combinations of parameters as a cartesian product + # of parameter values. + testcases = [ + dict(itertools.chain.from_iterable(case.items() + for case in cases)) + for cases in itertools.product(*testgrid) + ] + return _parameter_decorator(_ARGUMENT_REPR, testcases) + + +class TestGeneratorMetaclass(type): + """Metaclass for adding tests generated by parameterized decorators.""" + + def __new__(cls, class_name, bases, dct): + # NOTE: _test_params_repr is private to parameterized.TestCase and it's + # metaclass; do not use it outside of those classes. + test_params_reprs = dct.setdefault('_test_params_reprs', {}) + for name, obj in dct.copy().items(): + if (name.startswith(unittest.TestLoader.testMethodPrefix) and + _non_string_or_bytes_iterable(obj)): + # NOTE: `obj` might not be a _ParameterizedTestIter in two cases: + # 1. a class-level iterable named test* that isn't a test, such as + # a list of something. Such attributes get deleted from the class. + # + # 2. If a decorator is applied to the parameterized test, e.g. + # @morestuff + # @parameterized.parameters(...) + # def test_foo(...): ... + # + # This is OK so long as the underlying parameterized function state + # is forwarded (e.g. using functool.wraps() and **without** + # accessing explicitly accessing the internal attributes. + if isinstance(obj, _ParameterizedTestIter): + # Update the original test method name so it's more accurate. + # The mismatch might happen when another decorator is used inside + # the parameterized decrators, and the inner decorator doesn't + # preserve its __name__. + obj._original_name = name + iterator = iter(obj) + dct.pop(name) + _update_class_dict_for_param_test_case( + class_name, dct, test_params_reprs, name, iterator) + # If the base class is a subclass of parameterized.TestCase, inherit its + # _test_params_reprs too. + for base in bases: + # Check if the base has _test_params_reprs first, then check if it's a + # subclass of parameterized.TestCase. Otherwise when this is called for + # the parameterized.TestCase definition itself, this raises because + # itself is not defined yet. This works as long as absltest.TestCase does + # not define _test_params_reprs. + base_test_params_reprs = getattr(base, '_test_params_reprs', None) + if base_test_params_reprs and issubclass(base, TestCase): + for test_method, test_method_id in base_test_params_reprs.items(): + # test_method may both exists in base and this class. + # This class's method overrides base class's. + # That's why it should only inherit it if it does not exist. + test_params_reprs.setdefault(test_method, test_method_id) + + return type.__new__(cls, class_name, bases, dct) + + +def _update_class_dict_for_param_test_case( + test_class_name, dct, test_params_reprs, name, iterator): + """Adds individual test cases to a dictionary. + + Args: + test_class_name: The name of the class tests are added to. + dct: The target dictionary. + test_params_reprs: The dictionary for mapping names to test IDs. + name: The original name of the test case. + iterator: The iterator generating the individual test cases. + + Raises: + DuplicateTestNameError: Raised when a test name occurs multiple times. + RuntimeError: If non-parameterized functions are generated. + """ + for idx, func in enumerate(iterator): + assert callable(func), 'Test generators must yield callables, got %r' % ( + func,) + if not (getattr(func, '__x_use_name__', None) or + getattr(func, '__x_params_repr__', None)): + raise RuntimeError( + '{}.{} generated a test function without using the parameterized ' + 'decorators. Only tests generated using the decorators are ' + 'supported.'.format(test_class_name, name)) + + if getattr(func, '__x_use_name__', False): + original_name = func.__name__ + new_name = original_name + else: + original_name = name + new_name = '%s%d' % (original_name, idx) + + if new_name in dct: + raise DuplicateTestNameError(test_class_name, new_name, original_name) + + dct[new_name] = func + test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '') + + +class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass): + """Base class for test cases using the parameters decorator.""" + + # visibility: private; do not call outside this class. + def _get_params_repr(self): + return self._test_params_reprs.get(self._testMethodName, '') + + def __str__(self): + params_repr = self._get_params_repr() + if params_repr: + params_repr = ' ' + params_repr + return '{}{} ({})'.format( + self._testMethodName, params_repr, + unittest.util.strclass(self.__class__)) + + def id(self): + """Returns the descriptive ID of the test. + + This is used internally by the unittesting framework to get a name + for the test to be used in reports. + + Returns: + The test id. + """ + base = super().id() + params_repr = self._get_params_repr() + if params_repr: + # We include the params in the id so that, when reported in the + # test.xml file, the value is more informative than just "test_foo0". + # Use a space to separate them so that it's copy/paste friendly and + # easy to identify the actual test id. + return f'{base} {params_repr}' + else: + return base + + +# This function is kept CamelCase because it's used as a class's base class. +def CoopTestCase(other_base_class) -> type: # pylint: disable=invalid-name, g-bare-generic + """Returns a new base class with a cooperative metaclass base. + + This enables the TestCase to be used in combination + with other base classes that have custom metaclasses, such as + ``mox.MoxTestBase``. + + Only works with metaclasses that do not override ``type.__new__``. + + Example:: + + from absl.testing import parameterized + + class ExampleTest(parameterized.CoopTestCase(OtherTestCase)): + ... + + Args: + other_base_class: (class) A test case base class. + + Returns: + A new class object. + """ + # If the other base class has a metaclass of 'type' then trying to combine + # the metaclasses will result in an MRO error. So simply combine them and + # return. + if type(other_base_class) == type: # pylint: disable=unidiomatic-typecheck + warnings.warn( + 'CoopTestCase is only necessary when combining with a class that uses' + ' a metaclass. Use multiple inheritance like this instead: class' + f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):', + stacklevel=2, + ) + + class CoopTestCaseBase(other_base_class, TestCase): + pass + + return CoopTestCaseBase + else: + + class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass): # type: ignore # pylint: disable=unused-variable + pass + + class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass): # type: ignore + pass + + return CoopTestCaseBase diff --git a/lib/python3.10/site-packages/absl/testing/xml_reporter.py b/lib/python3.10/site-packages/absl/testing/xml_reporter.py new file mode 100644 index 0000000000000000000000000000000000000000..2db15b70e0813c2ada42322b80f5c9ae5802db3c --- /dev/null +++ b/lib/python3.10/site-packages/absl/testing/xml_reporter.py @@ -0,0 +1,570 @@ +# Copyright 2017 The Abseil Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A Python test reporter that generates test reports in JUnit XML format.""" + +import datetime +import re +import sys +import threading +import time +import traceback +from typing import Any, Dict +import unittest +from xml.sax import saxutils +from absl.testing import _pretty_print_reporter + + +# See http://www.w3.org/TR/REC-xml/#NT-Char +_bad_control_character_codes = set(range(0, 0x20)) - {0x9, 0xA, 0xD} + + +_control_character_conversions = { + chr(i): f'\\x{i:02x}' for i in _bad_control_character_codes +} + + +_escape_xml_attr_conversions = { + '"': '"', + "'": ''', + '\n': ' ', + '\t': ' ', + '\r': ' ', + ' ': ' '} +_escape_xml_attr_conversions.update(_control_character_conversions) + + +# When class or module level function fails, unittest/suite.py adds a +# _ErrorHolder instance instead of a real TestCase, and it has a description +# like "setUpClass (__main__.MyTestCase)". +_CLASS_OR_MODULE_LEVEL_TEST_DESC_REGEX = re.compile(r'^(\w+) \((\S+)\)$') + + +# NOTE: while saxutils.quoteattr() theoretically does the same thing; it +# seems to often end up being too smart for it's own good not escaping properly. +# This function is much more reliable. +def _escape_xml_attr(content): + """Escapes xml attributes.""" + # Note: saxutils doesn't escape the quotes. + return saxutils.escape(content, _escape_xml_attr_conversions) + + +def _escape_cdata(s): + """Escapes a string to be used as XML CDATA. + + CDATA characters are treated strictly as character data, not as XML markup, + but there are still certain restrictions on them. + + Args: + s: the string to be escaped. + Returns: + An escaped version of the input string. + """ + for char, escaped in _control_character_conversions.items(): + s = s.replace(char, escaped) + return s.replace(']]>', ']] >') + + +def _iso8601_timestamp(timestamp): + """Produces an ISO8601 datetime. + + Args: + timestamp: an Epoch based timestamp in seconds. + + Returns: + A iso8601 format timestamp if the input is a valid timestamp, None otherwise + """ + if timestamp is None or timestamp < 0: + return None + return datetime.datetime.fromtimestamp( + timestamp, tz=datetime.timezone.utc).isoformat() + + +def _print_xml_element_header(element, attributes, stream, indentation=''): + """Prints an XML header of an arbitrary element. + + Args: + element: element name (testsuites, testsuite, testcase) + attributes: 2-tuple list with (attributes, values) already escaped + stream: output stream to write test report XML to + indentation: indentation added to the element header + """ + stream.write('%s<%s' % (indentation, element)) + for attribute in attributes: + if (len(attribute) == 2 and attribute[0] is not None and + attribute[1] is not None): + stream.write(' %s="%s"' % (attribute[0], attribute[1])) + stream.write('>\n') + +# Copy time.time which ensures the real time is used internally. +# This prevents bad interactions with tests that stub out time. +_time_copy = time.time + + +def _safe_str(obj: object) -> str: + """Returns a string representation of an object.""" + try: + return str(obj) + except Exception: # pylint: disable=broad-except + return '' % ( + type(obj).__module__, + type(obj).__name__, + ) + + +class _TestCaseResult: + """Private helper for _TextAndXMLTestResult that represents a test result. + + Attributes: + test: A TestCase instance of an individual test method. + name: The name of the individual test method. + full_class_name: The full name of the test class. + run_time: The duration (in seconds) it took to run the test. + start_time: Epoch relative timestamp of when test started (in seconds) + errors: A list of error 4-tuples. Error tuple entries are + 1) a string identifier of either "failure" or "error" + 2) an exception_type + 3) an exception_message + 4) a string version of a sys.exc_info()-style tuple of values + ('error', err[0], err[1], self._exc_info_to_string(err)) + If the length of errors is 0, then the test is either passed or + skipped. + skip_reason: A string explaining why the test was skipped. + """ + + def __init__(self, test): + self.run_time = -1 + self.start_time = -1 + self.skip_reason = None + self.errors = [] + self.test = test + + # Parse the test id to get its test name and full class path. + # Unfortunately there is no better way of knowning the test and class. + # Worse, unittest uses _ErrorHandler instances to represent class / module + # level failures. + test_desc = test.id() or str(test) + # Check if it's something like "setUpClass (__main__.TestCase)". + match = _CLASS_OR_MODULE_LEVEL_TEST_DESC_REGEX.match(test_desc) + if match: + name = match.group(1) + full_class_name = match.group(2) + else: + class_name = unittest.util.strclass(test.__class__) + if isinstance(test, unittest.case._SubTest): + # If the test case is a _SubTest, the real TestCase instance is + # available as _SubTest.test_case. + class_name = unittest.util.strclass(test.test_case.__class__) + if test_desc.startswith(class_name + '.'): + # In a typical unittest.TestCase scenario, test.id() returns with + # a class name formatted using unittest.util.strclass. + name = test_desc[len(class_name)+1:] + full_class_name = class_name + else: + # Otherwise make a best effort to guess the test name and full class + # path. + parts = test_desc.rsplit('.', 1) + name = parts[-1] + full_class_name = parts[0] if len(parts) == 2 else '' + self.name = _escape_xml_attr(name) + self.full_class_name = _escape_xml_attr(full_class_name) + + def set_run_time(self, time_in_secs): + self.run_time = time_in_secs + + def set_start_time(self, time_in_secs): + self.start_time = time_in_secs + + def print_xml_summary(self, stream): + """Prints an XML Summary of a TestCase. + + Status and result are populated as per JUnit XML test result reporter. + A test that has been skipped will always have a skip reason, + as every skip method in Python's unittest requires the reason arg to be + passed. + + Args: + stream: output stream to write test report XML to + """ + + if self.skip_reason is None: + status = 'run' + result = 'completed' + else: + status = 'notrun' + result = 'suppressed' + + test_case_attributes = [ + ('name', '%s' % self.name), + ('status', '%s' % status), + ('result', '%s' % result), + ('time', '%.3f' % self.run_time), + ('classname', self.full_class_name), + ('timestamp', _iso8601_timestamp(self.start_time)), + ] + _print_xml_element_header('testcase', test_case_attributes, stream, ' ') + self._print_testcase_details(stream) + stream.write(' \n') + + def _print_testcase_details(self, stream): + for error in self.errors: + outcome, exception_type, message, error_msg = error # pylint: disable=unpacking-non-sequence + message = _escape_xml_attr(_safe_str(message)) + exception_type = _escape_xml_attr(str(exception_type)) + error_msg = _escape_cdata(error_msg) + stream.write(' <%s message="%s" type="%s">\n' + % (outcome, message, exception_type, error_msg, outcome)) + + +class _TestSuiteResult: + """Private helper for _TextAndXMLTestResult.""" + + def __init__(self): + self.suites = {} + self.failure_counts = {} + self.error_counts = {} + self.overall_start_time = -1 + self.overall_end_time = -1 + self._testsuites_properties = {} + + def add_test_case_result(self, test_case_result): + suite_name = type(test_case_result.test).__name__ + if suite_name == '_ErrorHolder': + # _ErrorHolder is a special case created by unittest for class / module + # level functions. + suite_name = test_case_result.full_class_name.rsplit('.')[-1] + if isinstance(test_case_result.test, unittest.case._SubTest): + # If the test case is a _SubTest, the real TestCase instance is + # available as _SubTest.test_case. + suite_name = type(test_case_result.test.test_case).__name__ + + self._setup_test_suite(suite_name) + self.suites[suite_name].append(test_case_result) + for error in test_case_result.errors: + # Only count the first failure or error so that the sum is equal to the + # total number of *testcases* that have failures or errors. + if error[0] == 'failure': + self.failure_counts[suite_name] += 1 + break + elif error[0] == 'error': + self.error_counts[suite_name] += 1 + break + + def print_xml_summary(self, stream): + overall_test_count = sum(len(x) for x in self.suites.values()) + overall_failures = sum(self.failure_counts.values()) + overall_errors = sum(self.error_counts.values()) + overall_attributes = [ + ('name', ''), + ('tests', '%d' % overall_test_count), + ('failures', '%d' % overall_failures), + ('errors', '%d' % overall_errors), + ('time', '%.3f' % (self.overall_end_time - self.overall_start_time)), + ('timestamp', _iso8601_timestamp(self.overall_start_time)), + ] + _print_xml_element_header('testsuites', overall_attributes, stream) + if self._testsuites_properties: + stream.write(' \n') + for name, value in sorted(self._testsuites_properties.items()): + stream.write(' \n' % + (_escape_xml_attr(name), _escape_xml_attr(str(value)))) + stream.write(' \n') + + for suite_name in self.suites: + suite = self.suites[suite_name] + suite_end_time = max(x.start_time + x.run_time for x in suite) + suite_start_time = min(x.start_time for x in suite) + failures = self.failure_counts[suite_name] + errors = self.error_counts[suite_name] + suite_attributes = [ + ('name', '%s' % suite_name), + ('tests', '%d' % len(suite)), + ('failures', '%d' % failures), + ('errors', '%d' % errors), + ('time', '%.3f' % (suite_end_time - suite_start_time)), + ('timestamp', _iso8601_timestamp(suite_start_time)), + ] + _print_xml_element_header('testsuite', suite_attributes, stream) + + # test_case_result entries are not guaranteed to be in any user-friendly + # order, especially when using subtests. So sort them. + for test_case_result in sorted(suite, key=lambda t: t.name): + test_case_result.print_xml_summary(stream) + stream.write('\n') + stream.write('\n') + + def _setup_test_suite(self, suite_name): + """Adds a test suite to the set of suites tracked by this test run. + + Args: + suite_name: string, The name of the test suite being initialized. + """ + if suite_name in self.suites: + return + self.suites[suite_name] = [] + self.failure_counts[suite_name] = 0 + self.error_counts[suite_name] = 0 + + def set_end_time(self, timestamp_in_secs): + """Sets the start timestamp of this test suite. + + Args: + timestamp_in_secs: timestamp in seconds since epoch + """ + self.overall_end_time = timestamp_in_secs + + def set_start_time(self, timestamp_in_secs): + """Sets the end timestamp of this test suite. + + Args: + timestamp_in_secs: timestamp in seconds since epoch + """ + self.overall_start_time = timestamp_in_secs + + +class _TextAndXMLTestResult(_pretty_print_reporter.TextTestResult): + """Private TestResult class that produces both formatted text results and XML. + + Used by TextAndXMLTestRunner. + """ + + _TEST_SUITE_RESULT_CLASS = _TestSuiteResult + _TEST_CASE_RESULT_CLASS = _TestCaseResult + + def __init__(self, xml_stream, stream, descriptions, verbosity, + time_getter=_time_copy, testsuites_properties=None): + super().__init__(stream, descriptions, verbosity) + self.xml_stream = xml_stream + self.pending_test_case_results = {} + self.suite = self._TEST_SUITE_RESULT_CLASS() + if testsuites_properties: + self.suite._testsuites_properties = testsuites_properties + self.time_getter = time_getter + + # This lock guards any mutations on pending_test_case_results. + self._pending_test_case_results_lock = threading.RLock() + + def startTest(self, test): + self.start_time = self.time_getter() + super().startTest(test) + + def stopTest(self, test): + # Grabbing the write lock to avoid conflicting with stopTestRun. + with self._pending_test_case_results_lock: + super().stopTest(test) + result = self.get_pending_test_case_result(test) + if not result: + test_name = test.id() or str(test) + sys.stderr.write('No pending test case: %s\n' % test_name) + return + if getattr(self, 'start_time', None) is None: + # startTest may not be called for skipped tests since Python 3.12.1. + self.start_time = self.time_getter() + test_id = id(test) + run_time = self.time_getter() - self.start_time + result.set_run_time(run_time) + result.set_start_time(self.start_time) + self.suite.add_test_case_result(result) + del self.pending_test_case_results[test_id] + + def startTestRun(self): + self.suite.set_start_time(self.time_getter()) + super().startTestRun() + + def stopTestRun(self): + self.suite.set_end_time(self.time_getter()) + # All pending_test_case_results will be added to the suite and removed from + # the pending_test_case_results dictionary. Grabbing the write lock to avoid + # results from being added during this process to avoid duplicating adds or + # accidentally erasing newly appended pending results. + with self._pending_test_case_results_lock: + # Errors in the test fixture (setUpModule, tearDownModule, + # setUpClass, tearDownClass) can leave a pending result which + # never gets added to the suite. The runner calls stopTestRun + # which gives us an opportunity to add these errors for + # reporting here. + for test_id in self.pending_test_case_results: + result = self.pending_test_case_results[test_id] + if getattr(self, 'start_time', None) is not None: + run_time = self.suite.overall_end_time - self.start_time + result.set_run_time(run_time) + result.set_start_time(self.start_time) + self.suite.add_test_case_result(result) + self.pending_test_case_results.clear() + + def _exc_info_to_string(self, err, test=None): + """Converts a sys.exc_info()-style tuple of values into a string. + + This method must be overridden because the method signature in + unittest.TestResult changed between Python 2.2 and 2.4. + + Args: + err: A sys.exc_info() tuple of values for an error. + test: The test method. + + Returns: + A formatted exception string. + """ + if test: + return super()._exc_info_to_string(err, test) + return ''.join(traceback.format_exception(*err)) + + def add_pending_test_case_result(self, test, error_summary=None, + skip_reason=None): + """Adds result information to a test case result which may still be running. + + If a result entry for the test already exists, add_pending_test_case_result + will add error summary tuples and/or overwrite skip_reason for the result. + If it does not yet exist, a result entry will be created. + Note that a test result is considered to have been run and passed + only if there are no errors or skip_reason. + + Args: + test: A test method as defined by unittest + error_summary: A 4-tuple with the following entries: + 1) a string identifier of either "failure" or "error" + 2) an exception_type + 3) an exception_message + 4) a string version of a sys.exc_info()-style tuple of values + ('error', err[0], err[1], self._exc_info_to_string(err)) + If the length of errors is 0, then the test is either passed or + skipped. + skip_reason: a string explaining why the test was skipped + """ + with self._pending_test_case_results_lock: + test_id = id(test) + if test_id not in self.pending_test_case_results: + self.pending_test_case_results[test_id] = self._TEST_CASE_RESULT_CLASS( + test) + if error_summary: + self.pending_test_case_results[test_id].errors.append(error_summary) + if skip_reason: + self.pending_test_case_results[test_id].skip_reason = skip_reason + + def delete_pending_test_case_result(self, test): + with self._pending_test_case_results_lock: + test_id = id(test) + del self.pending_test_case_results[test_id] + + def get_pending_test_case_result(self, test): + test_id = id(test) + return self.pending_test_case_results.get(test_id, None) + + def addSuccess(self, test): + super().addSuccess(test) + self.add_pending_test_case_result(test) + + def addError(self, test, err): + super().addError(test, err) + error_summary = ('error', err[0], err[1], + self._exc_info_to_string(err, test=test)) + self.add_pending_test_case_result(test, error_summary=error_summary) + + def addFailure(self, test, err): + super().addFailure(test, err) + error_summary = ('failure', err[0], err[1], + self._exc_info_to_string(err, test=test)) + self.add_pending_test_case_result(test, error_summary=error_summary) + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self.add_pending_test_case_result(test, skip_reason=reason) + + def addExpectedFailure(self, test, err): + super().addExpectedFailure(test, err) + if callable(getattr(test, 'recordProperty', None)): + test.recordProperty('EXPECTED_FAILURE', + self._exc_info_to_string(err, test=test)) + self.add_pending_test_case_result(test) + + def addUnexpectedSuccess(self, test): + super().addUnexpectedSuccess(test) + test_name = test.id() or str(test) + error_summary = ('error', '', '', + 'Test case %s should have failed, but passed.' + % (test_name)) + self.add_pending_test_case_result(test, error_summary=error_summary) + + def addSubTest(self, test, subtest, err): # pylint: disable=invalid-name + super().addSubTest(test, subtest, err) + if err is not None: + if issubclass(err[0], test.failureException): + error_summary = ('failure', err[0], err[1], + self._exc_info_to_string(err, test=test)) + else: + error_summary = ('error', err[0], err[1], + self._exc_info_to_string(err, test=test)) + else: + error_summary = None + self.add_pending_test_case_result(subtest, error_summary=error_summary) + + def printErrors(self): + super().printErrors() + self.xml_stream.write('\n') + self.suite.print_xml_summary(self.xml_stream) + + +class TextAndXMLTestRunner(unittest.TextTestRunner): + """A test runner that produces both formatted text results and XML. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + + _TEST_RESULT_CLASS = _TextAndXMLTestResult + + _xml_stream = None + _testsuites_properties: Dict[Any, Any] = {} + + def __init__(self, xml_stream=None, *args, **kwargs): + """Initialize a TextAndXMLTestRunner. + + Args: + xml_stream: file-like or None; XML-formatted test results are output + via this object's write() method. If None (the default), the + new instance behaves as described in the set_default_xml_stream method + documentation below. + *args: passed unmodified to unittest.TextTestRunner.__init__. + **kwargs: passed unmodified to unittest.TextTestRunner.__init__. + """ + super().__init__(*args, **kwargs) + if xml_stream is not None: + self._xml_stream = xml_stream + # else, do not set self._xml_stream to None -- this allows implicit fallback + # to the class attribute's value. + + @classmethod + def set_default_xml_stream(cls, xml_stream): + """Sets the default XML stream for the class. + + Args: + xml_stream: file-like or None; used for instances when xml_stream is None + or not passed to their constructors. If None is passed, instances + created with xml_stream=None will act as ordinary TextTestRunner + instances; this is the default state before any calls to this method + have been made. + """ + cls._xml_stream = xml_stream + + def _makeResult(self): + if self._xml_stream is None: + return super()._makeResult() + else: + return self._TEST_RESULT_CLASS( + self._xml_stream, self.stream, self.descriptions, self.verbosity, + testsuites_properties=self._testsuites_properties) + + @classmethod + def set_testsuites_property(cls, key, value): + cls._testsuites_properties[key] = value diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/INSTALLER b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/METADATA b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..aa710852bf76ca2a1f0429ec6296a8d049d8bcdc --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/METADATA @@ -0,0 +1,101 @@ +Metadata-Version: 2.4 +Name: absl-py +Version: 2.3.1 +Summary: Abseil Python Common Libraries, see https://github.com/abseil/abseil-py. +Project-URL: Changelog, https://github.com/abseil/abseil-py/blob/main/CHANGELOG.md +Project-URL: Documentation, https://abseil.io/docs/python/ +Project-URL: Issues, https://github.com/abseil/abseil-py/issues +Project-URL: Source, https://github.com/abseil/abseil-py +Author: The Abseil Authors +License-Expression: Apache-2.0 +License-File: AUTHORS +License-File: LICENSE +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +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: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.8 +Description-Content-Type: text/markdown + +[![Package version](https://img.shields.io/pypi/v/absl-py)](https://pypi.org/project/absl-py) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/absl-py.svg?style=flat-square)](https://pypi.org/project/absl-py) +[![License](https://img.shields.io/github/license/abseil/abseil-py)](https://github.com/abseil/abseil-py/blob/main/LICENSE) +[![Build Status](https://github.com/abseil/abseil-py/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/abseil/abseil-py/actions) +[![Overall downloads](https://pepy.tech/badge/absl-py)](https://pepy.tech/project/absl-py) +[![Last month downloads](https://pepy.tech/badge/absl-py/month)](https://pepy.tech/project/absl-py) + +# Abseil Python Common Libraries + +This repository is a collection of Python library code for building Python +applications. The code is collected from Google's own Python code base, and has +been extensively tested and used in production. + +## Features + +* Simple application startup +* Distributed commandline flags system +* Custom logging module with additional features +* Testing utilities + +## Getting Started + +### Installation + +To install the package, simply run: + +```bash +pip install absl-py +``` + +Or install from source: + +```bash +pip install . +``` + +### Running Tests + +To run Abseil tests, you can clone the git repo and run +[bazel](https://bazel.build/): + +```bash +git clone https://github.com/abseil/abseil-py.git +cd abseil-py +bazel test absl/... +``` + +Please also validate the type annotations against the latest mypy: + +```bash +pip install mypy +mypy absl +``` + +### Example Code + +Please refer to +[smoke_tests/sample_app.py](https://github.com/abseil/abseil-py/blob/main/smoke_tests/sample_app.py) +as an example to get started. + +## Documentation + +See the [Abseil Python Developer Guide](https://abseil.io/docs/python/). + +## Future Releases + +The current repository includes an initial set of libraries for early adoption. +More components and interoperability with Abseil C++ Common Libraries +will come in future releases. + +## License + +The Abseil Python library is licensed under the terms of the Apache +license. See [LICENSE](LICENSE) for more information. diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/RECORD b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..0775e3ebf75b79f0e06fc1acd5e6d62ddaa75c84 --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/RECORD @@ -0,0 +1,53 @@ +absl/__init__.py,sha256=9iJ5PwnoYh12ztceccjD2JbnSvx6d2JVElEurn9cqJk,607 +absl/__pycache__/__init__.cpython-310.pyc,, +absl/__pycache__/app.cpython-310.pyc,, +absl/__pycache__/command_name.cpython-310.pyc,, +absl/app.py,sha256=0WJpjLaYcgcXtl9NTn-i5jSEfAoweMg4SXAel2XLF0Y,15360 +absl/app.pyi,sha256=NdRh0OBqWHKnc3hEjdLaspLdu6aDHn3XP360ZPZeQE8,1885 +absl/command_name.py,sha256=FgWUMHmlX0yQxEuMPXxFxn5ayWWZSLB0cq8Vx361TpU,2283 +absl/flags/__init__.py,sha256=n_uLeSK-15_1DKLb0wXivmt6A8xvADDC87D1yoJdqVU,7665 +absl/flags/__pycache__/__init__.cpython-310.pyc,, +absl/flags/__pycache__/_argument_parser.cpython-310.pyc,, +absl/flags/__pycache__/_defines.cpython-310.pyc,, +absl/flags/__pycache__/_exceptions.cpython-310.pyc,, +absl/flags/__pycache__/_flag.cpython-310.pyc,, +absl/flags/__pycache__/_flagvalues.cpython-310.pyc,, +absl/flags/__pycache__/_helpers.cpython-310.pyc,, +absl/flags/__pycache__/_validators.cpython-310.pyc,, +absl/flags/__pycache__/_validators_classes.cpython-310.pyc,, +absl/flags/__pycache__/argparse_flags.cpython-310.pyc,, +absl/flags/_argument_parser.py,sha256=arzkHA3CbPJdJ7diqUyskdoaLtKt8Aij1EZCVH_qJHU,20629 +absl/flags/_defines.py,sha256=NAK489NGv3YMvbYj2Jzl6q89HmAtmMI65di1L_RK7XE,52895 +absl/flags/_exceptions.py,sha256=FZzlzhvkjqPImTxXqbS1pSPYKr_TvtOd5ellvoiVLDI,3619 +absl/flags/_flag.py,sha256=mdMJFklKQdCi9WsWPvUlmBRoQ2IAPW0Z0lDIKj0Lsx8,20079 +absl/flags/_flagvalues.py,sha256=cGzMsWxthqGYfpESReRSJCXjpRujosSmTT-efJW3CbQ,54364 +absl/flags/_helpers.py,sha256=MHbgtRkbNpVnrr7_NCdkFi_x3voQa-1-bypqsunHCJE,14154 +absl/flags/_validators.py,sha256=VcsJtZzohliNxsI974NECYpeozD8rswHNHXggrQ4BLo,14140 +absl/flags/_validators_classes.py,sha256=PGUWzO7v3wPOHb9leIKKzry3q-pPeKCoMB_O7prLdnY,6093 +absl/flags/argparse_flags.py,sha256=usJudgMpy3P6Vvq7-LmJNa2Rj3ygHM3hwDTGd1mbAzc,14386 +absl/logging/__init__.py,sha256=XpcmJrFEzDK2iWTlmDwtNSScSiGiNedFibWuW0tWMbk,43583 +absl/logging/__init__.pyi,sha256=39EEBOH_rAyDQJpwyito2vo4IAZP9hnw3-wXC_Gulvc,5925 +absl/logging/__pycache__/__init__.cpython-310.pyc,, +absl/logging/__pycache__/converter.cpython-310.pyc,, +absl/logging/converter.py,sha256=6eBymfv9UNkog0BGat4HPWlxC_oSqvHcQ46jnSdtaMg,6323 +absl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +absl/testing/__init__.py,sha256=7cM57swk2T1Hc5wxmt-JpcaR6xfdPJyL_lyRqgODvuM,584 +absl/testing/__pycache__/__init__.cpython-310.pyc,, +absl/testing/__pycache__/_bazelize_command.cpython-310.pyc,, +absl/testing/__pycache__/_pretty_print_reporter.cpython-310.pyc,, +absl/testing/__pycache__/absltest.cpython-310.pyc,, +absl/testing/__pycache__/flagsaver.cpython-310.pyc,, +absl/testing/__pycache__/parameterized.cpython-310.pyc,, +absl/testing/__pycache__/xml_reporter.cpython-310.pyc,, +absl/testing/_bazelize_command.py,sha256=qpioV02ln2sBBJ9kdlHgNpKk8_wxdz2hJGKbG6EWZMI,2287 +absl/testing/_pretty_print_reporter.py,sha256=PZh9NXSXBbXDi0FOk-BOmpse8LXa92Er16tgyBRogMs,3065 +absl/testing/absltest.py,sha256=PW4c4SVlOUPSvFIhayimJY2LEbJ7BAojob6u9SBUrck,105675 +absl/testing/flagsaver.py,sha256=HIWzFyayy-Pa8TqTAXF7BtHQ7s9Uk68KE8AVVAM0o0w,13346 +absl/testing/parameterized.py,sha256=TpTlWTUXjikGeUDE45AgubvnmHsPbFNj-wUtAwT-e6E,27817 +absl/testing/xml_reporter.py,sha256=YFkM7SROW8aeoCCn8IsGZ4cqXu4x8MwL1oN42-OtPKI,21430 +absl_py-2.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +absl_py-2.3.1.dist-info/METADATA,sha256=2HZS24vkmHw7GQqUWcDE_FdVXjW1dgVS7pWPdWJ5Yvg,3331 +absl_py-2.3.1.dist-info/RECORD,, +absl_py-2.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +absl_py-2.3.1.dist-info/licenses/AUTHORS,sha256=YoLudsylaQg7W5mLn4FroQMuEnuNx8RpQrhkd_xvv6U,296 +absl_py-2.3.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/WHEEL b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..12228d414b6cfed7c39d3781c85c63256a1d7fb5 --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/AUTHORS b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/AUTHORS new file mode 100644 index 0000000000000000000000000000000000000000..23b11ada16bb8e69695cf52e5994784d98054e0d --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/AUTHORS @@ -0,0 +1,7 @@ +# This is the list of Abseil authors for copyright purposes. +# +# This does not necessarily list everyone who has contributed code, since in +# some cases, their employer may be the copyright holder. To see the full list +# of contributors, see the revision history in source control. + +Google Inc. diff --git a/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/LICENSE b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7 --- /dev/null +++ b/lib/python3.10/site-packages/absl_py-2.3.1.dist-info/licenses/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/python3.10/site-packages/addict-2.4.0.dist-info/INSTALLER b/lib/python3.10/site-packages/addict-2.4.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/addict-2.4.0.dist-info/LICENSE b/lib/python3.10/site-packages/addict-2.4.0.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c9a0fae0f709d76c3246a876b03671cd866be162 --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mats Julian Olsen + +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/lib/python3.10/site-packages/addict-2.4.0.dist-info/METADATA b/lib/python3.10/site-packages/addict-2.4.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..864b661e77934b1dde26365b034e3bb5505cc1ee --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/METADATA @@ -0,0 +1,23 @@ +Metadata-Version: 2.1 +Name: addict +Version: 2.4.0 +Summary: Addict is a dictionary whose items can be set using both attribute and item syntax. +Home-page: https://github.com/mewwts/addict +Author: Mats Julian Olsen +Author-email: mats@plysjbyen.net +License: UNKNOWN +Platform: UNKNOWN +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Libraries :: Python Modules + +Addict is a module that exposes a dictionary subclass that allows items to be set like attributes. Values are gettable and settable using both attribute and item syntax. For more info check out the README at 'github.com/mewwts/addict'. + + diff --git a/lib/python3.10/site-packages/addict-2.4.0.dist-info/RECORD b/lib/python3.10/site-packages/addict-2.4.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..0ac49712a0a2059a32bb05372c8b4ddb7f3b0527 --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/RECORD @@ -0,0 +1,10 @@ +addict-2.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +addict-2.4.0.dist-info/LICENSE,sha256=ykiNM8US0LImFCCQr5DomuJmqQGik_if1kLf7JMeIsE,1085 +addict-2.4.0.dist-info/METADATA,sha256=ZjIJZ5lvhfohHxIWp79fkh0TtBsN5duX84na3bxU6vQ,1028 +addict-2.4.0.dist-info/RECORD,, +addict-2.4.0.dist-info/WHEEL,sha256=EVRjI69F5qVjm_YgqcTXPnTAv3BfSUr0WVAHuSP3Xoo,92 +addict-2.4.0.dist-info/top_level.txt,sha256=GGfQKsz1N-bSRYzNpyp8GY8uAEqh3SWaXRbAnBUKjHA,7 +addict/__init__.py,sha256=xmlNiYYVoAD3_FfmgfOM2cc3wI17GSgsMBODbFRs_mY,233 +addict/__pycache__/__init__.cpython-310.pyc,, +addict/__pycache__/addict.cpython-310.pyc,, +addict/addict.py,sha256=ScOfclaAM7ugyZoXOW2HHL9gCXt_XPe-i5bTLiY8ufU,4901 diff --git a/lib/python3.10/site-packages/addict-2.4.0.dist-info/WHEEL b/lib/python3.10/site-packages/addict-2.4.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..83ff02e961fce5ad7befce746ff02635e1616315 --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.35.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/addict-2.4.0.dist-info/top_level.txt b/lib/python3.10/site-packages/addict-2.4.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..b765b5f4af811ab630ffa864563a666850c266fc --- /dev/null +++ b/lib/python3.10/site-packages/addict-2.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +addict diff --git a/lib/python3.10/site-packages/addict/__init__.py b/lib/python3.10/site-packages/addict/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b0af039c6298a3889a8e8b2e0329fe2313c120d0 --- /dev/null +++ b/lib/python3.10/site-packages/addict/__init__.py @@ -0,0 +1,10 @@ +from .addict import Dict +from .addict import Dict as Addict + + +__title__ = 'addict' +__version__ = '2.4.0' +__author__ = 'Mats Julian Olsen' +__license__ = 'MIT' +__copyright__ = 'Copyright 2014-2020 Mats Julian Olsen' +__all__ = ['Dict'] diff --git a/lib/python3.10/site-packages/addict/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/addict/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..208b157e29b70f35633faa9f2221415d04ce358a Binary files /dev/null and b/lib/python3.10/site-packages/addict/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/addict/__pycache__/addict.cpython-310.pyc b/lib/python3.10/site-packages/addict/__pycache__/addict.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6578b333b191843a0974e0d8bc27acc04339601 Binary files /dev/null and b/lib/python3.10/site-packages/addict/__pycache__/addict.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/addict/addict.py b/lib/python3.10/site-packages/addict/addict.py new file mode 100644 index 0000000000000000000000000000000000000000..55e02d1d17596c77a6f3642ba02eeb30971048bd --- /dev/null +++ b/lib/python3.10/site-packages/addict/addict.py @@ -0,0 +1,159 @@ +import copy + + +class Dict(dict): + + def __init__(__self, *args, **kwargs): + object.__setattr__(__self, '__parent', kwargs.pop('__parent', None)) + object.__setattr__(__self, '__key', kwargs.pop('__key', None)) + object.__setattr__(__self, '__frozen', False) + for arg in args: + if not arg: + continue + elif isinstance(arg, dict): + for key, val in arg.items(): + __self[key] = __self._hook(val) + elif isinstance(arg, tuple) and (not isinstance(arg[0], tuple)): + __self[arg[0]] = __self._hook(arg[1]) + else: + for key, val in iter(arg): + __self[key] = __self._hook(val) + + for key, val in kwargs.items(): + __self[key] = __self._hook(val) + + def __setattr__(self, name, value): + if hasattr(self.__class__, name): + raise AttributeError("'Dict' object attribute " + "'{0}' is read-only".format(name)) + else: + self[name] = value + + def __setitem__(self, name, value): + isFrozen = (hasattr(self, '__frozen') and + object.__getattribute__(self, '__frozen')) + if isFrozen and name not in super(Dict, self).keys(): + raise KeyError(name) + super(Dict, self).__setitem__(name, value) + try: + p = object.__getattribute__(self, '__parent') + key = object.__getattribute__(self, '__key') + except AttributeError: + p = None + key = None + if p is not None: + p[key] = self + object.__delattr__(self, '__parent') + object.__delattr__(self, '__key') + + def __add__(self, other): + if not self.keys(): + return other + else: + self_type = type(self).__name__ + other_type = type(other).__name__ + msg = "unsupported operand type(s) for +: '{}' and '{}'" + raise TypeError(msg.format(self_type, other_type)) + + @classmethod + def _hook(cls, item): + if isinstance(item, dict): + return cls(item) + elif isinstance(item, (list, tuple)): + return type(item)(cls._hook(elem) for elem in item) + return item + + def __getattr__(self, item): + return self.__getitem__(item) + + def __missing__(self, name): + if object.__getattribute__(self, '__frozen'): + raise KeyError(name) + return self.__class__(__parent=self, __key=name) + + def __delattr__(self, name): + del self[name] + + def to_dict(self): + base = {} + for key, value in self.items(): + if isinstance(value, type(self)): + base[key] = value.to_dict() + elif isinstance(value, (list, tuple)): + base[key] = type(value)( + item.to_dict() if isinstance(item, type(self)) else + item for item in value) + else: + base[key] = value + return base + + def copy(self): + return copy.copy(self) + + def deepcopy(self): + return copy.deepcopy(self) + + def __deepcopy__(self, memo): + other = self.__class__() + memo[id(self)] = other + for key, value in self.items(): + other[copy.deepcopy(key, memo)] = copy.deepcopy(value, memo) + return other + + def update(self, *args, **kwargs): + other = {} + if args: + if len(args) > 1: + raise TypeError() + other.update(args[0]) + other.update(kwargs) + for k, v in other.items(): + if ((k not in self) or + (not isinstance(self[k], dict)) or + (not isinstance(v, dict))): + self[k] = v + else: + self[k].update(v) + + def __getnewargs__(self): + return tuple(self.items()) + + def __getstate__(self): + return self + + def __setstate__(self, state): + self.update(state) + + def __or__(self, other): + if not isinstance(other, (Dict, dict)): + return NotImplemented + new = Dict(self) + new.update(other) + return new + + def __ror__(self, other): + if not isinstance(other, (Dict, dict)): + return NotImplemented + new = Dict(other) + new.update(self) + return new + + def __ior__(self, other): + self.update(other) + return self + + def setdefault(self, key, default=None): + if key in self: + return self[key] + else: + self[key] = default + return default + + def freeze(self, shouldFreeze=True): + object.__setattr__(self, '__frozen', shouldFreeze) + for key, val in self.items(): + if isinstance(val, Dict): + val.freeze(shouldFreeze) + + def unfreeze(self): + self.freeze(False) diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/INSTALLER b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/LICENSE b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..8dada3edaf50dbc082c9a125058f25def75e625a --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/METADATA b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..2139497f4daccad0abc5140be0f6655c7285329c --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/METADATA @@ -0,0 +1,118 @@ +Metadata-Version: 2.1 +Name: asttokens +Version: 3.0.0 +Summary: Annotate AST trees with source code positions +Home-page: https://github.com/gristlabs/asttokens +Author: Dmitry Sagalovskiy, Grist Labs +Author-email: dmitry@getgrist.com +License: Apache 2.0 +Keywords: code,ast,parse,tokenize,refactor +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: Code Generators +Classifier: Topic :: Software Development :: Compilers +Classifier: Topic :: Software Development :: Interpreters +Classifier: Topic :: Software Development :: Pre-processors +Classifier: Environment :: Console +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3.8 +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 :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.8 +License-File: LICENSE +Provides-Extra: astroid +Requires-Dist: astroid<4,>=2; extra == "astroid" +Provides-Extra: test +Requires-Dist: astroid<4,>=2; extra == "test" +Requires-Dist: pytest; extra == "test" +Requires-Dist: pytest-cov; extra == "test" +Requires-Dist: pytest-xdist; extra == "test" + +ASTTokens +========= + +.. image:: https://img.shields.io/pypi/v/asttokens.svg + :target: https://pypi.python.org/pypi/asttokens/ +.. image:: https://img.shields.io/pypi/pyversions/asttokens.svg + :target: https://pypi.python.org/pypi/asttokens/ +.. image:: https://github.com/gristlabs/asttokens/actions/workflows/build-and-test.yml/badge.svg + :target: https://github.com/gristlabs/asttokens/actions/workflows/build-and-test.yml +.. image:: https://readthedocs.org/projects/asttokens/badge/?version=latest + :target: http://asttokens.readthedocs.io/en/latest/index.html +.. image:: https://coveralls.io/repos/github/gristlabs/asttokens/badge.svg + :target: https://coveralls.io/github/gristlabs/asttokens + +.. Start of user-guide + +The ``asttokens`` module annotates Python abstract syntax trees (ASTs) with the positions of tokens +and text in the source code that generated them. + +It makes it possible for tools that work with logical AST nodes to find the particular text that +resulted in those nodes, for example for automated refactoring or highlighting. + +Installation +------------ +asttokens is available on PyPI: https://pypi.python.org/pypi/asttokens/:: + + pip install asttokens + +The code is on GitHub: https://github.com/gristlabs/asttokens. + +The API Reference is here: http://asttokens.readthedocs.io/en/latest/api-index.html. + +Usage +----- + +ASTTokens can annotate both trees built by `ast `_, +AND those built by `astroid `_. + +Here's an example: + +.. code-block:: python + + import asttokens, ast + source = "Robot('blue').walk(steps=10*n)" + atok = asttokens.ASTTokens(source, parse=True) + +Once the tree has been marked, nodes get ``.first_token``, ``.last_token`` attributes, and +the ``ASTTokens`` object offers helpful methods: + +.. code-block:: python + + attr_node = next(n for n in ast.walk(atok.tree) if isinstance(n, ast.Attribute)) + print(atok.get_text(attr_node)) + start, end = attr_node.last_token.startpos, attr_node.last_token.endpos + print(atok.text[:start] + 'RUN' + atok.text[end:]) + +Which produces this output: + +.. code-block:: text + + Robot('blue').walk + Robot('blue').RUN(steps=10*n) + +The ``ASTTokens`` object also offers methods to walk and search the list of tokens that make up +the code (or a particular AST node), which is more useful and powerful than dealing with the text +directly. + + +Contribute +---------- + +To contribute: + +1. Fork this repository, and clone your fork. +2. Install the package with test dependencies (ideally in a virtualenv) with:: + + pip install -e '.[test]' + +3. Run tests in your current interpreter with the command ``pytest`` or ``python -m pytest``. +4. Run tests across all supported interpreters with the ``tox`` command. You will need to have the interpreters installed separately. We recommend ``pyenv`` for that. Use ``tox -p auto`` to run the tests in parallel. +5. By default certain tests which take a very long time to run are skipped, but they are run in CI. + These are marked using the ``pytest`` marker ``slow`` and can be run on their own with ``pytest -m slow`` or as part of the full suite with ``pytest -m ''``. diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/RECORD b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..a4893ab32cb2e6d1b77d5ead1362684e9a46d0ae --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/RECORD @@ -0,0 +1,23 @@ +asttokens-3.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +asttokens-3.0.0.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357 +asttokens-3.0.0.dist-info/METADATA,sha256=cg1yWNJgO6xzqQzaKsQoKJuKZMEfuJAh07iQLAgNv6k,4726 +asttokens-3.0.0.dist-info/RECORD,, +asttokens-3.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +asttokens-3.0.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91 +asttokens-3.0.0.dist-info/direct_url.json,sha256=AHdiu_laZlE1PUUpGFfYO7mjN2fxEN38SpGMY011qmw,105 +asttokens-3.0.0.dist-info/top_level.txt,sha256=nJDweSD7_NBhOlR3c8bkKJMKM-pxlAS8Kyh8GcCT2dk,10 +asttokens/__init__.py,sha256=8eONA3X-9s93-v-2gEoz4649fDUpvzBthFB5Ld7dHAg,962 +asttokens/__pycache__/__init__.cpython-39.pyc,, +asttokens/__pycache__/astroid_compat.cpython-39.pyc,, +asttokens/__pycache__/asttokens.cpython-39.pyc,, +asttokens/__pycache__/line_numbers.cpython-39.pyc,, +asttokens/__pycache__/mark_tokens.cpython-39.pyc,, +asttokens/__pycache__/util.cpython-39.pyc,, +asttokens/__pycache__/version.cpython-39.pyc,, +asttokens/astroid_compat.py,sha256=ilaVBRWcHpQ3ZLBSBs9usUwnLW3Orfn6sM89cMN8zNI,586 +asttokens/asttokens.py,sha256=CQZ0ppXgTzHGbK4dqI4toSLywHIiqNK8jIVqbQClzYI,17760 +asttokens/line_numbers.py,sha256=ODbdlHI4Iht4UnSfsxmOHCIVw4c2XX7j-MdaCa6F8bo,2834 +asttokens/mark_tokens.py,sha256=YKE88IHnYyQiNvlFlxqU-BDhRRWkYYjMEsjxKlF1cqw,21012 +asttokens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +asttokens/util.py,sha256=zkszPUVGR0-UxZJI-I4lTrA7yH2IUOz8IBmwGas-pbs,17286 +asttokens/version.py,sha256=EPmgXOdWKks5S__ZMH7Nu6xpAeVrZpfxaFy4pykuyeI,22 diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/REQUESTED b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/WHEEL b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..ae527e7d64811439e61b93aa375defb30e06edfe --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.6.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/direct_url.json b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..f78e697054fee58e00f908ff2708fedad889287b --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///home/conda/feedstock_root/build_artifacts/asttokens_1733250440834/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/top_level.txt b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..7adf4c51fd2d7b06ec051d95245af6cb8c5931ed --- /dev/null +++ b/lib/python3.10/site-packages/asttokens-3.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +asttokens diff --git a/lib/python3.10/site-packages/asttokens/__init__.py b/lib/python3.10/site-packages/asttokens/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..eeda0ed4fccc5c629a3bcb907b479683206eec92 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2016 Grist Labs, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module enhances the Python AST tree with token and source code information, sufficent to +detect the source text of each AST node. This is helpful for tools that make source code +transformations. +""" + +from .line_numbers import LineNumbers +from .asttokens import ASTText, ASTTokens, supports_tokenless + +__all__ = ['ASTText', 'ASTTokens', 'LineNumbers', 'supports_tokenless'] diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a08c88899d8412e8692ce812f99be8f32386ee0 Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/astroid_compat.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/astroid_compat.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2285a9545b31db3027466e917bbf0d3adf326b5 Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/astroid_compat.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/asttokens.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/asttokens.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc561d8939334eb17823cc32810c45099cfb3a5e Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/asttokens.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/line_numbers.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/line_numbers.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45e0b86e8007b158a02909f517978a32e4cde0fa Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/line_numbers.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/mark_tokens.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/mark_tokens.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ece352d2071ac34991b3031b4e936562fe46371 Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/mark_tokens.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/util.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39e1b62a1160c962fa83a5f32208dde89fd28184 Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/util.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/__pycache__/version.cpython-310.pyc b/lib/python3.10/site-packages/asttokens/__pycache__/version.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a73b8996cc76da13863348f05bf1c5af27805809 Binary files /dev/null and b/lib/python3.10/site-packages/asttokens/__pycache__/version.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/asttokens/astroid_compat.py b/lib/python3.10/site-packages/asttokens/astroid_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..9af3e17e028d5718e9a34da08d4bc3c6757c0596 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/astroid_compat.py @@ -0,0 +1,18 @@ +try: + from astroid import nodes as astroid_node_classes + + # astroid_node_classes should be whichever module has the NodeNG class + from astroid.nodes import NodeNG + from astroid.nodes import BaseContainer +except Exception: + try: + from astroid import node_classes as astroid_node_classes + from astroid.node_classes import NodeNG + from astroid.node_classes import _BaseContainer as BaseContainer + except Exception: # pragma: no cover + astroid_node_classes = None + NodeNG = None + BaseContainer = None + + +__all__ = ["astroid_node_classes", "NodeNG", "BaseContainer"] diff --git a/lib/python3.10/site-packages/asttokens/asttokens.py b/lib/python3.10/site-packages/asttokens/asttokens.py new file mode 100644 index 0000000000000000000000000000000000000000..b537786ef8cc296201d64956e3d180c5017463fd --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/asttokens.py @@ -0,0 +1,450 @@ +# Copyright 2016 Grist Labs, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import ast +import bisect +import sys +import token +from ast import Module +from typing import Iterable, Iterator, List, Optional, Tuple, Any, cast, TYPE_CHECKING + +from .line_numbers import LineNumbers +from .util import ( + Token, match_token, is_non_coding_token, patched_generate_tokens, last_stmt, + annotate_fstring_nodes, generate_tokens, is_module, is_stmt +) + +if TYPE_CHECKING: # pragma: no cover + from .util import AstNode, TokenInfo + + +class ASTTextBase(metaclass=abc.ABCMeta): + def __init__(self, source_text: str, filename: str) -> None: + self._filename = filename + + # Decode source after parsing to let Python 2 handle coding declarations. + # (If the encoding was not utf-8 compatible, then even if it parses correctly, + # we'll fail with a unicode error here.) + source_text = str(source_text) + + self._text = source_text + self._line_numbers = LineNumbers(source_text) + + @abc.abstractmethod + def get_text_positions(self, node, padded): + # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]] + """ + Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node. + If the positions can't be determined, or the nodes don't correspond to any particular text, + returns ``(1, 0)`` for both. + + ``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``. + This means that if ``padded`` is True, the start position will be adjusted to include + leading whitespace if ``node`` is a multiline statement. + """ + raise NotImplementedError # pragma: no cover + + def get_text_range(self, node, padded=True): + # type: (AstNode, bool) -> Tuple[int, int] + """ + Returns the (startpos, endpos) positions in source text corresponding to the given node. + Returns (0, 0) for nodes (like `Load`) that don't correspond to any particular text. + + See ``get_text_positions()`` for details on the ``padded`` argument. + """ + start, end = self.get_text_positions(node, padded) + return ( + self._line_numbers.line_to_offset(*start), + self._line_numbers.line_to_offset(*end), + ) + + def get_text(self, node, padded=True): + # type: (AstNode, bool) -> str + """ + Returns the text corresponding to the given node. + Returns '' for nodes (like `Load`) that don't correspond to any particular text. + + See ``get_text_positions()`` for details on the ``padded`` argument. + """ + start, end = self.get_text_range(node, padded) + return self._text[start: end] + + +class ASTTokens(ASTTextBase): + """ + ASTTokens maintains the text of Python code in several forms: as a string, as line numbers, and + as tokens, and is used to mark and access token and position information. + + ``source_text`` must be a unicode or UTF8-encoded string. If you pass in UTF8 bytes, remember + that all offsets you'll get are to the unicode text, which is available as the ``.text`` + property. + + If ``parse`` is set, the ``source_text`` will be parsed with ``ast.parse()``, and the resulting + tree marked with token info and made available as the ``.tree`` property. + + If ``tree`` is given, it will be marked and made available as the ``.tree`` property. In + addition to the trees produced by the ``ast`` module, ASTTokens will also mark trees produced + using ``astroid`` library . + + If only ``source_text`` is given, you may use ``.mark_tokens(tree)`` to mark the nodes of an AST + tree created separately. + """ + + def __init__(self, source_text, parse=False, tree=None, filename='', tokens=None): + # type: (Any, bool, Optional[Module], str, Iterable[TokenInfo]) -> None + super(ASTTokens, self).__init__(source_text, filename) + + self._tree = ast.parse(source_text, filename) if parse else tree + + # Tokenize the code. + if tokens is None: + tokens = generate_tokens(self._text) + self._tokens = list(self._translate_tokens(tokens)) + + # Extract the start positions of all tokens, so that we can quickly map positions to tokens. + self._token_offsets = [tok.startpos for tok in self._tokens] + + if self._tree: + self.mark_tokens(self._tree) + + def mark_tokens(self, root_node): + # type: (Module) -> None + """ + Given the root of the AST or Astroid tree produced from source_text, visits all nodes marking + them with token and position information by adding ``.first_token`` and + ``.last_token`` attributes. This is done automatically in the constructor when ``parse`` or + ``tree`` arguments are set, but may be used manually with a separate AST or Astroid tree. + """ + # The hard work of this class is done by MarkTokens + from .mark_tokens import MarkTokens # to avoid import loops + MarkTokens(self).visit_tree(root_node) + + def _translate_tokens(self, original_tokens): + # type: (Iterable[TokenInfo]) -> Iterator[Token] + """ + Translates the given standard library tokens into our own representation. + """ + for index, tok in enumerate(patched_generate_tokens(original_tokens)): + tok_type, tok_str, start, end, line = tok + yield Token(tok_type, tok_str, start, end, line, index, + self._line_numbers.line_to_offset(start[0], start[1]), + self._line_numbers.line_to_offset(end[0], end[1])) + + @property + def text(self): + # type: () -> str + """The source code passed into the constructor.""" + return self._text + + @property + def tokens(self): + # type: () -> List[Token] + """The list of tokens corresponding to the source code from the constructor.""" + return self._tokens + + @property + def tree(self): + # type: () -> Optional[Module] + """The root of the AST tree passed into the constructor or parsed from the source code.""" + return self._tree + + @property + def filename(self): + # type: () -> str + """The filename that was parsed""" + return self._filename + + def get_token_from_offset(self, offset): + # type: (int) -> Token + """ + Returns the token containing the given character offset (0-based position in source text), + or the preceeding token if the position is between tokens. + """ + return self._tokens[bisect.bisect(self._token_offsets, offset) - 1] + + def get_token(self, lineno, col_offset): + # type: (int, int) -> Token + """ + Returns the token containing the given (lineno, col_offset) position, or the preceeding token + if the position is between tokens. + """ + # TODO: add test for multibyte unicode. We need to translate offsets from ast module (which + # are in utf8) to offsets into the unicode text. tokenize module seems to use unicode offsets + # but isn't explicit. + return self.get_token_from_offset(self._line_numbers.line_to_offset(lineno, col_offset)) + + def get_token_from_utf8(self, lineno, col_offset): + # type: (int, int) -> Token + """ + Same as get_token(), but interprets col_offset as a UTF8 offset, which is what `ast` uses. + """ + return self.get_token(lineno, self._line_numbers.from_utf8_col(lineno, col_offset)) + + def next_token(self, tok, include_extra=False): + # type: (Token, bool) -> Token + """ + Returns the next token after the given one. If include_extra is True, includes non-coding + tokens from the tokenize module, such as NL and COMMENT. + """ + i = tok.index + 1 + if not include_extra: + while is_non_coding_token(self._tokens[i].type): + i += 1 + return self._tokens[i] + + def prev_token(self, tok, include_extra=False): + # type: (Token, bool) -> Token + """ + Returns the previous token before the given one. If include_extra is True, includes non-coding + tokens from the tokenize module, such as NL and COMMENT. + """ + i = tok.index - 1 + if not include_extra: + while is_non_coding_token(self._tokens[i].type): + i -= 1 + return self._tokens[i] + + def find_token(self, start_token, tok_type, tok_str=None, reverse=False): + # type: (Token, int, Optional[str], bool) -> Token + """ + Looks for the first token, starting at start_token, that matches tok_type and, if given, the + token string. Searches backwards if reverse is True. Returns ENDMARKER token if not found (you + can check it with `token.ISEOF(t.type)`). + """ + t = start_token + advance = self.prev_token if reverse else self.next_token + while not match_token(t, tok_type, tok_str) and not token.ISEOF(t.type): + t = advance(t, include_extra=True) + return t + + def token_range(self, + first_token, # type: Token + last_token, # type: Token + include_extra=False, # type: bool + ): + # type: (...) -> Iterator[Token] + """ + Yields all tokens in order from first_token through and including last_token. If + include_extra is True, includes non-coding tokens such as tokenize.NL and .COMMENT. + """ + for i in range(first_token.index, last_token.index + 1): + if include_extra or not is_non_coding_token(self._tokens[i].type): + yield self._tokens[i] + + def get_tokens(self, node, include_extra=False): + # type: (AstNode, bool) -> Iterator[Token] + """ + Yields all tokens making up the given node. If include_extra is True, includes non-coding + tokens such as tokenize.NL and .COMMENT. + """ + return self.token_range(node.first_token, node.last_token, include_extra=include_extra) + + def get_text_positions(self, node, padded): + # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]] + """ + Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node. + If the positions can't be determined, or the nodes don't correspond to any particular text, + returns ``(1, 0)`` for both. + + ``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``. + This means that if ``padded`` is True, the start position will be adjusted to include + leading whitespace if ``node`` is a multiline statement. + """ + if not hasattr(node, 'first_token'): + return (1, 0), (1, 0) + + start = node.first_token.start + end = node.last_token.end + if padded and any(match_token(t, token.NEWLINE) for t in self.get_tokens(node)): + # Set col_offset to 0 to include leading indentation for multiline statements. + start = (start[0], 0) + + return start, end + + +class ASTText(ASTTextBase): + """ + Supports the same ``get_text*`` methods as ``ASTTokens``, + but uses the AST to determine the text positions instead of tokens. + This is faster than ``ASTTokens`` as it requires less setup work. + + It also (sometimes) supports nodes inside f-strings, which ``ASTTokens`` doesn't. + + Some node types and/or Python versions are not supported. + In these cases the ``get_text*`` methods will fall back to using ``ASTTokens`` + which incurs the usual setup cost the first time. + If you want to avoid this, check ``supports_tokenless(node)`` before calling ``get_text*`` methods. + """ + def __init__(self, source_text, tree=None, filename=''): + # type: (Any, Optional[Module], str) -> None + super(ASTText, self).__init__(source_text, filename) + + self._tree = tree + if self._tree is not None: + annotate_fstring_nodes(self._tree) + + self._asttokens = None # type: Optional[ASTTokens] + + @property + def tree(self): + # type: () -> Module + if self._tree is None: + self._tree = ast.parse(self._text, self._filename) + annotate_fstring_nodes(self._tree) + return self._tree + + @property + def asttokens(self): + # type: () -> ASTTokens + if self._asttokens is None: + self._asttokens = ASTTokens( + self._text, + tree=self.tree, + filename=self._filename, + ) + return self._asttokens + + def _get_text_positions_tokenless(self, node, padded): + # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]] + """ + Version of ``get_text_positions()`` that doesn't use tokens. + """ + if is_module(node): + # Modules don't have position info, so just return the range of the whole text. + # The token-using method does something different, but its behavior seems weird and inconsistent. + # For example, in a file with only comments, it only returns the first line. + # It's hard to imagine a case when this matters. + return (1, 0), self._line_numbers.offset_to_line(len(self._text)) + + if getattr(node, 'lineno', None) is None: + return (1, 0), (1, 0) + + assert node # tell mypy that node is not None, which we allowed up to here for compatibility + + decorators = getattr(node, 'decorator_list', []) + if not decorators: + # Astroid uses node.decorators.nodes instead of node.decorator_list. + decorators_node = getattr(node, 'decorators', None) + decorators = getattr(decorators_node, 'nodes', []) + if decorators: + # Function/Class definition nodes are marked by AST as starting at def/class, + # not the first decorator. This doesn't match the token-using behavior, + # or inspect.getsource(), and just seems weird. + start_node = decorators[0] + else: + start_node = node + + start_lineno = start_node.lineno + end_node = last_stmt(node) + + # Include leading indentation for multiline statements. + # This doesn't mean simple statements that happen to be on multiple lines, + # but compound statements where inner indentation matters. + # So we don't just compare node.lineno and node.end_lineno, + # we check for a contained statement starting on a different line. + if padded and ( + start_lineno != end_node.lineno + or ( + # Astroid docstrings aren't treated as separate statements. + # So to handle function/class definitions with a docstring but no other body, + # we just check that the node is a statement with a docstring + # and spanning multiple lines in the simple, literal sense. + start_lineno != node.end_lineno + and getattr(node, "doc_node", None) + and is_stmt(node) + ) + ): + start_col_offset = 0 + else: + start_col_offset = self._line_numbers.from_utf8_col(start_lineno, start_node.col_offset) + + start = (start_lineno, start_col_offset) + + # To match the token-using behaviour, we exclude trailing semicolons and comments. + # This means that for blocks containing multiple statements, we have to use the last one + # instead of the actual node for end_lineno and end_col_offset. + end_lineno = cast(int, end_node.end_lineno) + end_col_offset = cast(int, end_node.end_col_offset) + end_col_offset = self._line_numbers.from_utf8_col(end_lineno, end_col_offset) + end = (end_lineno, end_col_offset) + + return start, end + + def get_text_positions(self, node, padded): + # type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]] + """ + Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node. + If the positions can't be determined, or the nodes don't correspond to any particular text, + returns ``(1, 0)`` for both. + + ``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``. + This means that if ``padded`` is True, the start position will be adjusted to include + leading whitespace if ``node`` is a multiline statement. + """ + if getattr(node, "_broken_positions", None): + # This node was marked in util.annotate_fstring_nodes as having untrustworthy lineno/col_offset. + return (1, 0), (1, 0) + + if supports_tokenless(node): + return self._get_text_positions_tokenless(node, padded) + + return self.asttokens.get_text_positions(node, padded) + + +# Node types that _get_text_positions_tokenless doesn't support. +# These initial values are missing lineno. +_unsupported_tokenless_types = ("arguments", "Arguments", "withitem") # type: Tuple[str, ...] +if sys.version_info[:2] == (3, 8): + # _get_text_positions_tokenless works incorrectly for these types due to bugs in Python 3.8. + _unsupported_tokenless_types += ("arg", "Starred") + # no lineno in 3.8 + _unsupported_tokenless_types += ("Slice", "ExtSlice", "Index", "keyword") + + +def supports_tokenless(node=None): + # type: (Any) -> bool + """ + Returns True if the Python version and the node (if given) are supported by + the ``get_text*`` methods of ``ASTText`` without falling back to ``ASTTokens``. + See ``ASTText`` for why this matters. + + The following cases are not supported: + + - PyPy + - ``ast.arguments`` / ``astroid.Arguments`` + - ``ast.withitem`` + - ``astroid.Comprehension`` + - ``astroid.AssignName`` inside ``astroid.Arguments`` or ``astroid.ExceptHandler`` + - The following nodes in Python 3.8 only: + - ``ast.arg`` + - ``ast.Starred`` + - ``ast.Slice`` + - ``ast.ExtSlice`` + - ``ast.Index`` + - ``ast.keyword`` + """ + return ( + type(node).__name__ not in _unsupported_tokenless_types + and not ( + # astroid nodes + not isinstance(node, ast.AST) and node is not None and ( + ( + type(node).__name__ == "AssignName" + and type(node.parent).__name__ in ("Arguments", "ExceptHandler") + ) + ) + ) + and 'pypy' not in sys.version.lower() + ) diff --git a/lib/python3.10/site-packages/asttokens/line_numbers.py b/lib/python3.10/site-packages/asttokens/line_numbers.py new file mode 100644 index 0000000000000000000000000000000000000000..745b9f8a482a1b782bdf68391c50a04e476b5a38 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/line_numbers.py @@ -0,0 +1,76 @@ +# Copyright 2016 Grist Labs, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import bisect +import re +from typing import Dict, List, Tuple + +_line_start_re = re.compile(r'^', re.M) + +class LineNumbers: + """ + Class to convert between character offsets in a text string, and pairs (line, column) of 1-based + line and 0-based column numbers, as used by tokens and AST nodes. + + This class expects unicode for input and stores positions in unicode. But it supports + translating to and from utf8 offsets, which are used by ast parsing. + """ + def __init__(self, text): + # type: (str) -> None + # A list of character offsets of each line's first character. + self._line_offsets = [m.start(0) for m in _line_start_re.finditer(text)] + self._text = text + self._text_len = len(text) + self._utf8_offset_cache = {} # type: Dict[int, List[int]] # maps line num to list of char offset for each byte in line + + def from_utf8_col(self, line, utf8_column): + # type: (int, int) -> int + """ + Given a 1-based line number and 0-based utf8 column, returns a 0-based unicode column. + """ + offsets = self._utf8_offset_cache.get(line) + if offsets is None: + end_offset = self._line_offsets[line] if line < len(self._line_offsets) else self._text_len + line_text = self._text[self._line_offsets[line - 1] : end_offset] + + offsets = [i for i,c in enumerate(line_text) for byte in c.encode('utf8')] + offsets.append(len(line_text)) + self._utf8_offset_cache[line] = offsets + + return offsets[max(0, min(len(offsets)-1, utf8_column))] + + def line_to_offset(self, line, column): + # type: (int, int) -> int + """ + Converts 1-based line number and 0-based column to 0-based character offset into text. + """ + line -= 1 + if line >= len(self._line_offsets): + return self._text_len + elif line < 0: + return 0 + else: + return min(self._line_offsets[line] + max(0, column), self._text_len) + + def offset_to_line(self, offset): + # type: (int) -> Tuple[int, int] + """ + Converts 0-based character offset to pair (line, col) of 1-based line and 0-based column + numbers. + """ + offset = max(0, min(self._text_len, offset)) + line_index = bisect.bisect_right(self._line_offsets, offset) - 1 + return (line_index + 1, offset - self._line_offsets[line_index]) + + diff --git a/lib/python3.10/site-packages/asttokens/mark_tokens.py b/lib/python3.10/site-packages/asttokens/mark_tokens.py new file mode 100644 index 0000000000000000000000000000000000000000..f866b1cbcbacae1e976534a163aebdbcddcacd12 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/mark_tokens.py @@ -0,0 +1,467 @@ +# Copyright 2016 Grist Labs, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ast +import numbers +import sys +import token +from ast import Module +from typing import Callable, List, Union, cast, Optional, Tuple, TYPE_CHECKING + +from . import util +from .asttokens import ASTTokens +from .astroid_compat import astroid_node_classes as nc, BaseContainer as AstroidBaseContainer + +if TYPE_CHECKING: + from .util import AstNode + + +# Mapping of matching braces. To find a token here, look up token[:2]. +_matching_pairs_left = { + (token.OP, '('): (token.OP, ')'), + (token.OP, '['): (token.OP, ']'), + (token.OP, '{'): (token.OP, '}'), +} + +_matching_pairs_right = { + (token.OP, ')'): (token.OP, '('), + (token.OP, ']'): (token.OP, '['), + (token.OP, '}'): (token.OP, '{'), +} + + +class MarkTokens: + """ + Helper that visits all nodes in the AST tree and assigns .first_token and .last_token attributes + to each of them. This is the heart of the token-marking logic. + """ + def __init__(self, code): + # type: (ASTTokens) -> None + self._code = code + self._methods = util.NodeMethods() + self._iter_children = None # type: Optional[Callable] + + def visit_tree(self, node): + # type: (Module) -> None + self._iter_children = util.iter_children_func(node) + util.visit_tree(node, self._visit_before_children, self._visit_after_children) + + def _visit_before_children(self, node, parent_token): + # type: (AstNode, Optional[util.Token]) -> Tuple[Optional[util.Token], Optional[util.Token]] + col = getattr(node, 'col_offset', None) + token = self._code.get_token_from_utf8(node.lineno, col) if col is not None else None + + if not token and util.is_module(node): + # We'll assume that a Module node starts at the start of the source code. + token = self._code.get_token(1, 0) + + # Use our own token, or our parent's if we don't have one, to pass to child calls as + # parent_token argument. The second value becomes the token argument of _visit_after_children. + return (token or parent_token, token) + + def _visit_after_children(self, node, parent_token, token): + # type: (AstNode, Optional[util.Token], Optional[util.Token]) -> None + # This processes the node generically first, after all children have been processed. + + # Get the first and last tokens that belong to children. Note how this doesn't assume that we + # iterate through children in order that corresponds to occurrence in source code. This + # assumption can fail (e.g. with return annotations). + first = token + last = None + for child in cast(Callable, self._iter_children)(node): + # astroid slices have especially wrong positions, we don't want them to corrupt their parents. + if util.is_empty_astroid_slice(child): + continue + if not first or child.first_token.index < first.index: + first = child.first_token + if not last or child.last_token.index > last.index: + last = child.last_token + + # If we don't have a first token from _visit_before_children, and there were no children, then + # use the parent's token as the first token. + first = first or parent_token + + # If no children, set last token to the first one. + last = last or first + + # Statements continue to before NEWLINE. This helps cover a few different cases at once. + if util.is_stmt(node): + last = self._find_last_in_stmt(cast(util.Token, last)) + + # Capture any unmatched brackets. + first, last = self._expand_to_matching_pairs(cast(util.Token, first), cast(util.Token, last), node) + + # Give a chance to node-specific methods to adjust. + nfirst, nlast = self._methods.get(self, node.__class__)(node, first, last) + + if (nfirst, nlast) != (first, last): + # If anything changed, expand again to capture any unmatched brackets. + nfirst, nlast = self._expand_to_matching_pairs(nfirst, nlast, node) + + node.first_token = nfirst + node.last_token = nlast + + def _find_last_in_stmt(self, start_token): + # type: (util.Token) -> util.Token + t = start_token + while (not util.match_token(t, token.NEWLINE) and + not util.match_token(t, token.OP, ';') and + not token.ISEOF(t.type)): + t = self._code.next_token(t, include_extra=True) + return self._code.prev_token(t) + + def _expand_to_matching_pairs(self, first_token, last_token, node): + # type: (util.Token, util.Token, AstNode) -> Tuple[util.Token, util.Token] + """ + Scan tokens in [first_token, last_token] range that are between node's children, and for any + unmatched brackets, adjust first/last tokens to include the closing pair. + """ + # We look for opening parens/braces among non-child tokens (i.e. tokens between our actual + # child nodes). If we find any closing ones, we match them to the opens. + to_match_right = [] # type: List[Tuple[int, str]] + to_match_left = [] + for tok in self._code.token_range(first_token, last_token): + tok_info = tok[:2] + if to_match_right and tok_info == to_match_right[-1]: + to_match_right.pop() + elif tok_info in _matching_pairs_left: + to_match_right.append(_matching_pairs_left[tok_info]) + elif tok_info in _matching_pairs_right: + to_match_left.append(_matching_pairs_right[tok_info]) + + # Once done, extend `last_token` to match any unclosed parens/braces. + for match in reversed(to_match_right): + last = self._code.next_token(last_token) + # Allow for trailing commas or colons (allowed in subscripts) before the closing delimiter + while any(util.match_token(last, token.OP, x) for x in (',', ':')): + last = self._code.next_token(last) + # Now check for the actual closing delimiter. + if util.match_token(last, *match): + last_token = last + + # And extend `first_token` to match any unclosed opening parens/braces. + for match in to_match_left: + first = self._code.prev_token(first_token) + if util.match_token(first, *match): + first_token = first + + return (first_token, last_token) + + #---------------------------------------------------------------------- + # Node visitors. Each takes a preliminary first and last tokens, and returns the adjusted pair + # that will actually be assigned. + + def visit_default(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # pylint: disable=no-self-use + # By default, we don't need to adjust the token we computed earlier. + return (first_token, last_token) + + def handle_comp(self, open_brace, node, first_token, last_token): + # type: (str, AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # For list/set/dict comprehensions, we only get the token of the first child, so adjust it to + # include the opening brace (the closing brace will be matched automatically). + before = self._code.prev_token(first_token) + util.expect_token(before, token.OP, open_brace) + return (before, last_token) + + def visit_comprehension(self, + node, # type: AstNode + first_token, # type: util.Token + last_token, # type: util.Token + ): + # type: (...) -> Tuple[util.Token, util.Token] + # The 'comprehension' node starts with 'for' but we only get first child; we search backwards + # to find the 'for' keyword. + first = self._code.find_token(first_token, token.NAME, 'for', reverse=True) + return (first, last_token) + + def visit_if(self, node, first_token, last_token): + # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token] + while first_token.string not in ('if', 'elif'): + first_token = self._code.prev_token(first_token) + return first_token, last_token + + def handle_attr(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # Attribute node has ".attr" (2 tokens) after the last child. + dot = self._code.find_token(last_token, token.OP, '.') + name = self._code.next_token(dot) + util.expect_token(name, token.NAME) + return (first_token, name) + + visit_attribute = handle_attr + visit_assignattr = handle_attr + visit_delattr = handle_attr + + def handle_def(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # With astroid, nodes that start with a doc-string can have an empty body, in which case we + # need to adjust the last token to include the doc string. + if not node.body and (getattr(node, 'doc_node', None) or getattr(node, 'doc', None)): # type: ignore[union-attr] + last_token = self._code.find_token(last_token, token.STRING) + + # Include @ from decorator + if first_token.index > 0: + prev = self._code.prev_token(first_token) + if util.match_token(prev, token.OP, '@'): + first_token = prev + return (first_token, last_token) + + visit_classdef = handle_def + visit_functiondef = handle_def + + def handle_following_brackets(self, node, last_token, opening_bracket): + # type: (AstNode, util.Token, str) -> util.Token + # This is for calls and subscripts, which have a pair of brackets + # at the end which may contain no nodes, e.g. foo() or bar[:]. + # We look for the opening bracket and then let the matching pair be found automatically + # Remember that last_token is at the end of all children, + # so we are not worried about encountering a bracket that belongs to a child. + first_child = next(cast(Callable, self._iter_children)(node)) + call_start = self._code.find_token(first_child.last_token, token.OP, opening_bracket) + if call_start.index > last_token.index: + last_token = call_start + return last_token + + def visit_call(self, node, first_token, last_token): + # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token] + last_token = self.handle_following_brackets(node, last_token, '(') + + # Handling a python bug with decorators with empty parens, e.g. + # @deco() + # def ... + if util.match_token(first_token, token.OP, '@'): + first_token = self._code.next_token(first_token) + return (first_token, last_token) + + def visit_matchclass(self, node, first_token, last_token): + # type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token] + last_token = self.handle_following_brackets(node, last_token, '(') + return (first_token, last_token) + + def visit_subscript(self, + node, # type: AstNode + first_token, # type: util.Token + last_token, # type: util.Token + ): + # type: (...) -> Tuple[util.Token, util.Token] + last_token = self.handle_following_brackets(node, last_token, '[') + return (first_token, last_token) + + def visit_slice(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # consume `:` tokens to the left and right. In Python 3.9, Slice nodes are + # given a col_offset, (and end_col_offset), so this will always start inside + # the slice, even if it is the empty slice. However, in 3.8 and below, this + # will only expand to the full slice if the slice contains a node with a + # col_offset. So x[:] will only get the correct tokens in 3.9, but x[1:] and + # x[:1] will even on earlier versions of Python. + while True: + prev = self._code.prev_token(first_token) + if prev.string != ':': + break + first_token = prev + while True: + next_ = self._code.next_token(last_token) + if next_.string != ':': + break + last_token = next_ + return (first_token, last_token) + + def handle_bare_tuple(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # A bare tuple doesn't include parens; if there is a trailing comma, make it part of the tuple. + maybe_comma = self._code.next_token(last_token) + if util.match_token(maybe_comma, token.OP, ','): + last_token = maybe_comma + return (first_token, last_token) + + # In Python3.8 parsed tuples include parentheses when present. + def handle_tuple_nonempty(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + assert isinstance(node, ast.Tuple) or isinstance(node, AstroidBaseContainer) + # It's a bare tuple if the first token belongs to the first child. The first child may + # include extraneous parentheses (which don't create new nodes), so account for those too. + child = node.elts[0] + if TYPE_CHECKING: + child = cast(AstNode, child) + child_first, child_last = self._gobble_parens(child.first_token, child.last_token, True) + if first_token == child_first: + return self.handle_bare_tuple(node, first_token, last_token) + return (first_token, last_token) + + def visit_tuple(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + assert isinstance(node, ast.Tuple) or isinstance(node, AstroidBaseContainer) + if not node.elts: + # An empty tuple is just "()", and we need no further info. + return (first_token, last_token) + return self.handle_tuple_nonempty(node, first_token, last_token) + + def _gobble_parens(self, first_token, last_token, include_all=False): + # type: (util.Token, util.Token, bool) -> Tuple[util.Token, util.Token] + # Expands a range of tokens to include one or all pairs of surrounding parentheses, and + # returns (first, last) tokens that include these parens. + while first_token.index > 0: + prev = self._code.prev_token(first_token) + next = self._code.next_token(last_token) + if util.match_token(prev, token.OP, '(') and util.match_token(next, token.OP, ')'): + first_token, last_token = prev, next + if include_all: + continue + break + return (first_token, last_token) + + def visit_str(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + return self.handle_str(first_token, last_token) + + def visit_joinedstr(self, + node, # type: AstNode + first_token, # type: util.Token + last_token, # type: util.Token + ): + # type: (...) -> Tuple[util.Token, util.Token] + if sys.version_info < (3, 12): + # Older versions don't tokenize the contents of f-strings + return self.handle_str(first_token, last_token) + + last = first_token + while True: + if util.match_token(last, getattr(token, "FSTRING_START")): + # Python 3.12+ has tokens for the start (e.g. `f"`) and end (`"`) + # of the f-string. We can't just look for the next FSTRING_END + # because f-strings can be nested, e.g. f"{f'{x}'}", so we need + # to treat this like matching balanced parentheses. + count = 1 + while count > 0: + last = self._code.next_token(last) + # mypy complains about token.FSTRING_START and token.FSTRING_END. + if util.match_token(last, getattr(token, "FSTRING_START")): + count += 1 + elif util.match_token(last, getattr(token, "FSTRING_END")): + count -= 1 + last_token = last + last = self._code.next_token(last_token) + elif util.match_token(last, token.STRING): + # Similar to handle_str, we also need to handle adjacent strings. + last_token = last + last = self._code.next_token(last_token) + else: + break + return (first_token, last_token) + + def visit_bytes(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + return self.handle_str(first_token, last_token) + + def handle_str(self, first_token, last_token): + # type: (util.Token, util.Token) -> Tuple[util.Token, util.Token] + # Multiple adjacent STRING tokens form a single string. + last = self._code.next_token(last_token) + while util.match_token(last, token.STRING): + last_token = last + last = self._code.next_token(last_token) + return (first_token, last_token) + + def handle_num(self, + node, # type: AstNode + value, # type: Union[complex, int, numbers.Number] + first_token, # type: util.Token + last_token, # type: util.Token + ): + # type: (...) -> Tuple[util.Token, util.Token] + # A constant like '-1' gets turned into two tokens; this will skip the '-'. + while util.match_token(last_token, token.OP): + last_token = self._code.next_token(last_token) + + if isinstance(value, complex): + # A complex number like -2j cannot be compared directly to 0 + # A complex number like 1-2j is expressed as a binary operation + # so we don't need to worry about it + value = value.imag + + # This makes sure that the - is included + if value < 0 and first_token.type == token.NUMBER: # type: ignore[operator] + first_token = self._code.prev_token(first_token) + return (first_token, last_token) + + def visit_num(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + return self.handle_num(node, cast(ast.Num, node).n, first_token, last_token) + + def visit_const(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + assert isinstance(node, ast.Constant) or isinstance(node, nc.Const) + if isinstance(node.value, numbers.Number): + return self.handle_num(node, node.value, first_token, last_token) + elif isinstance(node.value, (str, bytes)): + return self.visit_str(node, first_token, last_token) + return (first_token, last_token) + + visit_constant = visit_const + + def visit_keyword(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # Until python 3.9 (https://bugs.python.org/issue40141), + # ast.keyword nodes didn't have line info. Astroid has lineno None. + assert isinstance(node, ast.keyword) or isinstance(node, nc.Keyword) + if node.arg is not None and getattr(node, 'lineno', None) is None: + equals = self._code.find_token(first_token, token.OP, '=', reverse=True) + name = self._code.prev_token(equals) + util.expect_token(name, token.NAME, node.arg) + first_token = name + return (first_token, last_token) + + def visit_starred(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # Astroid has 'Starred' nodes (for "foo(*bar)" type args), but they need to be adjusted. + if not util.match_token(first_token, token.OP, '*'): + star = self._code.prev_token(first_token) + if util.match_token(star, token.OP, '*'): + first_token = star + return (first_token, last_token) + + def visit_assignname(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + # Astroid may turn 'except' clause into AssignName, but we need to adjust it. + if util.match_token(first_token, token.NAME, 'except'): + colon = self._code.find_token(last_token, token.OP, ':') + first_token = last_token = self._code.prev_token(colon) + return (first_token, last_token) + + # Async nodes should typically start with the word 'async' + # but Python < 3.7 doesn't put the col_offset there + # AsyncFunctionDef is slightly different because it might have + # decorators before that, which visit_functiondef handles + def handle_async(self, node, first_token, last_token): + # type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token] + if not first_token.string == 'async': + first_token = self._code.prev_token(first_token) + return (first_token, last_token) + + visit_asyncfor = handle_async + visit_asyncwith = handle_async + + def visit_asyncfunctiondef(self, + node, # type: AstNode + first_token, # type: util.Token + last_token, # type: util.Token + ): + # type: (...) -> Tuple[util.Token, util.Token] + if util.match_token(first_token, token.NAME, 'def'): + # Include the 'async' token + first_token = self._code.prev_token(first_token) + return self.visit_functiondef(node, first_token, last_token) diff --git a/lib/python3.10/site-packages/asttokens/py.typed b/lib/python3.10/site-packages/asttokens/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/asttokens/util.py b/lib/python3.10/site-packages/asttokens/util.py new file mode 100644 index 0000000000000000000000000000000000000000..df3e729b2b284de2bc12c69306b2e20a85f1a3b3 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/util.py @@ -0,0 +1,485 @@ +# Copyright 2016 Grist Labs, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import ast +import collections +import io +import sys +import token +import tokenize +from abc import ABCMeta +from ast import Module, expr, AST +from functools import lru_cache +from typing import ( + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + Union, + cast, + Any, + TYPE_CHECKING, + Type, +) + +if TYPE_CHECKING: # pragma: no cover + from .astroid_compat import NodeNG + + # Type class used to expand out the definition of AST to include fields added by this library + # It's not actually used for anything other than type checking though! + class EnhancedAST(AST): + # Additional attributes set by mark_tokens + first_token = None # type: Token + last_token = None # type: Token + lineno = 0 # type: int + + AstNode = Union[EnhancedAST, NodeNG] + + TokenInfo = tokenize.TokenInfo + + +def token_repr(tok_type, string): + # type: (int, Optional[str]) -> str + """Returns a human-friendly representation of a token with the given type and string.""" + # repr() prefixes unicode with 'u' on Python2 but not Python3; strip it out for consistency. + return '%s:%s' % (token.tok_name[tok_type], repr(string).lstrip('u')) + + +class Token(collections.namedtuple('Token', 'type string start end line index startpos endpos')): + """ + TokenInfo is an 8-tuple containing the same 5 fields as the tokens produced by the tokenize + module, and 3 additional ones useful for this module: + + - [0] .type Token type (see token.py) + - [1] .string Token (a string) + - [2] .start Starting (row, column) indices of the token (a 2-tuple of ints) + - [3] .end Ending (row, column) indices of the token (a 2-tuple of ints) + - [4] .line Original line (string) + - [5] .index Index of the token in the list of tokens that it belongs to. + - [6] .startpos Starting character offset into the input text. + - [7] .endpos Ending character offset into the input text. + """ + def __str__(self): + # type: () -> str + return token_repr(self.type, self.string) + + +def match_token(token, tok_type, tok_str=None): + # type: (Token, int, Optional[str]) -> bool + """Returns true if token is of the given type and, if a string is given, has that string.""" + return token.type == tok_type and (tok_str is None or token.string == tok_str) + + +def expect_token(token, tok_type, tok_str=None): + # type: (Token, int, Optional[str]) -> None + """ + Verifies that the given token is of the expected type. If tok_str is given, the token string + is verified too. If the token doesn't match, raises an informative ValueError. + """ + if not match_token(token, tok_type, tok_str): + raise ValueError("Expected token %s, got %s on line %s col %s" % ( + token_repr(tok_type, tok_str), str(token), + token.start[0], token.start[1] + 1)) + + +def is_non_coding_token(token_type): + # type: (int) -> bool + """ + These are considered non-coding tokens, as they don't affect the syntax tree. + """ + return token_type in (token.NL, token.COMMENT, token.ENCODING) + + +def generate_tokens(text): + # type: (str) -> Iterator[TokenInfo] + """ + Generates standard library tokens for the given code. + """ + # tokenize.generate_tokens is technically an undocumented API for Python3, but allows us to use the same API as for + # Python2. See http://stackoverflow.com/a/4952291/328565. + # FIXME: Remove cast once https://github.com/python/typeshed/issues/7003 gets fixed + return tokenize.generate_tokens(cast(Callable[[], str], io.StringIO(text).readline)) + + +def iter_children_func(node): + # type: (AST) -> Callable + """ + Returns a function which yields all direct children of a AST node, + skipping children that are singleton nodes. + The function depends on whether ``node`` is from ``ast`` or from the ``astroid`` module. + """ + return iter_children_astroid if hasattr(node, 'get_children') else iter_children_ast + + +def iter_children_astroid(node, include_joined_str=False): + # type: (NodeNG, bool) -> Union[Iterator, List] + if not include_joined_str and is_joined_str(node): + return [] + + return node.get_children() + + +SINGLETONS = {c for n, c in ast.__dict__.items() if isinstance(c, type) and + issubclass(c, (ast.expr_context, ast.boolop, ast.operator, ast.unaryop, ast.cmpop))} + + +def iter_children_ast(node, include_joined_str=False): + # type: (AST, bool) -> Iterator[Union[AST, expr]] + if not include_joined_str and is_joined_str(node): + return + + if isinstance(node, ast.Dict): + # override the iteration order: instead of , , + # yield keys and values in source order (key1, value1, key2, value2, ...) + for (key, value) in zip(node.keys, node.values): + if key is not None: + yield key + yield value + return + + for child in ast.iter_child_nodes(node): + # Skip singleton children; they don't reflect particular positions in the code and break the + # assumptions about the tree consisting of distinct nodes. Note that collecting classes + # beforehand and checking them in a set is faster than using isinstance each time. + if child.__class__ not in SINGLETONS: + yield child + + +stmt_class_names = {n for n, c in ast.__dict__.items() + if isinstance(c, type) and issubclass(c, ast.stmt)} +expr_class_names = ({n for n, c in ast.__dict__.items() + if isinstance(c, type) and issubclass(c, ast.expr)} | + {'AssignName', 'DelName', 'Const', 'AssignAttr', 'DelAttr'}) + +# These feel hacky compared to isinstance() but allow us to work with both ast and astroid nodes +# in the same way, and without even importing astroid. +def is_expr(node): + # type: (AstNode) -> bool + """Returns whether node is an expression node.""" + return node.__class__.__name__ in expr_class_names + +def is_stmt(node): + # type: (AstNode) -> bool + """Returns whether node is a statement node.""" + return node.__class__.__name__ in stmt_class_names + +def is_module(node): + # type: (AstNode) -> bool + """Returns whether node is a module node.""" + return node.__class__.__name__ == 'Module' + +def is_joined_str(node): + # type: (AstNode) -> bool + """Returns whether node is a JoinedStr node, used to represent f-strings.""" + # At the moment, nodes below JoinedStr have wrong line/col info, and trying to process them only + # leads to errors. + return node.__class__.__name__ == 'JoinedStr' + + +def is_expr_stmt(node): + # type: (AstNode) -> bool + """Returns whether node is an `Expr` node, which is a statement that is an expression.""" + return node.__class__.__name__ == 'Expr' + + + +CONSTANT_CLASSES: Tuple[Type, ...] = (ast.Constant,) +try: + from astroid import Const + CONSTANT_CLASSES += (Const,) +except ImportError: # pragma: no cover + # astroid is not available + pass + +def is_constant(node): + # type: (AstNode) -> bool + """Returns whether node is a Constant node.""" + return isinstance(node, CONSTANT_CLASSES) + + +def is_ellipsis(node): + # type: (AstNode) -> bool + """Returns whether node is an Ellipsis node.""" + return is_constant(node) and node.value is Ellipsis # type: ignore + + +def is_starred(node): + # type: (AstNode) -> bool + """Returns whether node is a starred expression node.""" + return node.__class__.__name__ == 'Starred' + + +def is_slice(node): + # type: (AstNode) -> bool + """Returns whether node represents a slice, e.g. `1:2` in `x[1:2]`""" + # Before 3.9, a tuple containing a slice is an ExtSlice, + # but this was removed in https://bugs.python.org/issue34822 + return ( + node.__class__.__name__ in ('Slice', 'ExtSlice') + or ( + node.__class__.__name__ == 'Tuple' + and any(map(is_slice, cast(ast.Tuple, node).elts)) + ) + ) + + +def is_empty_astroid_slice(node): + # type: (AstNode) -> bool + return ( + node.__class__.__name__ == "Slice" + and not isinstance(node, ast.AST) + and node.lower is node.upper is node.step is None + ) + + +# Sentinel value used by visit_tree(). +_PREVISIT = object() + +def visit_tree(node, previsit, postvisit): + # type: (Module, Callable[[AstNode, Optional[Token]], Tuple[Optional[Token], Optional[Token]]], Optional[Callable[[AstNode, Optional[Token], Optional[Token]], None]]) -> None + """ + Scans the tree under the node depth-first using an explicit stack. It avoids implicit recursion + via the function call stack to avoid hitting 'maximum recursion depth exceeded' error. + + It calls ``previsit()`` and ``postvisit()`` as follows: + + * ``previsit(node, par_value)`` - should return ``(par_value, value)`` + ``par_value`` is as returned from ``previsit()`` of the parent. + + * ``postvisit(node, par_value, value)`` - should return ``value`` + ``par_value`` is as returned from ``previsit()`` of the parent, and ``value`` is as + returned from ``previsit()`` of this node itself. The return ``value`` is ignored except + the one for the root node, which is returned from the overall ``visit_tree()`` call. + + For the initial node, ``par_value`` is None. ``postvisit`` may be None. + """ + if not postvisit: + postvisit = lambda node, pvalue, value: None + + iter_children = iter_children_func(node) + done = set() + ret = None + stack = [(node, None, _PREVISIT)] # type: List[Tuple[AstNode, Optional[Token], Union[Optional[Token], object]]] + while stack: + current, par_value, value = stack.pop() + if value is _PREVISIT: + assert current not in done # protect againt infinite loop in case of a bad tree. + done.add(current) + + pvalue, post_value = previsit(current, par_value) + stack.append((current, par_value, post_value)) + + # Insert all children in reverse order (so that first child ends up on top of the stack). + ins = len(stack) + for n in iter_children(current): + stack.insert(ins, (n, pvalue, _PREVISIT)) + else: + ret = postvisit(current, par_value, cast(Optional[Token], value)) + return ret + + +def walk(node, include_joined_str=False): + # type: (AST, bool) -> Iterator[Union[Module, AstNode]] + """ + Recursively yield all descendant nodes in the tree starting at ``node`` (including ``node`` + itself), using depth-first pre-order traversal (yieling parents before their children). + + This is similar to ``ast.walk()``, but with a different order, and it works for both ``ast`` and + ``astroid`` trees. Also, as ``iter_children()``, it skips singleton nodes generated by ``ast``. + + By default, ``JoinedStr`` (f-string) nodes and their contents are skipped + because they previously couldn't be handled. Set ``include_joined_str`` to True to include them. + """ + iter_children = iter_children_func(node) + done = set() + stack = [node] + while stack: + current = stack.pop() + assert current not in done # protect againt infinite loop in case of a bad tree. + done.add(current) + + yield current + + # Insert all children in reverse order (so that first child ends up on top of the stack). + # This is faster than building a list and reversing it. + ins = len(stack) + for c in iter_children(current, include_joined_str): + stack.insert(ins, c) + + +def replace(text, replacements): + # type: (str, List[Tuple[int, int, str]]) -> str + """ + Replaces multiple slices of text with new values. This is a convenience method for making code + modifications of ranges e.g. as identified by ``ASTTokens.get_text_range(node)``. Replacements is + an iterable of ``(start, end, new_text)`` tuples. + + For example, ``replace("this is a test", [(0, 4, "X"), (8, 9, "THE")])`` produces + ``"X is THE test"``. + """ + p = 0 + parts = [] + for (start, end, new_text) in sorted(replacements): + parts.append(text[p:start]) + parts.append(new_text) + p = end + parts.append(text[p:]) + return ''.join(parts) + + +class NodeMethods: + """ + Helper to get `visit_{node_type}` methods given a node's class and cache the results. + """ + def __init__(self): + # type: () -> None + self._cache = {} # type: Dict[Union[ABCMeta, type], Callable[[AstNode, Token, Token], Tuple[Token, Token]]] + + def get(self, obj, cls): + # type: (Any, Union[ABCMeta, type]) -> Callable + """ + Using the lowercase name of the class as node_type, returns `obj.visit_{node_type}`, + or `obj.visit_default` if the type-specific method is not found. + """ + method = self._cache.get(cls) + if not method: + name = "visit_" + cls.__name__.lower() + method = getattr(obj, name, obj.visit_default) + self._cache[cls] = method + return method + + +def patched_generate_tokens(original_tokens): + # type: (Iterable[TokenInfo]) -> Iterator[TokenInfo] + """ + Fixes tokens yielded by `tokenize.generate_tokens` to handle more non-ASCII characters in identifiers. + Workaround for https://github.com/python/cpython/issues/68382. + Should only be used when tokenizing a string that is known to be valid syntax, + because it assumes that error tokens are not actually errors. + Combines groups of consecutive NAME, NUMBER, and/or ERRORTOKEN tokens into a single NAME token. + """ + group = [] # type: List[tokenize.TokenInfo] + for tok in original_tokens: + if ( + tok.type in (tokenize.NAME, tokenize.ERRORTOKEN, tokenize.NUMBER) + # Only combine tokens if they have no whitespace in between + and (not group or group[-1].end == tok.start) + ): + group.append(tok) + else: + for combined_token in combine_tokens(group): + yield combined_token + group = [] + yield tok + for combined_token in combine_tokens(group): + yield combined_token + +def combine_tokens(group): + # type: (List[tokenize.TokenInfo]) -> List[tokenize.TokenInfo] + if not any(tok.type == tokenize.ERRORTOKEN for tok in group) or len({tok.line for tok in group}) != 1: + return group + return [ + tokenize.TokenInfo( + type=tokenize.NAME, + string="".join(t.string for t in group), + start=group[0].start, + end=group[-1].end, + line=group[0].line, + ) + ] + + +def last_stmt(node): + # type: (ast.AST) -> ast.AST + """ + If the given AST node contains multiple statements, return the last one. + Otherwise, just return the node. + """ + child_stmts = [ + child for child in iter_children_func(node)(node) + if is_stmt(child) or type(child).__name__ in ( + "excepthandler", + "ExceptHandler", + "match_case", + "MatchCase", + "TryExcept", + "TryFinally", + ) + ] + if child_stmts: + return last_stmt(child_stmts[-1]) + return node + + + +@lru_cache(maxsize=None) +def fstring_positions_work(): + # type: () -> bool + """ + The positions attached to nodes inside f-string FormattedValues have some bugs + that were fixed in Python 3.9.7 in https://github.com/python/cpython/pull/27729. + This checks for those bugs more concretely without relying on the Python version. + Specifically this checks: + - Values with a format spec or conversion + - Repeated (i.e. identical-looking) expressions + - f-strings implicitly concatenated over multiple lines. + - Multiline, triple-quoted f-strings. + """ + source = """( + f"a {b}{b} c {d!r} e {f:g} h {i:{j}} k {l:{m:n}}" + f"a {b}{b} c {d!r} e {f:g} h {i:{j}} k {l:{m:n}}" + f"{x + y + z} {x} {y} {z} {z} {z!a} {z:z}" + f''' + {s} {t} + {u} {v} + ''' + )""" + tree = ast.parse(source) + name_nodes = [node for node in ast.walk(tree) if isinstance(node, ast.Name)] + name_positions = [(node.lineno, node.col_offset) for node in name_nodes] + positions_are_unique = len(set(name_positions)) == len(name_positions) + correct_source_segments = all( + ast.get_source_segment(source, node) == node.id + for node in name_nodes + ) + return positions_are_unique and correct_source_segments + +def annotate_fstring_nodes(tree): + # type: (ast.AST) -> None + """ + Add a special attribute `_broken_positions` to nodes inside f-strings + if the lineno/col_offset cannot be trusted. + """ + if sys.version_info >= (3, 12): + # f-strings were weirdly implemented until https://peps.python.org/pep-0701/ + # In Python 3.12, inner nodes have sensible positions. + return + for joinedstr in walk(tree, include_joined_str=True): + if not isinstance(joinedstr, ast.JoinedStr): + continue + for part in joinedstr.values: + # The ast positions of the FormattedValues/Constant nodes span the full f-string, which is weird. + setattr(part, '_broken_positions', True) # use setattr for mypy + + if isinstance(part, ast.FormattedValue): + if not fstring_positions_work(): + for child in walk(part.value): + setattr(child, '_broken_positions', True) + + if part.format_spec: # this is another JoinedStr + # Again, the standard positions span the full f-string. + setattr(part.format_spec, '_broken_positions', True) diff --git a/lib/python3.10/site-packages/asttokens/version.py b/lib/python3.10/site-packages/asttokens/version.py new file mode 100644 index 0000000000000000000000000000000000000000..528787cfc8ad81ed41822a8104b60b4896632906 --- /dev/null +++ b/lib/python3.10/site-packages/asttokens/version.py @@ -0,0 +1 @@ +__version__ = "3.0.0" diff --git a/lib/python3.10/site-packages/attr/__init__.py b/lib/python3.10/site-packages/attr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6e0650bc4bf53806420d7ef5f881ecd2bd77ea --- /dev/null +++ b/lib/python3.10/site-packages/attr/__init__.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: MIT + +""" +Classes Without Boilerplate +""" + +from functools import partial +from typing import Callable, Literal, Protocol + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Converter, + Factory, + _Nothing, + attrib, + attrs, + evolve, + fields, + fields_dict, + make_class, + validate, +) +from ._next_gen import define, field, frozen, mutable +from ._version_info import VersionInfo + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + + +class AttrsInstance(Protocol): + pass + + +NothingType = Literal[_Nothing.NOTHING] + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "Converter", + "Factory", + "NothingType", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "get_run_validators", + "has", + "ib", + "make_class", + "mutable", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + + +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + if name not in ("__version__", "__version_info__"): + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + from importlib.metadata import metadata + + meta = metadata("attrs") + + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + + return meta["version"] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/lib/python3.10/site-packages/attr/__init__.pyi b/lib/python3.10/site-packages/attr/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..133e50105de3cef606889f32d85f324d94ee40f3 --- /dev/null +++ b/lib/python3.10/site-packages/attr/__init__.pyi @@ -0,0 +1,389 @@ +import enum +import sys + +from typing import ( + Any, + Callable, + Generic, + Literal, + Mapping, + Protocol, + Sequence, + TypeVar, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ +from ._version_info import VersionInfo +from attrs import ( + define as define, + field as field, + mutable as mutable, + frozen as frozen, + _EqOrderType, + _ValidatorType, + _ConverterType, + _ReprArgType, + _OnSetAttrType, + _OnSetAttrArgType, + _FieldTransformer, + _ValidatorArgType, +) + +if sys.version_info >= (3, 10): + from typing import TypeGuard, TypeAlias +else: + from typing_extensions import TypeGuard, TypeAlias + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_FilterType = Callable[["Attribute[_T]", _T], bool] + +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass + +_A = TypeVar("_A", bound=type[AttrsInstance]) + +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING +NothingType: TypeAlias = Literal[_Nothing.NOTHING] + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. + +@overload +def Factory(factory: Callable[[], _T]) -> _T: ... +@overload +def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], +) -> _T: ... +@overload +def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], +) -> _T: ... + +In = TypeVar("In") +Out = TypeVar("Out") + +class Converter(Generic[In, Out]): + @overload + def __init__(self, converter: Callable[[In], Out]) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance, Attribute], Out], + *, + takes_self: Literal[True], + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, Attribute], Out], + *, + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance], Out], + *, + takes_self: Literal[True], + ) -> None: ... + +class Attribute(Generic[_T]): + name: str + default: _T | None + validator: _ValidatorType[_T] | None + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: bool | None + init: bool + converter: Converter | None + metadata: dict[Any, Any] + type: type[_T] | None + kw_only: bool + on_setattr: _OnSetAttrType + alias: str | None + + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: object = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: _C, + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> _C: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> Callable[[_C], _C]: ... +def fields(cls: type[AttrsInstance]) -> Any: ... +def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... +def validate(inst: AttrsInstance) -> None: ... +def resolve_types( + cls: _A, + globalns: dict[str, Any] | None = ..., + localns: dict[str, Any] | None = ..., + attribs: list[Attribute[Any]] | None = ..., + include_extras: bool = ..., +) -> _A: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: list[str] | tuple[str, ...] | dict[str, Any], + bases: tuple[type, ...] = ..., + class_body: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + collect_by_mro: bool = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + dict_factory: type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ..., + tuple_keys: bool | None = ..., +) -> dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + tuple_factory: type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> tuple[Any, ...]: ... +def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/lib/python3.10/site-packages/attr/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..914ed73af5c97d1dbc6e2040e8dfda547796974a Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_cmp.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_cmp.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50b491318c2f4218bf23aec3d844f0ac69d69819 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_cmp.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_compat.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_compat.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..175860e6d4abecc21187a5d082d1d257d1159696 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_compat.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_config.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3b7f5fa7f60cc7524e1693d854767268d9499ff Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_config.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_funcs.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_funcs.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c4b3bab4230678177e4a4bd35b1b2f0a09e5ca8 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_funcs.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_make.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_make.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..167c8b15724e3fcbd020f2fc1306c4302ca59d43 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_make.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_next_gen.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_next_gen.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddf1ffa5b43f18d96c8b180c2826b130c558b258 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_next_gen.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/_version_info.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/_version_info.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..619e100c1635e988ea717be469ba150aeb2d6ad0 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/_version_info.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/converters.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/converters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa21308f3c410d8cbe78f230e36e74467852248e Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/converters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/exceptions.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/exceptions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a0b40910e104124eb614e4e95d803f816e6cb82 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/exceptions.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/filters.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/filters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..068b6dc04083f651fb38f322a53c87c2440e2f79 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/filters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/setters.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/setters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..673193fcb456e570ae4b8c8521a234e4aa2d5930 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/setters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/__pycache__/validators.cpython-310.pyc b/lib/python3.10/site-packages/attr/__pycache__/validators.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e7350b3990b2f8e17afb96d9ef515026e3ed4b3 Binary files /dev/null and b/lib/python3.10/site-packages/attr/__pycache__/validators.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attr/_cmp.py b/lib/python3.10/site-packages/attr/_cmp.py new file mode 100644 index 0000000000000000000000000000000000000000..09bab491f83ef4d15129f34b5f5a9e69bb34d63c --- /dev/null +++ b/lib/python3.10/site-packages/attr/_cmp.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: MIT + + +import functools +import types + +from ._make import __ne__ + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + Args: + eq (typing.Callable | None): + Callable used to evaluate equality of two objects. + + lt (typing.Callable | None): + Callable used to evaluate whether one object is less than another + object. + + le (typing.Callable | None): + Callable used to evaluate whether one object is less than or equal + to another object. + + gt (typing.Callable | None): + Callable used to evaluate whether one object is greater than + another object. + + ge (typing.Callable | None): + Callable used to evaluate whether one object is greater than or + equal to another object. + + require_same_type (bool): + When `True`, equality and ordering methods will return + `NotImplemented` if objects are not of the same type. + + class_name (str | None): Name of class. Defaults to "Comparable". + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = __ne__ + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = types.new_class( + class_name, (object,), {}, lambda ns: ns.update(body) + ) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + return all(func(self, other) for func in self._requirements) + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/lib/python3.10/site-packages/attr/_cmp.pyi b/lib/python3.10/site-packages/attr/_cmp.pyi new file mode 100644 index 0000000000000000000000000000000000000000..cc7893b04520afa719b1412c7646c3c1b39bf94b --- /dev/null +++ b/lib/python3.10/site-packages/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Any, Callable + +_CompareWithType = Callable[[Any, Any], bool] + +def cmp_using( + eq: _CompareWithType | None = ..., + lt: _CompareWithType | None = ..., + le: _CompareWithType | None = ..., + gt: _CompareWithType | None = ..., + ge: _CompareWithType | None = ..., + require_same_type: bool = ..., + class_name: str = ..., +) -> type: ... diff --git a/lib/python3.10/site-packages/attr/_compat.py b/lib/python3.10/site-packages/attr/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..22fcd78387b7b36f005ec5eee3fbf784ba87a93d --- /dev/null +++ b/lib/python3.10/site-packages/attr/_compat.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: MIT + +import inspect +import platform +import sys +import threading + +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias + + +PYPY = platform.python_implementation() == "PyPy" +PY_3_9_PLUS = sys.version_info[:2] >= (3, 9) +PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) +PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +PY_3_13_PLUS = sys.version_info[:2] >= (3, 13) +PY_3_14_PLUS = sys.version_info[:2] >= (3, 14) + + +if PY_3_14_PLUS: # pragma: no cover + import annotationlib + + _get_annotations = annotationlib.get_annotations + +else: + + def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + return cls.__dict__.get("__annotations__", {}) + + +class _AnnotationExtractor: + """ + Extract type annotations from a callable, returning None whenever there + is none. + """ + + __slots__ = ["sig"] + + def __init__(self, callable): + try: + self.sig = inspect.signature(callable) + except (ValueError, TypeError): # inspect failed + self.sig = None + + def get_first_param_type(self): + """ + Return the type annotation of the first argument if it's not empty. + """ + if not self.sig: + return None + + params = list(self.sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + return params[0].annotation + + return None + + def get_return_type(self): + """ + Return the return type if it's not empty. + """ + if ( + self.sig + and self.sig.return_annotation is not inspect.Signature.empty + ): + return self.sig.return_annotation + + return None + + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/lib/python3.10/site-packages/attr/_config.py b/lib/python3.10/site-packages/attr/_config.py new file mode 100644 index 0000000000000000000000000000000000000000..4b257726fb1e8b95583ecc3eee8d153336dc4089 --- /dev/null +++ b/lib/python3.10/site-packages/attr/_config.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT + +__all__ = ["get_run_validators", "set_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + msg = "'run' must be bool." + raise TypeError(msg) + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/lib/python3.10/site-packages/attr/_funcs.py b/lib/python3.10/site-packages/attr/_funcs.py new file mode 100644 index 0000000000000000000000000000000000000000..c39fb8aa5a9426c18157253aad4b0168084eeb1a --- /dev/null +++ b/lib/python3.10/site-packages/attr/_funcs.py @@ -0,0 +1,468 @@ +# SPDX-License-Identifier: MIT + + +import copy + +from ._compat import PY_3_9_PLUS, get_generic_base +from ._make import _OBJ_SETATTR, NOTHING, fields +from .exceptions import AttrsAttributeNotFoundError + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the *attrs* attribute values of *inst* as a dict. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + dict_factory (~typing.Callable): + A callable to produce dictionaries from. For example, to produce + ordered dictionaries instead of normal Python dictionaries, pass in + ``collections.OrderedDict``. + + retain_collection_types (bool): + Do not convert to `list` when encountering an attribute whose type + is `tuple` or `set`. Only meaningful if *recurse* is `True`. + + value_serializer (typing.Callable | None): + A hook that is called for every attribute or dict key/value. It + receives the current instance, field and value and must return the + (updated) value. The hook is run *after* the optional *filter* has + been applied. + + Returns: + Return type of *dict_factory*. + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 + If a dict has a collection for a key, it is serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + if has(v.__class__): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain_collection_types is True else list + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) + elif isinstance(v, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in v.items() + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + if getattr(val.__class__, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(val, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif isinstance(val, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in val.items() + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the *attrs* attribute values of *inst* as a tuple. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): + Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + tuple_factory (~typing.Callable): + A callable to produce tuples from. For example, to produce lists + instead of tuples. + + retain_collection_types (bool): + Do not convert to `list` or `dict` when encountering an attribute + which type is `tuple`, `dict` or `set`. Only meaningful if + *recurse* is `True`. + + Returns: + Return type of *tuple_factory* + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + if recurse is True: + if has(v.__class__): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif isinstance(v, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + items = [ + ( + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + ) + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) + elif isinstance(v, dict): + df = v.__class__ if retain is True else dict + rv.append( + df( + ( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk + ), + ( + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv + ), + ) + for kk, vv in v.items() + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with *attrs* attributes. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + Returns: + bool: + """ + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + Args: + inst: Instance of a class with *attrs* attributes. + + changes: Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + attrs.exceptions.AttrsAttributeNotFoundError: + If *attr_name* couldn't be found on *cls*. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. This function will not be + removed du to the slightly different approach compared to + `attrs.evolve`, though. + """ + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in changes.items(): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) + _OBJ_SETATTR(new, k, v) + return new + + +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in :class:`Attribute`'s + *type* field. In other words, you don't need to resolve your types if you + only use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, for example, if the name only + exists inside a method, you may pass *globalns* or *localns* to specify + other dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + Args: + cls (type): Class to resolve. + + globalns (dict | None): Dictionary containing global variables. + + localns (dict | None): Dictionary containing local variables. + + attribs (list | None): + List of attribs for the given class. This is necessary when calling + from inside a ``field_transformer`` since *cls* is not an *attrs* + class yet. + + include_extras (bool): + Resolve more accurately, if possible. Pass ``include_extras`` to + ``typing.get_hints``, if supported by the typing module. On + supported Python versions (3.9+), this resolves the types more + accurately. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class and you didn't pass any attribs. + + NameError: If types cannot be resolved because of missing variables. + + Returns: + *cls* so you can use this function also as a class decorator. Please + note that you have to apply it **after** `attrs.define`. That means the + decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + kwargs = {"globalns": globalns, "localns": localns} + + if PY_3_9_PLUS: + kwargs["include_extras"] = include_extras + + hints = typing.get_type_hints(cls, **kwargs) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _OBJ_SETATTR(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/lib/python3.10/site-packages/attr/_make.py b/lib/python3.10/site-packages/attr/_make.py new file mode 100644 index 0000000000000000000000000000000000000000..e84d9792a744f34934a45b26d457f669596f7dee --- /dev/null +++ b/lib/python3.10/site-packages/attr/_make.py @@ -0,0 +1,3123 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import abc +import contextlib +import copy +import enum +import inspect +import itertools +import linecache +import sys +import types +import unicodedata + +from collections.abc import Callable, Mapping +from functools import cached_property +from typing import Any, NamedTuple, TypeVar + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + PY_3_10_PLUS, + PY_3_11_PLUS, + PY_3_13_PLUS, + _AnnotationExtractor, + _get_annotations, + get_generic_base, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + UnannotatedAttributeError, +) + + +# This is used at least twice, so cache it here. +_OBJ_SETATTR = object.__setattr__ +_INIT_FACTORY_PAT = "__attr_factory_%s" +_CLASSVAR_PREFIXES = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_HASH_CACHE_FIELD = "_attrs_cached_hash" + +_EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) + +# Unique object for unequivocal getattr() defaults. +_SENTINEL = object() + +_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(enum.Enum): + """ + Sentinel to indicate the lack of a value when `None` is ambiguous. + + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. + """ + + NOTHING = enum.auto() + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + +NOTHING = _Nothing.NOTHING +""" +Sentinel to indicate the lack of a value when `None` is ambiguous. + +When using in 3rd party code, use `attrs.NothingType` for type annotations. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since `None` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new field / attribute on a class. + + Identical to `attrs.field`, except it's not keyword-only. + + Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, + though). + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attr.s` (or similar)! + + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is `None` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 + *converter* as a replacement for the deprecated *convert* to achieve + consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if factory is not None: + if default is not NOTHING: + msg = ( + "The `default` and `factory` arguments are mutually exclusive." + ) + raise ValueError(msg) + if not callable(factory): + msg = "The `factory` argument must be a callable." + raise ValueError(msg) + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + alias=alias, + ) + + +def _compile_and_eval( + script: str, + globs: dict[str, Any] | None, + locs: Mapping[str, object] | None = None, + filename: str = "", +) -> None: + """ + Evaluate the script with the given global (globs) and local (locs) + variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _linecache_and_compile( + script: str, + filename: str, + globs: dict[str, Any] | None, + locals: Mapping[str, object] | None = None, +) -> dict[str, Any]: + """ + Cache the script with _linecache_, compile it and return the _locals_. + """ + + locs = {} if locals is None else locals + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs + + +def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type: + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = f"{cls_name}Attributes" + body = {} + for i, attr_name in enumerate(attr_names): + + def getter(self, i=i): + return self[i] + + body[attr_name] = property(getter) + return type(attr_class_name, (tuple,), body) + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +class _Attributes(NamedTuple): + attrs: type + base_attrs: list[Attribute] + base_attrs_map: dict[str, type] + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_CLASSVAR_PREFIXES) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + """ + return attrib_name in cls.__dict__ + + +def _collect_base_attrs( + cls, taken_attr_names +) -> tuple[list[Attribute], dict[str, type]]: + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer +) -> _Attributes: + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + If *collect_by_mro* is True, collect them in the correct MRO order, + otherwise use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = list(these.items()) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if a.__class__ is not _CountingAttr: + a = attrib(a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if unannotated: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + ), + key=lambda e: e[1].counter, + ) + + fca = Attribute.from_counting_attr + own_attrs = [ + fca(attr_name, ca, anns.get(attr_name)) for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + if field_transformer is not None: + attrs = tuple(field_transformer(cls, attrs)) + + # Check attr order after executing the field_transformer. + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) + + if had_default is False and a.default is not NOTHING: + had_default = True + + # Resolve default field alias after executing field_transformer. + # This allows field_transformer to differentiate between explicit vs + # default aliases and supply their own defaults. + for a in attrs: + if not a.alias: + # Evolve is very slow, so we hold our nose and do it dirty. + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map) + + +def _make_cached_property_getattr(cached_properties, original_getattr, cls): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper(_cls):", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " try:", + " return super().__getattribute__(item)", + " except AttributeError:", + " if not hasattr(super(), '__getattr__'):", + " raise", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper(_cls)", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") + + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _OBJ_SETATTR.__get__, + "original_getattr": original_getattr, + } + + return _linecache_and_compile( + "\n".join(lines), unique_filename, glob, locals={"_cls": cls} + )["__getattr__"] + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + "__suppress_context__", + "__notes__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + if isinstance(self, BaseException) and name in ("__notes__",): + BaseException.__delattr__(self, name) + return + + raise FrozenInstanceError + + +def evolve(*args, **changes): + """ + Create a new instance, based on the first positional argument with + *changes* applied. + + .. tip:: + + On Python 3.13 and later, you can also use `copy.replace` instead. + + Args: + + inst: + Instance of a class with *attrs* attributes. *inst* must be passed + as a positional argument. + + changes: + Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + TypeError: + If *attr_name* couldn't be found in the class ``__init__``. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. + .. versionchanged:: 24.1.0 + *inst* can't be passed as a keyword argument anymore. + """ + try: + (inst,) = args + except ValueError: + msg = ( + f"evolve() takes 1 positional argument, but {len(args)} were given" + ) + raise TypeError(msg) from None + + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = a.alias + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +class _ClassBuilder: + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_add_method_dunders", + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_custom_setattr", + "_has_post_init", + "_has_pre_init", + "_is_exc", + "_on_setattr", + "_pre_init_has_args", + "_repr_added", + "_script_snippets", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + ) + + def __init__( + self, + cls: type, + these, + slots, + frozen, + weakref_slot, + getstate_setstate, + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_custom_setattr, + field_transformer, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if slots else {} + self._attrs = attrs + self._base_names = {a.name for a in base_attrs} + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = slots + self._frozen = frozen + self._weakref_slot = weakref_slot + self._cache_hash = cache_hash + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = is_exc + self._on_setattr = on_setattr + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + + if frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif on_setattr in ( + _DEFAULT_ON_SETATTR, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + on_setattr == _DEFAULT_ON_SETATTR + and not (has_validator or has_converter) + ) + or (on_setattr == setters.validate and not has_validator) + or (on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if getstate_setstate: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + # tuples of script, globs, hook + self._script_snippets: list[ + tuple[str, dict, Callable[[dict, dict], Any]] + ] = [] + self._repr_added = False + + # We want to only do this check once; in 99.9% of cases these + # exist. + if not hasattr(self._cls, "__module__") or not hasattr( + self._cls, "__qualname__" + ): + self._add_method_dunders = self._add_method_dunders_safe + else: + self._add_method_dunders = self._add_method_dunders_unsafe + + def __repr__(self): + return f"<_ClassBuilder(cls={self._cls.__name__})>" + + def _eval_snippets(self) -> None: + """ + Evaluate any registered snippets in one go. + """ + script = "\n".join([snippet[0] for snippet in self._script_snippets]) + globs = {} + for _, snippet_globs, _ in self._script_snippets: + globs.update(snippet_globs) + + locs = _linecache_and_compile( + script, + _generate_unique_filename(self._cls, "methods"), + globs, + ) + + for _, _, hook in self._script_snippets: + hook(self._cls_dict, locs) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + self._eval_snippets() + if self._slots is True: + cls = self._create_slots_class() + else: + cls = self._patch_original_class() + if PY_3_10_PLUS: + cls = abc.update_abstractmethods(cls) + + # The method gets only called if it's not inherited from a base class. + # _has_own_attribute does NOT work properly for classmethods. + if ( + getattr(cls, "__attrs_init_subclass__", None) + and "__attrs_init_subclass__" not in cls.__dict__ + ): + cls.__attrs_init_subclass__() + + return cls + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _SENTINEL) is not _SENTINEL + ): + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): + delattr(cls, name) + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = _OBJ_SETATTR + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in self._cls_dict.items() + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") + } + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = _OBJ_SETATTR + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = {} + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + cached_properties = { + name: cached_prop.func + for name, cached_prop in cd.items() + if isinstance(cached_prop, cached_property) + } + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + # Add cached properties to names for slotting. + names += (name,) + # Clear out function from class to avoid clashing. + del cd[name] + additional_closure_functions_to_update.append(func) + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overridden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in existing_slots.items() + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_HASH_CACHE_FIELD) + + cd["__slots__"] = tuple(slot_names) + + cd["__qualname__"] = self._cls.__qualname__ + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . + # If a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty + pass + else: + if match: + cell.cell_contents = cls + return cls + + def add_repr(self, ns): + script, globs = _make_repr_script(self._attrs, ns) + + def _attach_repr(cls_dict, globs): + cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"]) + + self._script_snippets.append((script, globs, _attach_repr)) + self._repr_added = True + return self + + def add_str(self): + if not self._repr_added: + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return {name: getattr(self, name) for name in state_attr_names} + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _OBJ_SETATTR.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_HASH_CACHE_FIELD, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + script, globs = _make_hash_script( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + + def attach_hash(cls_dict: dict, locs: dict) -> None: + cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"]) + + self._script_snippets.append((script, globs, attach_hash)) + + return self + + def add_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + + def _attach_init(cls_dict, globs): + init = globs["__init__"] + init.__annotations__ = annotations + cls_dict["__init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_init)) + + return self + + def add_replace(self): + self._cls_dict["__replace__"] = self._add_method_dunders( + lambda self, **changes: evolve(self, **changes) + ) + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + + def _attach_attrs_init(cls_dict, globs): + init = globs["__attrs_init__"] + init.__annotations__ = annotations + cls_dict["__attrs_init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_attrs_init)) + + return self + + def add_eq(self): + cd = self._cls_dict + + script, globs = _make_eq_script(self._attrs) + + def _attach_eq(cls_dict, globs): + cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"]) + + self._script_snippets.append((script, globs, _attach_eq)) + + cd["__ne__"] = __ne__ + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _OBJ_SETATTR(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders_unsafe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method*. + """ + method.__module__ = self._cls.__module__ + + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + method.__doc__ = ( + f"Method generated by attrs for class {self._cls.__qualname__}." + ) + + return method + + def _add_method_dunders_safe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + with contextlib.suppress(AttributeError): + method.__module__ = self._cls.__module__ + + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + with contextlib.suppress(AttributeError): + method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}." + + return method + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + unsafe_hash=None, +): + r""" + A class decorator that adds :term:`dunder methods` according to the + specified attributes using `attr.ib` or the *these* argument. + + Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will + *never* go away, though). + + Args: + repr_ns (str): + When using nested classes, there was no way in Python 2 to + automatically detect that. This argument allows to set a custom + name for a more meaningful ``repr`` output. This argument is + pointless in Python 3 and is therefore deprecated. + + .. caution:: + Refer to `attrs.define` for the rest of the parameters, but note that they + can have different defaults. + + Notably, leaving *on_setattr* as `None` will **not** add any hooks. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports `None` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. deprecated:: 24.1.0 *repr_ns* + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + """ + if repr_ns is not None: + import warnings + + warnings.warn( + DeprecationWarning( + "The `repr_ns` argument is deprecated and will be removed in or after August 2025." + ), + stacklevel=2, + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) + + builder = _ClassBuilder( + cls, + these, + slots, + is_frozen, + weakref_slot, + _determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + auto_attribs, + kw_only, + cache_hash, + is_exc, + collect_by_mro, + on_setattr, + has_own_setattr, + field_transformer, + ) + + if _determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ): + builder.add_repr(repr_ns) + + if str is True: + builder.add_str() + + eq = _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + if not is_exc and eq is True: + builder.add_eq() + if not is_exc and _determine_whether_to_implement( + cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") + ): + builder.add_order() + + if not frozen: + builder.add_setattr() + + nonlocal hash + if ( + hash is None + and auto_detect is True + and _has_own_attribute(cls, "__hash__") + ): + hash = False + + if hash is not True and hash is not False and hash is not None: + # Can't use `hash in` because 1 == True for example. + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if hash is False or (hash is None and eq is False) or is_exc: + # Don't do anything. Should fall back to __object__'s __hash__ + # which is by id. + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) + elif hash is True or ( + hash is None and eq is True and is_frozen is True + ): + # Build a __hash__ if told so, or if it's safe. + builder.add_hash() + else: + # Raise TypeError on attempts to hash. + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) + builder.make_unhashable() + + if _determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ): + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) + + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): + builder.add_replace() + + if ( + PY_3_10_PLUS + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ is _frozen_setattrs + + +def _generate_unique_filename(cls: type, func_name: str) -> str: + """ + Create a "filename" suitable for a function being generated. + """ + return ( + f"" + ) + + +def _make_hash_script( + cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool +) -> tuple[str, dict]: + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + type_hash = hash(_generate_unique_filename(cls, "hash")) + # If eq is custom generated, we need to include the functions in globs + globs = {} + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + hash_def += ", *" + + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + f" {type_hash},", + ] + ) + + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + globs[cmp_name] = a.eq_key + method_lines.append( + indent + f" {cmp_name}(self.{a.name})," + ) + else: + method_lines.append(indent + f" self.{a.name},") + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") + if frozen: + append_hash_computation_lines( + f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + f"self.{_HASH_CACHE_FIELD} = ", tab * 2 + ) + method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return script, globs + + +def _add_hash(cls: type, attrs: list[Attribute]): + """ + Add a hash method to *cls*. + """ + script, globs = _make_hash_script( + cls, attrs, frozen=False, cache_hash=False + ) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__hash__") + ) + cls.__hash__ = globs["__hash__"] + return cls + + +def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + +def _make_eq_script(attrs: list) -> tuple[str, dict]: + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + globs = {} + if attrs: + lines.append(" return (") + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})" + ) + else: + lines.append(f" self.{a.name} == other.{a.name}") + if a is not attrs[-1]: + lines[-1] = f"{lines[-1]} and" + lines.append(" )") + else: + lines.append(" return True") + + script = "\n".join(lines) + + return script, globs + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_eq_script(attrs) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__eq__") + ) + cls.__eq__ = globs["__eq__"] + cls.__ne__ = __ne__ + + return cls + + +def _make_repr_script(attrs, ns) -> tuple[str, dict]: + """ + Create the source and globs for a __repr__ and return it. + """ + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] + + return "\n".join(lines), globs + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_repr_script(attrs, ns) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__repr__") + ) + cls.__repr__ = globs["__repr__"] + return cls + + +def fields(cls): + """ + Return the tuple of *attrs* attributes for a class. + + The tuple also allows accessing the fields by their names (see below for + examples). + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. + """ + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + + attrs = getattr(cls, "__attrs_attrs__", None) + + if attrs is None: + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of *attrs* attributes for a class, whose keys + are the attribute names. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + dict[str, attrs.Attribute]: Dict of attribute name to definition + + .. versionadded:: 18.1.0 + """ + if not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + Args: + inst: Instance of a class with *attrs* attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + cls = base_attr_map.get(a_name) + return cls and "__slots__" in cls.__dict__ + + +def _make_init_script( + cls, + attrs, + pre_init, + pre_init_has_args, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +) -> tuple[str, dict, dict]: + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + pre_init_has_args, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + "__attrs_init__" if attrs_init else "__init__", + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ + + return script, globs, annotations + + +def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return f"_setattr('{attr_name}', {value_var})" + + +def _setattr_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return f"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name, value_var)})" + + +def _assign(attr_name: str, value: str, has_on_setattr: bool) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return f"self.{attr_name} = {value}" + + +def _assign_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True, converter) + + return f"self.{attr_name} = {converter._fmt_converter_call(attr_name, value_var)}" + + +def _determine_setters( + frozen: bool, slots: bool, base_attr_map: dict[str, type] +): + """ + Determine the correct setter functions based on whether a class is frozen + and/or slotted. + """ + if frozen is True: + if slots is True: + return (), _setattr, _setattr_with_converter + + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + + def fmt_setter( + attr_name: str, value_var: str, has_on_setattr: bool + ) -> str: + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return f"_inst_dict['{attr_name}'] = {value_var}" + + def fmt_setter_with_converter( + attr_name: str, + value_var: str, + has_on_setattr: bool, + converter: Converter, + ) -> str: + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr, converter + ) + + return f"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name, value_var)}" + + return ( + ("_inst_dict = self.__dict__",), + fmt_setter, + fmt_setter_with_converter, + ) + + # Not frozen -- we can just assign directly. + return (), _assign, _assign_with_converter + + +def _attrs_to_init_script( + attrs: list[Attribute], + is_frozen: bool, + is_slotted: bool, + call_pre_init: bool, + pre_init_has_args: bool, + call_post_init: bool, + does_cache_hash: bool, + base_attr_map: dict[str, type], + is_exc: bool, + needs_cached_setattr: bool, + has_cls_on_setattr: bool, + method_name: str, +) -> tuple[str, dict, dict]: + """ + Return a script of an initializer for *attrs*, a dict of globals, and + annotations for the initializer. + + The globals are required by the generated script. + """ + lines = ["self.__attrs_pre_init__()"] if call_pre_init else [] + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. Note _setattr will be used again below if + # does_cache_hash is True. + "_setattr = _cached_setattr_get(self)" + ) + + extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( + is_frozen, is_slotted, base_attr_map + ) + lines.extend(extra_lines) + + args = [] + kw_only_args = [] + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias + + has_factory = isinstance(a.default, Factory) + maybe_self = "self" if has_factory and a.default.takes_self else "" + + if a.converter is not None and not isinstance(a.converter, Converter): + converter = Converter(a.converter) + else: + converter = a.converter + + if a.init is False: + if has_factory: + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + elif converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = f"{arg_name}=attr_dict['{attr_name}'].default" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = f"{arg_name}=NOTHING" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + lines.append(f"if {arg_name} is not NOTHING:") + + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and converter is None: + annotations[arg_name] = a.type + elif converter is not None and converter._first_param_type: + # Use the type from the converter if present. + annotations[arg_name] = converter._first_param_type + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if call_post_init: + lines.append("self.__attrs_post_init__()") + + # Because this is set only after __attrs_post_init__ is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to field + # values during post-init combined with post-init accessing the hash code + # would result in silent bugs. + if does_cache_hash: + if is_frozen: + if is_slotted: + init_hash_cache = f"_setattr('{_HASH_CACHE_FIELD}', None)" + else: + init_hash_cache = f"_inst_dict['{_HASH_CACHE_FIELD}'] = None" + else: + init_hash_cache = f"self.{_HASH_CACHE_FIELD} = None" + lines.append(init_hash_cache) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) + + lines.append(f"BaseException.__init__(self, {vals})") + + args = ", ".join(args) + pre_init_args = args + if kw_only_args: + # leading comma & kw_only args + args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" + pre_init_kw_only_args = ", ".join( + [ + f"{kw_arg_name}={kw_arg_name}" + # We need to remove the defaults from the kw_only_args. + for kw_arg_name in (kwa.split("=")[0] for kwa in kw_only_args) + ] + ) + pre_init_args += ", " if pre_init_args else "" + pre_init_args += pre_init_kw_only_args + + if call_pre_init and pre_init_has_args: + # If pre init method has arguments, pass same arguments as `__init__`. + lines[0] = f"self.__attrs_pre_init__({pre_init_args})" + + # Python <3.12 doesn't allow backslashes in f-strings. + NL = "\n " + return ( + f"""def {method_name}(self, {args}): + {NL.join(lines) if lines else "pass"} +""", + names_for_globals, + annotations, + ) + + +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + +class Attribute: + """ + *Read-only* representation of an attribute. + + .. warning:: + + You should never instantiate this class yourself. + + The class has *all* arguments of `attr.ib` (except for ``factory`` which is + only syntactic sugar for ``default=Factory(...)`` plus the following: + + - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The + callables that are used for comparing and ordering objects by this + attribute, respectively. These are set by passing a callable to + `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also + :ref:`comparison customization `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* + + For the full version history of the fields, see `attr.ib`. + """ + + # These slots must NOT be reordered because we use them later for + # instantiation. + __slots__ = ( # noqa: RUF023 + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + "alias", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + alias=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _OBJ_SETATTR.__get__(self) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + types.MappingProxyType(dict(metadata)) # Shallow copy + if metadata + else _EMPTY_METADATA_SINGLETON + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) + + def __setattr__(self, name, value): + raise FrozenInstanceError + + @classmethod + def from_counting_attr(cls, name: str, ca: _CountingAttr, type=None): + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + msg = f"Type annotation and type argument cannot both be present for '{name}'." + raise ValueError(msg) + return cls( + name, + ca._default, + ca._validator, + ca.repr, + None, + ca.hash, + ca.init, + False, + ca.metadata, + type, + ca.converter, + ca.kw_only, + ca.eq, + ca.eq_key, + ca.order, + ca.order_key, + ca.on_setattr, + ca.alias, + ) + + # Don't use attrs.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attrs.evolve` but that function does not work + with :class:`attrs.Attribute`. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _OBJ_SETATTR.__get__(self) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + ( + types.MappingProxyType(dict(value)) + if value + else _EMPTY_METADATA_SINGLETON + ), + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr: + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "_default", + "_validator", + "alias", + "converter", + "counter", + "eq", + "eq_key", + "hash", + "init", + "kw_only", + "metadata", + "on_setattr", + "order", + "order_key", + "repr", + "type", + ) + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), + Attribute( + name="metadata", + alias="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + alias, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + self.alias = alias + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + Raises: + DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class Factory: + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + Args: + factory (typing.Callable): + A callable that takes either none or exactly one mandatory + positional argument depending on *takes_self*. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +class Converter: + """ + Stores a converter callable. + + Allows for the wrapped converter to take additional arguments. The + arguments are passed in the order they are documented. + + Args: + converter (Callable): A callable that converts the passed value. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. (default: `False`) + + takes_field (bool): + Pass the field definition (an :class:`Attribute`) into the + converter as a positional argument. (default: `False`) + + .. versionadded:: 24.1.0 + """ + + __slots__ = ( + "__call__", + "_first_param_type", + "_global_name", + "converter", + "takes_field", + "takes_self", + ) + + def __init__(self, converter, *, takes_self=False, takes_field=False): + self.converter = converter + self.takes_self = takes_self + self.takes_field = takes_field + + ex = _AnnotationExtractor(converter) + self._first_param_type = ex.get_first_param_type() + + if not (self.takes_self or self.takes_field): + self.__call__ = lambda value, _, __: self.converter(value) + elif self.takes_self and not self.takes_field: + self.__call__ = lambda value, instance, __: self.converter( + value, instance + ) + elif not self.takes_self and self.takes_field: + self.__call__ = lambda value, __, field: self.converter( + value, field + ) + else: + self.__call__ = lambda value, instance, field: self.converter( + value, instance, field + ) + + rt = ex.get_return_type() + if rt is not None: + self.__call__.__annotations__["return"] = rt + + @staticmethod + def _get_global_name(attr_name: str) -> str: + """ + Return the name that a converter for an attribute name *attr_name* + would have. + """ + return f"__attr_converter_{attr_name}" + + def _fmt_converter_call(self, attr_name: str, value_var: str) -> str: + """ + Return a string that calls the converter for an attribute name + *attr_name* and the value in variable named *value_var* according to + `self.takes_self` and `self.takes_field`. + """ + if not (self.takes_self or self.takes_field): + return f"{self._get_global_name(attr_name)}({value_var})" + + if self.takes_self and self.takes_field: + return f"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])" + + if self.takes_self: + return f"{self._get_global_name(attr_name)}({value_var}, self)" + + return f"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])" + + def __getstate__(self): + """ + Return a dict containing only converter and takes_self -- the rest gets + computed when loading. + """ + return { + "converter": self.converter, + "takes_self": self.takes_self, + "takes_field": self.takes_field, + } + + def __setstate__(self, state): + """ + Load instance from state. + """ + self.__init__(**state) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in ("converter", "takes_self", "takes_field") +] + +Converter = _add_hash( + _add_eq(_add_repr(Converter, attrs=_f), attrs=_f), attrs=_f +) + + +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" + A quick way to create a new class called *name* with *attrs*. + + .. note:: + + ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` + which means that it doesn't come with some of the improved defaults. + + For example, if you want the same ``on_setattr`` behavior as in + `attrs.define`, you have to pass the hooks yourself: ``make_class(..., + on_setattr=setters.pipe(setters.convert, setters.validate)`` + + .. warning:: + + It is *your* duty to ensure that the class name and the attribute names + are valid identifiers. ``make_class()`` will *not* validate them for + you. + + Args: + name (str): The name for the new class. + + attrs (list | dict): + A list of names or a dictionary of mappings of names to `attr.ib`\ + s / `attrs.field`\ s. + + The order is deduced from the order of the names or attributes + inside *attrs*. Otherwise the order of the definition of the + attributes is used. + + bases (tuple[type, ...]): Classes that the new class will subclass. + + class_body (dict): + An optional dictionary of class attributes for the new class. + + attributes_arguments: Passed unmodified to `attr.s`. + + Returns: + type: A new class with *attrs*. + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* + .. versionchanged:: 25.2.0 Class names can now be unicode. + """ + # Class identifiers are converted into the normal form NFKC while parsing + name = unicodedata.normalize("NFKC", name) + + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = {a: attrib() for a in attrs} + else: + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if class_body is not None: + body.update(class_body) + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + with contextlib.suppress(AttributeError, ValueError): + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + cls = _attrs(these=cls_dict, **attributes_arguments)(type_) + # Only add type annotations now or "_attrs()" will complain: + cls.__annotations__ = { + k: v.type for k, v in cls_dict.items() if v.type is not None + } + return cls + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, unsafe_hash=True) +class _AndValidator: + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if they + have any. + + converters (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + return_instance = any(isinstance(c, Converter) for c in converters) + + if return_instance: + + def pipe_converter(val, inst, field): + for c in converters: + val = ( + c(val, inst, field) if isinstance(c, Converter) else c(val) + ) + + return val + + else: + + def pipe_converter(val): + for c in converters: + val = c(val) + + return val + + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = TypeVar("A") + pipe_converter.__annotations__.update({"val": A, "return": A}) + else: + # Get parameter type from first converter. + t = _AnnotationExtractor(converters[0]).get_first_param_type() + if t: + pipe_converter.__annotations__["val"] = t + + last = converters[-1] + if not PY_3_11_PLUS and isinstance(last, Converter): + last = last.__call__ + + # Get return type from last converter. + rt = _AnnotationExtractor(last).get_return_type() + if rt: + pipe_converter.__annotations__["return"] = rt + + if return_instance: + return Converter(pipe_converter, takes_self=True, takes_field=True) + return pipe_converter diff --git a/lib/python3.10/site-packages/attr/_next_gen.py b/lib/python3.10/site-packages/attr/_next_gen.py new file mode 100644 index 0000000000000000000000000000000000000000..9290664b2dca9285855a4c4c38bb09ca9e616f69 --- /dev/null +++ b/lib/python3.10/site-packages/attr/_next_gen.py @@ -0,0 +1,623 @@ +# SPDX-License-Identifier: MIT + +""" +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. +""" + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + _DEFAULT_ON_SETATTR, + NOTHING, + _frozen_setattrs, + attrib, + attrs, +) +from .exceptions import UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + unsafe_hash=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, +): + r""" + A class decorator that adds :term:`dunder methods` according to + :term:`fields ` specified using :doc:`type annotations `, + `field()` calls, or the *these* argument. + + Since *attrs* patches or replaces an existing class, you cannot use + `object.__init_subclass__` with *attrs* classes, because it runs too early. + As a replacement, you can define ``__attrs_init_subclass__`` on your class. + It will be called by *attrs* classes that subclass it after they're + created. See also :ref:`init-subclass`. + + Args: + slots (bool): + Create a :term:`slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, + so we encourage you to read the :term:`glossary entry `. + + auto_detect (bool): + Instead of setting the *init*, *repr*, *eq*, and *hash* arguments + explicitly, assume they are set to True **unless any** of the + involved methods for one of the arguments is implemented in the + *current* class (meaning, it is *not* inherited from some base + class). + + So, for example by implementing ``__eq__`` on a class yourself, + *attrs* will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a + sensible ``__ne__`` by default, so it *should* be enough to only + implement ``__eq__`` in most cases). + + Passing True or False` to *init*, *repr*, *eq*, or *hash* + overrides whatever *auto_detect* would determine. + + auto_exc (bool): + If the class subclasses `BaseException` (which implicitly includes + any subclass of any exception), the following happens to behave + like a well-behaved Python exception class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids [#]_ , + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the + ``args`` attribute, + - the value of *str* is ignored leaving ``__str__`` to base + classes. + + .. [#] + Note that *attrs* will *not* remove existing implementations of + ``__hash__`` or the equality methods. It just won't add own + ones. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + A callable that is run whenever the user attempts to set an + attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same + arguments as validators: the instance, the attribute that is being + modified, and the new value. + + If no exception is raised, the attribute is set to the return value + of the callable. + + If a list of callables is passed, they're automatically wrapped in + an `attrs.setters.pipe`. + + If left None, the default behavior is to run converters and + validators whenever an attribute is set. + + init (bool): + Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name, + unless an alias is set on the attribute. + + .. seealso:: + `init` shows advanced ways to customize the generated + ``__init__`` method, including executing code before and after. + + repr(bool): + Create a ``__repr__`` method with a human readable representation + of *attrs* attributes. + + str (bool): + Create a ``__str__`` method that is identical to ``__repr__``. This + is usually not necessary except for `Exception`\ s. + + eq (bool | None): + If True or None (default), add ``__eq__`` and ``__ne__`` methods + that check two instances for equality. + + .. seealso:: + `comparison` describes how to customize the comparison behavior + going as far comparing NumPy arrays. + + order (bool | None): + If True, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` + methods that behave like *eq* above and allow instances to be + ordered. + + They compare the instances as if they were tuples of their *attrs* + attributes if and only if the types of both classes are + *identical*. + + If `None` mirror value of *eq*. + + .. seealso:: `comparison` + + unsafe_hash (bool | None): + If None (default), the ``__hash__`` method is generated according + how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for + you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set + to None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning + the ``__hash__`` method of the base class will be used. If the + base class is `object`, this means it will fall back to id-based + hashing. + + Although not recommended, you can decide for yourself and force + *attrs* to create one (for example, if the class is immutable even + though you didn't freeze it programmatically) by passing True or + not. Both of these cases are rather special and should be used + carefully. + + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ behavior + `_ for more + details. + + hash (bool | None): + Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence. + + cache_hash (bool): + Ensure that the object's hash code is computed only once and stored + on the object. If this is set to True, hashing must be either + explicitly or implicitly enabled for this class. If the hash code + is cached, avoid any reassignments of fields involved in hash code + computation or mutations of the objects those fields point to after + object creation. If such changes occur, the behavior of the + object's hash code is undefined. + + frozen (bool): + Make instances immutable after initialization. If someone attempts + to modify a frozen instance, `attrs.exceptions.FrozenInstanceError` + is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` + method on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other + words: ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You + can circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + kw_only (bool): + Make all attributes keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). + + weakref_slot (bool): + Make instances weak-referenceable. This has no effect unless + *slots* is True. + + field_transformer (~typing.Callable | None): + A function that is called with the original class object and all + fields right before *attrs* finalizes the class. You can use this, + for example, to automatically add converters or validators to + fields based on their types. + + .. seealso:: `transform-fields` + + match_args (bool): + If True (default), set ``__match_args__`` on the class to support + :pep:`634` (*Structural Pattern Matching*). It is a tuple of all + non-keyword-only ``__init__`` parameter names on Python 3.10 and + later. Ignored on older Python versions. + + collect_by_mro (bool): + If True, *attrs* collects attributes from base classes correctly + according to the `method resolution order + `_. If False, *attrs* + will mimic the (wrong) behavior of `dataclasses` and :pep:`681`. + + See also `issue #428 + `_. + + getstate_setstate (bool | None): + .. note:: + + This is usually only interesting for slotted classes and you + should probably just set *auto_detect* to True. + + If True, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left None, it's True by default for slotted classes + and False for dict classes. + + If *auto_detect* is True, and *getstate_setstate* is left None, and + **either** ``__getstate__`` or ``__setstate__`` is detected + directly on the class (meaning: not inherited), it is set to False + (this is usually what you want). + + auto_attribs (bool | None): + If True, look at type annotations to determine which attributes to + use, like `dataclasses`. If False, it will only look for explicit + :func:`field` class attributes, like classic *attrs*. + + If left None, it will guess: + + 1. If any attributes are annotated and no unannotated + `attrs.field`\ s are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.field`\ s. + + If *attrs* decides to look at type annotations, **all** fields + **must** be annotated. If *attrs* encounters a field that is set to + a :func:`field` / `attr.ib` but lacks a type annotation, an + `attrs.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = field(...)`` if you don't want to set a + type. + + .. warning:: + + For features that use the attribute name to create decorators + (for example, :ref:`validators `), you still *must* + assign :func:`field` / `attr.ib` to them. Otherwise Python will + either not find the name or try to use the default value to + call, for example, ``validator`` on it. + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `field()` are **ignored**. + + these (dict[str, object]): + A dictionary of name to the (private) return value of `field()` + mappings. This is useful to avoid the definition of your attributes + within the class body because you can't (for example, if you want + to add ``__repr__`` methods to Django models) or don't want to. + + If *these* is not `None`, *attrs* will *not* search the class body + for attributes and will *not* remove any attributes from it. + + The order is deduced from the order of the attributes inside + *these*. + + Arguably, this is a rather obscure feature. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionadded:: 24.3.0 + Unless already present, a ``__replace__`` method is automatically + created for `copy.replace` (Python 3.13+ only). + + .. note:: + + The main differences to the classic `attr.s` are: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - Converters and validators run when attributes are set by default -- + if *frozen* is `False`. + - *slots=True* + + Usually, this has only upsides and few visible effects in everyday + programming. But it *can* lead to some surprising behaviors, so + please make sure to read :term:`slotted classes`. + + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - Some options that were only relevant on Python 2 or were kept around + for backwards-compatibility have been removed. + + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + unsafe_hash=unsafe_hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _DEFAULT_ON_SETATTR + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=False, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new :term:`field` / :term:`attribute` on a class. + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attrs.define` (or similar)! + + Args: + default: + A value that is used if an *attrs*-generated ``__init__`` is used + and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will + be used to construct a new value (useful for mutable data types + like lists or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a + value *must* be supplied when instantiating; otherwise a + `TypeError` will be raised. + + .. seealso:: `defaults` + + factory (~typing.Callable): + Syntactic sugar for ``default=attr.Factory(factory)``. + + validator (~typing.Callable | list[~typing.Callable]): + Callable that is called by *attrs*-generated ``__init__`` methods + after the instance has been initialized. They receive the + initialized instance, the :func:`~attrs.Attribute`, and the passed + value. + + The return value is *not* inspected so the validator has to throw + an exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. + + The validator can also be set using decorator notation as shown + below. + + .. seealso:: :ref:`validators` + + repr (bool | ~typing.Callable): + Include this attribute in the generated ``__repr__`` method. If + True, include the attribute; if False, omit it. By default, the + built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value + and returns a string. Note that the resulting string is used as-is, + which means it will be used directly *instead* of calling + ``repr()`` (the default). + + eq (bool | ~typing.Callable): + If True (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + callable that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` + + order (bool | ~typing.Callable): + If True (default), include this attributes in the generated + ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a callable that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` + + hash (bool | None): + Include this attribute in the generated ``__hash__`` method. If + None (default), mirror *eq*'s value. This is the correct behavior + according the Python spec. Setting this value to anything else + than None is *discouraged*. + + .. seealso:: `hashing` + + init (bool): + Include this attribute in the generated ``__init__`` method. + + It is possible to set this to False and set a default value. In + that case this attributed is unconditionally initialized with the + specified default value or factory. + + .. seealso:: `init` + + converter (typing.Callable | Converter): + A callable that is called by *attrs*-generated ``__init__`` methods + to convert attribute's value to the desired format. + + If a vanilla callable is passed, it is given the passed-in value as + the only positional argument. It is possible to receive additional + arguments by wrapping the callable in a `Converter`. + + Either way, the returned value will be used as the new value of the + attribute. The value is converted before being passed to the + validator, if any. + + .. seealso:: :ref:`converters` + + metadata (dict | None): + An arbitrary mapping, to be used by third-party code. + + .. seealso:: `extending-metadata`. + + type (type): + The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). + This argument is provided for backwards-compatibility and for usage + with `make_class`. Regardless of the approach used, the type will + be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking `. + + kw_only (bool): + Make this attribute keyword-only in the generated ``__init__`` (if + ``init`` is False, this parameter is ignored). + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + Allows to overwrite the *on_setattr* setting from `attr.s`. If left + None, the *on_setattr* value from `attr.s` is used. Set to + `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `define()`. + + alias (str | None): + Override this attribute's parameter name in the generated + ``__init__`` method. If left None, default to ``name`` stripped + of leading underscores. See `private-attributes`. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. + + .. seealso:: + + `attr.ib` + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + type=type, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + alias=alias, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) diff --git a/lib/python3.10/site-packages/attr/_typing_compat.pyi b/lib/python3.10/site-packages/attr/_typing_compat.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ca7b71e906a28f88726bbd342fdfe636af0281e7 --- /dev/null +++ b/lib/python3.10/site-packages/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/lib/python3.10/site-packages/attr/_version_info.py b/lib/python3.10/site-packages/attr/_version_info.py new file mode 100644 index 0000000000000000000000000000000000000000..51a1312f9759f21063caea779a62882d7f7c86ae --- /dev/null +++ b/lib/python3.10/site-packages/attr/_version_info.py @@ -0,0 +1,86 @@ +# SPDX-License-Identifier: MIT + + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo: + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them diff --git a/lib/python3.10/site-packages/attr/_version_info.pyi b/lib/python3.10/site-packages/attr/_version_info.pyi new file mode 100644 index 0000000000000000000000000000000000000000..45ced086337783c4b73b26cd17d2c1c260e24029 --- /dev/null +++ b/lib/python3.10/site-packages/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/lib/python3.10/site-packages/attr/converters.py b/lib/python3.10/site-packages/attr/converters.py new file mode 100644 index 0000000000000000000000000000000000000000..0a79deef04282fb33a42f6aca59563d49e70d4cb --- /dev/null +++ b/lib/python3.10/site-packages/attr/converters.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +import typing + +from ._compat import _AnnotationExtractor +from ._make import NOTHING, Converter, Factory, pipe + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to `None`. + + Type annotations will be inferred from the wrapped converter's, if it has + any. + + Args: + converter (typing.Callable): + the converter that is used for non-`None` values. + + .. versionadded:: 17.1.0 + """ + + if isinstance(converter, Converter): + + def optional_converter(val, inst, field): + if val is None: + return None + return converter(val, inst, field) + + else: + + def optional_converter(val): + if val is None: + return None + return converter(val) + + xtr = _AnnotationExtractor(converter) + + t = xtr.get_first_param_type() + if t: + optional_converter.__annotations__["val"] = typing.Optional[t] + + rt = xtr.get_return_type() + if rt: + optional_converter.__annotations__["return"] = typing.Optional[rt] + + if isinstance(converter, Converter): + return Converter(optional_converter, takes_self=True, takes_field=True) + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace `None` values by *default* or the result + of *factory*. + + Args: + default: + Value to be used if `None` is passed. Passing an instance of + `attrs.Factory` is supported, however the ``takes_self`` option is + *not*. + + factory (typing.Callable): + A callable that takes no parameters whose result is used if `None` + is passed. + + Raises: + TypeError: If **neither** *default* or *factory* is passed. + + TypeError: If **both** *default* and *factory* are passed. + + ValueError: + If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) + + if default is not NOTHING and factory is not None: + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (for example, from environment variables) to real + booleans. + + Values mapping to `True`: + + - ``True`` + - ``"true"`` / ``"t"`` + - ``"yes"`` / ``"y"`` + - ``"on"`` + - ``"1"`` + - ``1`` + + Values mapping to `False`: + + - ``False`` + - ``"false"`` / ``"f"`` + - ``"no"`` / ``"n"`` + - ``"off"`` + - ``"0"`` + - ``0`` + + Raises: + ValueError: For any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + + if val in (True, "true", "t", "yes", "y", "on", "1", 1): + return True + if val in (False, "false", "f", "no", "n", "off", "0", 0): + return False + + msg = f"Cannot convert value to bool: {val!r}" + raise ValueError(msg) diff --git a/lib/python3.10/site-packages/attr/converters.pyi b/lib/python3.10/site-packages/attr/converters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..12bd0c4f17bdc60fb8904598af0a3d56d5874a9e --- /dev/null +++ b/lib/python3.10/site-packages/attr/converters.pyi @@ -0,0 +1,19 @@ +from typing import Callable, Any, overload + +from attrs import _ConverterType, _CallableConverterType + +@overload +def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ... +@overload +def pipe(*validators: _ConverterType) -> _ConverterType: ... +@overload +def optional(converter: _CallableConverterType) -> _CallableConverterType: ... +@overload +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: Any) -> _CallableConverterType: ... +@overload +def default_if_none( + *, factory: Callable[[], Any] +) -> _CallableConverterType: ... +def to_bool(val: str | int | bool) -> bool: ... diff --git a/lib/python3.10/site-packages/attr/exceptions.py b/lib/python3.10/site-packages/attr/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..3b7abb8154108aa1d0ae52fa9ee8e489f05b5563 --- /dev/null +++ b/lib/python3.10/site-packages/attr/exceptions.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import ClassVar + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + msg = "can't set attribute" + args: ClassVar[tuple[str]] = [msg] + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An *attrs* function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-*attrs* class has been passed into an *attrs* function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set when defining the field and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has a field without a type annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an *attrs* feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A field requiring a callable has been set with a value that is not + callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/lib/python3.10/site-packages/attr/exceptions.pyi b/lib/python3.10/site-packages/attr/exceptions.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f2680118b404db8f5227d04d27e8439331341c4d --- /dev/null +++ b/lib/python3.10/site-packages/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/lib/python3.10/site-packages/attr/filters.py b/lib/python3.10/site-packages/attr/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..689b1705a60ff110d6077bab996f8b4588e55b82 --- /dev/null +++ b/lib/python3.10/site-packages/attr/filters.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attrs.asdict` and `attrs.astuple`. +""" + +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Create a filter that only allows *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to include. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.1.0 Accept strings with field names. + """ + cls, names, attrs = _split_what(what) + + def include_(attribute, value): + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return include_ + + +def exclude(*what): + """ + Create a filter that does **not** allow *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to exclude. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.3.0 Accept field name string as input argument + """ + cls, names, attrs = _split_what(what) + + def exclude_(attribute, value): + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return exclude_ diff --git a/lib/python3.10/site-packages/attr/filters.pyi b/lib/python3.10/site-packages/attr/filters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..974abdcdb51152393d9c9e460c21aa025c45880c --- /dev/null +++ b/lib/python3.10/site-packages/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any + +from . import Attribute, _FilterType + +def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... +def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... diff --git a/lib/python3.10/site-packages/attr/py.typed b/lib/python3.10/site-packages/attr/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/attr/setters.py b/lib/python3.10/site-packages/attr/setters.py new file mode 100644 index 0000000000000000000000000000000000000000..78b08398a6713fc5fa827c2dc853e0d05de743c4 --- /dev/null +++ b/lib/python3.10/site-packages/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + # This can be removed once we drop 3.8 and use attrs.Converter instead. + from ._make import Converter + + if not isinstance(c, Converter): + return c(new_value) + + return c(new_value, instance, attrib) + + return new_value + + +# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. +# Sphinx's autodata stopped working, so the docstring is inlined in the API +# docs. +NO_OP = object() diff --git a/lib/python3.10/site-packages/attr/setters.pyi b/lib/python3.10/site-packages/attr/setters.pyi new file mode 100644 index 0000000000000000000000000000000000000000..73abf36e7d5b0f5f56e7fddeee716824c1c60d58 --- /dev/null +++ b/lib/python3.10/site-packages/attr/setters.pyi @@ -0,0 +1,20 @@ +from typing import Any, NewType, NoReturn, TypeVar + +from . import Attribute +from attrs import _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/lib/python3.10/site-packages/attr/validators.py b/lib/python3.10/site-packages/attr/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..e7b75525022a9d98f8153f17a0fed103b24c743b --- /dev/null +++ b/lib/python3.10/site-packages/attr/validators.py @@ -0,0 +1,710 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +import operator +import re + +from contextlib import contextmanager +from re import Pattern + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "min_len", + "not_", + "optional", + "or_", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + Args: + disabled (bool): If `True`, disable running all validators. + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + Returns: + bool:`True` if validators are currently disabled. + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(False) + try: + yield + finally: + set_run_validators(True) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InstanceOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type]): The type to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator: + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" + raise ValueError( + msg, + attr, + self.pattern, + value, + ) + + def __repr__(self): + return f"" + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called with a + string that doesn't match *regex*. + + Args: + regex (str, re.Pattern): + A regex string or precompiled pattern to match against + + flags (int): + Flags that will be passed to the underlying re function (default 0) + + func (typing.Callable): + Which underlying `re` function to call. Valid options are + `re.fullmatch`, `re.search`, and `re.match`; the default `None` + means `re.fullmatch`. For performance reasons, the pattern is + always precompiled using `re.compile`. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + valid_funcs = (re.fullmatch, None, re.search, re.match) + if func not in valid_funcs: + msg = "'func' must be one of {}.".format( + ", ".join( + sorted((e and e.__name__) or "None" for e in set(valid_funcs)) + ) + ) + raise ValueError(msg) + + if isinstance(regex, Pattern): + if flags: + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + else: + match_func = pattern.fullmatch + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OptionalValidator: + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return f"" + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to `None` in addition to satisfying the requirements of + the sub-validator. + + Args: + validator + (typing.Callable | tuple[typing.Callable] | list[typing.Callable]): + A validator (or validators) that is used for non-`None` values. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. + """ + if isinstance(validator, (list, tuple)): + return _OptionalValidator(_AndValidator(validator)) + + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InValidator: + options = attrib() + _original_options = attrib(hash=False) + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})" + raise ValueError( + msg, + attr, + self._original_options, + value, + ) + + def __repr__(self): + return f"" + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called with a + value that does not belong in the *options* provided. + + The check is performed using ``value in options``, so *options* has to + support that operation. + + To keep the validator hashable, dicts, lists, and sets are transparently + transformed into a `tuple`. + + Args: + options: Allowed options. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected options, and the value it got. + + .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. + .. versionchanged:: 24.1.0 + *options* that are a list, dict, or a set are now transformed into a + tuple to keep the validator hashable. + """ + repr_options = options + if isinstance(options, (list, dict, set)): + options = tuple(options) + + return _InValidator(options, repr_options) + + +@attrs(repr=False, slots=False, unsafe_hash=True) +class _IsCallableValidator: + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attrs.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute that is + not callable. + + .. versionadded:: 19.1.0 + + Raises: + attrs.exceptions.NotCallableError: + With a human readable error message containing the attribute + (`attrs.Attribute`) name, and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepIterable: + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else f" {self.iterable_validator!r}" + ) + return ( + f"" + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + Args: + member_validator: Validator to apply to iterable members. + + iterable_validator: + Validator to apply to iterable itself (optional). + + Raises + TypeError: if any sub-validators fail + + .. versionadded:: 19.1.0 + """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepMapping: + key_validator = attrib(validator=is_callable()) + value_validator = attrib(validator=is_callable()) + mapping_validator = attrib(default=None, validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + self.key_validator(inst, attr, key) + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return f"" + + +def deep_mapping(key_validator, value_validator, mapping_validator=None): + """ + A validator that performs deep validation of a dictionary. + + Args: + key_validator: Validator to apply to dictionary keys. + + value_validator: Validator to apply to dictionary values. + + mapping_validator: + Validator to apply to top-level mapping attribute (optional). + + .. versionadded:: 19.1.0 + + Raises: + TypeError: if any sub-validators fail + """ + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator: + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number larger or equal to *val*. + + The validator uses `operator.lt` to compare the values. + + Args: + val: Exclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number greater than *val*. + + The validator uses `operator.le` to compare the values. + + Args: + val: Inclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller than *val*. + + The validator uses `operator.ge` to compare the values. + + Args: + val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller or equal to *val*. + + The validator uses `operator.ge` to compare the values. + + Args: + val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator: + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + Args: + length (int): Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator: + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + Args: + length (int): Minimum length of the string or iterable + + .. versionadded:: 22.1.0 + """ + return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type, ...]): The type(s) to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return f"" + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + Args: + validator: A validator to be logically inverted. + + msg (str): + Message to raise if validator fails. Formatted with keys + ``exc_types`` and ``validator``. + + exc_types (tuple[type, ...]): + Exception type(s) to capture. Other types raised by child + validators will not be intercepted and pass through. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the validator that failed to raise an + exception, the value it got, and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OrValidator: + validators = attrib() + + def __call__(self, inst, attr, value): + for v in self.validators: + try: + v(inst, attr, value) + except Exception: # noqa: BLE001, PERF203, S112 + continue + else: + return + + msg = f"None of {self.validators!r} satisfied for value {value!r}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def or_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators until one of them is + satisfied. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + Raises: + ValueError: + If no validator is satisfied. Raised with a human-readable error + message listing all the wrapped validators and the value that + failed all of them. + + .. versionadded:: 24.1.0 + """ + vals = [] + for v in validators: + vals.extend(v.validators if isinstance(v, _OrValidator) else [v]) + + return _OrValidator(tuple(vals)) diff --git a/lib/python3.10/site-packages/attr/validators.pyi b/lib/python3.10/site-packages/attr/validators.pyi new file mode 100644 index 0000000000000000000000000000000000000000..a0fdda7c8773f791103938fca0d4b448859aff1f --- /dev/null +++ b/lib/python3.10/site-packages/attr/validators.pyi @@ -0,0 +1,86 @@ +from types import UnionType +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + Mapping, + Match, + Pattern, + TypeVar, + overload, +) + +from attrs import _ValidatorType +from attrs import _ValidatorArgType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2]], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2], type[_T3]], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... +@overload +def instance_of(type: UnionType) -> _ValidatorType[Any]: ... +def optional( + validator: ( + _ValidatorType[_T] + | list[_ValidatorType[_T]] + | tuple[_ValidatorType[_T]] + ), +) -> _ValidatorType[_T | None]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Pattern[AnyStr] | AnyStr, + flags: int = ..., + func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorArgType[_T], + iterable_validator: _ValidatorType[_I] | None = ..., +) -> _ValidatorType[_I]: ... +def deep_mapping( + key_validator: _ValidatorType[_K], + value_validator: _ValidatorType[_V], + mapping_validator: _ValidatorType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: str | None = None, + exc_types: type[Exception] | Iterable[type[Exception]] = ..., +) -> _ValidatorType[_T]: ... +def or_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... diff --git a/lib/python3.10/site-packages/attrdict/__init__.py b/lib/python3.10/site-packages/attrdict/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3a71a5301bfbac31e10799d6a4af0dff342cd573 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/__init__.py @@ -0,0 +1,10 @@ +""" +attrdict contains several mapping objects that allow access to their +keys as attributes. +""" +from attrdict.mapping import AttrMap +from attrdict.dictionary import AttrDict +from attrdict.default import AttrDefault + + +__all__ = ['AttrMap', 'AttrDict', 'AttrDefault'] diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4b55ffb9d173ffc860194a2b8e173ec902a8e0c0 Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/default.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/default.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c2d58f5bb18b4edb8e57f55f100bf58d58ba7ca Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/default.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/dictionary.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/dictionary.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d855ab5e1e19cfac5828cb03e1af724a56d25d64 Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/dictionary.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/mapping.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/mapping.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7af74f80e5369b7fe2538f55d5edcac4ddd49834 Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/mapping.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/merge.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/merge.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6027b4e4b6be840bfa6097a545ef010bddfb45d4 Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/merge.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/__pycache__/mixins.cpython-310.pyc b/lib/python3.10/site-packages/attrdict/__pycache__/mixins.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18c00b5c5d1b84053eefb20ec4a3af6cb97a9b9a Binary files /dev/null and b/lib/python3.10/site-packages/attrdict/__pycache__/mixins.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrdict/default.py b/lib/python3.10/site-packages/attrdict/default.py new file mode 100644 index 0000000000000000000000000000000000000000..eeb3f31de92aafc5754e2c2a7903324752fd5b7b --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/default.py @@ -0,0 +1,133 @@ +""" +A subclass of MutableAttr that has defaultdict support. +""" +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + +import six + +from attrdict.mixins import MutableAttr + + +__all__ = ['AttrDefault'] + + +class AttrDefault(MutableAttr): + """ + An implementation of MutableAttr with defaultdict support + """ + def __init__(self, default_factory=None, items=None, sequence_type=tuple, + pass_key=False): + if items is None: + items = {} + elif not isinstance(items, Mapping): + items = dict(items) + + self._setattr('_default_factory', default_factory) + self._setattr('_mapping', items) + self._setattr('_sequence_type', sequence_type) + self._setattr('_pass_key', pass_key) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for a AttrDefault instance + """ + return self._sequence_type, self._default_factory, self._pass_key + + def __getitem__(self, key): + """ + Access a value associated with a key. + + Note: values returned will not be wrapped, even if recursive + is True. + """ + if key in self._mapping: + return self._mapping[key] + elif self._default_factory is not None: + return self.__missing__(key) + + raise KeyError(key) + + def __setitem__(self, key, value): + """ + Add a key-value pair to the instance. + """ + self._mapping[key] = value + + def __delitem__(self, key): + """ + Delete a key-value pair + """ + del self._mapping[key] + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + Iterated through the keys. + """ + return iter(self._mapping) + + def __missing__(self, key): + """ + Add a missing element. + """ + if self._pass_key: + self[key] = value = self._default_factory(key) + else: + self[key] = value = self._default_factory() + + return value + + def __repr__(self): + """ + Return a string representation of the object. + """ + return six.u( + "AttrDefault({default_factory}, {pass_key}, {mapping})" + ).format( + default_factory=repr(self._default_factory), + pass_key=repr(self._pass_key), + mapping=repr(self._mapping), + ) + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self._default_factory, + self._mapping, + self._sequence_type, + self._pass_key, + self._allow_invalid_attributes, + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + (default_factory, mapping, sequence_type, pass_key, + allow_invalid_attributes) = state + + self._setattr('_default_factory', default_factory) + self._setattr('_mapping', mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_pass_key', pass_key) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + sequence_type, default_factory, pass_key = configuration + return cls(default_factory, mapping, sequence_type=sequence_type, + pass_key=pass_key) diff --git a/lib/python3.10/site-packages/attrdict/dictionary.py b/lib/python3.10/site-packages/attrdict/dictionary.py new file mode 100644 index 0000000000000000000000000000000000000000..874e4a4ede8b39867c7fb9a05531a56a6a37db66 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/dictionary.py @@ -0,0 +1,60 @@ +""" +A dict that implements MutableAttr. +""" +from attrdict.mixins import MutableAttr + +import six + + +__all__ = ['AttrDict'] + + +class AttrDict(dict, MutableAttr): + """ + A dict that implements MutableAttr. + """ + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + + self._setattr('_sequence_type', tuple) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for an attrmap instance. + """ + return self._sequence_type + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self.copy(), + self._sequence_type, + self._allow_invalid_attributes + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + mapping, sequence_type, allow_invalid_attributes = state + self.update(mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + def __repr__(self): + return six.u('AttrDict({contents})').format( + contents=super(AttrDict, self).__repr__() + ) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + attr = cls(mapping) + attr._setattr('_sequence_type', configuration) + + return attr diff --git a/lib/python3.10/site-packages/attrdict/mapping.py b/lib/python3.10/site-packages/attrdict/mapping.py new file mode 100644 index 0000000000000000000000000000000000000000..6e88c1ee8f5f32848f7bd1618f2a6b21777acc18 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/mapping.py @@ -0,0 +1,100 @@ +""" +An implementation of MutableAttr. +""" +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + +import six + +from attrdict.mixins import MutableAttr + + +__all__ = ['AttrMap'] + + +class AttrMap(MutableAttr): + """ + An implementation of MutableAttr. + """ + def __init__(self, items=None, sequence_type=tuple): + if items is None: + items = {} + elif not isinstance(items, Mapping): + items = dict(items) + + self._setattr('_sequence_type', sequence_type) + self._setattr('_mapping', items) + self._setattr('_allow_invalid_attributes', False) + + def _configuration(self): + """ + The configuration for an attrmap instance. + """ + return self._sequence_type + + def __getitem__(self, key): + """ + Access a value associated with a key. + """ + return self._mapping[key] + + def __setitem__(self, key, value): + """ + Add a key-value pair to the instance. + """ + self._mapping[key] = value + + def __delitem__(self, key): + """ + Delete a key-value pair + """ + del self._mapping[key] + + def __len__(self): + """ + Check the length of the mapping. + """ + return len(self._mapping) + + def __iter__(self): + """ + Iterated through the keys. + """ + return iter(self._mapping) + + def __repr__(self): + """ + Return a string representation of the object. + """ + # sequence type seems like more trouble than it is worth. + # If people want full serialization, they can pickle, and in + # 99% of cases, sequence_type won't change anyway + return six.u("AttrMap({mapping})").format(mapping=repr(self._mapping)) + + def __getstate__(self): + """ + Serialize the object. + """ + return ( + self._mapping, + self._sequence_type, + self._allow_invalid_attributes + ) + + def __setstate__(self, state): + """ + Deserialize the object. + """ + mapping, sequence_type, allow_invalid_attributes = state + self._setattr('_mapping', mapping) + self._setattr('_sequence_type', sequence_type) + self._setattr('_allow_invalid_attributes', allow_invalid_attributes) + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor. + """ + return cls(mapping, sequence_type=configuration) diff --git a/lib/python3.10/site-packages/attrdict/merge.py b/lib/python3.10/site-packages/attrdict/merge.py new file mode 100644 index 0000000000000000000000000000000000000000..fdd798a1b50bcd1056fee90d8f055ea66693460a --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/merge.py @@ -0,0 +1,47 @@ +""" +A right-favoring Mapping merge. +""" +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + + +__all__ = ['merge'] + + +def merge(left, right): + """ + Merge two mappings objects together, combining overlapping Mappings, + and favoring right-values + + left: The left Mapping object. + right: The right (favored) Mapping object. + + NOTE: This is not commutative (merge(a,b) != merge(b,a)). + """ + merged = {} + + left_keys = frozenset(left) + right_keys = frozenset(right) + + # Items only in the left Mapping + for key in left_keys - right_keys: + merged[key] = left[key] + + # Items only in the right Mapping + for key in right_keys - left_keys: + merged[key] = right[key] + + # in both + for key in left_keys & right_keys: + left_value = left[key] + right_value = right[key] + + if (isinstance(left_value, Mapping) and + isinstance(right_value, Mapping)): # recursive merge + merged[key] = merge(left_value, right_value) + else: # overwrite with right value + merged[key] = right_value + + return merged diff --git a/lib/python3.10/site-packages/attrdict/mixins.py b/lib/python3.10/site-packages/attrdict/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..f46719e9a07472973f36fbc6c738ee4c1e043d3a --- /dev/null +++ b/lib/python3.10/site-packages/attrdict/mixins.py @@ -0,0 +1,212 @@ +""" +Mixin Classes for Attr-support. +""" +from abc import ABCMeta, abstractmethod +try: + from collections.abc import Mapping, MutableMapping, Sequence +except: + from collections import Mapping, MutableMapping, Sequence +import re + +import six + +from attrdict.merge import merge + + +__all__ = ['Attr', 'MutableAttr'] + + +@six.add_metaclass(ABCMeta) +class Attr(Mapping): + """ + A mixin class for a mapping that allows for attribute-style access + of values. + + A key may be used as an attribute if: + * It is a string + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) + * The key doesn't overlap with any class attributes (for Attr, + those would be 'get', 'items', 'keys', 'values', 'mro', and + 'register'). + + If a values which is accessed as an attribute is a Sequence-type + (and is not a string/bytes), it will be converted to a + _sequence_type with any mappings within it converted to Attrs. + + NOTE: This means that if _sequence_type is not None, then a + sequence accessed as an attribute will be a different object + than if accessed as an attribute than if it is accessed as an + item. + """ + @abstractmethod + def _configuration(self): + """ + All required state for building a new instance with the same + settings as the current object. + """ + + @classmethod + def _constructor(cls, mapping, configuration): + """ + A standardized constructor used internally by Attr. + + mapping: A mapping of key-value pairs. It is HIGHLY recommended + that you use this as the internal key-value pair mapping, as + that will allow nested assignment (e.g., attr.foo.bar = baz) + configuration: The return value of Attr._configuration + """ + raise NotImplementedError("You need to implement this") + + def __call__(self, key): + """ + Dynamically access a key-value pair. + + key: A key associated with a value in the mapping. + + This differs from __getitem__, because it returns a new instance + of an Attr (if the value is a Mapping object). + """ + if key not in self: + raise AttributeError( + "'{cls} instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + return self._build(self[key]) + + def __getattr__(self, key): + """ + Access an item as an attribute. + """ + if key not in self or not self._valid_name(key): + raise AttributeError( + "'{cls}' instance has no attribute '{name}'".format( + cls=self.__class__.__name__, name=key + ) + ) + + return self._build(self[key]) + + def __add__(self, other): + """ + Add a mapping to this Attr, creating a new, merged Attr. + + other: A mapping. + + NOTE: Addition is not commutative. a + b != b + a. + """ + if not isinstance(other, Mapping): + return NotImplemented + + return self._constructor(merge(self, other), self._configuration()) + + def __radd__(self, other): + """ + Add this Attr to a mapping, creating a new, merged Attr. + + other: A mapping. + + NOTE: Addition is not commutative. a + b != b + a. + """ + if not isinstance(other, Mapping): + return NotImplemented + + return self._constructor(merge(other, self), self._configuration()) + + def _build(self, obj): + """ + Conditionally convert an object to allow for recursive mapping + access. + + obj: An object that was a key-value pair in the mapping. If obj + is a mapping, self._constructor(obj, self._configuration()) + will be called. If obj is a non-string/bytes sequence, and + self._sequence_type is not None, the obj will be converted + to type _sequence_type and build will be called on its + elements. + """ + if isinstance(obj, Mapping): + obj = self._constructor(obj, self._configuration()) + elif (isinstance(obj, Sequence) and + not isinstance(obj, (six.string_types, six.binary_type))): + sequence_type = getattr(self, '_sequence_type', None) + + if sequence_type: + obj = sequence_type(self._build(element) for element in obj) + + return obj + + @classmethod + def _valid_name(cls, key): + """ + Check whether a key is a valid attribute name. + + A key may be used as an attribute if: + * It is a string + * It matches /^[A-Za-z][A-Za-z0-9_]*$/ (i.e., a public attribute) + * The key doesn't overlap with any class attributes (for Attr, + those would be 'get', 'items', 'keys', 'values', 'mro', and + 'register'). + """ + return ( + isinstance(key, six.string_types) and + re.match('^[A-Za-z][A-Za-z0-9_]*$', key) and + not hasattr(cls, key) + ) + + +@six.add_metaclass(ABCMeta) +class MutableAttr(Attr, MutableMapping): + """ + A mixin class for a mapping that allows for attribute-style access + of values. + """ + def _setattr(self, key, value): + """ + Add an attribute to the object, without attempting to add it as + a key to the mapping. + """ + super(MutableAttr, self).__setattr__(key, value) + + def __setattr__(self, key, value): + """ + Add an attribute. + + key: The name of the attribute + value: The attributes contents + """ + if self._valid_name(key): + self[key] = value + elif getattr(self, '_allow_invalid_attributes', True): + super(MutableAttr, self).__setattr__(key, value) + else: + raise TypeError( + "'{cls}' does not allow attribute creation.".format( + cls=self.__class__.__name__ + ) + ) + + def _delattr(self, key): + """ + Delete an attribute from the object, without attempting to + remove it from the mapping. + """ + super(MutableAttr, self).__delattr__(key) + + def __delattr__(self, key, force=False): + """ + Delete an attribute. + + key: The name of the attribute + """ + if self._valid_name(key): + del self[key] + elif getattr(self, '_allow_invalid_attributes', True): + super(MutableAttr, self).__delattr__(key) + else: + raise TypeError( + "'{cls}' does not allow attribute deletion.".format( + cls=self.__class__.__name__ + ) + ) diff --git a/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/INSTALLER b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/LICENSE.txt b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..26dcf915d3c11ecb1e9bd86b8341cbf9e4754ab2 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013 Brendan Curran-Johnson + +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/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/METADATA b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..b07bd44cb5cdf1e32677acbc3f7fa1758a41be37 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/METADATA @@ -0,0 +1,217 @@ +Metadata-Version: 2.1 +Name: attrdict3 +Version: 2.0.2 +Summary: A dict with attribute-style access +Home-page: https://github.com/pirofti/AttrDict3 +Author: Paul Irofti +Author-email: paul@irofti.net +License: MIT License +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +License-File: LICENSE.txt +Requires-Dist: six + +======== +AttrDict +======== +.. image:: https://travis-ci.org/bcj/AttrDict.svg?branch=master + :target: https://travis-ci.org/bcj/AttrDict?branch=master +.. image:: https://coveralls.io/repos/bcj/AttrDict/badge.png?branch=master + :target: https://coveralls.io/r/bcj/AttrDict?branch=master + +AttrDict is an MIT-licensed library that provides mapping objects that allow +their elements to be accessed both as keys and as attributes:: + + > from attrdict import AttrDict + > a = AttrDict({'foo': 'bar'}) + > a.foo + 'bar' + > a['foo'] + 'bar' + +Attribute access makes it easy to create convenient, hierarchical settings +objects:: + + with open('settings.yaml') as fileobj: + settings = AttrDict(yaml.safe_load(fileobj)) + + cursor = connect(**settings.db.credentials).cursor() + + cursor.execute("SELECT column FROM table;") + +Installation +============ +AttrDict is in PyPI, so it can be installed directly using:: + + $ pip install attrdict + +Or from Github:: + + $ git clone https://github.com/bcj/AttrDict + $ cd AttrDict + $ python setup.py install + +Basic Usage +=========== +AttrDict comes with three different classes, `AttrMap`, `AttrDict`, and +`AttrDefault`. They are all fairly similar, as they all are MutableMappings ( +read: dictionaries) that allow creating, accessing, and deleting key-value +pairs as attributes. + +Valid Names +----------- +Any key can be used as an attribute as long as: + +#. The key represents a valid attribute (i.e., it is a string comprised only of + alphanumeric characters and underscores that doesn't start with a number) +#. The key represents a public attribute (i.e., it doesn't start with an + underscore). This is done (in part) so that implementation changes between + minor and micro versions don't force major version changes. +#. The key does not shadow a class attribute (e.g., get). + +Attributes vs. Keys +------------------- +There is a minor difference between accessing a value as an attribute vs. +accessing it as a key, is that when a dict is accessed as an attribute, it will +automatically be converted to an Attr object. This allows you to recursively +access keys:: + + > attr = AttrDict({'foo': {'bar': 'baz'}}) + > attr.foo.bar + 'baz' + +Relatedly, by default, sequence types that aren't `bytes`, `str`, or `unicode` +(e.g., lists, tuples) will automatically be converted to tuples, with any +mappings converted to Attrs:: + + > attr = AttrDict({'foo': [{'bar': 'baz'}, {'bar': 'qux'}]}) + > for sub_attr in attr.foo: + > print(sub_attr.foo) + 'baz' + 'qux' + +To get this recursive functionality for keys that cannot be used as attributes, +you can replicate the behavior by calling the Attr object:: + + > attr = AttrDict({1: {'two': 3}}) + > attr(1).two + 3 + +Classes +------- +AttrDict comes with three different objects, `AttrMap`, `AttrDict`, and +`AttrDefault`. + +AttrMap +^^^^^^^ +The most basic implementation. Use this if you want to limit the number of +invalid keys, or otherwise cannot use `AttrDict` + +AttrDict +^^^^^^^^ +An Attr object that subclasses `dict`. You should be able to use this +absolutely anywhere you can use a `dict`. While this is probably the class you +want to use, there are a few caveats that follow from this being a `dict` under +the hood. + +The `copy` method (which returns a shallow copy of the mapping) returns a +`dict` instead of an `AttrDict`. + +Recursive attribute access results in a shallow copy, so recursive assignment +will fail (as you will be writing to a copy of that dictionary):: + + > attr = AttrDict('foo': {}) + > attr.foo.bar = 'baz' + > attr.foo + {} + +Assignment as keys will still work:: + + > attr = AttrDict('foo': {}) + > attr['foo']['bar'] = 'baz' + > attr.foo + {'bar': 'baz'} + +If either of these caveats are deal-breakers, or you don't need your object to +be a `dict`, consider using `AttrMap` instead. + +AttrDefault +^^^^^^^^^^^ +At Attr object that behaves like a `defaultdict`. This allows on-the-fly, +automatic key creation:: + + > attr = AttrDefault(int, {}) + > attr.foo += 1 + > attr.foo + 1 + +AttrDefault also has a `pass_key` option that passes the supplied key to the +`default_factory`:: + + > attr = AttrDefault(sorted, {}, pass_key=True) + > attr.banana + ['a', 'a', 'a', 'b', 'n', 'n'] + +Merging +------- +All three Attr classes can be merged with eachother or other Mappings using the +``+`` operator. For conflicting keys, the right dict's value will be +preferred, but in the case of two dictionary values, they will be +recursively merged:: + + > a = {'foo': 'bar', 'alpha': {'beta': 'a', 'a': 'a'}} + > b = {'lorem': 'ipsum', 'alpha': {'bravo': 'b', 'a': 'b'}} + > AttrDict(a) + b + {'foo': 'bar', 'lorem': 'ipsum', 'alpha': {'beta': 'a', 'bravo': 'b', 'a': 'b'}} + +NOTE: AttrDict's add is not commutative, ``a + b != b + a``:: + + > a = {'foo': 'bar', 'alpha': {'beta': 'b', 'a': 0}} + > b = {'lorem': 'ipsum', 'alpha': {'bravo': 'b', 'a': 1}} + > b + AttrDict(a) + {'foo': 'bar', 'lorem': 'ipsum', 'alpha': {'beta': 'a', 'bravo': 'b', 'a': }} + +Sequences +--------- +By default, items in non-string Sequences (e.g. lists, tuples) will be +converted to AttrDicts:: + + > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}) + > for element in adict.list: + > element.value + 1 + 2 + +This will not occur if you access the AttrDict as a dictionary:: + + > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}) + > for element in adict['list']: + > isinstance(element, AttrDict) + False + False + +To disable this behavior globally, pass the attribute ``recursive=False`` to +the constructor:: + + > adict = AttrDict({'list': [{'value': 1}, {'value': 2}]}, recursive=False) + > for element in adict.list: + > isinstance(element, AttrDict) + False + False + +When merging an AttrDict with another mapping, this behavior will be disabled +if at least one of the merged items is an AttrDict that has set ``recursive`` +to ``False``. + +License +======= +AttrDict is released under a MIT license. + + diff --git a/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/RECORD b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..389eba3732cd2902175d9e2cdf52f96b7cf526fa --- /dev/null +++ b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/RECORD @@ -0,0 +1,20 @@ +attrdict/__init__.py,sha256=fdJfkB3hQK2tcqck7FSmCKjyzmjx29Z3MBQyAev43E0,267 +attrdict/__pycache__/__init__.cpython-310.pyc,, +attrdict/__pycache__/default.cpython-310.pyc,, +attrdict/__pycache__/dictionary.cpython-310.pyc,, +attrdict/__pycache__/mapping.cpython-310.pyc,, +attrdict/__pycache__/merge.cpython-310.pyc,, +attrdict/__pycache__/mixins.cpython-310.pyc,, +attrdict/default.py,sha256=q0ZwjaXdJDsiIQPqERN7NhmT_YlaKK1nvdMA_R6737k,3609 +attrdict/dictionary.py,sha256=EjolfMd-kzn5K009pTx2Mr_O4OCEPEg-57Z_6-Lsixw,1462 +attrdict/mapping.py,sha256=dlA70KhuJ-o_MW72KuGXX7U5x7H4eaekAXCtpdASNdY,2533 +attrdict/merge.py,sha256=4FcP0AOw8mLEPdKXqBwBaeYfBHZJAye-6705mg2hVm4,1152 +attrdict/mixins.py,sha256=Dd4vjL_-2AmoeY64OWL5K6uJsTzuf9ux6PNrDJFIKMY,6707 +attrdict3-2.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +attrdict3-2.0.2.dist-info/LICENSE.txt,sha256=7bXwDR-EXRD9ybjGNRq4IPk_ZEm3aWev8xkme2Fb4k4,1066 +attrdict3-2.0.2.dist-info/METADATA,sha256=0kpZb-or5zgbhdrq_4zx3n8zXmn37LLPwCcPgPBCcrA,6718 +attrdict3-2.0.2.dist-info/RECORD,, +attrdict3-2.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrdict3-2.0.2.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110 +attrdict3-2.0.2.dist-info/top_level.txt,sha256=2f1-Wyfr5ZHsGvOFLqcj3y6OfZglxI3gjETO12COZRc,9 +attrdict3-2.0.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 diff --git a/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/REQUESTED b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/top_level.txt b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f182865577508cd3a69aa35d907fcaf5f850a16 --- /dev/null +++ b/lib/python3.10/site-packages/attrdict3-2.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +attrdict diff --git a/lib/python3.10/site-packages/attrs-25.3.0.dist-info/licenses/LICENSE b/lib/python3.10/site-packages/attrs-25.3.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2bd6453d255e19b973f19b128596a8b6dd65b2c3 --- /dev/null +++ b/lib/python3.10/site-packages/attrs-25.3.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs 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/lib/python3.10/site-packages/attrs/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..84014edfd0e07dc94399d3b5703479fd2adb68a2 Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrs/__pycache__/converters.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/converters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b1b572102b5acb0b63bfde6435318d2554c558d Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/converters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrs/__pycache__/exceptions.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/exceptions.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c154d5f63b3e2fa88733f07c9864f1e5c5a7b23 Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/exceptions.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrs/__pycache__/filters.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/filters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a953e34fc7339a8932907f5f6a8892d730418d0 Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/filters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrs/__pycache__/setters.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/setters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6865f202d3aa21ff13131a2ebe117185cf6325be Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/setters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/attrs/__pycache__/validators.cpython-310.pyc b/lib/python3.10/site-packages/attrs/__pycache__/validators.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55672221131e10498caaea5ac7f20e180ad239dd Binary files /dev/null and b/lib/python3.10/site-packages/attrs/__pycache__/validators.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.py b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.py new file mode 100644 index 0000000000000000000000000000000000000000..797e423edf1c1d9dacc6c476304504a263b805c5 --- /dev/null +++ b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.py @@ -0,0 +1,111 @@ +import math +import subprocess + +print( + """Contributors +============ + +All contributors (by number of commits): +""" +) + + +email_map = { + # Maintainers. + "git@mikeboers.com": "github@mikeboers.com", + "mboers@keypics.com": "github@mikeboers.com", + "mikeb@loftysky.com": "github@mikeboers.com", + "mikeb@markmedia.co": "github@mikeboers.com", + "westernx@mikeboers.com": "github@mikeboers.com", + # Junk. + "mark@mark-VirtualBox.(none)": None, + # Aliases. + "a.davoudi@aut.ac.ir": "davoudialireza@gmail.com", + "tcaswell@bnl.gov": "tcaswell@gmail.com", + "xxr3376@gmail.com": "xxr@megvii.com", + "dallan@pha.jhu.edu": "daniel.b.allan@gmail.com", + "61652821+laggykiller@users.noreply.github.com": "chaudominic2@gmail.com", +} + +name_map = { + "caspervdw@gmail.com": "Casper van der Wel", + "daniel.b.allan@gmail.com": "Dan Allan", + "mgoacolou@cls.fr": "Manuel Goacolou", + "mindmark@gmail.com": "Mark Reid", + "moritzkassner@gmail.com": "Moritz Kassner", + "vidartf@gmail.com": "Vidar Tonaas Fauske", + "xxr@megvii.com": "Xinran Xu", +} + +github_map = { + "billy.shambrook@gmail.com": "billyshambrook", + "daniel.b.allan@gmail.com": "danielballan", + "davoudialireza@gmail.com": "adavoudi", + "github@mikeboers.com": "mikeboers", + "jeremy.laine@m4x.org": "jlaine", + "kalle.litterfeldt@gmail.com": "litterfeldt", + "mindmark@gmail.com": "markreidvfx", + "moritzkassner@gmail.com": "mkassner", + "rush@logic.cz": "radek-senfeld", + "self@brendanlong.com": "brendanlong", + "tcaswell@gmail.com": "tacaswell", + "ulrik.mikaelsson@magine.com": "rawler", + "vidartf@gmail.com": "vidartf", + "willpatera@gmail.com": "willpatera", + "xxr@megvii.com": "xxr3376", + "chaudominic2@gmail.com": "laggykiller", + "wyattblue@auto-editor.com": "WyattBlue", +} + + +email_count = {} +for line in ( + subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines() +): + name, email = line.strip().rsplit(",", 1) + + email = email_map.get(email, email) + if not email: + continue + + names = name_map.setdefault(email, set()) + if isinstance(names, set): + names.add(name) + + email_count[email] = email_count.get(email, 0) + 1 + + +last = None +block_i = 0 +for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): + # This is the natural log, because of course it should be. ;) + order = int(math.log(count)) + if last and last != order: + block_i += 1 + print() + last = order + + names = name_map[email] + if isinstance(names, set): + name = ", ".join(sorted(names)) + else: + name = names + + github = github_map.get(email) + + # The '-' vs '*' is so that Sphinx treats them as different lists, and + # introduces a gap bettween them. + if github: + print( + "%s %s <%s>; `@%s `_" + % ("-*"[block_i % 2], name, email, github, github) + ) + else: + print( + "%s %s <%s>" + % ( + "-*"[block_i % 2], + name, + email, + ) + ) diff --git a/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.rst b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.rst new file mode 100644 index 0000000000000000000000000000000000000000..b85c8ce1b41fe5a013790863dda86c5f72d0059f --- /dev/null +++ b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/AUTHORS.rst @@ -0,0 +1,111 @@ +Contributors +============ + +All contributors (by number of commits): + +- Mike Boers ; `@mikeboers `_ + +* WyattBlue ; `@WyattBlue `_ +* Jeremy Lainé ; `@jlaine `_ + +- Mark Reid ; `@markreidvfx `_ + +* Vidar Tonaas Fauske ; `@vidartf `_ +* laggykiller ; `@laggykiller `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* Dave Johansen +* JoeUgly <41972063+JoeUgly@users.noreply.github.com> +* Justin Wong <46082645+uvjustin@users.noreply.github.com> +* Mark Harfouche + +- Alba Mendez +- Xinran Xu ; `@xxr3376 `_ +- z-khan +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> +- Dan Allan ; `@danielballan `_ +- Moonsik Park +- Santtu Keskinen +- Christoph Rackwitz +- David Plowman +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Lukas Geiger +- Matthew Lai +- Moritz Kassner ; `@mkassner `_ +- Thomas A Caswell ; `@tacaswell `_ +- Ulrik Mikaelsson ; `@rawler `_ +- Wel C. van der +- Will Patera ; `@willpatera `_ + +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Dexer <73297572+DexerBR@users.noreply.github.com> +* rutsh +* Felix Vollmer +* Santiago Castro +* Christian Clauss +* Ihor Liubymov +* Johannes Erdfelt +* Karl Litterfeldt ; `@litterfeldt `_ +* Martin Larralde +* Simon-Martin Schröder +* Matteo Destro +* mephi42 +* Miles Kaufmann +* Pablo Prietz +* Andrew Wason +* Radek Senfeld ; `@radek-senfeld `_ +* robinechuca +* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* davidplowman <38045873+davidplowman@users.noreply.github.com> +* Hanz <40712686+HanzCEO@users.noreply.github.com> +* Artturin +* Ian Lee +* Ryan Huang +* Arthur Barros +* Carlos Ruiz +* Carlos Ruiz +* Maxime Desroches +* egao1980 +* Eric Kalosa-Kenyon +* elxy +* Gemfield +* henri-gasc +* Jonathan Martin +* Johan Jeppsson Karlin +* Philipp Klaus +* Kim Minjong +* Marcell Pardavi +* Matteo Destro +* Mattias Wadman +* Max Ehrlich +* Manuel Goacolou +* Julian Schweizer +* Nikhil Idiculla +* Ömer Sezgin Uğurlu +* Orivej Desh +* Philipp Krähenbühl +* Mattia Procopio +* ramoncaldeira +* Roland van Laar +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* hyenal +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy +* Sviatoslav Sydorenko (Святослав Сидоренко) +* Jiabei Zhu diff --git a/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/LICENSE.txt b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..9db1661e7d701c7fd262ed77efc2acc3831e41a2 --- /dev/null +++ b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright retained by original committers. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the project nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/__pycache__/AUTHORS.cpython-310.pyc b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/__pycache__/AUTHORS.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3839b1a53f72317b1238813138f0669557d5abf8 Binary files /dev/null and b/lib/python3.10/site-packages/av-15.0.0.dist-info/licenses/__pycache__/AUTHORS.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av.libs/libXau-154567c4.so.6.0.0 b/lib/python3.10/site-packages/av.libs/libXau-154567c4.so.6.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..ff06a58be7b9ff80cee9b8eb45d5e9a28cf67d1b Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libXau-154567c4.so.6.0.0 differ diff --git a/lib/python3.10/site-packages/av.libs/libdrm-b0291a67.so.2.4.0 b/lib/python3.10/site-packages/av.libs/libdrm-b0291a67.so.2.4.0 new file mode 100644 index 0000000000000000000000000000000000000000..02907d158f0bbd54243161f297f72a5d5d1bc9e9 Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libdrm-b0291a67.so.2.4.0 differ diff --git a/lib/python3.10/site-packages/av.libs/libwebpmux-601b9199.so.3.1.1 b/lib/python3.10/site-packages/av.libs/libwebpmux-601b9199.so.3.1.1 new file mode 100644 index 0000000000000000000000000000000000000000..fcba9a4298e9f78c21f69c14b4a6ec368b6e6e80 Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libwebpmux-601b9199.so.3.1.1 differ diff --git a/lib/python3.10/site-packages/av.libs/libxcb-shape-7716c890.so.0.0.0 b/lib/python3.10/site-packages/av.libs/libxcb-shape-7716c890.so.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..17e0047aecae22c5c19894122100d8ca3e4905e4 Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libxcb-shape-7716c890.so.0.0.0 differ diff --git a/lib/python3.10/site-packages/av.libs/libxcb-shm-0be6dfbf.so.0.0.0 b/lib/python3.10/site-packages/av.libs/libxcb-shm-0be6dfbf.so.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..ba6024c827334414edf857d5eca72d0f3c6f997b Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libxcb-shm-0be6dfbf.so.0.0.0 differ diff --git a/lib/python3.10/site-packages/av.libs/libxcb-xfixes-6de855b8.so.0.0.0 b/lib/python3.10/site-packages/av.libs/libxcb-xfixes-6de855b8.so.0.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..aff3e1f6d634056907c1a62e059e6d183ab1a262 Binary files /dev/null and b/lib/python3.10/site-packages/av.libs/libxcb-xfixes-6de855b8.so.0.0.0 differ diff --git a/lib/python3.10/site-packages/av/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e100d6f35c4280912b7bf3cb890cfebdf9f3b039 Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/__pycache__/__main__.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4bcecd95de7b015d6e697d718cddb86126fc944 Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/__main__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/__pycache__/about.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/about.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c41a1e7ae9a840dec71049a7ad9362e71f01b64d Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/about.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/__pycache__/datasets.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/datasets.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c9222c4da46bb022f8cccee0808daf97d3f18c2 Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/datasets.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/__pycache__/packet.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/packet.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d39dba3631fd11bad4604f1dc51a83d1f3e0cbfa Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/packet.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/__pycache__/stream.cpython-310.pyc b/lib/python3.10/site-packages/av/__pycache__/stream.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5ba906a011f7c4f4261e64c2e5f463f12c871d3 Binary files /dev/null and b/lib/python3.10/site-packages/av/__pycache__/stream.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__init__.pxd b/lib/python3.10/site-packages/av/audio/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/audio/__init__.py b/lib/python3.10/site-packages/av/audio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..74ddf696497af9293024c39df458c9b03dd0cf0a --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/__init__.py @@ -0,0 +1,2 @@ +from .frame import AudioFrame +from .stream import AudioStream diff --git a/lib/python3.10/site-packages/av/audio/__init__.pyi b/lib/python3.10/site-packages/av/audio/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..daefab6c928b7f0f6751b0c45afc63ea056ea2fd --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/__init__.pyi @@ -0,0 +1,16 @@ +from typing import Literal + +from .frame import AudioFrame +from .stream import AudioStream + +_AudioCodecName = Literal[ + "aac", + "libopus", + "mp2", + "mp3", + "pcm_alaw", + "pcm_mulaw", + "pcm_s16le", +] + +__all__ = ("AudioFrame", "AudioStream") diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dd117f2981aa8101975bf16a061b424cb10128b Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/codeccontext.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/codeccontext.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1831a782ba871b5e0a15924134c362e196378cb Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/codeccontext.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/format.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/format.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f91fc524903752a23ecac09cf38eecd19d490f36 Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/format.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/frame.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/frame.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66305e8def0c29ae353625c71d5badb3c1a54c0d Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/frame.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/plane.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/plane.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aba7b6b43edfdfee7523c576882f5e84ed87829b Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/plane.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/resampler.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/resampler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..246154fda88626e5f537714fe159a2cca4b7b334 Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/resampler.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/__pycache__/stream.cpython-310.pyc b/lib/python3.10/site-packages/av/audio/__pycache__/stream.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc6fdc309c381d2e74ba696c56b0d01faacc58c3 Binary files /dev/null and b/lib/python3.10/site-packages/av/audio/__pycache__/stream.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/audio/codeccontext.pxd b/lib/python3.10/site-packages/av/audio/codeccontext.pxd new file mode 100644 index 0000000000000000000000000000000000000000..55ad15e9f0404d2b77810682774fd25ab434fadb --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/codeccontext.pxd @@ -0,0 +1,11 @@ + +from av.audio.frame cimport AudioFrame +from av.audio.resampler cimport AudioResampler +from av.codec.context cimport CodecContext + + +cdef class AudioCodecContext(CodecContext): + # Hold onto the frames that we will decode until we have a full one. + cdef AudioFrame next_frame + # For encoding. + cdef AudioResampler resampler diff --git a/lib/python3.10/site-packages/av/audio/codeccontext.py b/lib/python3.10/site-packages/av/audio/codeccontext.py new file mode 100644 index 0000000000000000000000000000000000000000..2dc629917b5c5e69e888cf0a6ef496e0540bcf95 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/codeccontext.py @@ -0,0 +1,105 @@ +import cython +from cython.cimports import libav as lib +from cython.cimports.av.audio.format import AudioFormat, get_audio_format +from cython.cimports.av.audio.frame import AudioFrame, alloc_audio_frame +from cython.cimports.av.audio.layout import AudioLayout, get_audio_layout +from cython.cimports.av.frame import Frame +from cython.cimports.av.packet import Packet + + +@cython.cclass +class AudioCodecContext(CodecContext): + @cython.cfunc + def _prepare_frames_for_encode(self, input_frame: Frame | None): + frame: AudioFrame | None = input_frame + allow_var_frame_size: cython.bint = ( + self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + ) + + # Note that the resampler will simply return an input frame if there is + # no resampling to be done. The control flow was just a little easier this way. + if not self.resampler: + self.resampler = AudioResampler( + format=self.format, + layout=self.layout, + rate=self.ptr.sample_rate, + frame_size=None if allow_var_frame_size else self.ptr.frame_size, + ) + frames = self.resampler.resample(frame) + if input_frame is None: + frames.append(None) # flush if input frame is None + + return frames + + @cython.cfunc + def _alloc_next_frame(self) -> Frame: + return alloc_audio_frame() + + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet): + CodecContext._setup_decoded_frame(self, frame, packet) + aframe: AudioFrame = frame + aframe._init_user_attributes() + + @property + def frame_size(self): + """ + Number of samples per channel in an audio frame. + + :type: int + """ + return self.ptr.frame_size + + @property + def sample_rate(self): + """ + Sample rate of the audio data, in samples per second. + + :type: int + """ + return self.ptr.sample_rate + + @sample_rate.setter + def sample_rate(self, value: cython.int): + self.ptr.sample_rate = value + + @property + def rate(self): + """Another name for :attr:`sample_rate`.""" + return self.sample_rate + + @rate.setter + def rate(self, value): + self.sample_rate = value + + @property + def channels(self): + return self.layout.nb_channels + + @property + def layout(self): + """ + The audio channel layout. + + :type: AudioLayout + """ + return get_audio_layout(self.ptr.ch_layout) + + @layout.setter + def layout(self, value): + layout: AudioLayout = AudioLayout(value) + self.ptr.ch_layout = layout.layout + + @property + def format(self): + """ + The audio sample format. + + :type: AudioFormat + """ + return get_audio_format(self.ptr.sample_fmt) + + @format.setter + def format(self, value): + format: AudioFormat = AudioFormat(value) + self.ptr.sample_fmt = format.sample_fmt diff --git a/lib/python3.10/site-packages/av/audio/codeccontext.pyi b/lib/python3.10/site-packages/av/audio/codeccontext.pyi new file mode 100644 index 0000000000000000000000000000000000000000..b3ec3ce6e877a88a9fcf7661c9aded27201fc8fb --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/codeccontext.pyi @@ -0,0 +1,29 @@ +from typing import Iterator, Literal + +from av.codec.context import CodecContext +from av.packet import Packet + +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + +class AudioCodecContext(CodecContext): + frame_size: int + sample_rate: int + rate: int + type: Literal["audio"] + format: _Format + layout: _Layout + @property + def channels(self) -> int: ... + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/lib/python3.10/site-packages/av/audio/fifo.pxd b/lib/python3.10/site-packages/av/audio/fifo.pxd new file mode 100644 index 0000000000000000000000000000000000000000..0ace5e4b1ec84daaeb09f27f6f9689a56d026912 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/fifo.pxd @@ -0,0 +1,19 @@ +cimport libav as lib +from libc.stdint cimport int64_t, uint64_t + +from av.audio.frame cimport AudioFrame + + +cdef class AudioFifo: + + cdef lib.AVAudioFifo *ptr + + cdef AudioFrame template + + cdef readonly uint64_t samples_written + cdef readonly uint64_t samples_read + cdef readonly double pts_per_sample + + cpdef write(self, AudioFrame frame) + cpdef read(self, int samples=*, bint partial=*) + cpdef read_many(self, int samples, bint partial=*) diff --git a/lib/python3.10/site-packages/av/audio/fifo.pyi b/lib/python3.10/site-packages/av/audio/fifo.pyi new file mode 100644 index 0000000000000000000000000000000000000000..085ed4bba1039b77675da29e26ef9ccbd539c8e1 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/fifo.pyi @@ -0,0 +1,22 @@ +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioFifo: + def write(self, frame: AudioFrame) -> None: ... + def read(self, samples: int = 0, partial: bool = False) -> AudioFrame | None: ... + def read_many(self, samples: int, partial: bool = False) -> list[AudioFrame]: ... + @property + def format(self) -> AudioFormat: ... + @property + def layout(self) -> AudioLayout: ... + @property + def sample_rate(self) -> int: ... + @property + def samples(self) -> int: ... + @property + def samples_written(self) -> int: ... + @property + def samples_read(self) -> int: ... + @property + def pts_per_sample(self) -> float: ... diff --git a/lib/python3.10/site-packages/av/audio/fifo.pyx b/lib/python3.10/site-packages/av/audio/fifo.pyx new file mode 100644 index 0000000000000000000000000000000000000000..2ceb55f7790cdc1088b4af75a44d50e4ac97b7ca --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/fifo.pyx @@ -0,0 +1,197 @@ +from av.audio.frame cimport alloc_audio_frame +from av.error cimport err_check + + +cdef class AudioFifo: + """A simple audio sample FIFO (First In First Out) buffer.""" + + def __repr__(self): + try: + result = ( + f"" + ) + except AttributeError: + result = ( + f"" + ) + return result + + def __dealloc__(self): + if self.ptr: + lib.av_audio_fifo_free(self.ptr) + + cpdef write(self, AudioFrame frame): + """write(frame) + + Push a frame of samples into the queue. + + :param AudioFrame frame: The frame of samples to push. + + The FIFO will remember the attributes from the first frame, and use those + to populate all output frames. + + If there is a :attr:`~.Frame.pts` and :attr:`~.Frame.time_base` and + :attr:`~.AudioFrame.sample_rate`, then the FIFO will assert that the incoming + timestamps are continuous. + + """ + + if frame is None: + raise TypeError("AudioFifo must be given an AudioFrame.") + + if not frame.ptr.nb_samples: + return + + if not self.ptr: + + # Hold onto a copy of the attributes of the first frame to populate + # output frames with. + self.template = alloc_audio_frame() + self.template._copy_internal_attributes(frame) + self.template._init_user_attributes() + + # Figure out our "time_base". + if frame._time_base.num and frame.ptr.sample_rate: + self.pts_per_sample = frame._time_base.den / float(frame._time_base.num) + self.pts_per_sample /= frame.ptr.sample_rate + else: + self.pts_per_sample = 0 + + self.ptr = lib.av_audio_fifo_alloc( + frame.ptr.format, + frame.layout.nb_channels, + frame.ptr.nb_samples * 2, # Just a default number of samples; it will adjust. + ) + + if not self.ptr: + raise RuntimeError("Could not allocate AVAudioFifo.") + + # Make sure nothing changed. + elif ( + frame.ptr.format != self.template.ptr.format or + # TODO: frame.ptr.ch_layout != self.template.ptr.ch_layout or + frame.ptr.sample_rate != self.template.ptr.sample_rate or + (frame._time_base.num and self.template._time_base.num and ( + frame._time_base.num != self.template._time_base.num or + frame._time_base.den != self.template._time_base.den + )) + ): + raise ValueError("Frame does not match AudioFifo parameters.") + + # Assert that the PTS are what we expect. + cdef int64_t expected_pts + if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE: + expected_pts = (self.pts_per_sample * self.samples_written) + if frame.ptr.pts != expected_pts: + raise ValueError( + "Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts) + ) + + err_check(lib.av_audio_fifo_write( + self.ptr, + frame.ptr.extended_data, + frame.ptr.nb_samples, + )) + + self.samples_written += frame.ptr.nb_samples + + cpdef read(self, int samples=0, bint partial=False): + """read(samples=0, partial=False) + + Read samples from the queue. + + :param int samples: The number of samples to pull; 0 gets all. + :param bool partial: Allow returning less than requested. + :returns: New :class:`AudioFrame` or ``None`` (if empty). + + If the incoming frames had valid a :attr:`~.Frame.time_base`, + :attr:`~.AudioFrame.sample_rate` and :attr:`~.Frame.pts`, the returned frames + will have accurate timing. + + """ + + if not self.ptr: + return + + cdef int buffered_samples = lib.av_audio_fifo_size(self.ptr) + if buffered_samples < 1: + return + + samples = samples or buffered_samples + + if buffered_samples < samples: + if partial: + samples = buffered_samples + else: + return + + cdef AudioFrame frame = alloc_audio_frame() + frame._copy_internal_attributes(self.template) + frame._init( + self.template.ptr.format, + self.template.ptr.ch_layout, + samples, + 1, # Align? + ) + + err_check(lib.av_audio_fifo_read( + self.ptr, + frame.ptr.extended_data, + samples, + )) + + if self.pts_per_sample: + frame.ptr.pts = (self.pts_per_sample * self.samples_read) + else: + frame.ptr.pts = lib.AV_NOPTS_VALUE + + self.samples_read += samples + + return frame + + cpdef read_many(self, int samples, bint partial=False): + """read_many(samples, partial=False) + + Read as many frames as we can. + + :param int samples: How large for the frames to be. + :param bool partial: If we should return a partial frame. + :returns: A ``list`` of :class:`AudioFrame`. + + """ + + cdef AudioFrame frame + frames = [] + while True: + frame = self.read(samples, partial=partial) + if frame is not None: + frames.append(frame) + else: + break + + return frames + + @property + def format(self): + """The :class:`.AudioFormat` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") + return self.template.format + @property + def layout(self): + """The :class:`.AudioLayout` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") + return self.template.layout + @property + def sample_rate(self): + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") + return self.template.sample_rate + + @property + def samples(self): + """Number of audio samples (per channel) in the buffer.""" + return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0 diff --git a/lib/python3.10/site-packages/av/audio/format.pxd b/lib/python3.10/site-packages/av/audio/format.pxd new file mode 100644 index 0000000000000000000000000000000000000000..c4d4bc552a5a9736ab87a83817a97f6ca62d6b2a --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/format.pxd @@ -0,0 +1,7 @@ +cimport libav as lib + + +cdef class AudioFormat: + cdef lib.AVSampleFormat sample_fmt + +cdef AudioFormat get_audio_format(lib.AVSampleFormat format) diff --git a/lib/python3.10/site-packages/av/audio/format.py b/lib/python3.10/site-packages/av/audio/format.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5de1ad996fcc82f7b16c0ee96ba2b4c557d8a5 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/format.py @@ -0,0 +1,142 @@ +import sys + +import cython + +container_format_postfix: str = "le" if sys.byteorder == "little" else "be" +_cinit_bypass_sentinel = object() + + +@cython.cfunc +def get_audio_format(c_format: lib.AVSampleFormat) -> AudioFormat: + """Get an AudioFormat without going through a string.""" + + if c_format < 0: + return None + + format: AudioFormat = AudioFormat(_cinit_bypass_sentinel) + format.sample_fmt = c_format + return format + + +@cython.cclass +class AudioFormat: + """Descriptor of audio formats.""" + + def __cinit__(self, name): + if name is _cinit_bypass_sentinel: + return + + sample_fmt: lib.AVSampleFormat + if isinstance(name, AudioFormat): + sample_fmt = cython.cast(AudioFormat, name).sample_fmt + else: + sample_fmt = lib.av_get_sample_fmt(name) + + if sample_fmt < 0: + raise ValueError(f"Not a sample format: {name!r}") + + self.sample_fmt = sample_fmt + + def __repr__(self): + return f"" + + @property + def name(self): + """Canonical name of the sample format. + + >>> AudioFormat('s16p').name + 's16p' + + """ + return lib.av_get_sample_fmt_name(self.sample_fmt) + + @property + def bytes(self): + """Number of bytes per sample. + + >>> AudioFormat('s16p').bytes + 2 + + """ + return lib.av_get_bytes_per_sample(self.sample_fmt) + + @property + def bits(self): + """Number of bits per sample. + + >>> AudioFormat('s16p').bits + 16 + + """ + return lib.av_get_bytes_per_sample(self.sample_fmt) << 3 + + @property + def is_planar(self): + """Is this a planar format? + + Strictly opposite of :attr:`is_packed`. + + """ + return bool(lib.av_sample_fmt_is_planar(self.sample_fmt)) + + @property + def is_packed(self): + """Is this a packed format? + + Strictly opposite of :attr:`is_planar`. + + """ + return not lib.av_sample_fmt_is_planar(self.sample_fmt) + + @property + def planar(self): + """The planar variant of this format. + + Is itself when planar: + + >>> fmt = AudioFormat('s16p') + >>> fmt.planar is fmt + True + + """ + if self.is_planar: + return self + return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt)) + + @property + def packed(self): + """The packed variant of this format. + + Is itself when packed: + + >>> fmt = AudioFormat('s16') + >>> fmt.packed is fmt + True + + """ + if self.is_packed: + return self + return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt)) + + @property + def container_name(self): + """The name of a :class:`ContainerFormat` which directly accepts this data. + + :raises ValueError: when planar, since there are no such containers. + + """ + if self.is_planar: + raise ValueError("no planar container formats") + + if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: + return "u8" + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: + return "s16" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: + return "s32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: + return "f32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: + return "f64" + container_format_postfix + + raise ValueError("unknown layout") diff --git a/lib/python3.10/site-packages/av/audio/format.pyi b/lib/python3.10/site-packages/av/audio/format.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5f7e322edc85d831b5b6cc9979fd4dfca4b994c1 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/format.pyi @@ -0,0 +1,11 @@ +class AudioFormat: + name: str + bytes: int + bits: int + is_planar: bool + is_packed: bool + planar: AudioFormat + packed: AudioFormat + container_name: str + + def __init__(self, name: str | AudioFormat) -> None: ... diff --git a/lib/python3.10/site-packages/av/audio/frame.pxd b/lib/python3.10/site-packages/av/audio/frame.pxd new file mode 100644 index 0000000000000000000000000000000000000000..398d76d33ea1173e31a5e13809ba3910385572b3 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/frame.pxd @@ -0,0 +1,31 @@ +cimport libav as lib +from libc.stdint cimport uint8_t, uint64_t + +from av.audio.format cimport AudioFormat +from av.audio.layout cimport AudioLayout +from av.frame cimport Frame + + +cdef class AudioFrame(Frame): + # For raw storage of the frame's data; don't ever touch this. + cdef uint8_t *_buffer + cdef size_t _buffer_size + + cdef readonly AudioLayout layout + """ + The audio channel layout. + + :type: AudioLayout + """ + + cdef readonly AudioFormat format + """ + The audio sample format. + + :type: AudioFormat + """ + + cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align) + cdef _init_user_attributes(self) + +cdef AudioFrame alloc_audio_frame() diff --git a/lib/python3.10/site-packages/av/audio/frame.py b/lib/python3.10/site-packages/av/audio/frame.py new file mode 100644 index 0000000000000000000000000000000000000000..cf3ab5934ab21e42555841b106ae658454e27d58 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/frame.py @@ -0,0 +1,205 @@ +import cython +from cython.cimports.av.audio.format import get_audio_format +from cython.cimports.av.audio.layout import get_audio_layout +from cython.cimports.av.audio.plane import AudioPlane +from cython.cimports.av.error import err_check +from cython.cimports.av.utils import check_ndarray + +_cinit_bypass_sentinel = object() + + +@cython.cfunc +def alloc_audio_frame() -> AudioFrame: + return AudioFrame(_cinit_bypass_sentinel) + + +format_dtypes = { + "dbl": "f8", + "dblp": "f8", + "flt": "f4", + "fltp": "f4", + "s16": "i2", + "s16p": "i2", + "s32": "i4", + "s32p": "i4", + "u8": "u1", + "u8p": "u1", +} + + +@cython.cclass +class AudioFrame(Frame): + """A frame of audio.""" + + def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): + if format is _cinit_bypass_sentinel: + return + + cy_format: AudioFormat = AudioFormat(format) + cy_layout: AudioLayout = AudioLayout(layout) + self._init(cy_format.sample_fmt, cy_layout.layout, samples, align) + + @cython.cfunc + def _init( + self, + format: lib.AVSampleFormat, + layout: lib.AVChannelLayout, + nb_samples: cython.uint, + align: cython.uint, + ): + self.ptr.nb_samples = nb_samples + self.ptr.format = format + self.ptr.ch_layout = layout + + # Sometimes this is called twice. Oh well. + self._init_user_attributes() + + if self.layout.nb_channels != 0 and nb_samples: + # Cleanup the old buffer. + lib.av_freep(cython.address(self._buffer)) + + # Get a new one. + self._buffer_size = err_check( + lib.av_samples_get_buffer_size( + cython.NULL, self.layout.nb_channels, nb_samples, format, align + ) + ) + self._buffer = cython.cast( + cython.pointer[uint8_t], lib.av_malloc(self._buffer_size) + ) + if not self._buffer: + raise MemoryError("cannot allocate AudioFrame buffer") + + # Connect the data pointers to the buffer. + err_check( + lib.avcodec_fill_audio_frame( + self.ptr, + self.layout.nb_channels, + cython.cast(lib.AVSampleFormat, self.ptr.format), + self._buffer, + self._buffer_size, + align, + ) + ) + + def __dealloc__(self): + lib.av_freep(cython.address(self._buffer)) + + @cython.cfunc + def _init_user_attributes(self): + self.layout = get_audio_layout(self.ptr.ch_layout) + self.format = get_audio_format(cython.cast(lib.AVSampleFormat, self.ptr.format)) + + def __repr__(self): + return ( + f"" + ) + + @staticmethod + def from_ndarray(array, format="s16", layout="stereo"): + """ + Construct a frame from a numpy array. + """ + import numpy as np + + py_format = format if isinstance(format, AudioFormat) else AudioFormat(format) + py_layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) + format = py_format.name + + # map avcodec type to numpy type + try: + dtype = np.dtype(format_dtypes[format]) + except KeyError: + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) + + # check input format + nb_channels = py_layout.nb_channels + check_ndarray(array, dtype, 2) + if py_format.is_planar: + if array.shape[0] != nb_channels: + raise ValueError( + f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`" + ) + samples = array.shape[1] + else: + if array.shape[0] != 1: + raise ValueError( + f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`" + ) + samples = array.shape[1] // nb_channels + + frame = AudioFrame(format=py_format, layout=py_layout, samples=samples) + for i, plane in enumerate(frame.planes): + plane.update(array[i, :]) + return frame + + @property + def planes(self): + """ + A tuple of :class:`~av.audio.plane.AudioPlane`. + + :type: tuple + """ + plane_count: cython.int = 0 + while self.ptr.extended_data[plane_count]: + plane_count += 1 + + return tuple([AudioPlane(self, i) for i in range(plane_count)]) + + @property + def samples(self): + """ + Number of audio samples (per channel). + + :type: int + """ + return self.ptr.nb_samples + + @property + def sample_rate(self): + """ + Sample rate of the audio data, in samples per second. + + :type: int + """ + return self.ptr.sample_rate + + @sample_rate.setter + def sample_rate(self, value): + self.ptr.sample_rate = value + + @property + def rate(self): + """Another name for :attr:`sample_rate`.""" + return self.ptr.sample_rate + + @rate.setter + def rate(self, value): + self.ptr.sample_rate = value + + def to_ndarray(self): + """Get a numpy array of this frame. + + .. note:: Numpy must be installed. + + """ + import numpy as np + + try: + dtype = np.dtype(format_dtypes[self.format.name]) + except KeyError: + raise ValueError( + f"Conversion from {self.format.name!r} format to numpy array is not supported." + ) + + if self.format.is_planar: + count = self.samples + else: + count = self.samples * self.layout.nb_channels + + return np.vstack( + [np.frombuffer(x, dtype=dtype, count=count) for x in self.planes] + ) diff --git a/lib/python3.10/site-packages/av/audio/frame.pyi b/lib/python3.10/site-packages/av/audio/frame.pyi new file mode 100644 index 0000000000000000000000000000000000000000..6aeb86b4d27487dd44b6a8bd91f9998462c56af9 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/frame.pyi @@ -0,0 +1,49 @@ +from typing import Any, Union + +import numpy as np + +from av.frame import Frame + +from .format import AudioFormat +from .layout import AudioLayout +from .plane import AudioPlane + +format_dtypes: dict[str, str] +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.float64]], # f8 + np.ndarray[Any, np.dtype[np.float32]], # f4 + np.ndarray[Any, np.dtype[np.int32]], # i4 + np.ndarray[Any, np.dtype[np.int16]], # i2 + np.ndarray[Any, np.dtype[np.uint8]], # u1 +] + +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + +class AudioFrame(Frame): + planes: tuple[AudioPlane, ...] + samples: int + sample_rate: int + rate: int + format: _Format + layout: _Layout + + def __init__( + self, + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", + samples: int = 0, + align: int = 1, + ) -> None: ... + @staticmethod + def from_ndarray( + array: _SupportedNDarray, + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", + ) -> AudioFrame: ... + def to_ndarray(self) -> _SupportedNDarray: ... diff --git a/lib/python3.10/site-packages/av/audio/layout.pxd b/lib/python3.10/site-packages/av/audio/layout.pxd new file mode 100644 index 0000000000000000000000000000000000000000..c7a2368f15840dd241940868eda157cb6fb1527e --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/layout.pxd @@ -0,0 +1,8 @@ +cimport libav as lib + + +cdef class AudioLayout: + cdef lib.AVChannelLayout layout + cdef _init(self, lib.AVChannelLayout layout) + +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout) diff --git a/lib/python3.10/site-packages/av/audio/layout.pyi b/lib/python3.10/site-packages/av/audio/layout.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9fdf0ac1531ff4d74ac60ee191ab899755443cf1 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/layout.pyi @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +class AudioLayout: + name: str + nb_channels: int + channels: tuple[AudioChannel, ...] + def __init__(self, layout: str | AudioLayout): ... + +@dataclass +class AudioChannel: + name: str + description: str diff --git a/lib/python3.10/site-packages/av/audio/layout.pyx b/lib/python3.10/site-packages/av/audio/layout.pyx new file mode 100644 index 0000000000000000000000000000000000000000..ea259d0fdb4eae9b869f68e8ccf9163eec4eca24 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/layout.pyx @@ -0,0 +1,84 @@ +cimport libav as lib +from cpython.bytes cimport PyBytes_FromStringAndSize + +from dataclasses import dataclass + + +@dataclass +class AudioChannel: + name: str + description: str + + def __repr__(self): + return f"" + +cdef object _cinit_bypass_sentinel + +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout): + """Get an AudioLayout from Cython land.""" + cdef AudioLayout layout = AudioLayout.__new__(AudioLayout, _cinit_bypass_sentinel) + layout._init(c_layout) + return layout + + +cdef class AudioLayout: + def __init__(self, layout): + if layout is _cinit_bypass_sentinel: + return + + if type(layout) is str: + ret = lib.av_channel_layout_from_string(&c_layout, layout) + if ret != 0: + raise ValueError(f"Invalid layout: {layout}") + elif isinstance(layout, AudioLayout): + c_layout = (layout).layout + else: + raise TypeError(f"layout must be of type: string | av.AudioLayout, got {type(layout)}") + + self._init(c_layout) + + cdef _init(self, lib.AVChannelLayout layout): + self.layout = layout + + def __repr__(self): + return f"" + + def __eq__(self, other): + return isinstance(other, AudioLayout) and self.name == other.name and self.nb_channels == other.nb_channels + + @property + def nb_channels(self): + return self.layout.nb_channels + + @property + def channels(self): + cdef lib.AVChannel channel + cdef char buf[16] + cdef char buf2[128] + + results = [] + + for index in range(self.layout.nb_channels): + channel = lib.av_channel_layout_channel_from_index(&self.layout, index); + size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + results.append( + AudioChannel( + PyBytes_FromStringAndSize(buf, size).decode("utf-8"), + PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"), + ) + ) + + return tuple(results) + + @property + def name(self) -> str: + """The canonical name of the audio layout.""" + cdef char layout_name[128] + cdef int ret + + ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) + if ret < 0: + raise RuntimeError(f"Failed to get layout name: {ret}") + + return layout_name \ No newline at end of file diff --git a/lib/python3.10/site-packages/av/audio/plane.pxd b/lib/python3.10/site-packages/av/audio/plane.pxd new file mode 100644 index 0000000000000000000000000000000000000000..de912ac2216b9ff4e9bf3bdda8ee93763e870a4c --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/plane.pxd @@ -0,0 +1,6 @@ +from av.plane cimport Plane + + +cdef class AudioPlane(Plane): + cdef readonly size_t buffer_size + cdef size_t _buffer_size(self) diff --git a/lib/python3.10/site-packages/av/audio/plane.py b/lib/python3.10/site-packages/av/audio/plane.py new file mode 100644 index 0000000000000000000000000000000000000000..bdaf1570861ac15bc956a4e026f8e8eb006d17df --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/plane.py @@ -0,0 +1,13 @@ +import cython +from cython.cimports.av.audio.frame import AudioFrame + + +@cython.cclass +class AudioPlane(Plane): + def __cinit__(self, frame: AudioFrame, index: cython.int): + # Only the first linesize is ever populated, but it applies to every plane. + self.buffer_size = self.frame.ptr.linesize[0] + + @cython.cfunc + def _buffer_size(self) -> cython.size_t: + return self.buffer_size diff --git a/lib/python3.10/site-packages/av/audio/plane.pyi b/lib/python3.10/site-packages/av/audio/plane.pyi new file mode 100644 index 0000000000000000000000000000000000000000..64524dcdb5170ebfbee5aaafff86f37f9a476444 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/plane.pyi @@ -0,0 +1,4 @@ +from av.plane import Plane + +class AudioPlane(Plane): + buffer_size: int diff --git a/lib/python3.10/site-packages/av/audio/resampler.pxd b/lib/python3.10/site-packages/av/audio/resampler.pxd new file mode 100644 index 0000000000000000000000000000000000000000..20b74186ed1e80fcb879f5f341a7e10e6846825b --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/resampler.pxd @@ -0,0 +1,18 @@ +from av.audio.format cimport AudioFormat +from av.audio.frame cimport AudioFrame +from av.audio.layout cimport AudioLayout +from av.filter.graph cimport Graph + + +cdef class AudioResampler: + cdef readonly bint is_passthrough + cdef AudioFrame template + + # Destination descriptors + cdef readonly AudioFormat format + cdef readonly AudioLayout layout + cdef readonly int rate + cdef readonly unsigned int frame_size + + cdef Graph graph + cpdef list resample(self, AudioFrame) diff --git a/lib/python3.10/site-packages/av/audio/resampler.py b/lib/python3.10/site-packages/av/audio/resampler.py new file mode 100644 index 0000000000000000000000000000000000000000..d56ec06ed99c77cea408ec3fabb715d3a9401015 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/resampler.py @@ -0,0 +1,122 @@ +from errno import EAGAIN + +import cython +from cython.cimports.av.filter.context import FilterContext +from cython.cimports.av.filter.graph import Graph + +from av.error import FFmpegError + + +@cython.cclass +class AudioResampler: + """AudioResampler(format=None, layout=None, rate=None) + + :param AudioFormat format: The target format, or string that parses to one + (e.g. ``"s16"``). + :param AudioLayout layout: The target layout, or an int/string that parses + to one (e.g. ``"stereo"``). + :param int rate: The target sample rate. + """ + + def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): + if format is not None: + self.format = ( + format if isinstance(format, AudioFormat) else AudioFormat(format) + ) + + if layout is not None: + self.layout = AudioLayout(layout) + + self.rate = int(rate) if rate else 0 + self.frame_size = int(frame_size) if frame_size else 0 + self.graph = None + + @cython.ccall + def resample(self, frame: AudioFrame | None) -> list: + """resample(frame) + + Convert the ``sample_rate``, ``channel_layout`` and/or ``format`` of + a :class:`~.AudioFrame`. + + :param AudioFrame frame: The frame to convert or `None` to flush. + :returns: A list of :class:`AudioFrame` in new parameters. If the nothing is to be done return the same frame + as a single element list. + + """ + # We don't have any input, so don't bother even setting up. + if not self.graph and frame is None: + return [] + + # Shortcut for passthrough. + if self.is_passthrough: + return [frame] + + # Take source settings from the first frame. + if not self.graph: + self.template = frame + + # Set some default descriptors. + self.format = self.format or frame.format + self.layout = self.layout or frame.layout + self.rate = self.rate or frame.sample_rate + + # Check if we can passthrough or if there is actually work to do. + if ( + frame.format.sample_fmt == self.format.sample_fmt + and frame.layout == self.layout + and frame.sample_rate == self.rate + and self.frame_size == 0 + ): + self.is_passthrough = True + return [frame] + + # handle resampling with aformat filter + # (similar to configure_output_audio_filter from ffmpeg) + self.graph = Graph() + extra_args = {} + if frame.time_base is not None: + extra_args["time_base"] = f"{frame.time_base}" + + abuffer = self.graph.add( + "abuffer", + sample_rate=f"{frame.sample_rate}", + sample_fmt=AudioFormat(frame.format).name, + channel_layout=frame.layout.name, + **extra_args, + ) + aformat = self.graph.add( + "aformat", + sample_rates=f"{self.rate}", + sample_fmts=self.format.name, + channel_layouts=self.layout.name, + ) + abuffersink = self.graph.add("abuffersink") + abuffer.link_to(aformat) + aformat.link_to(abuffersink) + self.graph.configure() + + if self.frame_size > 0: + self.graph.set_audio_frame_size(self.frame_size) + + if frame is not None: + if ( + frame.format.sample_fmt != self.template.format.sample_fmt + or frame.layout != self.template.layout + or frame.sample_rate != self.template.rate + ): + raise ValueError("Frame does not match AudioResampler setup.") + + self.graph.push(frame) + + output: list = [] + while True: + try: + output.append(self.graph.pull()) + except EOFError: + break + except FFmpegError as e: + if e.errno != EAGAIN: + raise + break + + return output diff --git a/lib/python3.10/site-packages/av/audio/resampler.pyi b/lib/python3.10/site-packages/av/audio/resampler.pyi new file mode 100644 index 0000000000000000000000000000000000000000..cbf2134aa13911ebf13c7e2e676fadb96230f530 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/resampler.pyi @@ -0,0 +1,20 @@ +from av.filter.graph import Graph + +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioResampler: + rate: int + frame_size: int + format: AudioFormat + graph: Graph | None + + def __init__( + self, + format: str | int | AudioFormat | None = None, + layout: str | int | AudioLayout | None = None, + rate: int | None = None, + frame_size: int | None = None, + ) -> None: ... + def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ... diff --git a/lib/python3.10/site-packages/av/audio/stream.pxd b/lib/python3.10/site-packages/av/audio/stream.pxd new file mode 100644 index 0000000000000000000000000000000000000000..8462061f8e0da6cd418527a73ac80d8ce59d7f0e --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/stream.pxd @@ -0,0 +1,9 @@ +from av.packet cimport Packet +from av.stream cimport Stream + +from .frame cimport AudioFrame + + +cdef class AudioStream(Stream): + cpdef encode(self, AudioFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/lib/python3.10/site-packages/av/audio/stream.py b/lib/python3.10/site-packages/av/audio/stream.py new file mode 100644 index 0000000000000000000000000000000000000000..7c150c84bb67a42d81a072e27635a72aaabfdbaf --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/stream.py @@ -0,0 +1,46 @@ +import cython +from cython.cimports.av.audio.frame import AudioFrame +from cython.cimports.av.packet import Packet + + +@cython.cclass +class AudioStream(Stream): + def __repr__(self): + form = self.format.name if self.format else None + return ( + f"" + ) + + def __getattr__(self, name): + return getattr(self.codec_context, name) + + @cython.ccall + def encode(self, frame: AudioFrame | None = None): + """ + Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. + + :rtype: list[Packet] + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + packet: Packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + + return packets + + @cython.ccall + def decode(self, packet: Packet | None = None): + """ + Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. + + :rtype: list[AudioFrame] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + + return self.codec_context.decode(packet) diff --git a/lib/python3.10/site-packages/av/audio/stream.pyi b/lib/python3.10/site-packages/av/audio/stream.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f92fb52ba2a941f1f539b690bb3b26fc434a8ec3 --- /dev/null +++ b/lib/python3.10/site-packages/av/audio/stream.pyi @@ -0,0 +1,32 @@ +from typing import Literal + +from av.packet import Packet +from av.stream import Stream + +from .codeccontext import AudioCodecContext +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + +class AudioStream(Stream): + codec_context: AudioCodecContext + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... + + # From codec context + frame_size: int + sample_rate: int + bit_rate: int + rate: int + channels: int + type: Literal["audio"] + format: _Format + layout: _Layout diff --git a/lib/python3.10/site-packages/av/codec/__init__.pxd b/lib/python3.10/site-packages/av/codec/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/codec/__init__.py b/lib/python3.10/site-packages/av/codec/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f35f9b7d4e5b36ea62d1d6268aea5f8dc0007b5a --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/__init__.py @@ -0,0 +1,11 @@ +from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available +from .context import CodecContext + +__all__ = ( + "Capabilities", + "Codec", + "Properties", + "codec_descriptor", + "codecs_available", + "CodecContext", +) diff --git a/lib/python3.10/site-packages/av/codec/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/codec/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a26cfe83315630538968c1236384a60a776d7b7 Binary files /dev/null and b/lib/python3.10/site-packages/av/codec/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/codec/codec.pxd b/lib/python3.10/site-packages/av/codec/codec.pxd new file mode 100644 index 0000000000000000000000000000000000000000..576c659b46939204543b2313f33de1d6ae3ac2dd --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/codec.pxd @@ -0,0 +1,15 @@ +cimport libav as lib + + +cdef class Codec: + + cdef const lib.AVCodec *ptr + cdef const lib.AVCodecDescriptor *desc + cdef readonly bint is_encoder + + cdef tuple _hardware_configs + + cdef _init(self, name=?) + + +cdef Codec wrap_codec(const lib.AVCodec *ptr) diff --git a/lib/python3.10/site-packages/av/codec/codec.pyi b/lib/python3.10/site-packages/av/codec/codec.pyi new file mode 100644 index 0000000000000000000000000000000000000000..4270c641f83a132ff0bcc99117ac891066cd3c7b --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/codec.pyi @@ -0,0 +1,115 @@ +from enum import Flag, IntEnum +from fractions import Fraction +from typing import ClassVar, Literal, cast, overload + +from av.audio.codeccontext import AudioCodecContext +from av.audio.format import AudioFormat +from av.descriptor import Descriptor +from av.subtitles.codeccontext import SubtitleCodecContext +from av.video.codeccontext import VideoCodecContext +from av.video.format import VideoFormat + +from .context import CodecContext + +class Properties(Flag): + NONE = cast(ClassVar[Properties], ...) + INTRA_ONLY = cast(ClassVar[Properties], ...) + LOSSY = cast(ClassVar[Properties], ...) + LOSSLESS = cast(ClassVar[Properties], ...) + REORDER = cast(ClassVar[Properties], ...) + BITMAP_SUB = cast(ClassVar[Properties], ...) + TEXT_SUB = cast(ClassVar[Properties], ...) + +class Capabilities(IntEnum): + none = cast(int, ...) + draw_horiz_band = cast(int, ...) + dr1 = cast(int, ...) + hwaccel = cast(int, ...) + delay = cast(int, ...) + small_last_frame = cast(int, ...) + hwaccel_vdpau = cast(int, ...) + subframes = cast(int, ...) + experimental = cast(int, ...) + channel_conf = cast(int, ...) + neg_linesizes = cast(int, ...) + frame_threads = cast(int, ...) + slice_threads = cast(int, ...) + param_change = cast(int, ...) + auto_threads = cast(int, ...) + variable_frame_size = cast(int, ...) + avoid_probing = cast(int, ...) + hardware = cast(int, ...) + hybrid = cast(int, ...) + encoder_reordered_opaque = cast(int, ...) + encoder_flush = cast(int, ...) + encoder_recon_frame = cast(int, ...) + +class UnknownCodecError(ValueError): ... + +class Codec: + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... + @property + def mode(self) -> Literal["r", "w"]: ... + descriptor: Descriptor + @property + def name(self) -> str: ... + @property + def canonical_name(self) -> str: ... + @property + def long_name(self) -> str: ... + @property + def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... + @property + def id(self) -> int: ... + frame_rates: list[Fraction] | None + audio_rates: list[int] | None + video_formats: list[VideoFormat] | None + audio_formats: list[AudioFormat] | None + + @property + def properties(self) -> int: ... + @property + def intra_only(self) -> bool: ... + @property + def lossy(self) -> bool: ... + @property + def lossless(self) -> bool: ... + @property + def reorder(self) -> bool: ... + @property + def bitmap_sub(self) -> bool: ... + @property + def text_sub(self) -> bool: ... + @property + def capabilities(self) -> int: ... + @property + def experimental(self) -> bool: ... + @property + def delay(self) -> bool: ... + def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... + @overload + def create(self, kind: Literal["video"]) -> VideoCodecContext: ... + @overload + def create(self, kind: Literal["audio"]) -> AudioCodecContext: ... + @overload + def create(self, kind: Literal["subtitle"]) -> SubtitleCodecContext: ... + @overload + def create(self, kind: None = None) -> CodecContext: ... + @overload + def create( + self, kind: Literal["video", "audio", "subtitle"] | None = None + ) -> ( + VideoCodecContext | AudioCodecContext | SubtitleCodecContext | CodecContext + ): ... + +class codec_descriptor: + name: str + options: tuple[int, ...] + +codecs_available: set[str] + +def dump_codecs() -> None: ... +def dump_hwconfigs() -> None: ... diff --git a/lib/python3.10/site-packages/av/codec/codec.pyx b/lib/python3.10/site-packages/av/codec/codec.pyx new file mode 100644 index 0000000000000000000000000000000000000000..a28db758e659039f57eeb9a5a7b3ff91efb64de8 --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/codec.pyx @@ -0,0 +1,390 @@ +cimport libav as lib + +from av.audio.format cimport get_audio_format +from av.codec.hwaccel cimport wrap_hwconfig +from av.descriptor cimport wrap_avclass +from av.utils cimport avrational_to_fraction +from av.video.format cimport get_video_format + +from enum import Flag, IntEnum + + +cdef object _cinit_sentinel = object() + +cdef Codec wrap_codec(const lib.AVCodec *ptr): + cdef Codec codec = Codec(_cinit_sentinel) + codec.ptr = ptr + codec.is_encoder = lib.av_codec_is_encoder(ptr) + codec._init() + return codec + +class Properties(Flag): + NONE = 0 + INTRA_ONLY = lib.AV_CODEC_PROP_INTRA_ONLY + LOSSY = lib.AV_CODEC_PROP_LOSSY + LOSSLESS = lib.AV_CODEC_PROP_LOSSLESS + REORDER = lib.AV_CODEC_PROP_REORDER + BITMAP_SUB = lib.AV_CODEC_PROP_BITMAP_SUB + TEXT_SUB = lib.AV_CODEC_PROP_TEXT_SUB + + +class Capabilities(IntEnum): + none = 0 + draw_horiz_band = lib.AV_CODEC_CAP_DRAW_HORIZ_BAND + dr1 = lib.AV_CODEC_CAP_DR1 + hwaccel = 1 << 4 + delay = lib.AV_CODEC_CAP_DELAY + small_last_frame = lib.AV_CODEC_CAP_SMALL_LAST_FRAME + hwaccel_vdpau = 1 << 7 + subframes = lib.AV_CODEC_CAP_SUBFRAMES + experimental = lib.AV_CODEC_CAP_EXPERIMENTAL + channel_conf = lib.AV_CODEC_CAP_CHANNEL_CONF + neg_linesizes = 1 << 11 + frame_threads = lib.AV_CODEC_CAP_FRAME_THREADS + slice_threads = lib.AV_CODEC_CAP_SLICE_THREADS + param_change = lib.AV_CODEC_CAP_PARAM_CHANGE + auto_threads = lib.AV_CODEC_CAP_OTHER_THREADS + variable_frame_size = lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + avoid_probing = lib.AV_CODEC_CAP_AVOID_PROBING + hardware = lib.AV_CODEC_CAP_HARDWARE + hybrid = lib.AV_CODEC_CAP_HYBRID + encoder_reordered_opaque = 1 << 20 + encoder_flush = 1 << 21 + encoder_recon_frame = 1 << 22 + + +class UnknownCodecError(ValueError): + pass + + +cdef class Codec: + """Codec(name, mode='r') + + :param str name: The codec name. + :param str mode: ``'r'`` for decoding or ``'w'`` for encoding. + + This object exposes information about an available codec, and an avenue to + create a :class:`.CodecContext` to encode/decode directly. + + :: + + >>> codec = Codec('mpeg4', 'r') + >>> codec.name + 'mpeg4' + >>> codec.type + 'video' + >>> codec.is_encoder + False + + """ + + def __cinit__(self, name, mode="r"): + if name is _cinit_sentinel: + return + + if mode == "w": + self.ptr = lib.avcodec_find_encoder_by_name(name) + if not self.ptr: + self.desc = lib.avcodec_descriptor_get_by_name(name) + if self.desc: + self.ptr = lib.avcodec_find_encoder(self.desc.id) + + elif mode == "r": + self.ptr = lib.avcodec_find_decoder_by_name(name) + if not self.ptr: + self.desc = lib.avcodec_descriptor_get_by_name(name) + if self.desc: + self.ptr = lib.avcodec_find_decoder(self.desc.id) + + else: + raise ValueError('Invalid mode; must be "r" or "w".', mode) + + self._init(name) + + # Sanity check. + if (mode == "w") != self.is_encoder: + raise RuntimeError("Found codec does not match mode.", name, mode) + + cdef _init(self, name=None): + if not self.ptr: + raise UnknownCodecError(name) + + if not self.desc: + self.desc = lib.avcodec_descriptor_get(self.ptr.id) + if not self.desc: + raise RuntimeError("No codec descriptor for %r." % name) + + self.is_encoder = lib.av_codec_is_encoder(self.ptr) + + # Sanity check. + if self.is_encoder and lib.av_codec_is_decoder(self.ptr): + raise RuntimeError("%s is both encoder and decoder.") + + def __repr__(self): + mode = self.mode + return f"" + + def create(self, kind = None): + """Create a :class:`.CodecContext` for this codec. + + :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. + """ + from .context import CodecContext + return CodecContext.create(self) + + @property + def mode(self): + return "w" if self.is_encoder else "r" + + @property + def is_decoder(self): + return not self.is_encoder + + @property + def descriptor(self): return wrap_avclass(self.ptr.priv_class) + + @property + def name(self): return self.ptr.name or "" + + @property + def canonical_name(self): + """ + Returns the name of the codec, not a specific encoder. + """ + return lib.avcodec_get_name(self.ptr.id) + + @property + def long_name(self): return self.ptr.long_name or "" + + @property + def type(self): + """ + The media type of this codec. + + E.g: ``'audio'``, ``'video'``, ``'subtitle'``. + + """ + return lib.av_get_media_type_string(self.ptr.type) + + @property + def id(self): return self.ptr.id + + @property + def frame_rates(self): + """A list of supported frame rates (:class:`fractions.Fraction`), or ``None``.""" + if not self.ptr.supported_framerates: + return + + ret = [] + cdef int i = 0 + while self.ptr.supported_framerates[i].denum: + ret.append(avrational_to_fraction(&self.ptr.supported_framerates[i])) + i += 1 + return ret + + @property + def audio_rates(self): + """A list of supported audio sample rates (``int``), or ``None``.""" + if not self.ptr.supported_samplerates: + return + + ret = [] + cdef int i = 0 + while self.ptr.supported_samplerates[i]: + ret.append(self.ptr.supported_samplerates[i]) + i += 1 + return ret + + @property + def video_formats(self): + """A list of supported :class:`.VideoFormat`, or ``None``.""" + if not self.ptr.pix_fmts: + return + + ret = [] + cdef int i = 0 + while self.ptr.pix_fmts[i] != -1: + ret.append(get_video_format(self.ptr.pix_fmts[i], 0, 0)) + i += 1 + return ret + + @property + def audio_formats(self): + """A list of supported :class:`.AudioFormat`, or ``None``.""" + if not self.ptr.sample_fmts: + return + + ret = [] + cdef int i = 0 + while self.ptr.sample_fmts[i] != -1: + ret.append(get_audio_format(self.ptr.sample_fmts[i])) + i += 1 + return ret + + @property + def hardware_configs(self): + if self._hardware_configs: + return self._hardware_configs + ret = [] + cdef int i = 0 + cdef const lib.AVCodecHWConfig *ptr + while True: + ptr = lib.avcodec_get_hw_config(self.ptr, i) + if not ptr: + break + ret.append(wrap_hwconfig(ptr)) + i += 1 + ret = tuple(ret) + self._hardware_configs = ret + return ret + + @property + def properties(self): + return self.desc.props + + @property + def intra_only(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_INTRA_ONLY) + + @property + def lossy(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSY) + + @property + def lossless(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSLESS) + + @property + def reorder(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_REORDER) + + @property + def bitmap_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_BITMAP_SUB) + + @property + def text_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_TEXT_SUB) + + @property + def capabilities(self): + """ + Get the capabilities bitmask of the codec. + + This method returns an integer representing the codec capabilities bitmask, + which can be used to check specific codec features by performing bitwise + operations with the Capabilities enum values. + + :example: + + .. code-block:: python + + from av.codec import Codec, Capabilities + + codec = Codec("h264", "w") + + # Check if the codec can be fed a final frame with a smaller size. + # This can be used to prevent truncation of the last audio samples. + small_last_frame = bool(codec.capabilities & Capabilities.small_last_frame) + + :rtype: int + """ + return self.ptr.capabilities + + @property + def experimental(self): + """ + Check if codec is experimental and is thus avoided in favor of non experimental encoders. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_EXPERIMENTAL) + + @property + def delay(self): + """ + If true, encoder or decoder requires flushing with `None` at the end in order to give the complete and correct output. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_DELAY) + +cdef get_codec_names(): + names = set() + cdef const lib.AVCodec *ptr + cdef void *opaque = NULL + while True: + ptr = lib.av_codec_iterate(&opaque) + if ptr: + names.add(ptr.name) + else: + break + return names + + +codecs_available = get_codec_names() +codec_descriptor = wrap_avclass(lib.avcodec_get_class()) + + +def dump_codecs(): + """Print information about available codecs.""" + + print( + """Codecs: + D..... = Decoding supported + .E.... = Encoding supported + ..V... = Video codec + ..A... = Audio codec + ..S... = Subtitle codec + ...I.. = Intra frame-only codec + ....L. = Lossy compression + .....S = Lossless compression + ------""" + ) + + for name in sorted(codecs_available): + try: + e_codec = Codec(name, "w") + except ValueError: + e_codec = None + + try: + d_codec = Codec(name, "r") + except ValueError: + d_codec = None + + # TODO: Assert these always have the same properties. + codec = e_codec or d_codec + + try: + print( + " %s%s%s%s%s%s %-18s %s" + % ( + ".D"[bool(d_codec)], + ".E"[bool(e_codec)], + codec.type[0].upper(), + ".I"[codec.intra_only], + ".L"[codec.lossy], + ".S"[codec.lossless], + codec.name, + codec.long_name, + ) + ) + except Exception as e: + print(f"...... {codec.name:<18} ERROR: {e}") + +def dump_hwconfigs(): + print("Hardware configs:") + for name in sorted(codecs_available): + try: + codec = Codec(name, "r") + except ValueError: + continue + + configs = codec.hardware_configs + if not configs: + continue + + print(" ", codec.name) + for config in configs: + print(" ", config) diff --git a/lib/python3.10/site-packages/av/codec/context.pxd b/lib/python3.10/site-packages/av/codec/context.pxd new file mode 100644 index 0000000000000000000000000000000000000000..7ba89dab75a619221ace0169cbe119aac7dea48d --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/context.pxd @@ -0,0 +1,64 @@ +cimport libav as lib +from libc.stdint cimport int64_t + +from av.bytesource cimport ByteSource +from av.codec.codec cimport Codec +from av.codec.hwaccel cimport HWAccel +from av.frame cimport Frame +from av.packet cimport Packet + + +cdef class CodecContext: + cdef lib.AVCodecContext *ptr + + # Whether AVCodecContext.extradata should be de-allocated upon destruction. + cdef bint extradata_set + + # Used as a signal that this is within a stream, and also for us to access that + # stream. This is set "manually" by the stream after constructing this object. + cdef int stream_index + + cdef lib.AVCodecParserContext *parser + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel) + + # Public API. + cdef readonly bint is_open + cdef readonly Codec codec + cdef readonly HWAccel hwaccel + cdef public dict options + cpdef open(self, bint strict=?) + + # Wraps both versions of the transcode API, returning lists. + cpdef encode(self, Frame frame=?) + cpdef decode(self, Packet packet=?) + cpdef flush_buffers(self) + + # Used by hardware-accelerated decode. + cdef HWAccel hwaccel_ctx + + # Used by both transcode APIs to setup user-land objects. + # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets + # are bogus). It should take all info it needs from the context and/or stream. + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) + cdef _prepare_frames_for_encode(self, Frame frame) + cdef _setup_encoded_packet(self, Packet) + cdef _setup_decoded_frame(self, Frame, Packet) + + # Implemented by base for the generic send/recv API. + # Note that the user cannot send without receiving. This is because + # `_prepare_frames_for_encode` may expand a frame into multiple (e.g. when + # resampling audio to a higher rate but with fixed size frames), and the + # send/recv buffer may be limited to a single frame. Ergo, we need to flush + # the buffer as often as possible. + cdef _recv_packet(self) + cdef _send_packet_and_recv(self, Packet packet) + cdef _recv_frame(self) + + cdef _transfer_hwframe(self, Frame frame) + + # Implemented by children for the generic send/recv API, so we have the + # correct subclass of Frame. + cdef Frame _next_frame + cdef Frame _alloc_next_frame(self) + +cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*, HWAccel hwaccel) diff --git a/lib/python3.10/site-packages/av/codec/context.pyi b/lib/python3.10/site-packages/av/codec/context.pyi new file mode 100644 index 0000000000000000000000000000000000000000..77810d9ed317a35fe9e6b34c6abe8a155d9f7cbf --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/context.pyi @@ -0,0 +1,118 @@ +from enum import Flag, IntEnum +from fractions import Fraction +from typing import ClassVar, Literal, cast, overload + +from av.audio import _AudioCodecName +from av.audio.codeccontext import AudioCodecContext +from av.packet import Packet +from av.video import _VideoCodecName +from av.video.codeccontext import VideoCodecContext + +from .codec import Codec +from .hwaccel import HWAccel + +class ThreadType(Flag): + NONE = cast(ClassVar[ThreadType], ...) + FRAME = cast(ClassVar[ThreadType], ...) + SLICE = cast(ClassVar[ThreadType], ...) + AUTO = cast(ClassVar[ThreadType], ...) + def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... + def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... + +class Flags(IntEnum): + unaligned = cast(int, ...) + qscale = cast(int, ...) + four_mv = cast(int, ...) + output_corrupt = cast(int, ...) + qpel = cast(int, ...) + drop_changed = cast(int, ...) + recon_frame = cast(int, ...) + copy_opaque = cast(int, ...) + frame_duration = cast(int, ...) + pass1 = cast(int, ...) + pass2 = cast(int, ...) + loop_filter = cast(int, ...) + gray = cast(int, ...) + psnr = cast(int, ...) + interlaced_dct = cast(int, ...) + low_delay = cast(int, ...) + global_header = cast(int, ...) + bitexact = cast(int, ...) + ac_pred = cast(int, ...) + interlaced_me = cast(int, ...) + closed_gop = cast(int, ...) + +class Flags2(IntEnum): + fast = cast(int, ...) + no_output = cast(int, ...) + local_header = cast(int, ...) + chunks = cast(int, ...) + ignore_crop = cast(int, ...) + show_all = cast(int, ...) + export_mvs = cast(int, ...) + skip_manual = cast(int, ...) + ro_flush_noop = cast(int, ...) + +class CodecContext: + name: str + type: Literal["video", "audio", "data", "subtitle", "attachment"] + options: dict[str, str] + profile: str | None + @property + def profiles(self) -> list[str]: ... + extradata: bytes | None + time_base: Fraction + codec_tag: str + bit_rate: int | None + bit_rate_tolerance: int + thread_count: int + thread_type: ThreadType + skip_frame: Literal[ + "NONE", "DEFAULT", "NONREF", "BIDIR", "NONINTRA", "NONKEY", "ALL" + ] + flags: int + qscale: bool + copy_opaque: bool + flags2: int + @property + def is_open(self) -> bool: ... + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... + @property + def codec(self) -> Codec: ... + @property + def max_bit_rate(self) -> int | None: ... + @property + def delay(self) -> bool: ... + @property + def extradata_size(self) -> int: ... + @property + def is_hwaccel(self) -> bool: ... + def open(self, strict: bool = True) -> None: ... + @overload + @staticmethod + def create( + codec: _AudioCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> AudioCodecContext: ... + @overload + @staticmethod + def create( + codec: _VideoCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> VideoCodecContext: ... + @overload + @staticmethod + def create( + codec: str | Codec, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> CodecContext: ... + def parse( + self, raw_input: bytes | bytearray | memoryview | None = None + ) -> list[Packet]: ... + def flush_buffers(self) -> None: ... diff --git a/lib/python3.10/site-packages/av/codec/context.pyx b/lib/python3.10/site-packages/av/codec/context.pyx new file mode 100644 index 0000000000000000000000000000000000000000..5ca8f24a43204f9f74b439b7deab5b34cc90c6cc --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/context.pyx @@ -0,0 +1,672 @@ +cimport libav as lib +from libc.errno cimport EAGAIN +from libc.stdint cimport uint8_t +from libc.string cimport memcpy + +from av.bytesource cimport ByteSource, bytesource +from av.codec.codec cimport Codec, wrap_codec +from av.dictionary cimport _Dictionary +from av.error cimport err_check +from av.packet cimport Packet +from av.utils cimport avrational_to_fraction, to_avrational + +from enum import Flag, IntEnum + +from av.dictionary import Dictionary + + +cdef object _cinit_sentinel = object() + + +cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, HWAccel hwaccel): + """Build an av.CodecContext for an existing AVCodecContext.""" + + cdef CodecContext py_ctx + + if c_ctx.codec_type == lib.AVMEDIA_TYPE_VIDEO: + from av.video.codeccontext import VideoCodecContext + py_ctx = VideoCodecContext(_cinit_sentinel) + elif c_ctx.codec_type == lib.AVMEDIA_TYPE_AUDIO: + from av.audio.codeccontext import AudioCodecContext + py_ctx = AudioCodecContext(_cinit_sentinel) + elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: + from av.subtitles.codeccontext import SubtitleCodecContext + py_ctx = SubtitleCodecContext(_cinit_sentinel) + else: + py_ctx = CodecContext(_cinit_sentinel) + + py_ctx._init(c_ctx, c_codec, hwaccel) + + return py_ctx + + +class ThreadType(Flag): + NONE = 0 + FRAME: "Decode more than one frame at once" = lib.FF_THREAD_FRAME + SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE + AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME + +class Flags(IntEnum): + unaligned = lib.AV_CODEC_FLAG_UNALIGNED + qscale = lib.AV_CODEC_FLAG_QSCALE + four_mv = lib.AV_CODEC_FLAG_4MV + output_corrupt = lib.AV_CODEC_FLAG_OUTPUT_CORRUPT + qpel = lib.AV_CODEC_FLAG_QPEL + drop_changed = 1 << 5 + recon_frame = lib.AV_CODEC_FLAG_RECON_FRAME + copy_opaque = lib.AV_CODEC_FLAG_COPY_OPAQUE + frame_duration = lib.AV_CODEC_FLAG_FRAME_DURATION + pass1 = lib.AV_CODEC_FLAG_PASS1 + pass2 = lib.AV_CODEC_FLAG_PASS2 + loop_filter = lib.AV_CODEC_FLAG_LOOP_FILTER + gray = lib.AV_CODEC_FLAG_GRAY + psnr = lib.AV_CODEC_FLAG_PSNR + interlaced_dct = lib.AV_CODEC_FLAG_INTERLACED_DCT + low_delay = lib.AV_CODEC_FLAG_LOW_DELAY + global_header = lib.AV_CODEC_FLAG_GLOBAL_HEADER + bitexact = lib.AV_CODEC_FLAG_BITEXACT + ac_pred = lib.AV_CODEC_FLAG_AC_PRED + interlaced_me = lib.AV_CODEC_FLAG_INTERLACED_ME + closed_gop = lib.AV_CODEC_FLAG_CLOSED_GOP + +class Flags2(IntEnum): + fast = lib.AV_CODEC_FLAG2_FAST + no_output = lib.AV_CODEC_FLAG2_NO_OUTPUT + local_header = lib.AV_CODEC_FLAG2_LOCAL_HEADER + chunks = lib.AV_CODEC_FLAG2_CHUNKS + ignore_crop = lib.AV_CODEC_FLAG2_IGNORE_CROP + show_all = lib.AV_CODEC_FLAG2_SHOW_ALL + export_mvs = lib.AV_CODEC_FLAG2_EXPORT_MVS + skip_manual = lib.AV_CODEC_FLAG2_SKIP_MANUAL + ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP + + +cdef class CodecContext: + @staticmethod + def create(codec, mode=None, hwaccel=None): + cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) + cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr) + return wrap_codec_context(c_ctx, cy_codec.ptr, hwaccel) + + def __cinit__(self, sentinel=None, *args, **kwargs): + if sentinel is not _cinit_sentinel: + raise RuntimeError("Cannot instantiate CodecContext") + + self.options = {} + self.stream_index = -1 # This is set by the container immediately. + self.is_open = False + + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + self.ptr = ptr + if self.ptr.codec and codec and self.ptr.codec != codec: + raise RuntimeError("Wrapping CodecContext with mismatched codec.") + self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) + self.hwaccel = hwaccel + + # Set reasonable threading defaults. + self.ptr.thread_count = 0 # use as many threads as there are CPUs. + self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. + + @property + def flags(self): + """ + Get and set the flags bitmask of CodecContext. + + :rtype: int + """ + return self.ptr.flags + + @flags.setter + def flags(self, int value): + self.ptr.flags = value + + @property + def qscale(self): + """ + Use fixed qscale. + + :rtype: bool + """ + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_QSCALE) + + @qscale.setter + def qscale(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_QSCALE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_QSCALE + + @property + def copy_opaque(self): + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_COPY_OPAQUE) + + @copy_opaque.setter + def copy_opaque(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_COPY_OPAQUE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_COPY_OPAQUE + + @property + def flags2(self): + """ + Get and set the flags2 bitmask of CodecContext. + + :rtype: int + """ + return self.ptr.flags2 + + @flags2.setter + def flags2(self, int value): + self.ptr.flags2 = value + + @property + def extradata(self): + if self.ptr is NULL: + return None + if self.ptr.extradata_size > 0: + return (self.ptr.extradata)[:self.ptr.extradata_size] + return None + + @extradata.setter + def extradata(self, data): + if data is None: + lib.av_freep(&self.ptr.extradata) + self.ptr.extradata_size = 0 + else: + source = bytesource(data) + self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) + if not self.ptr.extradata: + raise MemoryError("Cannot allocate extradata") + memcpy(self.ptr.extradata, source.ptr, source.length) + self.ptr.extradata_size = source.length + self.extradata_set = True + + @property + def extradata_size(self): + return self.ptr.extradata_size + + @property + def is_encoder(self): + if self.ptr is NULL: + return False + return lib.av_codec_is_encoder(self.ptr.codec) + + @property + def is_decoder(self): + if self.ptr is NULL: + return False + return lib.av_codec_is_decoder(self.ptr.codec) + + cpdef open(self, bint strict=True): + if self.is_open: + if strict: + raise ValueError("CodecContext is already open.") + return + + cdef _Dictionary options = Dictionary() + options.update(self.options or {}) + + if not self.ptr.time_base.num and self.is_encoder: + if self.type == "video": + self.ptr.time_base.num = self.ptr.framerate.den or 1 + self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE + elif self.type == "audio": + self.ptr.time_base.num = 1 + self.ptr.time_base.den = self.ptr.sample_rate + else: + self.ptr.time_base.num = 1 + self.ptr.time_base.den = lib.AV_TIME_BASE + + err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr), "avcodec_open2(" + self.codec.name + ")") + self.is_open = True + self.options = dict(options) + + def __dealloc__(self): + if self.ptr and self.extradata_set: + lib.av_freep(&self.ptr.extradata) + if self.ptr: + lib.avcodec_free_context(&self.ptr) + if self.parser: + lib.av_parser_close(self.parser) + + def __repr__(self): + _type = self.type or "" + name = self.name or "" + return f"" + + def parse(self, raw_input=None): + """Split up a byte stream into list of :class:`.Packet`. + + This is only effectively splitting up a byte stream, and does no + actual interpretation of the data. + + It will return all packets that are fully contained within the given + input, and will buffer partial packets until they are complete. + + :param ByteSource raw_input: A chunk of a byte-stream to process. + Anything that can be turned into a :class:`.ByteSource` is fine. + ``None`` or empty inputs will flush the parser's buffers. + + :return: ``list`` of :class:`.Packet` newly available. + + """ + + if not self.parser: + self.parser = lib.av_parser_init(self.codec.ptr.id) + if not self.parser: + raise ValueError(f"No parser for {self.codec.name}") + + cdef ByteSource source = bytesource(raw_input, allow_none=True) + + cdef unsigned char *in_data = source.ptr if source is not None else NULL + cdef int in_size = source.length if source is not None else 0 + + cdef unsigned char *out_data + cdef int out_size + cdef int consumed + cdef Packet packet = None + + packets = [] + + while True: + with nogil: + consumed = lib.av_parser_parse2( + self.parser, + self.ptr, + &out_data, &out_size, + in_data, in_size, + lib.AV_NOPTS_VALUE, lib.AV_NOPTS_VALUE, + 0 + ) + err_check(consumed) + + if out_size: + # We copy the data immediately, as we have yet to figure out + # the expected lifetime of the buffer we get back. All of the + # examples decode it immediately. + # + # We've also tried: + # packet = Packet() + # packet.data = out_data + # packet.size = out_size + # packet.source = source + # + # ... but this results in corruption. + + packet = Packet(out_size) + memcpy(packet.ptr.data, out_data, out_size) + + packets.append(packet) + + if not in_size: + # This was a flush. Only one packet should ever be returned. + break + + in_data += consumed + in_size -= consumed + + if not in_size: + break + + return packets + + @property + def is_hwaccel(self): + """ + Returns ``True`` if this codec context is hardware accelerated, ``False`` otherwise. + """ + return self.hwaccel_ctx is not None + + def _send_frame_and_recv(self, Frame frame): + cdef Packet packet + + cdef int res + with nogil: + res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) + err_check(res, "avcodec_send_frame()") + + packet = self._recv_packet() + while packet: + yield packet + packet = self._recv_packet() + + cdef _send_packet_and_recv(self, Packet packet): + cdef Frame frame + + cdef int res + with nogil: + res = lib.avcodec_send_packet(self.ptr, packet.ptr if packet is not None else NULL) + err_check(res, "avcodec_send_packet()") + + out = [] + while True: + frame = self._recv_frame() + if frame: + out.append(frame) + else: + break + return out + + cdef _prepare_frames_for_encode(self, Frame frame): + return [frame] + + cdef Frame _alloc_next_frame(self): + raise NotImplementedError("Base CodecContext cannot decode.") + + cdef _recv_frame(self): + if not self._next_frame: + self._next_frame = self._alloc_next_frame() + cdef Frame frame = self._next_frame + + cdef int res + with nogil: + res = lib.avcodec_receive_frame(self.ptr, frame.ptr) + + if res == -EAGAIN or res == lib.AVERROR_EOF: + return + err_check(res, "avcodec_receive_frame()") + + frame = self._transfer_hwframe(frame) + + if not res: + self._next_frame = None + return frame + + cdef _transfer_hwframe(self, Frame frame): + return frame + + cdef _recv_packet(self): + cdef Packet packet = Packet() + + cdef int res + with nogil: + res = lib.avcodec_receive_packet(self.ptr, packet.ptr) + if res == -EAGAIN or res == lib.AVERROR_EOF: + return + err_check(res, "avcodec_receive_packet()") + + if not res: + return packet + + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame): + if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: + raise NotImplementedError("Encoding is only supported for audio and video.") + + self.open(strict=False) + + frames = self._prepare_frames_for_encode(frame) + + # Assert the frames are in our time base. + # TODO: Don't mutate time. + for frame in frames: + if frame is not None: + frame._rebase_time(self.ptr.time_base) + + return frames + + cpdef encode(self, Frame frame=None): + """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" + res = [] + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): + for packet in self._send_frame_and_recv(frame): + self._setup_encoded_packet(packet) + res.append(packet) + return res + + def encode_lazy(self, Frame frame=None): + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): + for packet in self._send_frame_and_recv(frame): + self._setup_encoded_packet(packet) + yield packet + + cdef _setup_encoded_packet(self, Packet packet): + # We coerced the frame's time_base into the CodecContext's during encoding, + # and FFmpeg copied the frame's pts/dts to the packet, so keep track of + # this time_base in case the frame needs to be muxed to a container with + # a different time_base. + # + # NOTE: if the CodecContext's time_base is altered during encoding, all bets + # are off! + packet._time_base = self.ptr.time_base + + cpdef decode(self, Packet packet=None): + """Decode a list of :class:`.Frame` from the given :class:`.Packet`. + + If the packet is None, the buffers will be flushed. This is useful if + you do not want the library to automatically re-order frames for you + (if they are encoded with a codec that has B-frames). + + """ + + if not self.codec.ptr: + raise ValueError("cannot decode unknown codec") + + self.open(strict=False) + + res = [] + for frame in self._send_packet_and_recv(packet): + if isinstance(frame, Frame): + self._setup_decoded_frame(frame, packet) + res.append(frame) + return res + + cpdef flush_buffers(self): + """Reset the internal codec state and discard all internal buffers. + + Should be called before you start decoding from a new position e.g. + when seeking or when switching to a different stream. + + """ + if self.is_open: + with nogil: + lib.avcodec_flush_buffers(self.ptr) + + cdef _setup_decoded_frame(self, Frame frame, Packet packet): + # Propagate our manual times. + # While decoding, frame times are in stream time_base, which PyAV + # is carrying around. + # TODO: Somehow get this from the stream so we can not pass the + # packet here (because flushing packets are bogus). + if packet is not None: + frame._time_base = packet._time_base + + @property + def name(self): + return self.codec.name + + @property + def type(self): + return self.codec.type + + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + ret = [] + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return ret + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + ret.append(desc.profiles[i].name) + i += 1 + + return ret + + @property + def profile(self): + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].profile == self.ptr.profile: + return desc.profiles[i].name + i += 1 + + @profile.setter + def profile(self, value): + if not self.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].name == value: + self.ptr.profile = desc.profiles[i].profile + return + i += 1 + + @property + def time_base(self): + if self.is_decoder: + raise RuntimeError("Cannot access 'time_base' as a decoder") + return avrational_to_fraction(&self.ptr.time_base) + + @time_base.setter + def time_base(self, value): + if self.is_decoder: + raise RuntimeError("Cannot access 'time_base' as a decoder") + to_avrational(value, &self.ptr.time_base) + + @property + def codec_tag(self): + return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( + encoding="ascii") + + @codec_tag.setter + def codec_tag(self, value): + if isinstance(value, str) and len(value) == 4: + self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), + byteorder="little", signed=False) + else: + raise ValueError("Codec tag should be a 4 character string.") + + @property + def bit_rate(self): + return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None + + @bit_rate.setter + def bit_rate(self, int value): + self.ptr.bit_rate = value + + @property + def max_bit_rate(self): + if self.ptr.rc_max_rate > 0: + return self.ptr.rc_max_rate + else: + return None + + @property + def bit_rate_tolerance(self): + self.ptr.bit_rate_tolerance + + @bit_rate_tolerance.setter + def bit_rate_tolerance(self, int value): + self.ptr.bit_rate_tolerance = value + + @property + def thread_count(self): + """How many threads to use; 0 means auto. + + Wraps :ffmpeg:`AVCodecContext.thread_count`. + + """ + return self.ptr.thread_count + + @thread_count.setter + def thread_count(self, int value): + if self.is_open: + raise RuntimeError("Cannot change thread_count after codec is open.") + self.ptr.thread_count = value + + @property + def thread_type(self): + """One of :class:`.ThreadType`. + + Wraps :ffmpeg:`AVCodecContext.thread_type`. + + """ + return ThreadType(self.ptr.thread_type) + + @thread_type.setter + def thread_type(self, value): + if self.is_open: + raise RuntimeError("Cannot change thread_type after codec is open.") + if type(value) is int: + self.ptr.thread_type = value + elif type(value) is str: + self.ptr.thread_type = ThreadType[value].value + else: + self.ptr.thread_type = value.value + + @property + def skip_frame(self): + """Returns one of the following str literals: + + "NONE" Discard nothing + "DEFAULT" Discard useless packets like 0 size packets in AVI + "NONREF" Discard all non reference + "BIDIR" Discard all bidirectional frames + "NONINTRA" Discard all non intra frames + "NONKEY Discard all frames except keyframes + "ALL" Discard all + + Wraps :ffmpeg:`AVCodecContext.skip_frame`. + """ + value = self.ptr.skip_frame + if value == lib.AVDISCARD_NONE: + return "NONE" + if value == lib.AVDISCARD_DEFAULT: + return "DEFAULT" + if value == lib.AVDISCARD_NONREF: + return "NONREF" + if value == lib.AVDISCARD_BIDIR: + return "BIDIR" + if value == lib.AVDISCARD_NONINTRA: + return "NONINTRA" + if value == lib.AVDISCARD_NONKEY: + return "NONKEY" + if value == lib.AVDISCARD_ALL: + return "ALL" + return f"{value}" + + @skip_frame.setter + def skip_frame(self, value): + if value == "NONE": + self.ptr.skip_frame = lib.AVDISCARD_NONE + elif value == "DEFAULT": + self.ptr.skip_frame = lib.AVDISCARD_DEFAULT + elif value == "NONREF": + self.ptr.skip_frame = lib.AVDISCARD_NONREF + elif value == "BIDIR": + self.ptr.skip_frame = lib.AVDISCARD_BIDIR + elif value == "NONINTRA": + self.ptr.skip_frame = lib.AVDISCARD_NONINTRA + elif value == "NONKEY": + self.ptr.skip_frame = lib.AVDISCARD_NONKEY + elif value == "ALL": + self.ptr.skip_frame = lib.AVDISCARD_ALL + else: + raise ValueError("Invalid skip_frame type") + + @property + def delay(self): + """Codec delay. + + Wraps :ffmpeg:`AVCodecContext.delay`. + + """ + return self.ptr.delay diff --git a/lib/python3.10/site-packages/av/codec/hwaccel.pxd b/lib/python3.10/site-packages/av/codec/hwaccel.pxd new file mode 100644 index 0000000000000000000000000000000000000000..46efdaf3b96f6bc50b72fed0fa4109b072adb1ad --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/hwaccel.pxd @@ -0,0 +1,21 @@ +cimport libav as lib + +from av.codec.codec cimport Codec + + +cdef class HWConfig: + cdef object __weakref__ + cdef lib.AVCodecHWConfig *ptr + cdef void _init(self, lib.AVCodecHWConfig *ptr) + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr) + +cdef class HWAccel: + cdef int _device_type + cdef str _device + cdef readonly Codec codec + cdef readonly HWConfig config + cdef lib.AVBufferRef *ptr + cdef public bint allow_software_fallback + cdef public dict options + cdef public int flags diff --git a/lib/python3.10/site-packages/av/codec/hwaccel.pyi b/lib/python3.10/site-packages/av/codec/hwaccel.pyi new file mode 100644 index 0000000000000000000000000000000000000000..8bdc0a6e01d28e92f75cf2445c52b06e213513bb --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/hwaccel.pyi @@ -0,0 +1,50 @@ +from enum import IntEnum +from typing import cast + +from av.codec.codec import Codec +from av.video.format import VideoFormat + +class HWDeviceType(IntEnum): + none = cast(int, ...) + vdpau = cast(int, ...) + cuda = cast(int, ...) + vaapi = cast(int, ...) + dxva2 = cast(int, ...) + qsv = cast(int, ...) + videotoolbox = cast(int, ...) + d3d11va = cast(int, ...) + drm = cast(int, ...) + opencl = cast(int, ...) + mediacodec = cast(int, ...) + vulkan = cast(int, ...) + d3d12va = cast(int, ...) + +class HWConfigMethod(IntEnum): + none = cast(int, ...) + hw_device_ctx = cast(int, ...) + hw_frame_ctx = cast(int, ...) + internal = cast(int, ...) + ad_hoc = cast(int, ...) + +class HWConfig: + @property + def device_type(self) -> HWDeviceType: ... + @property + def format(self) -> VideoFormat: ... + @property + def methods(self) -> HWConfigMethod: ... + @property + def is_supported(self) -> bool: ... + +class HWAccel: + def __init__( + self, + device_type: str | HWDeviceType, + device: str | None = None, + allow_software_fallback: bool = False, + options: dict[str, object] | None = None, + flags: int | None = None, + ) -> None: ... + def create(self, codec: Codec) -> HWAccel: ... + +def hwdevices_available() -> list[str]: ... diff --git a/lib/python3.10/site-packages/av/codec/hwaccel.pyx b/lib/python3.10/site-packages/av/codec/hwaccel.pyx new file mode 100644 index 0000000000000000000000000000000000000000..257e6e7b2d07d1d1358f418ee843e8329291b4b6 --- /dev/null +++ b/lib/python3.10/site-packages/av/codec/hwaccel.pyx @@ -0,0 +1,156 @@ +import weakref +from enum import IntEnum + +cimport libav as lib + +from av.codec.codec cimport Codec +from av.dictionary cimport _Dictionary +from av.error cimport err_check +from av.video.format cimport get_video_format + +from av.dictionary import Dictionary + + +class HWDeviceType(IntEnum): + none = lib.AV_HWDEVICE_TYPE_NONE + vdpau = lib.AV_HWDEVICE_TYPE_VDPAU + cuda = lib.AV_HWDEVICE_TYPE_CUDA + vaapi = lib.AV_HWDEVICE_TYPE_VAAPI + dxva2 = lib.AV_HWDEVICE_TYPE_DXVA2 + qsv = lib.AV_HWDEVICE_TYPE_QSV + videotoolbox = lib.AV_HWDEVICE_TYPE_VIDEOTOOLBOX + d3d11va = lib.AV_HWDEVICE_TYPE_D3D11VA + drm = lib.AV_HWDEVICE_TYPE_DRM + opencl = lib.AV_HWDEVICE_TYPE_OPENCL + mediacodec = lib.AV_HWDEVICE_TYPE_MEDIACODEC + vulkan = lib.AV_HWDEVICE_TYPE_VULKAN + d3d12va = lib.AV_HWDEVICE_TYPE_D3D12VA + +class HWConfigMethod(IntEnum): + none = 0 + hw_device_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX # This is the only one we support. + hw_frame_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX + internal = lib.AV_CODEC_HW_CONFIG_METHOD_INTERNAL + ad_hoc = lib.AV_CODEC_HW_CONFIG_METHOD_AD_HOC + + +cdef object _cinit_sentinel = object() +cdef object _singletons = weakref.WeakValueDictionary() + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr): + try: + return _singletons[ptr] + except KeyError: + pass + cdef HWConfig config = HWConfig(_cinit_sentinel) + config._init(ptr) + _singletons[ptr] = config + return config + + +cdef class HWConfig: + def __init__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("Cannot instantiate CodecContext") + + cdef void _init(self, lib.AVCodecHWConfig *ptr): + self.ptr = ptr + + def __repr__(self): + return ( + f"self.ptr:x}>" + ) + + @property + def device_type(self): + return HWDeviceType(self.ptr.device_type) + + @property + def format(self): + return get_video_format(self.ptr.pix_fmt, 0, 0) + + @property + def methods(self): + return HWConfigMethod(self.ptr.methods) + + @property + def is_supported(self): + return bool(self.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) + + +cpdef hwdevices_available(): + result = [] + + cdef lib.AVHWDeviceType x = lib.AV_HWDEVICE_TYPE_NONE + while True: + x = lib.av_hwdevice_iterate_types(x) + if x == lib.AV_HWDEVICE_TYPE_NONE: + break + result.append(lib.av_hwdevice_get_type_name(HWDeviceType(x))) + + return result + + +cdef class HWAccel: + def __init__(self, device_type, device=None, allow_software_fallback=True, options=None, flags=None): + if isinstance(device_type, HWDeviceType): + self._device_type = device_type + elif isinstance(device_type, str): + self._device_type = int(lib.av_hwdevice_find_type_by_name(device_type)) + elif isinstance(device_type, int): + self._device_type = device_type + else: + raise ValueError("Unknown type for device_type") + + self._device = device + self.allow_software_fallback = allow_software_fallback + self.options = {} if not options else dict(options) + self.flags = 0 if not flags else flags + self.ptr = NULL + self.config = None + + def _initialize_hw_context(self, Codec codec not None): + cdef HWConfig config + for config in codec.hardware_configs: + if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): + continue + if self._device_type and config.device_type != self._device_type: + continue + break + else: + raise NotImplementedError(f"No supported hardware config for {codec}") + + self.config = config + + cdef char *c_device = NULL + if self._device: + device_bytes = self._device.encode() + c_device = device_bytes + cdef _Dictionary c_options = Dictionary(self.options) + + err_check( + lib.av_hwdevice_ctx_create( + &self.ptr, config.ptr.device_type, c_device, c_options.ptr, self.flags + ) + ) + + def create(self, Codec codec not None): + """Create a new hardware accelerator context with the given codec""" + if self.ptr: + raise RuntimeError("Hardware context already initialized") + + ret = HWAccel( + device_type=self._device_type, + device=self._device, + allow_software_fallback=self.allow_software_fallback, + options=self.options + ) + ret._initialize_hw_context(codec) + return ret + + def __dealloc__(self): + if self.ptr: + lib.av_buffer_unref(&self.ptr) diff --git a/lib/python3.10/site-packages/av/container/__init__.pxd b/lib/python3.10/site-packages/av/container/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/container/__init__.py b/lib/python3.10/site-packages/av/container/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..98d49dd4e89c66b2ab3a92cd675ca86b4b719c90 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/__init__.py @@ -0,0 +1,3 @@ +from .core import Container, Flags, open +from .input import InputContainer +from .output import OutputContainer diff --git a/lib/python3.10/site-packages/av/container/__init__.pyi b/lib/python3.10/site-packages/av/container/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..7160777cc06b6ebf854ad7988da98d9740b7a36b --- /dev/null +++ b/lib/python3.10/site-packages/av/container/__init__.pyi @@ -0,0 +1,3 @@ +from .core import * +from .input import * +from .output import * diff --git a/lib/python3.10/site-packages/av/container/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/container/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..227e83ffe9074f08090c4552d0c49a5293b790b3 Binary files /dev/null and b/lib/python3.10/site-packages/av/container/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/container/__pycache__/output.cpython-310.pyc b/lib/python3.10/site-packages/av/container/__pycache__/output.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbb8e0c85eb496af34ee6ef9737ea7661f7cd698 Binary files /dev/null and b/lib/python3.10/site-packages/av/container/__pycache__/output.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/container/core.pxd b/lib/python3.10/site-packages/av/container/core.pxd new file mode 100644 index 0000000000000000000000000000000000000000..87bb792b3c07711c270ae0e7232843e497fd5082 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/core.pxd @@ -0,0 +1,51 @@ +cimport libav as lib + +from av.codec.hwaccel cimport HWAccel +from av.container.pyio cimport PyIOFile +from av.container.streams cimport StreamContainer +from av.dictionary cimport _Dictionary +from av.format cimport ContainerFormat +from av.stream cimport Stream + +# Interrupt callback information, times are in seconds. +ctypedef struct timeout_info: + double start_time + double timeout + + +cdef class Container: + + cdef readonly bint writeable + cdef lib.AVFormatContext *ptr + + cdef readonly object name + cdef readonly str metadata_encoding + cdef readonly str metadata_errors + + cdef readonly PyIOFile file + cdef int buffer_size + cdef bint input_was_opened + cdef readonly object io_open + cdef readonly object open_files + + cdef readonly ContainerFormat format + + cdef readonly dict options + cdef readonly dict container_options + cdef readonly list stream_options + + cdef HWAccel hwaccel + + cdef readonly StreamContainer streams + cdef readonly dict metadata + + # Private API. + cdef _assert_open(self) + cdef int err_check(self, int value) except -1 + + # Timeouts + cdef readonly object open_timeout + cdef readonly object read_timeout + cdef timeout_info interrupt_callback_info + cdef set_timeout(self, object) + cdef start_timeout(self) diff --git a/lib/python3.10/site-packages/av/container/core.pyi b/lib/python3.10/site-packages/av/container/core.pyi new file mode 100644 index 0000000000000000000000000000000000000000..d61d071108d221e3b923c3755988c3198a6e35c3 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/core.pyi @@ -0,0 +1,159 @@ +from enum import Flag, IntEnum +from fractions import Fraction +from pathlib import Path +from types import TracebackType +from typing import Any, Callable, ClassVar, Literal, Type, cast, overload + +from av.codec.hwaccel import HWAccel +from av.format import ContainerFormat + +from .input import InputContainer +from .output import OutputContainer +from .streams import StreamContainer + +Real = int | float | Fraction + +class Flags(Flag): + gen_pts = cast(ClassVar[Flags], ...) + ign_idx = cast(ClassVar[Flags], ...) + non_block = cast(ClassVar[Flags], ...) + ign_dts = cast(ClassVar[Flags], ...) + no_fillin = cast(ClassVar[Flags], ...) + no_parse = cast(ClassVar[Flags], ...) + no_buffer = cast(ClassVar[Flags], ...) + custom_io = cast(ClassVar[Flags], ...) + discard_corrupt = cast(ClassVar[Flags], ...) + flush_packets = cast(ClassVar[Flags], ...) + bitexact = cast(ClassVar[Flags], ...) + sort_dts = cast(ClassVar[Flags], ...) + fast_seek = cast(ClassVar[Flags], ...) + shortest = cast(ClassVar[Flags], ...) + auto_bsf = cast(ClassVar[Flags], ...) + +class AudioCodec(IntEnum): + none = cast(int, ...) + pcm_alaw = cast(int, ...) + pcm_bluray = cast(int, ...) + pcm_dvd = cast(int, ...) + pcm_f16le = cast(int, ...) + pcm_f24le = cast(int, ...) + pcm_f32be = cast(int, ...) + pcm_f32le = cast(int, ...) + pcm_f64be = cast(int, ...) + pcm_f64le = cast(int, ...) + pcm_lxf = cast(int, ...) + pcm_mulaw = cast(int, ...) + pcm_s16be = cast(int, ...) + pcm_s16be_planar = cast(int, ...) + pcm_s16le = cast(int, ...) + pcm_s16le_planar = cast(int, ...) + pcm_s24be = cast(int, ...) + pcm_s24daud = cast(int, ...) + pcm_s24le = cast(int, ...) + pcm_s24le_planar = cast(int, ...) + pcm_s32be = cast(int, ...) + pcm_s32le = cast(int, ...) + pcm_s32le_planar = cast(int, ...) + pcm_s64be = cast(int, ...) + pcm_s64le = cast(int, ...) + pcm_s8 = cast(int, ...) + pcm_s8_planar = cast(int, ...) + pcm_u16be = cast(int, ...) + pcm_u16le = cast(int, ...) + pcm_u24be = cast(int, ...) + pcm_u24le = cast(int, ...) + pcm_u32be = cast(int, ...) + pcm_u32le = cast(int, ...) + pcm_u8 = cast(int, ...) + pcm_vidc = cast(int, ...) + +class Container: + writeable: bool + name: str + metadata_encoding: str + metadata_errors: str + file: Any + buffer_size: int + input_was_opened: bool + io_open: Any + open_files: Any + format: ContainerFormat + options: dict[str, str] + container_options: dict[str, str] + stream_options: list[dict[str, str]] + streams: StreamContainer + metadata: dict[str, str] + open_timeout: Real | None + read_timeout: Real | None + flags: int + + def __enter__(self) -> Container: ... + def __exit__( + self, + exc_type: Type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: ... + def set_timeout(self, timeout: Real | None) -> None: ... + def start_timeout(self) -> None: ... + +@overload +def open( + file: Any, + mode: Literal["r"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, +) -> InputContainer: ... +@overload +def open( + file: str | Path, + mode: Literal["r"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, +) -> InputContainer: ... +@overload +def open( + file: Any, + mode: Literal["w"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, +) -> OutputContainer: ... +@overload +def open( + file: Any, + mode: Literal["r", "w"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, +) -> InputContainer | OutputContainer: ... diff --git a/lib/python3.10/site-packages/av/container/core.pyx b/lib/python3.10/site-packages/av/container/core.pyx new file mode 100644 index 0000000000000000000000000000000000000000..2b9a1244b1f85a082f06b71fa550e39159601fde --- /dev/null +++ b/lib/python3.10/site-packages/av/container/core.pyx @@ -0,0 +1,430 @@ +from cython.operator cimport dereference +from libc.stdint cimport int64_t + +import os +import time +from enum import Flag, IntEnum +from pathlib import Path + +cimport libav as lib + +from av.codec.hwaccel cimport HWAccel +from av.container.core cimport timeout_info +from av.container.input cimport InputContainer +from av.container.output cimport OutputContainer +from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil +from av.error cimport err_check, stash_exception +from av.format cimport build_container_format +from av.utils cimport avdict_to_dict + +from av.dictionary import Dictionary +from av.logging import Capture as LogCapture + + +cdef object _cinit_sentinel = object() + + +# We want to use the monotonic clock if it is available. +cdef object clock = getattr(time, "monotonic", time.time) + +cdef int interrupt_cb (void *p) noexcept nogil: + cdef timeout_info info = dereference( p) + if info.timeout < 0: # timeout < 0 means no timeout + return 0 + + cdef double current_time + with gil: + current_time = clock() + + # Check if the clock has been changed. + if current_time < info.start_time: + # Raise this when we get back to Python. + stash_exception((RuntimeError, RuntimeError("Clock has been changed to before timeout start"), None)) + return 1 + + if current_time > info.start_time + info.timeout: + return 1 + + return 0 + + +cdef int pyav_io_open(lib.AVFormatContext *s, + lib.AVIOContext **pb, + const char *url, + int flags, + lib.AVDictionary **options) noexcept nogil: + with gil: + return pyav_io_open_gil(s, pb, url, flags, options) + + +cdef int pyav_io_open_gil(lib.AVFormatContext *s, + lib.AVIOContext **pb, + const char *url, + int flags, + lib.AVDictionary **options) noexcept: + cdef Container container + cdef object file + cdef PyIOFile pyio_file + try: + container = dereference(s).opaque + + if options is not NULL: + options_dict = avdict_to_dict( + dereference(options), + encoding=container.metadata_encoding, + errors=container.metadata_errors + ) + else: + options_dict = {} + + file = container.io_open( + url if url is not NULL else "", + flags, + options_dict + ) + + pyio_file = PyIOFile( + file, + container.buffer_size, + (flags & lib.AVIO_FLAG_WRITE) != 0 + ) + + # Add it to the container to avoid it being deallocated + container.open_files[pyio_file.iocontext.opaque] = pyio_file + + pb[0] = pyio_file.iocontext + return 0 + + except Exception: + return stash_exception() + + +cdef int pyav_io_close(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept nogil: + with gil: + return pyav_io_close_gil(s, pb) + +cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept: + cdef Container container + cdef int result = 0 + try: + container = dereference(s).opaque + + if container.open_files is not None and pb.opaque in container.open_files: + result = pyio_close_custom_gil(pb) + + # Remove it from the container so that it can be deallocated + del container.open_files[pb.opaque] + else: + result = pyio_close_gil(pb) + + except Exception: + stash_exception() + result = lib.AVERROR_UNKNOWN # Or another appropriate error code + + return result + + +class Flags(Flag): + gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS + ign_idx: "Ignore index." = lib.AVFMT_FLAG_IGNIDX + non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK + ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS + no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN + no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE + no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER + custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO + discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT + flush_packets: "Flush the AVIOContext every packet." = lib.AVFMT_FLAG_FLUSH_PACKETS + bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT + sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS + fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK + shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST + auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF + +class AudioCodec(IntEnum): + """Enumeration for audio codec IDs.""" + none = lib.AV_CODEC_ID_NONE # No codec. + pcm_alaw = lib.AV_CODEC_ID_PCM_ALAW # PCM A-law. + pcm_bluray = lib.AV_CODEC_ID_PCM_BLURAY # PCM Blu-ray. + pcm_dvd = lib.AV_CODEC_ID_PCM_DVD # PCM DVD. + pcm_f16le = lib.AV_CODEC_ID_PCM_F16LE # PCM F16 little-endian. + pcm_f24le = lib.AV_CODEC_ID_PCM_F24LE # PCM F24 little-endian. + pcm_f32be = lib.AV_CODEC_ID_PCM_F32BE # PCM F32 big-endian. + pcm_f32le = lib.AV_CODEC_ID_PCM_F32LE # PCM F32 little-endian. + pcm_f64be = lib.AV_CODEC_ID_PCM_F64BE # PCM F64 big-endian. + pcm_f64le = lib.AV_CODEC_ID_PCM_F64LE # PCM F64 little-endian. + pcm_lxf = lib.AV_CODEC_ID_PCM_LXF # PCM LXF. + pcm_mulaw = lib.AV_CODEC_ID_PCM_MULAW # PCM μ-law. + pcm_s16be = lib.AV_CODEC_ID_PCM_S16BE # PCM signed 16-bit big-endian. + pcm_s16be_planar = lib.AV_CODEC_ID_PCM_S16BE_PLANAR # PCM signed 16-bit big-endian planar. + pcm_s16le = lib.AV_CODEC_ID_PCM_S16LE # PCM signed 16-bit little-endian. + pcm_s16le_planar = lib.AV_CODEC_ID_PCM_S16LE_PLANAR # PCM signed 16-bit little-endian planar. + pcm_s24be = lib.AV_CODEC_ID_PCM_S24BE # PCM signed 24-bit big-endian. + pcm_s24daud = lib.AV_CODEC_ID_PCM_S24DAUD # PCM signed 24-bit D-Cinema audio. + pcm_s24le = lib.AV_CODEC_ID_PCM_S24LE # PCM signed 24-bit little-endian. + pcm_s24le_planar = lib.AV_CODEC_ID_PCM_S24LE_PLANAR # PCM signed 24-bit little-endian planar. + pcm_s32be = lib.AV_CODEC_ID_PCM_S32BE # PCM signed 32-bit big-endian. + pcm_s32le = lib.AV_CODEC_ID_PCM_S32LE # PCM signed 32-bit little-endian. + pcm_s32le_planar = lib.AV_CODEC_ID_PCM_S32LE_PLANAR # PCM signed 32-bit little-endian planar. + pcm_s64be = lib.AV_CODEC_ID_PCM_S64BE # PCM signed 64-bit big-endian. + pcm_s64le = lib.AV_CODEC_ID_PCM_S64LE # PCM signed 64-bit little-endian. + pcm_s8 = lib.AV_CODEC_ID_PCM_S8 # PCM signed 8-bit. + pcm_s8_planar = lib.AV_CODEC_ID_PCM_S8_PLANAR # PCM signed 8-bit planar. + pcm_u16be = lib.AV_CODEC_ID_PCM_U16BE # PCM unsigned 16-bit big-endian. + pcm_u16le = lib.AV_CODEC_ID_PCM_U16LE # PCM unsigned 16-bit little-endian. + pcm_u24be = lib.AV_CODEC_ID_PCM_U24BE # PCM unsigned 24-bit big-endian. + pcm_u24le = lib.AV_CODEC_ID_PCM_U24LE # PCM unsigned 24-bit little-endian. + pcm_u32be = lib.AV_CODEC_ID_PCM_U32BE # PCM unsigned 32-bit big-endian. + pcm_u32le = lib.AV_CODEC_ID_PCM_U32LE # PCM unsigned 32-bit little-endian. + pcm_u8 = lib.AV_CODEC_ID_PCM_U8 # PCM unsigned 8-bit. + pcm_vidc = lib.AV_CODEC_ID_PCM_VIDC # PCM VIDC. + + +cdef class Container: + def __cinit__(self, sentinel, file_, format_name, options, + container_options, stream_options, hwaccel, + metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, + io_open): + + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot construct base Container") + + self.writeable = isinstance(self, OutputContainer) + if not self.writeable and not isinstance(self, InputContainer): + raise RuntimeError("Container cannot be directly extended.") + + if isinstance(file_, str): + self.name = file_ + else: + self.name = str(getattr(file_, "name", "")) + + self.options = dict(options or ()) + self.container_options = dict(container_options or ()) + self.stream_options = [dict(x) for x in stream_options or ()] + + self.hwaccel = hwaccel + + self.metadata_encoding = metadata_encoding + self.metadata_errors = metadata_errors + + self.open_timeout = open_timeout + self.read_timeout = read_timeout + + self.buffer_size = buffer_size + self.io_open = io_open + + acodec = None # no audio codec specified + if format_name is not None: + if ":" in format_name: + format_name, acodec = format_name.split(":") + self.format = ContainerFormat(format_name) + + self.input_was_opened = False + cdef int res + + cdef bytes name_obj = os.fsencode(self.name) + cdef char *name = name_obj + + cdef lib.AVOutputFormat *ofmt + if self.writeable: + + ofmt = self.format.optr if self.format else lib.av_guess_format(NULL, name, NULL) + if ofmt == NULL: + raise ValueError("Could not determine output format") + + with nogil: + # This does not actually open the file. + res = lib.avformat_alloc_output_context2( + &self.ptr, + ofmt, + NULL, + name, + ) + self.err_check(res) + + else: + # We need the context before we open the input AND setup Python IO. + self.ptr = lib.avformat_alloc_context() + + # Setup interrupt callback + if self.open_timeout is not None or self.read_timeout is not None: + self.ptr.interrupt_callback.callback = interrupt_cb + self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info + + if acodec is not None: + self.ptr.audio_codec_id = getattr(AudioCodec, acodec) + + self.ptr.flags |= lib.AVFMT_FLAG_GENPTS + self.ptr.opaque = self + + # Setup Python IO. + self.open_files = {} + if not isinstance(file_, basestring): + self.file = PyIOFile(file_, buffer_size, self.writeable) + self.ptr.pb = self.file.iocontext + + if io_open is not None: + self.ptr.io_open = pyav_io_open + self.ptr.io_close2 = pyav_io_close + self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO + + cdef lib.AVInputFormat *ifmt + cdef _Dictionary c_options + if not self.writeable: + ifmt = self.format.iptr if self.format else NULL + c_options = Dictionary(self.options, self.container_options) + + self.set_timeout(self.open_timeout) + self.start_timeout() + with nogil: + res = lib.avformat_open_input(&self.ptr, name, ifmt, &c_options.ptr) + self.set_timeout(None) + self.err_check(res) + self.input_was_opened = True + + if format_name is None: + self.format = build_container_format(self.ptr.iformat, self.ptr.oformat) + + def __dealloc__(self): + with nogil: + lib.avformat_free_context(self.ptr) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def __repr__(self): + return f"" + + cdef int err_check(self, int value) except -1: + return err_check(value, filename=self.name) + + def dumps_format(self): + self._assert_open() + with LogCapture() as logs: + lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer)) + return "".join(log[2] for log in logs) + + cdef set_timeout(self, timeout): + if timeout is None: + self.interrupt_callback_info.timeout = -1.0 + else: + self.interrupt_callback_info.timeout = timeout + + cdef start_timeout(self): + self.interrupt_callback_info.start_time = clock() + + cdef _assert_open(self): + if self.ptr == NULL: + raise AssertionError("Container is not open") + + @property + def flags(self): + self._assert_open() + return self.ptr.flags + + @flags.setter + def flags(self, int value): + self._assert_open() + self.ptr.flags = value + +def open( + file, + mode=None, + format=None, + options=None, + container_options=None, + stream_options=None, + metadata_encoding="utf-8", + metadata_errors="strict", + buffer_size=32768, + timeout=None, + io_open=None, + hwaccel=None +): + """open(file, mode='r', **kwargs) + + Main entrypoint to opening files/streams. + + :param str file: The file to open, which can be either a string or a file-like object. + :param str mode: ``"r"`` for reading and ``"w"`` for writing. + :param str format: Specific format to use. Defaults to autodect. + :param dict options: Options to pass to the container and all streams. + :param dict container_options: Options to pass to the container. + :param list stream_options: Options to pass to each stream. + :param str metadata_encoding: Encoding to use when reading or writing file metadata. + Defaults to ``"utf-8"``. + :param str metadata_errors: Specifies how to handle encoding errors; behaves like + ``str.encode`` parameter. Defaults to ``"strict"``. + :param int buffer_size: Size of buffer for Python input/output operations in bytes. + Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). + :param timeout: How many seconds to wait for data before giving up, as a float, or a + ``(open timeout, read timeout)`` tuple. + :param callable io_open: Custom I/O callable for opening files/streams. + This option is intended for formats that need to open additional + file-like objects to ``file`` using custom I/O. + The callable signature is ``io_open(url: str, flags: int, options: dict)``, where + ``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and + ``options`` is a dictionary of additional options. The callable should return a + file-like object. + :param HWAccel hwaccel: Optional settings for hardware-accelerated decoding. + :rtype: Container + + For devices (via ``libavdevice``), pass the name of the device to ``format``, + e.g.:: + + >>> # Open webcam on MacOS. + >>> av.open('0', format='avfoundation') # doctest: +SKIP + + For DASH and custom I/O using ``io_open``, add a protocol prefix to the ``file`` to + prevent the DASH encoder defaulting to the file protocol and using temporary files. + The custom I/O callable can be used to remove the protocol prefix to reveal the actual + name for creating the file-like object. E.g.:: + + >>> av.open("customprotocol://manifest.mpd", "w", io_open=custom_io) # doctest: +SKIP + + .. seealso:: :ref:`garbage_collection` + + More information on using input and output devices is available on the + `FFmpeg website `_. + """ + + if not (mode is None or (isinstance(mode, str) and mode == "r" or mode == "w")): + raise ValueError(f"mode must be 'r', 'w', or None, got: {mode}") + + if isinstance(file, str): + pass + elif isinstance(file, Path): + file = f"{file}" + elif mode is None: + mode = getattr(file, "mode", None) + + if mode is None: + mode = "r" + + if isinstance(timeout, tuple): + if not len(timeout) == 2: + raise ValueError("timeout must be `float` or `tuple[float, float]`") + + open_timeout, read_timeout = timeout + else: + open_timeout = timeout + read_timeout = timeout + + if mode.startswith("r"): + return InputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, + ) + + if stream_options: + raise ValueError( + "Provide stream options via Container.add_stream(..., options={})." + ) + return OutputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, None, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, + ) diff --git a/lib/python3.10/site-packages/av/container/input.pxd b/lib/python3.10/site-packages/av/container/input.pxd new file mode 100644 index 0000000000000000000000000000000000000000..8c369d8ad24b97981e6f6c34ceea21986ec94681 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/input.pxd @@ -0,0 +1,9 @@ +cimport libav as lib + +from av.container.core cimport Container +from av.stream cimport Stream + + +cdef class InputContainer(Container): + + cdef flush_buffers(self) diff --git a/lib/python3.10/site-packages/av/container/input.pyi b/lib/python3.10/site-packages/av/container/input.pyi new file mode 100644 index 0000000000000000000000000000000000000000..90154c331746478d7cd8d22100c21164b37331b7 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/input.pyi @@ -0,0 +1,49 @@ +from typing import Any, Iterator, overload + +from av.audio.frame import AudioFrame +from av.audio.stream import AudioStream +from av.packet import Packet +from av.stream import Stream +from av.subtitles.stream import SubtitleStream +from av.subtitles.subtitle import SubtitleSet +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .core import Container + +class InputContainer(Container): + start_time: int + duration: int | None + bit_rate: int + size: int + + def __enter__(self) -> InputContainer: ... + def close(self) -> None: ... + def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... + @overload + def decode(self, video: int) -> Iterator[VideoFrame]: ... + @overload + def decode(self, audio: int) -> Iterator[AudioFrame]: ... + @overload + def decode(self, subtitles: int) -> Iterator[SubtitleSet]: ... + @overload + def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... + @overload + def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ... + @overload + def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ... + @overload + def decode( + self, *args: Any, **kwargs: Any + ) -> Iterator[VideoFrame | AudioFrame | SubtitleSet]: ... + def seek( + self, + offset: int, + *, + backward: bool = True, + any_frame: bool = False, + stream: Stream | VideoStream | AudioStream | None = None, + unsupported_frame_offset: bool = False, + unsupported_byte_offset: bool = False, + ) -> None: ... + def flush_buffers(self) -> None: ... diff --git a/lib/python3.10/site-packages/av/container/input.pyx b/lib/python3.10/site-packages/av/container/input.pyx new file mode 100644 index 0000000000000000000000000000000000000000..1ba4750d725315e31d8c9fe6cbb8212386e0de83 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/input.pyx @@ -0,0 +1,290 @@ +from libc.stdint cimport int64_t +from libc.stdlib cimport free, malloc + +from av.codec.context cimport CodecContext, wrap_codec_context +from av.container.streams cimport StreamContainer +from av.dictionary cimport _Dictionary +from av.error cimport err_check +from av.packet cimport Packet +from av.stream cimport Stream, wrap_stream +from av.utils cimport avdict_to_dict + +from av.dictionary import Dictionary + + +cdef close_input(InputContainer self): + self.streams = StreamContainer() + if self.input_was_opened: + with nogil: + # This causes `self.ptr` to be set to NULL. + lib.avformat_close_input(&self.ptr) + self.input_was_opened = False + + +cdef class InputContainer(Container): + def __cinit__(self, *args, **kwargs): + cdef CodecContext py_codec_context + cdef unsigned int i + cdef lib.AVStream *stream + cdef lib.AVCodec *codec + cdef lib.AVCodecContext *codec_context + + # If we have either the global `options`, or a `stream_options`, prepare + # a mashup of those options for each stream. + cdef lib.AVDictionary **c_options = NULL + cdef _Dictionary base_dict, stream_dict + if self.options or self.stream_options: + base_dict = Dictionary(self.options) + c_options = malloc(self.ptr.nb_streams * sizeof(void*)) + for i in range(self.ptr.nb_streams): + c_options[i] = NULL + if i < len(self.stream_options) and self.stream_options: + stream_dict = base_dict.copy() + stream_dict.update(self.stream_options[i]) + lib.av_dict_copy(&c_options[i], stream_dict.ptr, 0) + else: + lib.av_dict_copy(&c_options[i], base_dict.ptr, 0) + + self.set_timeout(self.open_timeout) + self.start_timeout() + with nogil: + # This peeks are the first few frames to: + # - set stream.disposition from codec.audio_service_type (not exposed); + # - set stream.codec.bits_per_coded_sample; + # - set stream.duration; + # - set stream.start_time; + # - set stream.r_frame_rate to average value; + # - open and closes codecs with the options provided. + ret = lib.avformat_find_stream_info( + self.ptr, + c_options + ) + self.set_timeout(None) + self.err_check(ret) + + # Cleanup all of our options. + if c_options: + for i in range(self.ptr.nb_streams): + lib.av_dict_free(&c_options[i]) + free(c_options) + + at_least_one_accelerated_context = False + + self.streams = StreamContainer() + for i in range(self.ptr.nb_streams): + stream = self.ptr.streams[i] + codec = lib.avcodec_find_decoder(stream.codecpar.codec_id) + if codec: + # allocate and initialise decoder + codec_context = lib.avcodec_alloc_context3(codec) + err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) + codec_context.pkt_timebase = stream.time_base + py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) + if py_codec_context.is_hwaccel: + at_least_one_accelerated_context = True + else: + # no decoder is available + py_codec_context = None + self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) + + if self.hwaccel and not self.hwaccel.allow_software_fallback and not at_least_one_accelerated_context: + raise RuntimeError("Hardware accelerated decode requested but no stream is compatible") + + self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors) + + def __dealloc__(self): + close_input(self) + + @property + def start_time(self): + self._assert_open() + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time + + @property + def duration(self): + self._assert_open() + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration + + @property + def bit_rate(self): + self._assert_open() + return self.ptr.bit_rate + + @property + def size(self): + self._assert_open() + return lib.avio_size(self.ptr.pb) + + def close(self): + close_input(self) + + def demux(self, *args, **kwargs): + """demux(streams=None, video=None, audio=None, subtitles=None, data=None) + + Yields a series of :class:`.Packet` from the given set of :class:`.Stream`:: + + for packet in container.demux(): + # Do something with `packet`, often: + for frame in packet.decode(): + # Do something with `frame`. + + .. seealso:: :meth:`.StreamContainer.get` for the interpretation of + the arguments. + + .. note:: The last packets are dummy packets that when decoded will flush the buffers. + + """ + self._assert_open() + + # For whatever reason, Cython does not like us directly passing kwargs + # from one method to another. Without kwargs, it ends up passing a + # NULL reference, which segfaults. So we force it to do something with it. + # This is likely a bug in Cython; see https://github.com/cython/cython/issues/2166 + # (and others). + id(kwargs) + + streams = self.streams.get(*args, **kwargs) + + cdef bint *include_stream = malloc(self.ptr.nb_streams * sizeof(bint)) + if include_stream == NULL: + raise MemoryError() + + cdef unsigned int i + cdef Packet packet + cdef int ret + + self.set_timeout(self.read_timeout) + try: + for i in range(self.ptr.nb_streams): + include_stream[i] = False + for stream in streams: + i = stream.index + if i >= self.ptr.nb_streams: + raise ValueError(f"stream index {i} out of range") + include_stream[i] = True + + while True: + packet = Packet() + try: + self.start_timeout() + with nogil: + ret = lib.av_read_frame(self.ptr, packet.ptr) + self.err_check(ret) + except EOFError: + break + + if include_stream[packet.ptr.stream_index]: + # If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams + # may also appear in av_read_frame(). + # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html + # TODO: find better way to handle this + if packet.ptr.stream_index < len(self.streams): + packet._stream = self.streams[packet.ptr.stream_index] + # Keep track of this so that remuxing is easier. + packet._time_base = packet._stream.ptr.time_base + yield packet + + # Flush! + for i in range(self.ptr.nb_streams): + if include_stream[i]: + packet = Packet() + packet._stream = self.streams[i] + packet._time_base = packet._stream.ptr.time_base + yield packet + + finally: + self.set_timeout(None) + free(include_stream) + + def decode(self, *args, **kwargs): + """decode(streams=None, video=None, audio=None, subtitles=None, data=None) + + Yields a series of :class:`.Frame` from the given set of streams:: + + for frame in container.decode(): + # Do something with `frame`. + + .. seealso:: :meth:`.StreamContainer.get` for the interpretation of + the arguments. + + """ + self._assert_open() + id(kwargs) # Avoid Cython bug; see demux(). + for packet in self.demux(*args, **kwargs): + for frame in packet.decode(): + yield frame + + def seek( + self, offset, *, bint backward=True, bint any_frame=False, Stream stream=None, + bint unsupported_frame_offset=False, bint unsupported_byte_offset=False + ): + """seek(offset, *, backward=True, any_frame=False, stream=None) + + Seek to a (key)frame nearsest to the given timestamp. + + :param int offset: Time to seek to, expressed in``stream.time_base`` if ``stream`` + is given, otherwise in :data:`av.time_base`. + :param bool backward: If there is not a (key)frame at the given offset, + look backwards for it. + :param bool any_frame: Seek to any frame, not just a keyframe. + :param Stream stream: The stream who's ``time_base`` the ``offset`` is in. + + :param bool unsupported_frame_offset: ``offset`` is a frame + index instead of a time; not supported by any known format. + :param bool unsupported_byte_offset: ``offset`` is a byte + location in the file; not supported by any known format. + + After seeking, packets that you demux should correspond (roughly) to + the position you requested. + + In most cases, the defaults of ``backwards = True`` and ``any_frame = False`` + are the best course of action, followed by you demuxing/decoding to + the position that you want. This is becase to properly decode video frames + you need to start from the previous keyframe. + + .. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags. + + """ + self._assert_open() + + # We used to take floats here and assume they were in seconds. This + # was super confusing, so lets go in the complete opposite direction + # and reject non-ints. + if not isinstance(offset, int): + raise TypeError("Container.seek only accepts integer offset.", type(offset)) + + cdef int64_t c_offset = offset + + cdef int flags = 0 + cdef int ret + + if backward: + flags |= lib.AVSEEK_FLAG_BACKWARD + if any_frame: + flags |= lib.AVSEEK_FLAG_ANY + + # If someone really wants (and to experiment), expose these. + if unsupported_frame_offset: + flags |= lib.AVSEEK_FLAG_FRAME + if unsupported_byte_offset: + flags |= lib.AVSEEK_FLAG_BYTE + + cdef int stream_index = stream.index if stream else -1 + with nogil: + ret = lib.av_seek_frame(self.ptr, stream_index, c_offset, flags) + err_check(ret) + + self.flush_buffers() + + cdef flush_buffers(self): + self._assert_open() + + cdef Stream stream + cdef CodecContext codec_context + + for stream in self.streams: + codec_context = stream.codec_context + if codec_context: + codec_context.flush_buffers() diff --git a/lib/python3.10/site-packages/av/container/output.pxd b/lib/python3.10/site-packages/av/container/output.pxd new file mode 100644 index 0000000000000000000000000000000000000000..51d3f308edcec45b301ed55f9d85bc5cf77d505d --- /dev/null +++ b/lib/python3.10/site-packages/av/container/output.pxd @@ -0,0 +1,12 @@ +cimport libav as lib + +from av.container.core cimport Container +from av.stream cimport Stream + + +cdef class OutputContainer(Container): + cdef bint _started + cdef bint _done + cdef lib.AVPacket *packet_ptr + + cpdef start_encoding(self) diff --git a/lib/python3.10/site-packages/av/container/output.py b/lib/python3.10/site-packages/av/container/output.py new file mode 100644 index 0000000000000000000000000000000000000000..d035b0265e604ed60ad71565a0171cd15481f04c --- /dev/null +++ b/lib/python3.10/site-packages/av/container/output.py @@ -0,0 +1,399 @@ +import os +from fractions import Fraction + +import cython +from cython.cimports import libav as lib +from cython.cimports.av.codec.codec import Codec +from cython.cimports.av.codec.context import CodecContext, wrap_codec_context +from cython.cimports.av.container.streams import StreamContainer +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream, wrap_stream +from cython.cimports.av.utils import dict_to_avdict, to_avrational + +from av.dictionary import Dictionary + + +@cython.cfunc +def close_output(self: OutputContainer): + self.streams = StreamContainer() + if self._started and not self._done: + # We must only ever call av_write_trailer *once*, otherwise we get a + # segmentation fault. Therefore no matter whether it succeeds or not + # we must absolutely set self._done. + try: + self.err_check(lib.av_write_trailer(self.ptr)) + finally: + if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE): + lib.avio_closep(cython.address(self.ptr.pb)) + self._done = True + + +@cython.cclass +class OutputContainer(Container): + def __cinit__(self, *args, **kwargs): + self.streams = StreamContainer() + self.metadata = {} + with cython.nogil: + self.packet_ptr = lib.av_packet_alloc() + + def __dealloc__(self): + close_output(self) + with cython.nogil: + lib.av_packet_free(cython.address(self.packet_ptr)) + + def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwargs): + """add_stream(codec_name, rate=None) + + Creates a new stream from a codec name and returns it. + Supports video, audio, and subtitle streams. + + :param codec_name: The name of a codec. + :type codec_name: str + :param dict options: Stream options. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. + + """ + + codec_obj: Codec = Codec(codec_name, "w") + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr + + # Assert that this format supports the requested codec. + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): + raise ValueError( + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" + ) + + # Create new stream in the AVFormatContext, set AVCodecContext values. + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) + + # Now lets set some more sane video defaults + if codec.type == lib.AVMEDIA_TYPE_VIDEO: + ctx.pix_fmt = lib.AV_PIX_FMT_YUV420P + ctx.width = kwargs.pop("width", 640) + ctx.height = kwargs.pop("height", 480) + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) + try: + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) + except KeyError: + pass + to_avrational(rate or 24, cython.address(ctx.framerate)) + + stream.avg_frame_rate = ctx.framerate + stream.time_base = ctx.time_base + + # Some sane audio defaults + elif codec.type == lib.AVMEDIA_TYPE_AUDIO: + ctx.sample_fmt = codec.sample_fmts[0] + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) + try: + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) + except KeyError: + pass + + if rate is None: + ctx.sample_rate = 48000 + elif type(rate) is int: + ctx.sample_rate = rate + else: + raise TypeError("audio stream `rate` must be: int | None") + stream.time_base = ctx.time_base + lib.av_channel_layout_default(cython.address(ctx.ch_layout), 2) + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialise stream codec parameters to populate the codec type. + # + # Subsequent changes to the codec context will be applied just before + # encoding starts in `start_encoding()`. + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) + + # Construct the user-land stream + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if options: + py_stream.options.update(options) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + + def add_stream_from_template( + self, template: Stream, opaque: bool | None = None, **kwargs + ): + """ + Creates a new stream from a template. Supports video, audio, and subtitle streams. + + :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param opaque: If True, copy opaque data from the template's codec context. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. + """ + if opaque is None: + opaque = template.type != "video" + + codec_obj: Codec + if opaque: # Copy ctx from template. + codec_obj = template.codec_context.codec + else: # Construct new codec object. + codec_obj = Codec(template.codec_context.codec.name, "w") + + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr + + # Assert that this format supports the requested codec. + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): + raise ValueError( + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" + ) + + # Create new stream in the AVFormatContext, set AVCodecContext values. + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) + + err_check(lib.avcodec_parameters_to_context(ctx, template.ptr.codecpar)) + # Reset the codec tag assuming we are remuxing. + ctx.codec_tag = 0 + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Copy flags If we're creating a new codec object. This fixes some muxing issues. + # Overwriting `ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER` is intentional. + if not opaque: + ctx.flags = template.codec_context.flags + + # Initialize stream codec parameters to populate the codec type. Subsequent changes to + # the codec context will be applied just before encoding starts in `start_encoding()`. + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) + + # Construct the user-land stream + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + + def add_data_stream(self, codec_name=None, options: dict | None = None): + """add_data_stream(codec_name=None) + + Creates a new data stream and returns it. + + :param codec_name: Optional name of the data codec (e.g. 'klv') + :type codec_name: str | None + :param dict options: Stream options. + :rtype: The new :class:`~av.data.stream.DataStream`. + """ + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + + if codec_name is not None: + codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) + if codec == cython.NULL: + raise ValueError(f"Unknown data codec: {codec_name}") + + # Assert that this format supports the requested codec + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) + + # Create new stream in the AVFormatContext + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + # Set up codec context if we have a codec + ctx: cython.pointer[lib.AVCodecContext] = cython.NULL + if codec != cython.NULL: + ctx = lib.avcodec_alloc_context3(codec) + if ctx == cython.NULL: + raise MemoryError("Could not allocate codec context") + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialize stream codec parameters + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) + else: + # For raw data streams, just set the codec type + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA + + # Construct the user-land stream + py_codec_context: CodecContext | None = None + if ctx != cython.NULL: + py_codec_context = wrap_codec_context(ctx, codec, None) + + py_stream: Stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if options: + py_stream.options.update(options) + + return py_stream + + @cython.ccall + def start_encoding(self): + """Write the file header! Called automatically.""" + if self._started: + return + + # TODO: This does NOT handle options coming from 3 sources. + # This is only a rough approximation of what would be cool to do. + used_options: set = set() + stream: Stream + + # Finalize and open all streams. + for stream in self.streams: + ctx = stream.codec_context + # Skip codec context handling for data streams without codecs + if ctx is None: + if stream.type != "data": + raise ValueError(f"Stream {stream.index} has no codec context") + continue + + if not ctx.is_open: + for k, v in self.options.items(): + ctx.options.setdefault(k, v) + ctx.open() + + # Track option consumption. + for k in self.options: + if k not in ctx.options: + used_options.add(k) + + stream._finalize_for_output() + + # Open the output file, if needed. + name_obj: bytes = os.fsencode(self.name if self.file is None else "") + name: cython.p_char = name_obj + if self.ptr.pb == cython.NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: + err_check( + lib.avio_open(cython.address(self.ptr.pb), name, lib.AVIO_FLAG_WRITE) + ) + + # Copy the metadata dict. + dict_to_avdict( + cython.address(self.ptr.metadata), + self.metadata, + encoding=self.metadata_encoding, + errors=self.metadata_errors, + ) + + all_options: _Dictionary = Dictionary(self.options, self.container_options) + options: _Dictionary = all_options.copy() + self.err_check(lib.avformat_write_header(self.ptr, cython.address(options.ptr))) + + # Track option usage... + for k in all_options: + if k not in options: + used_options.add(k) + + # ... and warn if any weren't used. + unused_options = { + k: v for k, v in self.options.items() if k not in used_options + } + if unused_options: + import logging + + log = logging.getLogger(__name__) + log.warning("Some options were not used: %s" % unused_options) + + self._started = True + + @property + def supported_codecs(self): + """ + Returns a set of all codecs this format supports. + """ + result: set = set() + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + opaque: cython.p_void = cython.NULL + + while True: + codec = lib.av_codec_iterate(cython.address(opaque)) + if codec == cython.NULL: + break + + if ( + lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ) + == 1 + ): + result.add(codec.name) + + return result + + @property + def default_video_codec(self): + """ + Returns the default video codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.video_codec) + + @property + def default_audio_codec(self): + """ + Returns the default audio codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.audio_codec) + + @property + def default_subtitle_codec(self): + """ + Returns the default subtitle codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.subtitle_codec) + + def close(self): + close_output(self) + + def mux(self, packets): + # We accept either a Packet, or a sequence of packets. This should smooth out + # the transition to the new encode API which returns a sequence of packets. + if isinstance(packets, Packet): + self.mux_one(packets) + else: + for packet in packets: + self.mux_one(packet) + + def mux_one(self, packet: Packet): + self.start_encoding() + + # Assert the packet is in stream time. + if ( + packet.ptr.stream_index < 0 + or cython.cast(cython.uint, packet.ptr.stream_index) >= self.ptr.nb_streams + ): + raise ValueError("Bad Packet stream_index.") + + stream: cython.pointer[lib.AVStream] = self.ptr.streams[packet.ptr.stream_index] + packet._rebase_time(stream.time_base) + + # Make another reference to the packet, as `av_interleaved_write_frame()` + # takes ownership of the reference. + self.err_check(lib.av_packet_ref(self.packet_ptr, packet.ptr)) + + with cython.nogil: + ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) + self.err_check(ret) diff --git a/lib/python3.10/site-packages/av/container/output.pyi b/lib/python3.10/site-packages/av/container/output.pyi new file mode 100644 index 0000000000000000000000000000000000000000..b370095de88df6307eb8b40f520abc3f18d953af --- /dev/null +++ b/lib/python3.10/site-packages/av/container/output.pyi @@ -0,0 +1,59 @@ +from fractions import Fraction +from typing import Sequence, TypeVar, Union, overload + +from av.audio import _AudioCodecName +from av.audio.stream import AudioStream +from av.packet import Packet +from av.stream import DataStream +from av.subtitles.stream import SubtitleStream +from av.video import _VideoCodecName +from av.video.stream import VideoStream + +from .core import Container + +_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream]) + +class OutputContainer(Container): + def __enter__(self) -> OutputContainer: ... + @overload + def add_stream( + self, + codec_name: _AudioCodecName, + rate: int | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> AudioStream: ... + @overload + def add_stream( + self, + codec_name: _VideoCodecName, + rate: Fraction | int | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> VideoStream: ... + @overload + def add_stream( + self, + codec_name: str, + rate: Fraction | int | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> VideoStream | AudioStream | SubtitleStream: ... + def add_stream_from_template( + self, template: _StreamT, opaque: bool | None = None, **kwargs + ) -> _StreamT: ... + def add_data_stream( + self, codec_name: str | None = None, options: dict[str, str] | None = None + ) -> DataStream: ... + def start_encoding(self) -> None: ... + def close(self) -> None: ... + def mux(self, packets: Packet | Sequence[Packet]) -> None: ... + def mux_one(self, packet: Packet) -> None: ... + @property + def default_video_codec(self) -> str: ... + @property + def default_audio_codec(self) -> str: ... + @property + def default_subtitle_codec(self) -> str: ... + @property + def supported_codecs(self) -> set[str]: ... diff --git a/lib/python3.10/site-packages/av/container/pyio.pxd b/lib/python3.10/site-packages/av/container/pyio.pxd new file mode 100644 index 0000000000000000000000000000000000000000..80edc8a6b7a95f6b3568fd24d93a0a4036f135b2 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/pyio.pxd @@ -0,0 +1,24 @@ +cimport libav as lib +from libc.stdint cimport int64_t, uint8_t + + +cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil +cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil +cdef int pyio_close_gil(lib.AVIOContext *pb) +cdef int pyio_close_custom_gil(lib.AVIOContext *pb) + +cdef class PyIOFile: + # File-like source. + cdef readonly object file + cdef object fread + cdef object fwrite + cdef object fseek + cdef object ftell + cdef object fclose + + # Custom IO for above. + cdef lib.AVIOContext *iocontext + cdef unsigned char *buffer + cdef long pos + cdef bint pos_is_valid diff --git a/lib/python3.10/site-packages/av/container/pyio.pyx b/lib/python3.10/site-packages/av/container/pyio.pyx new file mode 100644 index 0000000000000000000000000000000000000000..c8b82f96aa13ac8c363e453edcfbe784ca0d3e46 --- /dev/null +++ b/lib/python3.10/site-packages/av/container/pyio.pyx @@ -0,0 +1,169 @@ +cimport libav as lib +from libc.string cimport memcpy + +from av.error cimport stash_exception + +ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil + + +cdef class PyIOFile: + def __cinit__(self, file, buffer_size, writeable=None): + self.file = file + + cdef seek_func_t seek_func = NULL + + readable = getattr(self.file, "readable", None) + writable = getattr(self.file, "writable", None) + seekable = getattr(self.file, "seekable", None) + self.fread = getattr(self.file, "read", None) + self.fwrite = getattr(self.file, "write", None) + self.fseek = getattr(self.file, "seek", None) + self.ftell = getattr(self.file, "tell", None) + self.fclose = getattr(self.file, "close", None) + + # To be seekable the file object must have `seek` and `tell` methods. + # If it also has a `seekable` method, it must return True. + if ( + self.fseek is not None + and self.ftell is not None + and (seekable is None or seekable()) + ): + seek_func = pyio_seek + + if writeable is None: + writeable = self.fwrite is not None + + if writeable: + if self.fwrite is None or (writable is not None and not writable()): + raise ValueError("File object has no write() method, or writable() returned False.") + else: + if self.fread is None or (readable is not None and not readable()): + raise ValueError("File object has no read() method, or readable() returned False.") + + self.pos = 0 + self.pos_is_valid = True + + # This is effectively the maximum size of reads. + self.buffer = lib.av_malloc(buffer_size) + + self.iocontext = lib.avio_alloc_context( + self.buffer, + buffer_size, + writeable, + self, # User data. + pyio_read, + pyio_write, + seek_func + ) + + if seek_func: + self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL + self.iocontext.max_packet_size = buffer_size + + def __dealloc__(self): + with nogil: + # FFmpeg will not release custom input, so it's up to us to free it. + # Do not touch our original buffer as it may have been freed and replaced. + if self.iocontext: + lib.av_freep(&self.iocontext.buffer) + lib.av_freep(&self.iocontext) + + # We likely errored badly if we got here, and so are still + # responsible for our buffer. + else: + lib.av_freep(&self.buffer) + + +cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: + with gil: + return pyio_read_gil(opaque, buf, buf_size) + +cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: + cdef PyIOFile self + cdef bytes res + try: + self = opaque + res = self.fread(buf_size) + memcpy(buf, res, len(res)) + self.pos += len(res) + if not res: + return lib.AVERROR_EOF + return len(res) + except Exception: + return stash_exception() + + +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil: + with gil: + return pyio_write_gil(opaque, buf, buf_size) + +cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept: + cdef PyIOFile self + cdef bytes bytes_to_write + cdef int bytes_written + try: + self = opaque + bytes_to_write = buf[:buf_size] + ret_value = self.fwrite(bytes_to_write) + bytes_written = ret_value if isinstance(ret_value, int) else buf_size + self.pos += bytes_written + return bytes_written + except Exception: + return stash_exception() + + +cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil: + # Seek takes the standard flags, but also a ad-hoc one which means that + # the library wants to know how large the file is. We are generally + # allowed to ignore this. + if whence == lib.AVSEEK_SIZE: + return -1 + with gil: + return pyio_seek_gil(opaque, offset, whence) + +cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): + cdef PyIOFile self + try: + self = opaque + res = self.fseek(offset, whence) + + # Track the position for the user. + if whence == 0: + self.pos = offset + elif whence == 1: + self.pos += offset + else: + self.pos_is_valid = False + if res is None: + if self.pos_is_valid: + res = self.pos + else: + res = self.ftell() + return res + except Exception: + return stash_exception() + + +cdef int pyio_close_gil(lib.AVIOContext *pb): + try: + return lib.avio_close(pb) + + except Exception: + stash_exception() + + +cdef int pyio_close_custom_gil(lib.AVIOContext *pb): + cdef PyIOFile self + try: + self = pb.opaque + + # Flush bytes in the AVIOContext buffers to the custom I/O + lib.avio_flush(pb) + + if self.fclose is not None: + self.fclose() + + return 0 + + except Exception: + stash_exception() diff --git a/lib/python3.10/site-packages/av/container/streams.pxd b/lib/python3.10/site-packages/av/container/streams.pxd new file mode 100644 index 0000000000000000000000000000000000000000..097176e10b3e2c0f9b48c6b852f3829b03977b2b --- /dev/null +++ b/lib/python3.10/site-packages/av/container/streams.pxd @@ -0,0 +1,21 @@ +cimport libav as lib + +from av.stream cimport Stream + +from .core cimport Container + + +cdef class StreamContainer: + cdef list _streams + + # For the different types. + cdef readonly tuple video + cdef readonly tuple audio + cdef readonly tuple subtitles + cdef readonly tuple attachments + cdef readonly tuple data + cdef readonly tuple other + + cdef add_stream(self, Stream stream) + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept + diff --git a/lib/python3.10/site-packages/av/container/streams.pyi b/lib/python3.10/site-packages/av/container/streams.pyi new file mode 100644 index 0000000000000000000000000000000000000000..52b98818f7c48b44106733cdd4e5801cd1c294de --- /dev/null +++ b/lib/python3.10/site-packages/av/container/streams.pyi @@ -0,0 +1,35 @@ +from typing import Iterator, Literal, overload + +from av.audio.stream import AudioStream +from av.stream import AttachmentStream, DataStream, Stream +from av.subtitles.stream import SubtitleStream +from av.video.stream import VideoStream + +class StreamContainer: + video: tuple[VideoStream, ...] + audio: tuple[AudioStream, ...] + subtitles: tuple[SubtitleStream, ...] + attachments: tuple[AttachmentStream, ...] + data: tuple[DataStream, ...] + other: tuple[Stream, ...] + + def __init__(self) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Stream]: ... + @overload + def __getitem__(self, index: int) -> Stream: ... + @overload + def __getitem__(self, index: slice) -> list[Stream]: ... + @overload + def __getitem__(self, index: int | slice) -> Stream | list[Stream]: ... + def get( + self, + *args: int | Stream | dict[str, int | tuple[int, ...]], + **kwargs: int | tuple[int, ...], + ) -> list[Stream]: ... + def best( + self, + type: Literal["video", "audio", "subtitle", "data", "attachment"], + /, + related: Stream | None = None, + ) -> Stream | None: ... diff --git a/lib/python3.10/site-packages/av/container/streams.pyx b/lib/python3.10/site-packages/av/container/streams.pyx new file mode 100644 index 0000000000000000000000000000000000000000..17e4992d8ba4f2f62e7dcebb7f2ec8cd1ff6eabe --- /dev/null +++ b/lib/python3.10/site-packages/av/container/streams.pyx @@ -0,0 +1,170 @@ +cimport libav as lib + + +def _flatten(input_): + for x in input_: + if isinstance(x, (tuple, list)): + for y in _flatten(x): + yield y + else: + yield x + +cdef lib.AVMediaType _get_media_type_enum(str type): + if type == "video": + return lib.AVMEDIA_TYPE_VIDEO + elif type == "audio": + return lib.AVMEDIA_TYPE_AUDIO + elif type == "subtitle": + return lib.AVMEDIA_TYPE_SUBTITLE + elif type == "attachment": + return lib.AVMEDIA_TYPE_ATTACHMENT + elif type == "data": + return lib.AVMEDIA_TYPE_DATA + else: + raise ValueError(f"Invalid stream type: {type}") + +cdef class StreamContainer: + """ + + A tuple-like container of :class:`Stream`. + + :: + + # There are a few ways to pulling out streams. + first = container.streams[0] + video = container.streams.video[0] + audio = container.streams.get(audio=(0, 1)) + + + """ + + def __cinit__(self): + self._streams = [] + self.video = () + self.audio = () + self.subtitles = () + self.data = () + self.attachments = () + self.other = () + + cdef add_stream(self, Stream stream): + + assert stream.ptr.index == len(self._streams) + self._streams.append(stream) + + if stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: + self.video = self.video + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: + self.audio = self.audio + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: + self.subtitles = self.subtitles + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + self.attachments = self.attachments + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: + self.data = self.data + (stream, ) + else: + self.other = self.other + (stream, ) + + # Basic tuple interface. + def __len__(self): + return len(self._streams) + + def __iter__(self): + return iter(self._streams) + + def __getitem__(self, index): + if isinstance(index, int): + return self.get(index)[0] + else: + return self.get(index) + + def get(self, *args, **kwargs): + """get(streams=None, video=None, audio=None, subtitles=None, data=None) + + Get a selection of :class:`.Stream` as a ``list``. + + Positional arguments may be ``int`` (which is an index into the streams), + or ``list`` or ``tuple`` of those:: + + # Get the first channel. + streams.get(0) + + # Get the first two audio channels. + streams.get(audio=(0, 1)) + + Keyword arguments (or dicts as positional arguments) as interpreted + as ``(stream_type, index_value_or_set)`` pairs:: + + # Get the first video channel. + streams.get(video=0) + # or + streams.get({'video': 0}) + + :class:`.Stream` objects are passed through untouched. + + If nothing is selected, then all streams are returned. + + """ + + selection = [] + + for x in _flatten((args, kwargs)): + if x is None: + pass + + elif isinstance(x, Stream): + selection.append(x) + + elif isinstance(x, int): + selection.append(self._streams[x]) + + elif isinstance(x, dict): + for type_, indices in x.items(): + if type_ == "streams": # For compatibility with the pseudo signature + streams = self._streams + else: + streams = getattr(self, type_) + if not isinstance(indices, (tuple, list)): + indices = [indices] + for i in indices: + selection.append(streams[i]) + + else: + raise TypeError("Argument must be Stream or int.", type(x)) + + return selection or self._streams[:] + + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept: + cdef int stream_index + + if related is None: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, -1, NULL, 0) + else: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, related.ptr.index, NULL, 0) + + return stream_index + + def best(self, str type, /, Stream related = None): + """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) + Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`. Example:: + + stream = container.streams.best("video") + + :param type: The type of stream to find + :param related: A related stream to use as a reference (optional) + :return: The best stream of the specified type + :rtype: Stream | None + """ + cdef type_enum = _get_media_type_enum(type) + + if len(self._streams) == 0: + return None + + cdef container = self._streams[0].container + + cdef int stream_index = self._get_best_stream_index(container, type_enum, related) + + if stream_index < 0: + return None + + return self._streams[stream_index] diff --git a/lib/python3.10/site-packages/av/filter/__init__.pxd b/lib/python3.10/site-packages/av/filter/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/filter/__init__.py b/lib/python3.10/site-packages/av/filter/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5dd4430d474e27d1714b54a0f218709993def47c --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/__init__.py @@ -0,0 +1,3 @@ +from .filter import Filter, FilterFlags, filter_descriptor, filters_available +from .graph import Graph +from .loudnorm import stats diff --git a/lib/python3.10/site-packages/av/filter/__init__.pyi b/lib/python3.10/site-packages/av/filter/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5be1326c99efab97108a8dba6887cda669b6996a --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/__init__.pyi @@ -0,0 +1,4 @@ +from .context import * +from .filter import * +from .graph import * +from .loudnorm import * diff --git a/lib/python3.10/site-packages/av/filter/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/filter/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..512a61b8b4bb88508cfb6f14d2bbf0a0f0e7059c Binary files /dev/null and b/lib/python3.10/site-packages/av/filter/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/filter/__pycache__/loudnorm.cpython-310.pyc b/lib/python3.10/site-packages/av/filter/__pycache__/loudnorm.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bde60fb20b890e11e75da14be1269720e59d3fa Binary files /dev/null and b/lib/python3.10/site-packages/av/filter/__pycache__/loudnorm.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/filter/context.pxd b/lib/python3.10/site-packages/av/filter/context.pxd new file mode 100644 index 0000000000000000000000000000000000000000..ae9f27c99f85e2de9ee526095b391464049f9efc --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/context.pxd @@ -0,0 +1,19 @@ +cimport libav as lib + +from av.filter.filter cimport Filter +from av.filter.graph cimport Graph + + +cdef class FilterContext: + + cdef lib.AVFilterContext *ptr + cdef readonly object _graph + cdef readonly Filter filter + + cdef object _inputs + cdef object _outputs + + cdef bint inited + + +cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr) diff --git a/lib/python3.10/site-packages/av/filter/context.pyi b/lib/python3.10/site-packages/av/filter/context.pyi new file mode 100644 index 0000000000000000000000000000000000000000..7c00087a928d37033f1e20a3692c0ede9276dfaa --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/context.pyi @@ -0,0 +1,18 @@ +from av.filter import Graph +from av.frame import Frame + +from .pad import FilterContextPad + +class FilterContext: + name: str | None + inputs: tuple[FilterContextPad, ...] + outputs: tuple[FilterContextPad, ...] + + def init(self, args: str | None = None, **kwargs: str | None) -> None: ... + def link_to( + self, input_: FilterContext, output_idx: int = 0, input_idx: int = 0 + ) -> None: ... + @property + def graph(self) -> Graph: ... + def push(self, frame: Frame) -> None: ... + def pull(self) -> Frame: ... diff --git a/lib/python3.10/site-packages/av/filter/context.pyx b/lib/python3.10/site-packages/av/filter/context.pyx new file mode 100644 index 0000000000000000000000000000000000000000..b820d3d1829ac4c8edb2b1b468d3b1dc76c16eeb --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/context.pyx @@ -0,0 +1,134 @@ +import weakref + +from av.audio.frame cimport alloc_audio_frame +from av.dictionary cimport _Dictionary +from av.dictionary import Dictionary +from av.error cimport err_check +from av.filter.pad cimport alloc_filter_pads +from av.frame cimport Frame +from av.utils cimport avrational_to_fraction +from av.video.frame cimport alloc_video_frame + + +cdef object _cinit_sentinel = object() + + +cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr): + cdef FilterContext self = FilterContext(_cinit_sentinel) + self._graph = weakref.ref(graph) + self.filter = filter + self.ptr = ptr + return self + + +cdef class FilterContext: + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot construct FilterContext") + + def __repr__(self): + if self.ptr != NULL: + name = repr(self.ptr.name) if self.ptr.name != NULL else "" + else: + name = "None" + + parent = self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None + return f"" + + @property + def name(self): + if self.ptr.name != NULL: + return self.ptr.name + + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) + return self._outputs + + def init(self, args=None, **kwargs): + if self.inited: + raise ValueError("already inited") + if args and kwargs: + raise ValueError("cannot init from args and kwargs") + + cdef _Dictionary dict_ = None + cdef char *c_args = NULL + if args or not kwargs: + if args: + c_args = args + err_check(lib.avfilter_init_str(self.ptr, c_args)) + else: + dict_ = Dictionary(kwargs) + err_check(lib.avfilter_init_dict(self.ptr, &dict_.ptr)) + + self.inited = True + if dict_: + raise ValueError(f"unused config: {', '.join(sorted(dict_))}") + + def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): + err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) + + @property + def graph(self): + if (graph := self._graph()): + return graph + else: + raise RuntimeError("graph is unallocated") + + def push(self, Frame frame): + cdef int res + + if frame is None: + with nogil: + res = lib.av_buffersrc_write_frame(self.ptr, NULL) + err_check(res) + return + elif self.filter.name in ("abuffer", "buffer"): + with nogil: + res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) + err_check(res) + return + + # Delegate to the input. + if len(self.inputs) != 1: + raise ValueError( + f"cannot delegate push without single input; found {len(self.inputs)}" + ) + if not self.inputs[0].link: + raise ValueError("cannot delegate push without linked input") + self.inputs[0].linked.context.push(frame) + + def pull(self): + cdef Frame frame + cdef int res + + if self.filter.name == "buffersink": + frame = alloc_video_frame() + elif self.filter.name == "abuffersink": + frame = alloc_audio_frame() + else: + # Delegate to the output. + if len(self.outputs) != 1: + raise ValueError( + f"cannot delegate pull without single output; found {len(self.outputs)}" + ) + if not self.outputs[0].link: + raise ValueError("cannot delegate pull without linked output") + return self.outputs[0].linked.context.pull() + + self.graph.configure() + + with nogil: + res = lib.av_buffersink_get_frame(self.ptr, frame.ptr) + err_check(res) + + frame._init_user_attributes() + frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) + return frame diff --git a/lib/python3.10/site-packages/av/filter/filter.pxd b/lib/python3.10/site-packages/av/filter/filter.pxd new file mode 100644 index 0000000000000000000000000000000000000000..27501ae575f7ed16741cee1ab497eb8a7c4a5bb7 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/filter.pxd @@ -0,0 +1,15 @@ +cimport libav as lib + +from av.descriptor cimport Descriptor + + +cdef class Filter: + + cdef const lib.AVFilter *ptr + + cdef object _inputs + cdef object _outputs + cdef Descriptor _descriptor + + +cdef Filter wrap_filter(const lib.AVFilter *ptr) diff --git a/lib/python3.10/site-packages/av/filter/filter.pyi b/lib/python3.10/site-packages/av/filter/filter.pyi new file mode 100644 index 0000000000000000000000000000000000000000..2751e973cf5a8350a5f266604b65270ab6045324 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/filter.pyi @@ -0,0 +1,23 @@ +from av.descriptor import Descriptor +from av.option import Option + +from .pad import FilterPad + +class Filter: + name: str + description: str + + descriptor: Descriptor + options: tuple[Option, ...] | None + flags: int + dynamic_inputs: bool + dynamic_outputs: bool + timeline_support: bool + slice_threads: bool + command_support: bool + inputs: tuple[FilterPad, ...] + outputs: tuple[FilterPad, ...] + + def __init__(self, name: str) -> None: ... + +filters_available: set[str] diff --git a/lib/python3.10/site-packages/av/filter/filter.pyx b/lib/python3.10/site-packages/av/filter/filter.pyx new file mode 100644 index 0000000000000000000000000000000000000000..d4880dc156e40bd8200ab2e54dd6be72d940106f --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/filter.pyx @@ -0,0 +1,106 @@ +cimport libav as lib + +from av.descriptor cimport wrap_avclass +from av.filter.pad cimport alloc_filter_pads + + +cdef object _cinit_sentinel = object() + + +cdef Filter wrap_filter(const lib.AVFilter *ptr): + cdef Filter filter_ = Filter(_cinit_sentinel) + filter_.ptr = ptr + return filter_ + + +cpdef enum FilterFlags: + DYNAMIC_INPUTS = lib.AVFILTER_FLAG_DYNAMIC_INPUTS + DYNAMIC_OUTPUTS = lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS + SLICE_THREADS = lib.AVFILTER_FLAG_SLICE_THREADS + SUPPORT_TIMELINE_GENERIC = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC + SUPPORT_TIMELINE_INTERNAL = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL + + +cdef class Filter: + def __cinit__(self, name): + if name is _cinit_sentinel: + return + if not isinstance(name, str): + raise TypeError("takes a filter name as a string") + self.ptr = lib.avfilter_get_by_name(name) + if not self.ptr: + raise ValueError(f"no filter {name}") + + @property + def descriptor(self): + if self._descriptor is None: + self._descriptor = wrap_avclass(self.ptr.priv_class) + return self._descriptor + + @property + def options(self): + if self.descriptor is None: + return + return self.descriptor.options + + @property + def name(self): + return self.ptr.name + + @property + def description(self): + return self.ptr.description + + @property + def flags(self): + return self.ptr.flags + + @property + def dynamic_inputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) + + @property + def dynamic_outputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) + + @property + def timeline_support(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) + + @property + def slice_threads(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) + + @property + def command_support(self): + return self.ptr.process_command != NULL + + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self, self.ptr.inputs, True) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self, self.ptr.outputs, False) + return self._outputs + + +cdef get_filter_names(): + names = set() + cdef const lib.AVFilter *ptr + cdef void *opaque = NULL + while True: + ptr = lib.av_filter_iterate(&opaque) + if ptr: + names.add(ptr.name) + else: + break + return names + +filters_available = get_filter_names() + + +filter_descriptor = wrap_avclass(lib.avfilter_get_class()) diff --git a/lib/python3.10/site-packages/av/filter/graph.pxd b/lib/python3.10/site-packages/av/filter/graph.pxd new file mode 100644 index 0000000000000000000000000000000000000000..2e52bd6ec3a67992dc0f2a40bbd5da8e5ed41fda --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/graph.pxd @@ -0,0 +1,22 @@ +cimport libav as lib + +from av.filter.context cimport FilterContext + + +cdef class Graph: + cdef object __weakref__ + + cdef lib.AVFilterGraph *ptr + + cdef readonly bint configured + cpdef configure(self, bint auto_buffer=*, bint force=*) + + cdef dict _name_counts + cdef str _get_unique_name(self, str name) + + cdef _register_context(self, FilterContext) + cdef _auto_register(self) + cdef int _nb_filters_seen + cdef dict _context_by_ptr + cdef dict _context_by_name + cdef dict _context_by_type diff --git a/lib/python3.10/site-packages/av/filter/graph.pyi b/lib/python3.10/site-packages/av/filter/graph.pyi new file mode 100644 index 0000000000000000000000000000000000000000..e170c2ce77da8ba8fa81561c24285886200d3d0d --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/graph.pyi @@ -0,0 +1,47 @@ +from fractions import Fraction +from typing import Any + +from av.audio.format import AudioFormat +from av.audio.frame import AudioFrame +from av.audio.layout import AudioLayout +from av.audio.stream import AudioStream +from av.video.format import VideoFormat +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .context import FilterContext +from .filter import Filter + +class Graph: + configured: bool + + def __init__(self) -> None: ... + def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def link_nodes(self, *nodes: FilterContext) -> Graph: ... + def add( + self, filter: str | Filter, args: Any = None, **kwargs: str + ) -> FilterContext: ... + def add_buffer( + self, + template: VideoStream | None = None, + width: int | None = None, + height: int | None = None, + format: VideoFormat | None = None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def add_abuffer( + self, + template: AudioStream | None = None, + sample_rate: int | None = None, + format: AudioFormat | str | None = None, + layout: AudioLayout | str | None = None, + channels: int | None = None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def set_audio_frame_size(self, frame_size: int) -> None: ... + def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... + def pull(self) -> VideoFrame | AudioFrame: ... + def vpush(self, frame: VideoFrame | None) -> None: ... + def vpull(self) -> VideoFrame: ... diff --git a/lib/python3.10/site-packages/av/filter/graph.pyx b/lib/python3.10/site-packages/av/filter/graph.pyx new file mode 100644 index 0000000000000000000000000000000000000000..c1a2d7a0646146a8fe9300d5a779792d42658fc9 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/graph.pyx @@ -0,0 +1,224 @@ +import warnings +from fractions import Fraction + +from av.audio.format cimport AudioFormat +from av.audio.frame cimport AudioFrame +from av.audio.layout cimport AudioLayout +from av.error cimport err_check +from av.filter.context cimport FilterContext, wrap_filter_context +from av.filter.filter cimport Filter, wrap_filter +from av.video.format cimport VideoFormat +from av.video.frame cimport VideoFrame + + +cdef class Graph: + def __cinit__(self): + self.ptr = lib.avfilter_graph_alloc() + self.configured = False + self._name_counts = {} + + self._nb_filters_seen = 0 + self._context_by_ptr = {} + self._context_by_name = {} + self._context_by_type = {} + + def __dealloc__(self): + if self.ptr: + # This frees the graph, filter contexts, links, etc.. + lib.avfilter_graph_free(&self.ptr) + + cdef str _get_unique_name(self, str name): + count = self._name_counts.get(name, 0) + self._name_counts[name] = count + 1 + if count: + return "%s_%s" % (name, count) + else: + return name + + cpdef configure(self, bint auto_buffer=True, bint force=False): + if self.configured and not force: + return + + err_check(lib.avfilter_graph_config(self.ptr, NULL)) + self.configured = True + + # We get auto-inserted stuff here. + self._auto_register() + + def link_nodes(self, *nodes): + """ + Links nodes together for simple filter graphs. + """ + for c, n in zip(nodes, nodes[1:]): + c.link_to(n) + return self + + def add(self, filter, args=None, **kwargs): + cdef Filter cy_filter + if isinstance(filter, str): + cy_filter = Filter(filter) + elif isinstance(filter, Filter): + cy_filter = filter + else: + raise TypeError("filter must be a string or Filter") + + cdef str name = self._get_unique_name(kwargs.pop("name", None) or cy_filter.name) + + cdef lib.AVFilterContext *ptr = lib.avfilter_graph_alloc_filter(self.ptr, cy_filter.ptr, name) + if not ptr: + raise RuntimeError("Could not allocate AVFilterContext") + + # Manually construct this context (so we can return it). + cdef FilterContext ctx = wrap_filter_context(self, cy_filter, ptr) + ctx.init(args, **kwargs) + self._register_context(ctx) + + # There might have been automatic contexts added (e.g. resamplers, + # fifos, and scalers). It is more likely to see them after the graph + # is configured, but we want to be safe. + self._auto_register() + + return ctx + + cdef _register_context(self, FilterContext ctx): + self._context_by_ptr[ctx.ptr] = ctx + self._context_by_name[ctx.ptr.name] = ctx + self._context_by_type.setdefault(ctx.filter.ptr.name, []).append(ctx) + + cdef _auto_register(self): + cdef int i + cdef lib.AVFilterContext *c_ctx + cdef Filter filter_ + cdef FilterContext py_ctx + # We assume that filters are never removed from the graph. At this + # point we don't expose that in the API, so we should be okay... + for i in range(self._nb_filters_seen, self.ptr.nb_filters): + c_ctx = self.ptr.filters[i] + if c_ctx in self._context_by_ptr: + continue + filter_ = wrap_filter(c_ctx.filter) + py_ctx = wrap_filter_context(self, filter_, c_ctx) + self._register_context(py_ctx) + self._nb_filters_seen = self.ptr.nb_filters + + def add_buffer(self, template=None, width=None, height=None, format=None, name=None, time_base=None): + if template is not None: + if width is None: + width = template.width + if height is None: + height = template.height + if format is None: + format = template.format + if time_base is None: + time_base = template.time_base + + if width is None: + raise ValueError("missing width") + if height is None: + raise ValueError("missing height") + if format is None: + raise ValueError("missing format") + if time_base is None: + warnings.warn("missing time_base. Guessing 1/1000 time base. " + "This is deprecated and may be removed in future releases.", + DeprecationWarning) + time_base = Fraction(1, 1000) + + return self.add( + "buffer", + name=name, + video_size=f"{width}x{height}", + pix_fmt=str(int(VideoFormat(format))), + time_base=str(time_base), + pixel_aspect="1/1", + ) + + def add_abuffer(self, template=None, sample_rate=None, format=None, layout=None, channels=None, name=None, time_base=None): + """ + Convenience method for adding `abuffer `_. + """ + + if template is not None: + if sample_rate is None: + sample_rate = template.sample_rate + if format is None: + format = template.format + if layout is None: + layout = template.layout.name + if channels is None: + channels = template.channels + if time_base is None: + time_base = template.time_base + + if sample_rate is None: + raise ValueError("missing sample_rate") + if format is None: + raise ValueError("missing format") + if layout is None and channels is None: + raise ValueError("missing layout or channels") + if time_base is None: + time_base = Fraction(1, sample_rate) + + kwargs = dict( + sample_rate=str(sample_rate), + sample_fmt=AudioFormat(format).name, + time_base=str(time_base), + ) + if layout: + kwargs["channel_layout"] = AudioLayout(layout).name + if channels: + kwargs["channels"] = str(channels) + + return self.add("abuffer", name=name, **kwargs) + + def set_audio_frame_size(self, frame_size): + """ + Set the audio frame size for the graphs `abuffersink`. + See `av_buffersink_set_frame_size `_. + """ + if not self.configured: + raise ValueError("graph not configured") + sinks = self._context_by_type.get("abuffersink", []) + if not sinks: + raise ValueError("missing abuffersink filter") + for sink in sinks: + lib.av_buffersink_set_frame_size((sink).ptr, frame_size) + + def push(self, frame): + if frame is None: + contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) + elif isinstance(frame, VideoFrame): + contexts = self._context_by_type.get("buffer", []) + elif isinstance(frame, AudioFrame): + contexts = self._context_by_type.get("abuffer", []) + else: + raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") + + for ctx in contexts: + ctx.push(frame) + + def vpush(self, VideoFrame frame): + """Like `push`, but only for VideoFrames.""" + for ctx in self._context_by_type.get("buffer", []): + ctx.push(frame) + + + # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). + def pull(self): + vsinks = self._context_by_type.get("buffersink", []) + asinks = self._context_by_type.get("abuffersink", []) + + nsinks = len(vsinks) + len(asinks) + if nsinks != 1: + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") + + return (vsinks or asinks)[0].pull() + + def vpull(self): + """Like `pull`, but only for VideoFrames.""" + vsinks = self._context_by_type.get("buffersink", []) + nsinks = len(vsinks) + if nsinks != 1: + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") + + return vsinks[0].pull() diff --git a/lib/python3.10/site-packages/av/filter/link.pxd b/lib/python3.10/site-packages/av/filter/link.pxd new file mode 100644 index 0000000000000000000000000000000000000000..a6a4b1c092163e4739735026b6c26502003daa24 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/link.pxd @@ -0,0 +1,16 @@ +cimport libav as lib + +from av.filter.graph cimport Graph +from av.filter.pad cimport FilterContextPad + + +cdef class FilterLink: + + cdef readonly Graph graph + cdef lib.AVFilterLink *ptr + + cdef FilterContextPad _input + cdef FilterContextPad _output + + +cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr) diff --git a/lib/python3.10/site-packages/av/filter/link.pyi b/lib/python3.10/site-packages/av/filter/link.pyi new file mode 100644 index 0000000000000000000000000000000000000000..dd420ad91e94856492769f1efea588d2ee9e30ff --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/link.pyi @@ -0,0 +1,5 @@ +from .pad import FilterContextPad + +class FilterLink: + input: FilterContextPad + output: FilterContextPad diff --git a/lib/python3.10/site-packages/av/filter/link.pyx b/lib/python3.10/site-packages/av/filter/link.pyx new file mode 100644 index 0000000000000000000000000000000000000000..78b7da30f718227cce3a96bdc27177b9443106c9 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/link.pyx @@ -0,0 +1,53 @@ +cimport libav as lib + +from av.filter.graph cimport Graph + + +cdef _cinit_sentinel = object() + + +cdef class FilterLink: + + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot instantiate FilterLink") + + @property + def input(self): + if self._input: + return self._input + cdef lib.AVFilterContext *cctx = self.ptr.src + cdef unsigned int i + for i in range(cctx.nb_outputs): + if self.ptr == cctx.outputs[i]: + break + else: + raise RuntimeError("could not find link in context") + ctx = self.graph._context_by_ptr[cctx] + self._input = ctx.outputs[i] + return self._input + + @property + def output(self): + if self._output: + return self._output + cdef lib.AVFilterContext *cctx = self.ptr.dst + cdef unsigned int i + for i in range(cctx.nb_inputs): + if self.ptr == cctx.inputs[i]: + break + else: + raise RuntimeError("could not find link in context") + try: + ctx = self.graph._context_by_ptr[cctx] + except KeyError: + raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) + self._output = ctx.inputs[i] + return self._output + + +cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): + cdef FilterLink link = FilterLink(_cinit_sentinel) + link.graph = graph + link.ptr = ptr + return link diff --git a/lib/python3.10/site-packages/av/filter/loudnorm.pxd b/lib/python3.10/site-packages/av/filter/loudnorm.pxd new file mode 100644 index 0000000000000000000000000000000000000000..2729dd8be0e2d4d53d64c5348f4cf909f161be2c --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/loudnorm.pxd @@ -0,0 +1,19 @@ +from av.audio.stream cimport AudioStream + + +cdef extern from "libavcodec/avcodec.h": + ctypedef struct AVCodecContext: + pass + +cdef extern from "libavformat/avformat.h": + ctypedef struct AVFormatContext: + pass + +cdef extern from "loudnorm_impl.h": + char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args + ) nogil + +cpdef bytes stats(str loudnorm_args, AudioStream stream) diff --git a/lib/python3.10/site-packages/av/filter/loudnorm.py b/lib/python3.10/site-packages/av/filter/loudnorm.py new file mode 100644 index 0000000000000000000000000000000000000000..0be991ab6b17c5ef7050abcb69f55387f003a48d --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/loudnorm.py @@ -0,0 +1,48 @@ +import cython +from cython.cimports.av.audio.stream import AudioStream +from cython.cimports.av.container.core import Container +from cython.cimports.libc.stdlib import free + +from av.logging import get_level, set_level + + +@cython.ccall +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: + """ + Get loudnorm statistics for an audio stream. + + Args: + loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") + stream (AudioStream): Input audio stream to analyze + + Returns: + bytes: JSON string containing the loudnorm statistics + """ + + if "print_format=json" not in loudnorm_args: + loudnorm_args = loudnorm_args + ":print_format=json" + + container: Container = stream.container + format_ptr: cython.pointer[AVFormatContext] = container.ptr + container.ptr = cython.NULL # Prevent double-free + + stream_index: cython.int = stream.index + py_args: bytes = loudnorm_args.encode("utf-8") + c_args: cython.p_const_char = py_args + result: cython.p_char + + # Save log level since C function overwrite it. + level = get_level() + + with cython.nogil: + result = loudnorm_get_stats(format_ptr, stream_index, c_args) + + if result == cython.NULL: + raise RuntimeError("Failed to get loudnorm stats") + + py_result = result[:] # Make a copy of the string + free(result) # Free the C string + + set_level(level) + + return py_result diff --git a/lib/python3.10/site-packages/av/filter/loudnorm.pyi b/lib/python3.10/site-packages/av/filter/loudnorm.pyi new file mode 100644 index 0000000000000000000000000000000000000000..c680f638d43a8908438d81c56c07387524488412 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/loudnorm.pyi @@ -0,0 +1,3 @@ +from av.audio.stream import AudioStream + +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: ... diff --git a/lib/python3.10/site-packages/av/filter/loudnorm_impl.c b/lib/python3.10/site-packages/av/filter/loudnorm_impl.c new file mode 100644 index 0000000000000000000000000000000000000000..f6e22e4cebde12aa05cd8587ca4407ee74ac7c69 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/loudnorm_impl.c @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +#ifdef _WIN32 + static CRITICAL_SECTION json_mutex; + static CONDITION_VARIABLE json_cond; + static int mutex_initialized = 0; +#else + static pthread_mutex_t json_mutex = PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t json_cond = PTHREAD_COND_INITIALIZER; +#endif + +static char json_buffer[2048] = {0}; +static int json_captured = 0; + +// Custom logging callback +static void logging_callback(void *ptr, int level, const char *fmt, va_list vl) { + char line[2048]; + vsnprintf(line, sizeof(line), fmt, vl); + + const char *json_start = strstr(line, "{"); + if (json_start) { + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + #else + pthread_mutex_lock(&json_mutex); + #endif + + strncpy(json_buffer, json_start, sizeof(json_buffer) - 1); + json_captured = 1; + + #ifdef _WIN32 + WakeConditionVariable(&json_cond); + LeaveCriticalSection(&json_mutex); + #else + pthread_cond_signal(&json_cond); + pthread_mutex_unlock(&json_mutex); + #endif + } +} + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +) { + char* result = NULL; + json_captured = 0; // Reset the captured flag + memset(json_buffer, 0, sizeof(json_buffer)); // Clear the buffer + + #ifdef _WIN32 + // Initialize synchronization objects if needed + if (!mutex_initialized) { + InitializeCriticalSection(&json_mutex); + InitializeConditionVariable(&json_cond); + mutex_initialized = 1; + } + #endif + + av_log_set_callback(logging_callback); + + AVFilterGraph *filter_graph = NULL; + AVFilterContext *src_ctx = NULL, *sink_ctx = NULL, *loudnorm_ctx = NULL; + + AVCodec *codec = NULL; + AVCodecContext *codec_ctx = NULL; + int ret; + + AVCodecParameters *codecpar = fmt_ctx->streams[audio_stream_index]->codecpar; + codec = (AVCodec *)avcodec_find_decoder(codecpar->codec_id); + codec_ctx = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codec_ctx, codecpar); + avcodec_open2(codec_ctx, codec, NULL); + + char ch_layout_str[64]; + av_channel_layout_describe(&codecpar->ch_layout, ch_layout_str, sizeof(ch_layout_str)); + + filter_graph = avfilter_graph_alloc(); + + char args[512]; + snprintf(args, sizeof(args), + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=%s", + fmt_ctx->streams[audio_stream_index]->time_base.num, + fmt_ctx->streams[audio_stream_index]->time_base.den, + codecpar->sample_rate, + av_get_sample_fmt_name(codec_ctx->sample_fmt), + ch_layout_str); + + avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("abuffer"), + "src", args, NULL, filter_graph); + avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("abuffersink"), + "sink", NULL, NULL, filter_graph); + avfilter_graph_create_filter(&loudnorm_ctx, avfilter_get_by_name("loudnorm"), + "loudnorm", loudnorm_args, NULL, filter_graph); + + avfilter_link(src_ctx, 0, loudnorm_ctx, 0); + avfilter_link(loudnorm_ctx, 0, sink_ctx, 0); + avfilter_graph_config(filter_graph, NULL); + + AVPacket *packet = av_packet_alloc(); + AVFrame *frame = av_frame_alloc(); + AVFrame *filt_frame = av_frame_alloc(); + + while ((ret = av_read_frame(fmt_ctx, packet)) >= 0) { + if (packet->stream_index != audio_stream_index) { + av_packet_unref(packet); + continue; + } + + ret = avcodec_send_packet(codec_ctx, packet); + if (ret < 0) { + av_packet_unref(packet); + continue; + } + + while (ret >= 0) { + ret = avcodec_receive_frame(codec_ctx, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + + ret = av_buffersrc_add_frame_flags(src_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF); + if (ret < 0) goto end; + + while (1) { + ret = av_buffersink_get_frame(sink_ctx, filt_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + av_frame_unref(filt_frame); + } + } + av_packet_unref(packet); + } + + // Flush decoder + avcodec_send_packet(codec_ctx, NULL); + while (avcodec_receive_frame(codec_ctx, frame) >= 0) { + ret = av_buffersrc_add_frame(src_ctx, frame); + if (ret < 0) goto end; + } + + // Flush filter + ret = av_buffersrc_add_frame(src_ctx, NULL); + if (ret < 0) goto end; + while (av_buffersink_get_frame(sink_ctx, filt_frame) >= 0) { + av_frame_unref(filt_frame); + } + + // Pushes graph + avfilter_graph_free(&filter_graph); + +end: + avcodec_free_context(&codec_ctx); + avformat_close_input(&fmt_ctx); + av_frame_free(&filt_frame); + av_frame_free(&frame); + av_packet_free(&packet); + + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + while (!json_captured) { + if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = _strdup(json_buffer); // Use _strdup on Windows + } + LeaveCriticalSection(&json_mutex); + #else + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 5; // 5 second timeout + + pthread_mutex_lock(&json_mutex); + while (json_captured == 0) { + int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); + if (ret == ETIMEDOUT) { + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = strdup(json_buffer); + } + pthread_mutex_unlock(&json_mutex); + #endif + + av_log_set_callback(av_log_default_callback); + return result; +} diff --git a/lib/python3.10/site-packages/av/filter/loudnorm_impl.h b/lib/python3.10/site-packages/av/filter/loudnorm_impl.h new file mode 100644 index 0000000000000000000000000000000000000000..7357e46689d5cbd5795ce00c5864f40e197e09dd --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/loudnorm_impl.h @@ -0,0 +1,12 @@ +#ifndef AV_FILTER_LOUDNORM_H +#define AV_FILTER_LOUDNORM_H + +#include + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +); + +#endif // AV_FILTER_LOUDNORM_H \ No newline at end of file diff --git a/lib/python3.10/site-packages/av/filter/pad.pxd b/lib/python3.10/site-packages/av/filter/pad.pxd new file mode 100644 index 0000000000000000000000000000000000000000..15ac950fca1a3bcec2602b177775a456d9c20280 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/pad.pxd @@ -0,0 +1,23 @@ +cimport libav as lib + +from av.filter.context cimport FilterContext +from av.filter.filter cimport Filter +from av.filter.link cimport FilterLink + + +cdef class FilterPad: + + cdef readonly Filter filter + cdef readonly FilterContext context + cdef readonly bint is_input + cdef readonly int index + + cdef const lib.AVFilterPad *base_ptr + + +cdef class FilterContextPad(FilterPad): + + cdef FilterLink _link + + +cdef tuple alloc_filter_pads(Filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=?) diff --git a/lib/python3.10/site-packages/av/filter/pad.pyi b/lib/python3.10/site-packages/av/filter/pad.pyi new file mode 100644 index 0000000000000000000000000000000000000000..1a6c9bda66dbc730f34b86104e8a4001d90e9dfd --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/pad.pyi @@ -0,0 +1,10 @@ +from .link import FilterLink + +class FilterPad: + is_output: bool + name: str + type: str + +class FilterContextPad(FilterPad): + link: FilterLink | None + linked: FilterContextPad | None diff --git a/lib/python3.10/site-packages/av/filter/pad.pyx b/lib/python3.10/site-packages/av/filter/pad.pyx new file mode 100644 index 0000000000000000000000000000000000000000..873b31b04b481782f7427e9de54bcf55b9f3af65 --- /dev/null +++ b/lib/python3.10/site-packages/av/filter/pad.pyx @@ -0,0 +1,92 @@ +from av.filter.link cimport wrap_filter_link + + +cdef object _cinit_sentinel = object() + + +cdef class FilterPad: + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot construct FilterPad") + + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + + return f"" + + @property + def is_output(self): + return not self.is_input + + @property + def name(self): + return lib.avfilter_pad_get_name(self.base_ptr, self.index) + + @property + def type(self): + """ + The media type of this filter pad. + + Examples: `'audio'`, `'video'`, `'subtitle'`. + + :type: str + """ + return lib.av_get_media_type_string(lib.avfilter_pad_get_type(self.base_ptr, self.index)) + + +cdef class FilterContextPad(FilterPad): + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + context = self.context.name + + return f"" + + @property + def link(self): + if self._link: + return self._link + cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + cdef lib.AVFilterLink *link = links[self.index] + if not link: + return + self._link = wrap_filter_link(self.context.graph, link) + return self._link + + @property + def linked(self): + cdef FilterLink link = self.link + if link: + return link.input if self.is_input else link.output + + +cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): + if not ptr: + return () + + pads = [] + + # We need to be careful and check our bounds if we know what they are, + # since the arrays on a AVFilterContext are not NULL terminated. + cdef int i = 0 + cdef int count + if context is None: + # This is a custom function defined using a macro in avfilter.pxd. Its usage + # can be changed after we stop supporting FFmpeg < 5.0. + count = lib.pyav_get_num_pads(filter.ptr, not is_input, ptr) + else: + count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) + + cdef FilterPad pad + while (i < count): + pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) + pads.append(pad) + pad.filter = filter + pad.context = context + pad.is_input = is_input + pad.base_ptr = ptr + pad.index = i + i += 1 + + return tuple(pads) diff --git a/lib/python3.10/site-packages/av/sidedata/__init__.pxd b/lib/python3.10/site-packages/av/sidedata/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/sidedata/__init__.py b/lib/python3.10/site-packages/av/sidedata/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/sidedata/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/sidedata/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9ec5fca65c094bf010297ef03d9da4dbdb53be4 Binary files /dev/null and b/lib/python3.10/site-packages/av/sidedata/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/sidedata/motionvectors.pxd b/lib/python3.10/site-packages/av/sidedata/motionvectors.pxd new file mode 100644 index 0000000000000000000000000000000000000000..993c5e283f4b3031c16d989f719ab217d060563b --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/motionvectors.pxd @@ -0,0 +1,16 @@ +cimport libav as lib + +from av.frame cimport Frame +from av.sidedata.sidedata cimport SideData + + +cdef class _MotionVectors(SideData): + + cdef dict _vectors + cdef int _len + + +cdef class MotionVector: + + cdef _MotionVectors parent + cdef lib.AVMotionVector *ptr diff --git a/lib/python3.10/site-packages/av/sidedata/motionvectors.pyi b/lib/python3.10/site-packages/av/sidedata/motionvectors.pyi new file mode 100644 index 0000000000000000000000000000000000000000..eb514eb7055f4af559803e7981f33ab26c280737 --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/motionvectors.pyi @@ -0,0 +1,27 @@ +from typing import Any, Sequence, overload + +import numpy as np + +from .sidedata import SideData + +class MotionVectors(SideData, Sequence[MotionVector]): + @overload + def __getitem__(self, index: int): ... + @overload + def __getitem__(self, index: slice): ... + @overload + def __getitem__(self, index: int | slice): ... + def __len__(self) -> int: ... + def to_ndarray(self) -> np.ndarray[Any, Any]: ... + +class MotionVector: + source: int + w: int + h: int + src_x: int + src_y: int + dst_x: int + dst_y: int + motion_x: int + motion_y: int + motion_scale: int diff --git a/lib/python3.10/site-packages/av/sidedata/motionvectors.pyx b/lib/python3.10/site-packages/av/sidedata/motionvectors.pyx new file mode 100644 index 0000000000000000000000000000000000000000..b0b0b705f320594886fadcaaf158914b63c36e1a --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/motionvectors.pyx @@ -0,0 +1,104 @@ +from collections.abc import Sequence + + +cdef object _cinit_bypass_sentinel = object() + + +# Cython doesn't let us inherit from the abstract Sequence, so we will subclass +# it later. +cdef class _MotionVectors(SideData): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._vectors = {} + self._len = self.ptr.size // sizeof(lib.AVMotionVector) + + def __repr__(self): + return f"self.ptr.data:0x}" + + def __getitem__(self, int index): + + try: + return self._vectors[index] + except KeyError: + pass + + if index >= self._len: + raise IndexError(index) + + vector = self._vectors[index] = MotionVector(_cinit_bypass_sentinel, self, index) + return vector + + def __len__(self): + return self._len + + def to_ndarray(self): + import numpy as np + return np.frombuffer(self, dtype=np.dtype([ + ("source", "int32"), + ("w", "uint8"), + ("h", "uint8"), + ("src_x", "int16"), + ("src_y", "int16"), + ("dst_x", "int16"), + ("dst_y", "int16"), + ("flags", "uint64"), + ("motion_x", "int32"), + ("motion_y", "int32"), + ("motion_scale", "uint16"), + ], align=True)) + + +class MotionVectors(_MotionVectors, Sequence): + pass + + +cdef class MotionVector: + def __init__(self, sentinel, _MotionVectors parent, int index): + if sentinel is not _cinit_bypass_sentinel: + raise RuntimeError("cannot manually instatiate MotionVector") + self.parent = parent + cdef lib.AVMotionVector *base = parent.ptr.data + self.ptr = base + index + + def __repr__(self): + return f"" + + @property + def source(self): + return self.ptr.source + + @property + def w(self): + return self.ptr.w + + @property + def h(self): + return self.ptr.h + + @property + def src_x(self): + return self.ptr.src_x + + @property + def src_y(self): + return self.ptr.src_y + + @property + def dst_x(self): + return self.ptr.dst_x + + @property + def dst_y(self): + return self.ptr.dst_y + + @property + def motion_x(self): + return self.ptr.motion_x + + @property + def motion_y(self): + return self.ptr.motion_y + + @property + def motion_scale(self): + return self.ptr.motion_scale diff --git a/lib/python3.10/site-packages/av/sidedata/sidedata.pxd b/lib/python3.10/site-packages/av/sidedata/sidedata.pxd new file mode 100644 index 0000000000000000000000000000000000000000..5e6e5bf4cf9a269dc15b22da34a9d6f33788e683 --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/sidedata.pxd @@ -0,0 +1,23 @@ + +cimport libav as lib + +from av.buffer cimport Buffer +from av.dictionary cimport _Dictionary, wrap_dictionary +from av.frame cimport Frame + + +cdef class SideData(Buffer): + cdef Frame frame + cdef lib.AVFrameSideData *ptr + cdef _Dictionary metadata + + +cdef SideData wrap_side_data(Frame frame, int index) + +cdef int get_display_rotation(Frame frame) + +cdef class _SideDataContainer: + cdef Frame frame + + cdef list _by_index + cdef dict _by_type diff --git a/lib/python3.10/site-packages/av/sidedata/sidedata.pyi b/lib/python3.10/site-packages/av/sidedata/sidedata.pyi new file mode 100644 index 0000000000000000000000000000000000000000..0093fabd098d30c317e659c4b9dceb1067223965 --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/sidedata.pyi @@ -0,0 +1,52 @@ +from collections.abc import Mapping +from enum import Enum +from typing import ClassVar, Iterator, Sequence, cast, overload + +from av.buffer import Buffer +from av.frame import Frame + +class Type(Enum): + PANSCAN = cast(ClassVar[Type], ...) + A53_CC = cast(ClassVar[Type], ...) + STEREO3D = cast(ClassVar[Type], ...) + MATRIXENCODING = cast(ClassVar[Type], ...) + DOWNMIX_INFO = cast(ClassVar[Type], ...) + REPLAYGAIN = cast(ClassVar[Type], ...) + DISPLAYMATRIX = cast(ClassVar[Type], ...) + AFD = cast(ClassVar[Type], ...) + MOTION_VECTORS = cast(ClassVar[Type], ...) + SKIP_SAMPLES = cast(ClassVar[Type], ...) + AUDIO_SERVICE_TYPE = cast(ClassVar[Type], ...) + MASTERING_DISPLAY_METADATA = cast(ClassVar[Type], ...) + GOP_TIMECODE = cast(ClassVar[Type], ...) + SPHERICAL = cast(ClassVar[Type], ...) + CONTENT_LIGHT_LEVEL = cast(ClassVar[Type], ...) + ICC_PROFILE = cast(ClassVar[Type], ...) + S12M_TIMECODE = cast(ClassVar[Type], ...) + DYNAMIC_HDR_PLUS = cast(ClassVar[Type], ...) + REGIONS_OF_INTEREST = cast(ClassVar[Type], ...) + VIDEO_ENC_PARAMS = cast(ClassVar[Type], ...) + SEI_UNREGISTERED = cast(ClassVar[Type], ...) + FILM_GRAIN_PARAMS = cast(ClassVar[Type], ...) + DETECTION_BBOXES = cast(ClassVar[Type], ...) + DOVI_RPU_BUFFER = cast(ClassVar[Type], ...) + DOVI_METADATA = cast(ClassVar[Type], ...) + DYNAMIC_HDR_VIVID = cast(ClassVar[Type], ...) + AMBIENT_VIEWING_ENVIRONMENT = cast(ClassVar[Type], ...) + VIDEO_HINT = cast(ClassVar[Type], ...) + +class SideData(Buffer): + type: Type + +class SideDataContainer(Mapping): + frame: Frame + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[SideData]: ... + @overload + def __getitem__(self, key: str | int | Type) -> SideData: ... + @overload + def __getitem__(self, key: slice) -> Sequence[SideData]: ... + @overload + def __getitem__( + self, key: str | int | Type | slice + ) -> SideData | Sequence[SideData]: ... diff --git a/lib/python3.10/site-packages/av/sidedata/sidedata.pyx b/lib/python3.10/site-packages/av/sidedata/sidedata.pyx new file mode 100644 index 0000000000000000000000000000000000000000..24dbae119f9353cefd1e0589e1fcba801effbe80 --- /dev/null +++ b/lib/python3.10/site-packages/av/sidedata/sidedata.pyx @@ -0,0 +1,116 @@ +from libc.stdint cimport int32_t + +from collections.abc import Mapping +from enum import Enum + +from av.sidedata.motionvectors import MotionVectors + + +cdef object _cinit_bypass_sentinel = object() + + +class Type(Enum): + """ + Enum class representing different types of frame data in audio/video processing. + Values are mapped to corresponding AV_FRAME_DATA constants from FFmpeg. + + From: https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/frame.h + """ + PANSCAN = lib.AV_FRAME_DATA_PANSCAN + A53_CC = lib.AV_FRAME_DATA_A53_CC + STEREO3D = lib.AV_FRAME_DATA_STEREO3D + MATRIXENCODING = lib.AV_FRAME_DATA_MATRIXENCODING + DOWNMIX_INFO = lib.AV_FRAME_DATA_DOWNMIX_INFO + REPLAYGAIN = lib.AV_FRAME_DATA_REPLAYGAIN + DISPLAYMATRIX = lib.AV_FRAME_DATA_DISPLAYMATRIX + AFD = lib.AV_FRAME_DATA_AFD + MOTION_VECTORS = lib.AV_FRAME_DATA_MOTION_VECTORS + SKIP_SAMPLES = lib.AV_FRAME_DATA_SKIP_SAMPLES + AUDIO_SERVICE_TYPE = lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE + MASTERING_DISPLAY_METADATA = lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA + GOP_TIMECODE = lib.AV_FRAME_DATA_GOP_TIMECODE + SPHERICAL = lib.AV_FRAME_DATA_SPHERICAL + CONTENT_LIGHT_LEVEL = lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL + ICC_PROFILE = lib.AV_FRAME_DATA_ICC_PROFILE + S12M_TIMECODE = lib.AV_FRAME_DATA_S12M_TIMECODE + DYNAMIC_HDR_PLUS = lib.AV_FRAME_DATA_DYNAMIC_HDR_PLUS + REGIONS_OF_INTEREST = lib.AV_FRAME_DATA_REGIONS_OF_INTEREST + VIDEO_ENC_PARAMS = lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS + SEI_UNREGISTERED = lib.AV_FRAME_DATA_SEI_UNREGISTERED + FILM_GRAIN_PARAMS = lib.AV_FRAME_DATA_FILM_GRAIN_PARAMS + DETECTION_BBOXES = lib.AV_FRAME_DATA_DETECTION_BBOXES + DOVI_RPU_BUFFER = lib.AV_FRAME_DATA_DOVI_RPU_BUFFER + DOVI_METADATA = lib.AV_FRAME_DATA_DOVI_METADATA + DYNAMIC_HDR_VIVID = lib.AV_FRAME_DATA_DYNAMIC_HDR_VIVID + AMBIENT_VIEWING_ENVIRONMENT = lib.AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT + VIDEO_HINT = lib.AV_FRAME_DATA_VIDEO_HINT + + +cdef SideData wrap_side_data(Frame frame, int index): + if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: + return MotionVectors(_cinit_bypass_sentinel, frame, index) + else: + return SideData(_cinit_bypass_sentinel, frame, index) + + +cdef int get_display_rotation(Frame frame): + for i in range(frame.ptr.nb_side_data): + if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX: + return int(lib.av_display_rotation_get(frame.ptr.side_data[i].data)) + return 0 + + +cdef class SideData(Buffer): + def __init__(self, sentinel, Frame frame, int index): + if sentinel is not _cinit_bypass_sentinel: + raise RuntimeError("cannot manually instatiate SideData") + self.frame = frame + self.ptr = frame.ptr.side_data[index] + self.metadata = wrap_dictionary(self.ptr.metadata) + + cdef size_t _buffer_size(self): + return self.ptr.size + + cdef void* _buffer_ptr(self): + return self.ptr.data + + cdef bint _buffer_writable(self): + return False + + def __repr__(self): + return f"self.ptr.data:0x}>" + + @property + def type(self): + return Type(self.ptr.type) + + +cdef class _SideDataContainer: + def __init__(self, Frame frame): + self.frame = frame + self._by_index = [] + self._by_type = {} + + cdef int i + cdef SideData data + for i in range(self.frame.ptr.nb_side_data): + data = wrap_side_data(frame, i) + self._by_index.append(data) + self._by_type[data.type] = data + + def __len__(self): + return len(self._by_index) + + def __iter__(self): + return iter(self._by_index) + + def __getitem__(self, key): + if isinstance(key, int): + return self._by_index[key] + if isinstance(key, str): + return self._by_type[Type[key]] + return self._by_type[key] + + +class SideDataContainer(_SideDataContainer, Mapping): + pass diff --git a/lib/python3.10/site-packages/av/subtitles/__init__.pxd b/lib/python3.10/site-packages/av/subtitles/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/subtitles/__init__.py b/lib/python3.10/site-packages/av/subtitles/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/subtitles/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/subtitles/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ce722a20d5442b3c463a162f6b94f2bdba26bc3 Binary files /dev/null and b/lib/python3.10/site-packages/av/subtitles/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/subtitles/__pycache__/codeccontext.cpython-310.pyc b/lib/python3.10/site-packages/av/subtitles/__pycache__/codeccontext.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bde89c475b8604d803bb543832c2f7d8cdf5d27f Binary files /dev/null and b/lib/python3.10/site-packages/av/subtitles/__pycache__/codeccontext.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/subtitles/__pycache__/stream.cpython-310.pyc b/lib/python3.10/site-packages/av/subtitles/__pycache__/stream.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88b5fe1affbca418b79f741aacbba6d8556afe12 Binary files /dev/null and b/lib/python3.10/site-packages/av/subtitles/__pycache__/stream.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/subtitles/__pycache__/subtitle.cpython-310.pyc b/lib/python3.10/site-packages/av/subtitles/__pycache__/subtitle.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12e1805eee06b853b877b1930c97146f4d1993b3 Binary files /dev/null and b/lib/python3.10/site-packages/av/subtitles/__pycache__/subtitle.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/subtitles/codeccontext.pxd b/lib/python3.10/site-packages/av/subtitles/codeccontext.pxd new file mode 100644 index 0000000000000000000000000000000000000000..c94744e45e6a50b59643b69784842b71e1f569b9 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/codeccontext.pxd @@ -0,0 +1,6 @@ +from av.codec.context cimport CodecContext +from av.packet cimport Packet + + +cdef class SubtitleCodecContext(CodecContext): + cpdef decode2(self, Packet packet) diff --git a/lib/python3.10/site-packages/av/subtitles/codeccontext.py b/lib/python3.10/site-packages/av/subtitles/codeccontext.py new file mode 100644 index 0000000000000000000000000000000000000000..0b3dda063f8a94dddfe574f20b29cb8e2f7c59d9 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/codeccontext.py @@ -0,0 +1,55 @@ +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet + + +@cython.cclass +class SubtitleCodecContext(CodecContext): + @cython.cfunc + def _send_packet_and_recv(self, packet: Packet | None): + if packet is None: + raise RuntimeError("packet cannot be None") + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return [] + + @cython.ccall + def decode2(self, packet: Packet): + """ + Returns SubtitleSet if you really need it. + """ + if not self.codec.ptr: + raise ValueError("cannot decode unknown codec") + + self.open(strict=False) + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return None diff --git a/lib/python3.10/site-packages/av/subtitles/codeccontext.pyi b/lib/python3.10/site-packages/av/subtitles/codeccontext.pyi new file mode 100644 index 0000000000000000000000000000000000000000..90c700935c89e55734684908338a22196d32c518 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/codeccontext.pyi @@ -0,0 +1,9 @@ +from typing import Literal + +from av.codec.context import CodecContext +from av.packet import Packet +from av.subtitles.subtitle import SubtitleSet + +class SubtitleCodecContext(CodecContext): + type: Literal["subtitle"] + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/lib/python3.10/site-packages/av/subtitles/stream.pxd b/lib/python3.10/site-packages/av/subtitles/stream.pxd new file mode 100644 index 0000000000000000000000000000000000000000..745032af956c812f0d72f2f3dda9c888b23b1b7f --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/stream.pxd @@ -0,0 +1,6 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + +cdef class SubtitleStream(Stream): + cpdef decode(self, Packet packet=?) diff --git a/lib/python3.10/site-packages/av/subtitles/stream.py b/lib/python3.10/site-packages/av/subtitles/stream.py new file mode 100644 index 0000000000000000000000000000000000000000..525440e9db97876251b543d318f47d9b92809a07 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/stream.py @@ -0,0 +1,23 @@ +import cython +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream + + +@cython.cclass +class SubtitleStream(Stream): + def __getattr__(self, name): + return getattr(self.codec_context, name) + + @cython.ccall + def decode(self, packet: Packet | None = None): + """ + Decode a :class:`.Packet` and returns a subtitle object. + + :rtype: list[AssSubtitle] | list[BitmapSubtitle] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/lib/python3.10/site-packages/av/subtitles/stream.pyi b/lib/python3.10/site-packages/av/subtitles/stream.pyi new file mode 100644 index 0000000000000000000000000000000000000000..ac8083802b7d6d0ef627f35a6062479abc866649 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/stream.pyi @@ -0,0 +1,9 @@ +from av.packet import Packet +from av.stream import Stream +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet + +class SubtitleStream(Stream): + def decode( + self, packet: Packet | None = None + ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/lib/python3.10/site-packages/av/subtitles/subtitle.pxd b/lib/python3.10/site-packages/av/subtitles/subtitle.pxd new file mode 100644 index 0000000000000000000000000000000000000000..508eb903480a4a0b112f6580eb66ba9ae9820491 --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/subtitle.pxd @@ -0,0 +1,31 @@ +cimport libav as lib + + +cdef class SubtitleProxy: + cdef lib.AVSubtitle struct + + +cdef class SubtitleSet: + cdef SubtitleProxy proxy + cdef readonly tuple rects + + +cdef class Subtitle: + cdef SubtitleProxy proxy + cdef lib.AVSubtitleRect *ptr + cdef readonly bytes type + +cdef class TextSubtitle(Subtitle): + pass + +cdef class ASSSubtitle(Subtitle): + pass + +cdef class BitmapSubtitle(Subtitle): + cdef readonly planes + +cdef class BitmapSubtitlePlane: + cdef readonly BitmapSubtitle subtitle + cdef readonly int index + cdef readonly long buffer_size + cdef void *_buffer diff --git a/lib/python3.10/site-packages/av/subtitles/subtitle.py b/lib/python3.10/site-packages/av/subtitles/subtitle.py new file mode 100644 index 0000000000000000000000000000000000000000..1acadf0b5a5d9a2806e87782716f4761bd90609d --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/subtitle.py @@ -0,0 +1,232 @@ +import cython +from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString +from cython.cimports.libc.stdint import uint64_t + + +@cython.cclass +class SubtitleProxy: + def __dealloc__(self): + lib.avsubtitle_free(cython.address(self.struct)) + + +@cython.cclass +class SubtitleSet: + """ + A :class:`SubtitleSet` can contain many :class:`Subtitle` objects. + + Wraps :ffmpeg:`AVSubtitle`. + """ + + def __cinit__(self, proxy: SubtitleProxy): + self.proxy = proxy + self.rects = tuple( + build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + ) + + def __repr__(self): + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + ) + + @property + def format(self): + return self.proxy.struct.format + + @property + def start_display_time(self): + return self.proxy.struct.start_display_time + + @property + def end_display_time(self): + return self.proxy.struct.end_display_time + + @property + def pts(self): + """Same as packet pts, in av.time_base.""" + return self.proxy.struct.pts + + def __len__(self): + return len(self.rects) + + def __iter__(self): + return iter(self.rects) + + def __getitem__(self, i): + return self.rects[i] + + +@cython.cfunc +def build_subtitle(subtitle: SubtitleSet, index: cython.int) -> Subtitle: + """Build an av.Stream for an existing AVStream. + + The AVStream MUST be fully constructed and ready for use before this is called. + """ + if index < 0 or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects: + raise ValueError("subtitle rect index out of range") + + ptr: cython.pointer[lib.AVSubtitleRect] = subtitle.proxy.struct.rects[index] + + if ptr.type == lib.SUBTITLE_BITMAP: + return BitmapSubtitle(subtitle, index) + if ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: + return AssSubtitle(subtitle, index) + + raise ValueError("unknown subtitle type %r" % ptr.type) + + +@cython.cclass +class Subtitle: + """ + An abstract base class for each concrete type of subtitle. + Wraps :ffmpeg:`AVSubtitleRect` + """ + + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): + if ( + index < 0 + or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects + ): + raise ValueError("subtitle rect index out of range") + self.proxy = subtitle.proxy + self.ptr = self.proxy.struct.rects[index] + + if self.ptr.type == lib.SUBTITLE_NONE: + self.type = b"none" + elif self.ptr.type == lib.SUBTITLE_BITMAP: + self.type = b"bitmap" + elif self.ptr.type == lib.SUBTITLE_TEXT: + self.type = b"text" + elif self.ptr.type == lib.SUBTITLE_ASS: + self.type = b"ass" + else: + raise ValueError(f"unknown subtitle type {self.ptr.type!r}") + + def __repr__(self): + return f"" + + +@cython.cclass +class BitmapSubtitle(Subtitle): + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): + self.planes = tuple( + BitmapSubtitlePlane(self, i) for i in range(4) if self.ptr.linesize[i] + ) + + def __repr__(self): + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.width}x{self.height} at {self.x},{self.y}; at 0x{id(self):x}>" + ) + + @property + def x(self): + return self.ptr.x + + @property + def y(self): + return self.ptr.y + + @property + def width(self): + return self.ptr.w + + @property + def height(self): + return self.ptr.h + + @property + def nb_colors(self): + return self.ptr.nb_colors + + def __len__(self): + return len(self.planes) + + def __iter__(self): + return iter(self.planes) + + def __getitem__(self, i): + return self.planes[i] + + +@cython.cclass +class BitmapSubtitlePlane: + def __cinit__(self, subtitle: BitmapSubtitle, index: cython.int): + if index >= 4: + raise ValueError("BitmapSubtitles have only 4 planes") + if not subtitle.ptr.linesize[index]: + raise ValueError("plane does not exist") + + self.subtitle = subtitle + self.index = index + self.buffer_size = subtitle.ptr.w * subtitle.ptr.h + self._buffer = cython.cast(cython.p_void, subtitle.ptr.data[index]) + + # New-style buffer support. + def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): + PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) + + +@cython.cclass +class AssSubtitle(Subtitle): + """ + Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. + """ + + def __repr__(self): + return f"" + + @property + def ass(self): + """ + Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. + """ + if self.ptr.ass is not cython.NULL: + return PyBytes_FromString(self.ptr.ass) + return b"" + + @property + def dialogue(self): + """ + Extract the dialogue from the ass format. Strip comments. + """ + comma_count: cython.short = 0 + i: uint64_t = 0 + state: cython.bint = False + ass_text: bytes = self.ass + char, next_char = cython.declare(cython.char) + result: bytearray = bytearray() + text_len: cython.Py_ssize_t = len(ass_text) + + while comma_count < 8 and i < text_len: + if ass_text[i] == b","[0]: + comma_count += 1 + i += 1 + + while i < text_len: + char = ass_text[i] + next_char = 0 if i + 1 >= text_len else ass_text[i + 1] + + if char == b"\\"[0] and next_char == b"N"[0]: + result.append(b"\n"[0]) + i += 2 + continue + + if not state: + if char == b"{"[0] and next_char != b"\\"[0]: + state = True + else: + result.append(char) + elif char == b"}"[0]: + state = False + i += 1 + + return bytes(result) + + @property + def text(self): + """ + Rarely used attribute. You're probably looking for dialogue. + """ + if self.ptr.text is not cython.NULL: + return PyBytes_FromString(self.ptr.text) + return b"" diff --git a/lib/python3.10/site-packages/av/subtitles/subtitle.pyi b/lib/python3.10/site-packages/av/subtitles/subtitle.pyi new file mode 100644 index 0000000000000000000000000000000000000000..2a35d0a5546694b1030c53cae6e8914e237ff62d --- /dev/null +++ b/lib/python3.10/site-packages/av/subtitles/subtitle.pyi @@ -0,0 +1,37 @@ +from typing import Iterator, Literal + +class SubtitleSet: + format: int + start_display_time: int + end_display_time: int + pts: int + rects: tuple[Subtitle] + + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Subtitle]: ... + def __getitem__(self, i: int) -> Subtitle: ... + +class Subtitle: ... + +class BitmapSubtitle(Subtitle): + type: Literal[b"bitmap"] + x: int + y: int + width: int + height: int + nb_colors: int + planes: tuple[BitmapSubtitlePlane, ...] + +class BitmapSubtitlePlane: + subtitle: BitmapSubtitle + index: int + buffer_size: int + +class AssSubtitle(Subtitle): + type: Literal[b"ass", b"text"] + @property + def ass(self) -> bytes: ... + @property + def dialogue(self) -> bytes: ... + @property + def text(self) -> bytes: ... diff --git a/lib/python3.10/site-packages/av/video/__init__.pxd b/lib/python3.10/site-packages/av/video/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/av/video/__init__.py b/lib/python3.10/site-packages/av/video/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4a25d88376662b42451db9fe11a704f642f94744 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/__init__.py @@ -0,0 +1,2 @@ +from .frame import VideoFrame +from .stream import VideoStream diff --git a/lib/python3.10/site-packages/av/video/__init__.pyi b/lib/python3.10/site-packages/av/video/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..58a19a63fbfcb54ed33028383ac368ada7ef27d3 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/__init__.pyi @@ -0,0 +1,17 @@ +from typing import Literal + +from .frame import VideoFrame +from .stream import VideoStream + +_VideoCodecName = Literal[ + "gif", + "h264", + "hevc", + "libvpx", + "libx264", + "mpeg4", + "png", + "qtrle", +] + +__all__ = ("VideoFrame", "VideoStream") diff --git a/lib/python3.10/site-packages/av/video/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/av/video/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36fa6af6ca61ddd9d58304b0654327fe52be3ff0 Binary files /dev/null and b/lib/python3.10/site-packages/av/video/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/video/__pycache__/frame.cpython-310.pyc b/lib/python3.10/site-packages/av/video/__pycache__/frame.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef5c1ac352800bb84e137e426886349344d3d554 Binary files /dev/null and b/lib/python3.10/site-packages/av/video/__pycache__/frame.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/video/__pycache__/stream.cpython-310.pyc b/lib/python3.10/site-packages/av/video/__pycache__/stream.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67e308ecb6d2fa4a4775cb1cefe8228b8c86990f Binary files /dev/null and b/lib/python3.10/site-packages/av/video/__pycache__/stream.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/av/video/codeccontext.pxd b/lib/python3.10/site-packages/av/video/codeccontext.pxd new file mode 100644 index 0000000000000000000000000000000000000000..895ba74b15d77a991137d433c45a93b444a3d017 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/codeccontext.pxd @@ -0,0 +1,33 @@ +cimport libav as lib + +from av.codec.context cimport CodecContext +from av.video.format cimport VideoFormat +from av.video.frame cimport VideoFrame +from av.video.reformatter cimport VideoReformatter + + +# The get_format callback in AVCodecContext is called by the decoder to pick a format out of a list. +# When we want accelerated decoding, we need to figure out ahead of time what the format should be, +# and find a way to pass that into our callback so we can return it to the decoder. We use the 'opaque' +# user data field in AVCodecContext for that. This is the struct we store a pointer to in that field. +cdef struct AVCodecPrivateData: + lib.AVPixelFormat hardware_pix_fmt + bint allow_software_fallback + + +cdef class VideoCodecContext(CodecContext): + + cdef AVCodecPrivateData _private_data + + cdef VideoFormat _format + cdef _build_format(self) + + cdef int last_w + cdef int last_h + cdef readonly VideoReformatter reformatter + + # For encoding. + cdef readonly int encoded_frame_count + + # For decoding. + cdef VideoFrame next_frame diff --git a/lib/python3.10/site-packages/av/video/codeccontext.pyi b/lib/python3.10/site-packages/av/video/codeccontext.pyi new file mode 100644 index 0000000000000000000000000000000000000000..da72053c4ac8340304fe1e471d405ee484bc46ab --- /dev/null +++ b/lib/python3.10/site-packages/av/video/codeccontext.pyi @@ -0,0 +1,35 @@ +from fractions import Fraction +from typing import Iterator, Literal + +from av.codec.context import CodecContext +from av.packet import Packet + +from .format import VideoFormat +from .frame import VideoFrame + +class VideoCodecContext(CodecContext): + format: VideoFormat | None + width: int + height: int + bits_per_coded_sample: int + pix_fmt: str | None + framerate: Fraction + rate: Fraction + gop_size: int + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None + has_b_frames: bool + max_b_frames: int + coded_width: int + coded_height: int + color_range: int + color_primaries: int + color_trc: int + colorspace: int + qmin: int + qmax: int + type: Literal["video"] + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/lib/python3.10/site-packages/av/video/codeccontext.pyx b/lib/python3.10/site-packages/av/video/codeccontext.pyx new file mode 100644 index 0000000000000000000000000000000000000000..c9d8eb4c09da5d3cffc3267eded93d84a8ac2bc1 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/codeccontext.pyx @@ -0,0 +1,370 @@ +cimport libav as lib +from libc.stdint cimport int64_t + +from av.codec.context cimport CodecContext +from av.codec.hwaccel cimport HWAccel, HWConfig +from av.error cimport err_check +from av.frame cimport Frame +from av.packet cimport Packet +from av.utils cimport avrational_to_fraction, to_avrational +from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format +from av.video.frame cimport VideoFrame, alloc_video_frame +from av.video.reformatter cimport VideoReformatter + + +cdef lib.AVPixelFormat _get_hw_format(lib.AVCodecContext *ctx, const lib.AVPixelFormat *pix_fmts) noexcept: + # In the case where we requested accelerated decoding, the decoder first calls this function + # with a list that includes both the hardware format and software formats. + # First we try to pick the hardware format if it's in the list. + # However, if the decoder fails to initialize the hardware, it will call this function again, + # with only software formats in pix_fmts. We return ctx->sw_pix_fmt regardless in this case, + # because that should be in the candidate list. If not, we are out of ideas anyways. + cdef AVCodecPrivateData* private_data = ctx.opaque + i = 0 + while pix_fmts[i] != -1: + if pix_fmts[i] == private_data.hardware_pix_fmt: + return pix_fmts[i] + i += 1 + return ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE + + +cdef class VideoCodecContext(CodecContext): + + def __cinit__(self, *args, **kwargs): + self.last_w = 0 + self.last_h = 0 + + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + CodecContext._init(self, ptr, codec, hwaccel) # TODO: Can this be `super`? + + if hwaccel is not None: + try: + self.hwaccel_ctx = hwaccel.create(self.codec) + self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) + self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self.ptr.get_format = _get_hw_format + self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self._private_data.allow_software_fallback = self.hwaccel.allow_software_fallback + self.ptr.opaque = &self._private_data + except NotImplementedError: + # Some streams may not have a hardware decoder. For example, many action + # cam videos have a low resolution mjpeg stream, which is usually not + # compatible with hardware decoders. + # The user may have passed in a hwaccel because they want to decode the main + # stream with it, so we shouldn't abort even if we find a stream that can't + # be HW decoded. + # If the user wants to make sure hwaccel is actually used, they can check with the + # is_hwaccel() function on each stream's codec context. + self.hwaccel_ctx = None + + self._build_format() + self.encoded_frame_count = 0 + + cdef _prepare_frames_for_encode(self, Frame input): + if not input: + return [None] + + cdef VideoFrame vframe = input + + if self._format is None: + raise ValueError("self._format is None, cannot encode") + + # Reformat if it doesn't match. + if ( + vframe.format.pix_fmt != self._format.pix_fmt or + vframe.width != self.ptr.width or + vframe.height != self.ptr.height + ): + if not self.reformatter: + self.reformatter = VideoReformatter() + + vframe = self.reformatter.reformat( + vframe, self.ptr.width, self.ptr.height, self._format + ) + + # There is no pts, so create one. + if vframe.ptr.pts == lib.AV_NOPTS_VALUE: + vframe.ptr.pts = self.encoded_frame_count + + self.encoded_frame_count += 1 + + return [vframe] + + cdef Frame _alloc_next_frame(self): + return alloc_video_frame() + + cdef _setup_decoded_frame(self, Frame frame, Packet packet): + CodecContext._setup_decoded_frame(self, frame, packet) + cdef VideoFrame vframe = frame + vframe._init_user_attributes() + + cdef _transfer_hwframe(self, Frame frame): + if self.hwaccel_ctx is None: + return frame + + if frame.ptr.format != self.hwaccel_ctx.config.ptr.pix_fmt: + # If we get a software frame, that means we are in software fallback mode, and don't actually + # need to transfer. + return frame + + cdef Frame frame_sw + + frame_sw = self._alloc_next_frame() + + err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) + + # TODO: Is there anything else to transfer?! + frame_sw.pts = frame.pts + + return frame_sw + + cdef _build_format(self): + self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) + + @property + def format(self): + return self._format + + @format.setter + def format(self, VideoFormat format): + self.ptr.pix_fmt = format.pix_fmt + self.ptr.width = format.width + self.ptr.height = format.height + self._build_format() # Kinda wasteful. + + @property + def width(self): + if self.ptr is NULL: + return 0 + return self.ptr.width + + @width.setter + def width(self, unsigned int value): + self.ptr.width = value + self._build_format() + + @property + def height(self): + if self.ptr is NULL: + return 0 + return self.ptr.height + + @height.setter + def height(self, unsigned int value): + self.ptr.height = value + self._build_format() + + @property + def bits_per_coded_sample(self): + """ + The number of bits per sample in the codedwords. It's mandatory for this to be set for some formats to decode properly. + + Wraps :ffmpeg:`AVCodecContext.bits_per_coded_sample`. + + :type: int + """ + return self.ptr.bits_per_coded_sample + + @bits_per_coded_sample.setter + def bits_per_coded_sample(self, int value): + if self.is_encoder: + raise ValueError("Not supported for encoders") + + self.ptr.bits_per_coded_sample = value + self._build_format() + + @property + def pix_fmt(self): + """ + The pixel format's name. + + :type: str | None + """ + return getattr(self._format, "name", None) + + @pix_fmt.setter + def pix_fmt(self, value): + self.ptr.pix_fmt = get_pix_fmt(value) + self._build_format() + + @property + def framerate(self): + """ + The frame rate, in frames per second. + + :type: fractions.Fraction + """ + return avrational_to_fraction(&self.ptr.framerate) + + @framerate.setter + def framerate(self, value): + to_avrational(value, &self.ptr.framerate) + + @property + def rate(self): + """Another name for :attr:`framerate`.""" + return self.framerate + + @rate.setter + def rate(self, value): + self.framerate = value + + @property + def gop_size(self): + """ + Sets the number of frames between keyframes. Used only for encoding. + + :type: int + """ + if self.is_decoder: + raise RuntimeError("Cannnot access 'gop_size' as a decoder") + return self.ptr.gop_size + + @gop_size.setter + def gop_size(self, int value): + if self.is_decoder: + raise RuntimeError("Cannnot access 'gop_size' as a decoder") + self.ptr.gop_size = value + + @property + def sample_aspect_ratio(self): + return avrational_to_fraction(&self.ptr.sample_aspect_ratio) + + @sample_aspect_ratio.setter + def sample_aspect_ratio(self, value): + to_avrational(value, &self.ptr.sample_aspect_ratio) + + @property + def display_aspect_ratio(self): + cdef lib.AVRational dar + + lib.av_reduce( + &dar.num, &dar.den, + self.ptr.width * self.ptr.sample_aspect_ratio.num, + self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) + + return avrational_to_fraction(&dar) + + @property + def has_b_frames(self): + """ + :type: bool + """ + return bool(self.ptr.has_b_frames) + + @property + def coded_width(self): + """ + :type: int + """ + return self.ptr.coded_width + + @property + def coded_height(self): + """ + :type: int + """ + return self.ptr.coded_height + + @property + def color_range(self): + """ + Describes the signal range of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_range`. + + :type: int + """ + return self.ptr.color_range + + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value + + @property + def color_primaries(self): + """ + Describes the RGB/XYZ matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_primaries`. + + :type: int + """ + return self.ptr.color_primaries + + @color_primaries.setter + def color_primaries(self, value): + self.ptr.color_primaries = value + + @property + def color_trc(self): + """ + Describes the linearization function (a.k.a. transformation characteristics) of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_trc`. + + :type: int + """ + return self.ptr.color_trc + + @color_trc.setter + def color_trc(self, value): + self.ptr.color_trc = value + + @property + def colorspace(self): + """ + Describes the YUV/RGB transformation matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + :type: int + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + + @property + def max_b_frames(self): + """ + The maximum run of consecutive B frames when encoding a video. + + :type: int + """ + return self.ptr.max_b_frames + + @max_b_frames.setter + def max_b_frames(self, value): + self.ptr.max_b_frames = value + + @property + def qmin(self): + """ + The minimum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmin`. + + :type: int + """ + return self.ptr.qmin + + @qmin.setter + def qmin(self, value): + self.ptr.qmin = value + + @property + def qmax(self): + """ + The maximum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmax`. + + :type: int + """ + return self.ptr.qmax + + @qmax.setter + def qmax(self, value): + self.ptr.qmax = value diff --git a/lib/python3.10/site-packages/av/video/format.pxd b/lib/python3.10/site-packages/av/video/format.pxd new file mode 100644 index 0000000000000000000000000000000000000000..a2efa9d1d26976debb5d4cca1d2da3883c9fb281 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/format.pxd @@ -0,0 +1,27 @@ +cimport libav as lib + + +cdef class VideoFormat: + + cdef lib.AVPixelFormat pix_fmt + cdef const lib.AVPixFmtDescriptor *ptr + cdef readonly unsigned int width, height + + cdef readonly tuple components + + cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height) + + cpdef chroma_width(self, int luma_width=?) + cpdef chroma_height(self, int luma_height=?) + + +cdef class VideoFormatComponent: + + cdef VideoFormat format + cdef readonly unsigned int index + cdef const lib.AVComponentDescriptor *ptr + + +cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height) + +cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE diff --git a/lib/python3.10/site-packages/av/video/format.pyi b/lib/python3.10/site-packages/av/video/format.pyi new file mode 100644 index 0000000000000000000000000000000000000000..e102ef4c00ce98a2291e6013b634a81f45af3a1a --- /dev/null +++ b/lib/python3.10/site-packages/av/video/format.pyi @@ -0,0 +1,30 @@ +class VideoFormat: + name: str + bits_per_pixel: int + padded_bits_per_pixel: int + is_big_endian: bool + has_palette: bool + is_bit_stream: bool + is_planar: bool + @property + def is_rgb(self) -> bool: ... + @property + def is_bayer(self) -> bool: ... + width: int + height: int + components: tuple[VideoFormatComponent, ...] + + def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... + def chroma_width(self, luma_width: int = 0) -> int: ... + def chroma_height(self, luma_height: int = 0) -> int: ... + +class VideoFormatComponent: + plane: int + bits: int + is_alpha: bool + is_luma: bool + is_chroma: bool + width: int + height: int + + def __init__(self, format: VideoFormat, index: int) -> None: ... diff --git a/lib/python3.10/site-packages/av/video/format.pyx b/lib/python3.10/site-packages/av/video/format.pyx new file mode 100644 index 0000000000000000000000000000000000000000..8779f715187a5a7ec28263ce49bf789a7d8d2b95 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/format.pyx @@ -0,0 +1,198 @@ + +cdef object _cinit_bypass_sentinel = object() + +cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height): + if c_format == lib.AV_PIX_FMT_NONE: + return None + + cdef VideoFormat format = VideoFormat.__new__(VideoFormat, _cinit_bypass_sentinel) + format._init(c_format, width, height) + return format + +cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: + """Wrapper for lib.av_get_pix_fmt with error checking.""" + + cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) + + if pix_fmt == lib.AV_PIX_FMT_NONE: + raise ValueError("not a pixel format: %r" % name) + + return pix_fmt + + +cdef class VideoFormat: + """ + + >>> format = VideoFormat('rgb24') + >>> format.name + 'rgb24' + + """ + + def __cinit__(self, name, width=0, height=0): + if name is _cinit_bypass_sentinel: + return + + cdef VideoFormat other + if isinstance(name, VideoFormat): + other = name + self._init(other.pix_fmt, width or other.width, height or other.height) + return + + cdef lib.AVPixelFormat pix_fmt = get_pix_fmt(name) + self._init(pix_fmt, width, height) + + cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height): + self.pix_fmt = pix_fmt + self.ptr = lib.av_pix_fmt_desc_get(pix_fmt) + self.width = width + self.height = height + self.components = tuple( + VideoFormatComponent(self, i) + for i in range(self.ptr.nb_components) + ) + + def __repr__(self): + if self.width or self.height: + return f"" + else: + return f"" + + def __int__(self): + return int(self.pix_fmt) + + @property + def name(self): + """Canonical name of the pixel format.""" + return self.ptr.name + + @property + def bits_per_pixel(self): + return lib.av_get_bits_per_pixel(self.ptr) + + @property + def padded_bits_per_pixel(self): return lib.av_get_padded_bits_per_pixel(self.ptr) + + @property + def is_big_endian(self): + """Pixel format is big-endian.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) + + + @property + def has_palette(self): + """Pixel format has a palette in data[1], values are indexes in this palette.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) + + + @property + def is_bit_stream(self): + """All values of a component are bit-wise packed end to end.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) + + + # Skipping PIX_FMT_HWACCEL + # """Pixel format is an HW accelerated format.""" + + @property + def is_planar(self): + """At least one pixel component is not in the first data plane.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) + + + @property + def is_rgb(self): + """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + + + @property + def is_bayer(self): + """The pixel format contains Bayer data.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BAYER) + + cpdef chroma_width(self, int luma_width=0): + """chroma_width(luma_width=0) + + Width of a chroma plane relative to a luma plane. + + :param int luma_width: Width of the luma plane; defaults to ``self.width``. + + """ + luma_width = luma_width or self.width + return -((-luma_width) >> self.ptr.log2_chroma_w) if luma_width else 0 + + cpdef chroma_height(self, int luma_height=0): + """chroma_height(luma_height=0) + + Height of a chroma plane relative to a luma plane. + + :param int luma_height: Height of the luma plane; defaults to ``self.height``. + + """ + luma_height = luma_height or self.height + return -((-luma_height) >> self.ptr.log2_chroma_h) if luma_height else 0 + + +cdef class VideoFormatComponent: + def __cinit__(self, VideoFormat format, size_t index): + self.format = format + self.index = index + self.ptr = &format.ptr.comp[index] + + @property + def plane(self): + """The index of the plane which contains this component.""" + return self.ptr.plane + + @property + def bits(self): + """Number of bits in the component.""" + return self.ptr.depth + + @property + def is_alpha(self): + """Is this component an alpha channel?""" + return ((self.index == 1 and self.format.ptr.nb_components == 2) or + (self.index == 3 and self.format.ptr.nb_components == 4)) + + @property + def is_luma(self): + """Is this compoment a luma channel?""" + return self.index == 0 and ( + self.format.ptr.nb_components == 1 or + self.format.ptr.nb_components == 2 or + not self.format.is_rgb + ) + + @property + def is_chroma(self): + """Is this component a chroma channel?""" + return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) + + @property + def width(self): + """The width of this component's plane. + + Requires the parent :class:`VideoFormat` to have a width. + + """ + return self.format.chroma_width() if self.is_chroma else self.format.width + + @property + def height(self): + """The height of this component's plane. + + Requires the parent :class:`VideoFormat` to have a height. + + """ + return self.format.chroma_height() if self.is_chroma else self.format.height + + +names = set() +cdef const lib.AVPixFmtDescriptor *desc = NULL +while True: + desc = lib.av_pix_fmt_desc_next(desc) + if not desc: + break + names.add(desc.name) diff --git a/lib/python3.10/site-packages/av/video/frame.pxd b/lib/python3.10/site-packages/av/video/frame.pxd new file mode 100644 index 0000000000000000000000000000000000000000..d352a7ab863c8346e82942a843149227522ab4e0 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/frame.pxd @@ -0,0 +1,22 @@ +cimport libav as lib +from libc.stdint cimport uint8_t + +from av.frame cimport Frame +from av.video.format cimport VideoFormat +from av.video.reformatter cimport VideoReformatter + + +cdef class VideoFrame(Frame): + # This is the buffer that is used to back everything in the AVFrame. + # We don't ever actually access it directly. + cdef uint8_t *_buffer + cdef object _np_buffer + + cdef VideoReformatter reformatter + cdef readonly VideoFormat format + + cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) + cdef _init_user_attributes(self) + cpdef save(self, object filepath) + +cdef VideoFrame alloc_video_frame() diff --git a/lib/python3.10/site-packages/av/video/frame.py b/lib/python3.10/site-packages/av/video/frame.py new file mode 100644 index 0000000000000000000000000000000000000000..970ecc4b8985aeb7eca8c27938453863f100e928 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/frame.py @@ -0,0 +1,1154 @@ +import sys +from enum import IntEnum + +import cython +from cython.cimports.av.error import err_check +from cython.cimports.av.sidedata.sidedata import get_display_rotation +from cython.cimports.av.utils import check_ndarray +from cython.cimports.av.video.format import get_pix_fmt, get_video_format +from cython.cimports.av.video.plane import VideoPlane +from cython.cimports.libc.stdint import uint8_t + +_cinit_bypass_sentinel = object() + +# `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() +supported_np_pix_fmts = { + "abgr", + "argb", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_bggr8", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_gbrg8", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_grbg8", + "bayer_rggb16be", + "bayer_rggb16le", + "bayer_rggb8", + "bgr24", + "bgr48be", + "bgr48le", + "bgr8", + "bgra", + "bgra64be", + "bgra64le", + "gbrap", + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + "gbrapf32be", + "gbrapf32le", + "gbrp", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + "gbrp9be", + "gbrp9le", + "gbrpf32be", + "gbrpf32le", + "gray", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "gray8", + "gray9be", + "gray9le", + "grayf32be", + "grayf32le", + "nv12", + "pal8", + "rgb24", + "rgb48be", + "rgb48le", + "rgb8", + "rgba", + "rgba64be", + "rgba64le", + "rgbaf16be", + "rgbaf16le", + "rgbaf32be", + "rgbaf32le", + "rgbf32be", + "rgbf32le", + "yuv420p", + "yuv422p10le", + "yuv444p", + "yuv444p16be", + "yuv444p16le", + "yuva444p16be", + "yuva444p16le", + "yuvj420p", + "yuvj444p", + "yuyv422", +} + + +@cython.cfunc +def alloc_video_frame() -> VideoFrame: + """Get a mostly uninitialized VideoFrame. + + You MUST call VideoFrame._init(...) or VideoFrame._init_user_attributes() + before exposing to the user. + + """ + return VideoFrame(_cinit_bypass_sentinel) + + +class PictureType(IntEnum): + NONE = lib.AV_PICTURE_TYPE_NONE # Undefined + I = lib.AV_PICTURE_TYPE_I # Intra + P = lib.AV_PICTURE_TYPE_P # Predicted + B = lib.AV_PICTURE_TYPE_B # Bi-directional predicted + S = lib.AV_PICTURE_TYPE_S # S(GMC)-VOP MPEG-4 + SI = lib.AV_PICTURE_TYPE_SI # Switching intra + SP = lib.AV_PICTURE_TYPE_SP # Switching predicted + BI = lib.AV_PICTURE_TYPE_BI # BI type + + +@cython.cfunc +def byteswap_array(array, big_endian: cython.bint): + if (sys.byteorder == "big") != big_endian: + return array.byteswap() + return array + + +@cython.cfunc +def copy_bytes_to_plane( + img_bytes, + plane: VideoPlane, + bytes_per_pixel: cython.uint, + flip_horizontal: cython.bint, + flip_vertical: cython.bint, +): + i_buf: cython.const[uint8_t][:] = img_bytes + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.width * bytes_per_pixel + + o_buf: uint8_t[:] = plane + o_pos: cython.size_t = 0 + o_stride: cython.size_t = abs(plane.line_size) + + start_row, end_row, step = cython.declare(cython.int) + if flip_vertical: + start_row = plane.height - 1 + end_row = -1 + step = -1 + else: + start_row = 0 + end_row = plane.height + step = 1 + + for row in range(start_row, end_row, step): + i_pos = row * i_stride + if flip_horizontal: + for i in range(0, i_stride, bytes_per_pixel): + for j in range(bytes_per_pixel): + o_buf[o_pos + i + j] = i_buf[ + i_pos + i_stride - i - bytes_per_pixel + j + ] + else: + o_buf[o_pos : o_pos + i_stride] = i_buf[i_pos : i_pos + i_stride] + o_pos += o_stride + + +@cython.cfunc +def copy_array_to_plane(array, plane: VideoPlane, bytes_per_pixel: cython.uint): + imgbytes: bytes = array.tobytes() + copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) + + +@cython.cfunc +def useful_array( + plane: VideoPlane, bytes_per_pixel: cython.uint = 1, dtype: str = "uint8" +): + """ + Return the useful part of the VideoPlane as a single dimensional array. + + We are simply discarding any padding which was added for alignment. + """ + import numpy as np + + total_line_size: cython.size_t = abs(plane.line_size) + useful_line_size: cython.size_t = plane.width * bytes_per_pixel + arr = np.frombuffer(plane, np.uint8) + if total_line_size != useful_line_size: + arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) + return arr.view(np.dtype(dtype)) + + +@cython.cfunc +def check_ndarray_shape(array: object, ok: cython.bint): + if not ok: + raise ValueError(f"Unexpected numpy array shape `{array.shape}`") + + +@cython.cclass +class VideoFrame(Frame): + def __cinit__(self, width=0, height=0, format="yuv420p"): + if width is _cinit_bypass_sentinel: + return + + c_format: lib.AVPixelFormat = get_pix_fmt(format) + self._init(c_format, width, height) + + @cython.cfunc + def _init(self, format: lib.AVPixelFormat, width: cython.uint, height: cython.uint): + res: cython.int = 0 + + with cython.nogil: + self.ptr.width = width + self.ptr.height = height + self.ptr.format = format + + # We enforce aligned buffers, otherwise `sws_scale` can perform + # poorly or even cause out-of-bounds reads and writes. + if width and height: + res = lib.av_image_alloc( + self.ptr.data, self.ptr.linesize, width, height, format, 16 + ) + self._buffer = self.ptr.data[0] + + if res: + err_check(res) + + self._init_user_attributes() + + @cython.cfunc + def _init_user_attributes(self): + self.format = get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.format), + self.ptr.width, + self.ptr.height, + ) + + def __dealloc__(self): + # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, + # as opposed to a buffer allocated by a decoder. + lib.av_freep(cython.address(self._buffer)) + # Let go of the reference from the numpy buffers if we made one + self._np_buffer = None + + def __repr__(self): + return ( + f"" + ) + + @property + def planes(self): + """ + A tuple of :class:`.VideoPlane` objects. + """ + # We need to detect which planes actually exist, but also constrain ourselves to + # the maximum plane count (as determined only by VideoFrames so far), in case + # the library implementation does not set the last plane to NULL. + max_plane_count: cython.int = 0 + for i in range(self.format.ptr.nb_components): + count = self.format.ptr.comp[i].plane + 1 + if max_plane_count < count: + max_plane_count = count + if self.format.name == "pal8": + max_plane_count = 2 + + plane_count: cython.int = 0 + while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: + plane_count += 1 + return tuple([VideoPlane(self, i) for i in range(plane_count)]) + + @property + def width(self): + """Width of the image, in pixels.""" + return self.ptr.width + + @property + def height(self): + """Height of the image, in pixels.""" + return self.ptr.height + + @property + def rotation(self): + """The rotation component of the `DISPLAYMATRIX` transformation matrix. + + Returns: + int: The angle (in degrees) by which the transformation rotates the frame + counterclockwise. The angle will be in range [-180, 180]. + """ + return get_display_rotation(self) + + @property + def interlaced_frame(self): + """Is this frame an interlaced or progressive?""" + + return bool(self.ptr.flags & lib.AV_FRAME_FLAG_INTERLACED) + + @property + def pict_type(self): + """Returns an integer that corresponds to the PictureType enum. + + Wraps :ffmpeg:`AVFrame.pict_type` + + :type: int + """ + return self.ptr.pict_type + + @pict_type.setter + def pict_type(self, value): + self.ptr.pict_type = value + + @property + def colorspace(self): + """Colorspace of frame. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + + @property + def color_range(self): + """Color range of frame. + + Wraps :ffmpeg:`AVFrame.color_range`. + + """ + return self.ptr.color_range + + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value + + def reformat(self, *args, **kwargs): + """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None) + + Create a new :class:`VideoFrame` with the given width/height/format/colorspace. + + .. seealso:: :meth:`.VideoReformatter.reformat` for arguments. + + """ + if not self.reformatter: + self.reformatter = VideoReformatter() + return self.reformatter.reformat(self, *args, **kwargs) + + def to_rgb(self, **kwargs): + """Get an RGB version of this frame. + + Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. + + >>> frame = VideoFrame(1920, 1080) + >>> frame.format.name + 'yuv420p' + >>> frame.to_rgb().format.name + 'rgb24' + + """ + return self.reformat(format="rgb24", **kwargs) + + @cython.ccall + def save(self, filepath: object): + """Save a VideoFrame as a JPG or PNG. + + :param filepath: str | Path + """ + is_jpg: cython.bint + + if filepath.endswith(".png"): + is_jpg = False + elif filepath.endswith(".jpg") or filepath.endswith(".jpeg"): + is_jpg = True + else: + raise ValueError("filepath must end with png or jpg.") + + encoder: str = "mjpeg" if is_jpg else "png" + pix_fmt: str = "yuvj420p" if is_jpg else "rgb24" + + from av.container.core import open + + with open(filepath, "w", options={"update": "1"}) as output: + output_stream = output.add_stream(encoder, pix_fmt=pix_fmt) + output_stream.width = self.width + output_stream.height = self.height + + output.mux(output_stream.encode(self.reformat(format=pix_fmt))) + output.mux(output_stream.encode(None)) + + def to_image(self, **kwargs): + """Get an RGB ``PIL.Image`` of this frame. + + Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. + + .. note:: PIL or Pillow must be installed. + + """ + from PIL import Image + + plane: VideoPlane = self.reformat(format="rgb24", **kwargs).planes[0] + + i_buf: cython.const[uint8_t][:] = plane + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.line_size + + o_pos: cython.size_t = 0 + o_stride: cython.size_t = plane.width * 3 + o_size: cython.size_t = plane.height * o_stride + o_buf: bytearray = bytearray(o_size) + + while o_pos < o_size: + o_buf[o_pos : o_pos + o_stride] = i_buf[i_pos : i_pos + o_stride] + i_pos += i_stride + o_pos += o_stride + + return Image.frombytes( + "RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1 + ) + + def to_ndarray(self, channel_last=False, **kwargs): + """Get a numpy array of this frame. + + Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. + + The array returned is generally of dimension (height, width, channels). + + :param bool channel_last: If True, the shape of array will be + (height, width, channels) rather than (channels, height, width) for + the "yuv444p" and "yuvj444p" formats. + + .. note:: Numpy must be installed. + + .. note:: For formats which return an array of ``uint16``, ``float16`` or ``float32``, + the samples will be in the system's native byte order. + + .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, + with the palette being in ARGB (PyAV will swap bytes if needed). + + .. note:: For ``gbrp`` formats, channels are flipped to RGB order. + + """ + frame: VideoFrame = self.reformat(**kwargs) + + import numpy as np + + # check size + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + assert frame.width % 2 == 0, ( + "the width has to be even for this pixel format" + ) + assert frame.height % 2 == 0, ( + "the height has to be even for this pixel format" + ) + + # cases planes are simply concatenated in shape (height, width, channels) + itemsize, dtype = { + "abgr": (4, "uint8"), + "argb": (4, "uint8"), + "bayer_bggr8": (1, "uint8"), + "bayer_gbrg8": (1, "uint8"), + "bayer_grbg8": (1, "uint8"), + "bayer_rggb8": (1, "uint8"), + "bayer_bggr16le": (2, "uint16"), + "bayer_bggr16be": (2, "uint16"), + "bayer_gbrg16le": (2, "uint16"), + "bayer_gbrg16be": (2, "uint16"), + "bayer_grbg16le": (2, "uint16"), + "bayer_grbg16be": (2, "uint16"), + "bayer_rggb16le": (2, "uint16"), + "bayer_rggb16be": (2, "uint16"), + "bgr24": (3, "uint8"), + "bgr48be": (6, "uint16"), + "bgr48le": (6, "uint16"), + "bgr8": (1, "uint8"), + "bgra": (4, "uint8"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), + "gbrap": (1, "uint8"), + "gbrap10be": (2, "uint16"), + "gbrap10le": (2, "uint16"), + "gbrap12be": (2, "uint16"), + "gbrap12le": (2, "uint16"), + "gbrap14be": (2, "uint16"), + "gbrap14le": (2, "uint16"), + "gbrap16be": (2, "uint16"), + "gbrap16le": (2, "uint16"), + "gbrapf32be": (4, "float32"), + "gbrapf32le": (4, "float32"), + "gbrp": (1, "uint8"), + "gbrp10be": (2, "uint16"), + "gbrp10le": (2, "uint16"), + "gbrp12be": (2, "uint16"), + "gbrp12le": (2, "uint16"), + "gbrp14be": (2, "uint16"), + "gbrp14le": (2, "uint16"), + "gbrp16be": (2, "uint16"), + "gbrp16le": (2, "uint16"), + "gbrp9be": (2, "uint16"), + "gbrp9le": (2, "uint16"), + "gbrpf32be": (4, "float32"), + "gbrpf32le": (4, "float32"), + "gray": (1, "uint8"), + "gray10be": (2, "uint16"), + "gray10le": (2, "uint16"), + "gray12be": (2, "uint16"), + "gray12le": (2, "uint16"), + "gray14be": (2, "uint16"), + "gray14le": (2, "uint16"), + "gray16be": (2, "uint16"), + "gray16le": (2, "uint16"), + "gray8": (1, "uint8"), + "gray9be": (2, "uint16"), + "gray9le": (2, "uint16"), + "grayf32be": (4, "float32"), + "grayf32le": (4, "float32"), + "rgb24": (3, "uint8"), + "rgb48be": (6, "uint16"), + "rgb48le": (6, "uint16"), + "rgb8": (1, "uint8"), + "rgba": (4, "uint8"), + "rgba64be": (8, "uint16"), + "rgba64le": (8, "uint16"), + "rgbaf16be": (8, "float16"), + "rgbaf16le": (8, "float16"), + "rgbaf32be": (16, "float32"), + "rgbaf32le": (16, "float32"), + "rgbf32be": (12, "float32"), + "rgbf32le": (12, "float32"), + "yuv444p": (1, "uint8"), + "yuv444p16be": (2, "uint16"), + "yuv444p16le": (2, "uint16"), + "yuva444p16be": (2, "uint16"), + "yuva444p16le": (2, "uint16"), + "yuvj444p": (1, "uint8"), + "yuyv422": (2, "uint8"), + }.get(frame.format.name, (None, None)) + if itemsize is not None: + layers = [ + useful_array(plan, itemsize, dtype).reshape( + frame.height, frame.width, -1 + ) + for plan in frame.planes + ] + if len(layers) == 1: # shortcut, avoid memory copy + array = layers[0] + else: # general case + array = np.concatenate(layers, axis=2) + array = byteswap_array(array, frame.format.name.endswith("be")) + if array.shape[2] == 1: # skip last channel for gray images + return array.squeeze(2) + if frame.format.name.startswith("gbr"): # gbr -> rgb + buffer = array[:, :, 0].copy() + array[:, :, 0] = array[:, :, 2] + array[:, :, 2] = array[:, :, 1] + array[:, :, 1] = buffer + if not channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 2, 0) + return array + + # special cases + if frame.format.name in {"yuv420p", "yuvj420p"}: + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]), + ] + ).reshape(-1, frame.width) + if frame.format.name == "yuv422p10le": + # Read planes as uint16 at their original width + y = useful_array(frame.planes[0], 2, "uint16").reshape( + frame.height, frame.width + ) + u = useful_array(frame.planes[1], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) + v = useful_array(frame.planes[2], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) + + # Double the width of U and V by repeating each value + u_full = np.repeat(u, 2, axis=1) + v_full = np.repeat(v, 2, axis=1) + if channel_last: + return np.stack([y, u_full, v_full], axis=2) + return np.stack([y, u_full, v_full], axis=0) + if frame.format.name == "pal8": + image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) + palette = ( + np.frombuffer(frame.planes[1], "i4") + .astype(">i4") + .reshape(-1, 1) + .view(np.uint8) + ) + return image, palette + if frame.format.name == "nv12": + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1], 2), + ] + ).reshape(-1, frame.width) + + raise ValueError( + f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + ) + + @staticmethod + def from_image(img): + """ + Construct a frame from a ``PIL.Image``. + """ + if img.mode != "RGB": + img = img.convert("RGB") + + frame: VideoFrame = VideoFrame(img.size[0], img.size[1], "rgb24") + copy_array_to_plane(img, frame.planes[0], 3) + + return frame + + @staticmethod + def from_numpy_buffer(array, format="rgb24", width=0): + """ + Construct a frame from a numpy buffer. + + :param int width: optional width of actual image, if different from the array width. + + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, + the samples must be in the system's native byte order. + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + + .. note:: For formats where width of the array is not the same as the width of the image, + for example with yuv420p images the UV rows at the bottom have padding bytes in the middle of the + row as well as at the end. To cope with these, callers need to be able to pass the actual width. + """ + import numpy as np + + height = array.shape[0] + if not width: + width = array.shape[1] + + if format in {"rgb24", "bgr24"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgb48le", "rgb48be", "bgr48le", "bgr48be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgbf32le", "rgbf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgba", "bgra", "argb", "abgr"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgba64le", "rgba64be", "bgra64le", "bgra64be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgbaf16le", "rgbaf16be"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgbaf32le", "rgbaf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in { + "gray", + "gray8", + "rgb8", + "bgr8", + "bayer_bggr8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_rggb8", + }: + check_ndarray(array, "uint8", 2) + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in { + "gray9be", + "gray9le", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_rggb16be", + "bayer_rggb16le", + }: + check_ndarray(array, "uint16", 2) + if array.strides[1] != 2: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"grayf32le", "grayf32be"}: + check_ndarray(array, "float32", 2) + if array.strides[1] != 4: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"gbrp"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in { + "gbrp9be", + "gbrp9le", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrpf32be", "gbrpf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrap"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in { + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in {"gbrapf32be", "gbrapf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in {"yuv420p", "yuvj420p", "nv12"}: + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + height = height // 6 * 4 + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + if format in {"yuv420p", "yuvj420p"}: + # For YUV420 planar formats, the UV plane stride is always half the Y stride. + linesizes = ( + array.strides[0], + array.strides[0] // 2, + array.strides[0] // 2, + ) + else: + # Planes where U and V are interleaved have the same stride as Y. + linesizes = (array.strides[0], array.strides[0]) + else: + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) + + if format.startswith("gbrap"): # rgba -> gbra + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0, 3]], -1, 0)) + elif format.startswith("gbrp"): # rgb -> gbr + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0]], -1, 0)) + + frame = VideoFrame(_cinit_bypass_sentinel) + frame._image_fill_pointers_numpy(array, width, height, linesizes, format) + return frame + + def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): + c_format: lib.AVPixelFormat + c_ptr: cython.pointer[uint8_t] + c_data: cython.size_t + + # If you want to use the numpy notation, then you need to include the following lines at the top of the file: + # cimport numpy as cnp + # cnp.import_array() + + # And add the numpy include directories to the setup.py files + # hint np.get_include() + # cdef cnp.ndarray[ + # dtype=cnp.uint8_t, ndim=1, + # negative_indices=False, mode='c'] c_buffer + # c_buffer = buffer.reshape(-1) + # c_ptr = &c_buffer[0] + # c_ptr = ((buffer.ctypes.data)) + + # Using buffer.ctypes.data helps avoid any kind of usage of the c-api from + # numpy, which avoid the need to add numpy as a compile time dependency. + + c_data = buffer.ctypes.data + c_ptr = cython.cast(cython.pointer[uint8_t], c_data) + c_format = get_pix_fmt(format) + lib.av_freep(cython.address(self._buffer)) + + # Hold on to a reference for the numpy buffer so that it doesn't get accidentally garbage collected + self._np_buffer = buffer + self.ptr.format = c_format + self.ptr.width = width + self.ptr.height = height + for i, linesize in enumerate(linesizes): + self.ptr.linesize[i] = linesize + + res = lib.av_image_fill_pointers( + self.ptr.data, + cython.cast(lib.AVPixelFormat, self.ptr.format), + self.ptr.height, + c_ptr, + self.ptr.linesize, + ) + + if res: + err_check(res) + self._init_user_attributes() + + @staticmethod + def from_ndarray(array, format="rgb24", channel_last=False): + """ + Construct a frame from a numpy array. + + :param bool channel_last: If False (default), the shape for the yuv444p and yuvj444p + is given by (channels, height, width) rather than (height, width, channels). + + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, + the samples must be in the system's native byte order. + + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must + have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + + """ + import numpy as np + + # case layers are concatenated + channels, itemsize, dtype = { + "bayer_bggr16be": (1, 2, "uint16"), + "bayer_bggr16le": (1, 2, "uint16"), + "bayer_bggr8": (1, 1, "uint8"), + "bayer_gbrg16be": (1, 2, "uint16"), + "bayer_gbrg16le": (1, 2, "uint16"), + "bayer_gbrg8": (1, 1, "uint8"), + "bayer_grbg16be": (1, 2, "uint16"), + "bayer_grbg16le": (1, 2, "uint16"), + "bayer_grbg8": (1, 1, "uint8"), + "bayer_rggb16be": (1, 2, "uint16"), + "bayer_rggb16le": (1, 2, "uint16"), + "bayer_rggb8": (1, 1, "uint8"), + "bgr8": (1, 1, "uint8"), + "gbrap": (4, 1, "uint8"), + "gbrap10be": (4, 2, "uint16"), + "gbrap10le": (4, 2, "uint16"), + "gbrap12be": (4, 2, "uint16"), + "gbrap12le": (4, 2, "uint16"), + "gbrap14be": (4, 2, "uint16"), + "gbrap14le": (4, 2, "uint16"), + "gbrap16be": (4, 2, "uint16"), + "gbrap16le": (4, 2, "uint16"), + "gbrapf32be": (4, 4, "float32"), + "gbrapf32le": (4, 4, "float32"), + "gbrp": (3, 1, "uint8"), + "gbrp10be": (3, 2, "uint16"), + "gbrp10le": (3, 2, "uint16"), + "gbrp12be": (3, 2, "uint16"), + "gbrp12le": (3, 2, "uint16"), + "gbrp14be": (3, 2, "uint16"), + "gbrp14le": (3, 2, "uint16"), + "gbrp16be": (3, 2, "uint16"), + "gbrp16le": (3, 2, "uint16"), + "gbrp9be": (3, 2, "uint16"), + "gbrp9le": (3, 2, "uint16"), + "gbrpf32be": (3, 4, "float32"), + "gbrpf32le": (3, 4, "float32"), + "gray": (1, 1, "uint8"), + "gray10be": (1, 2, "uint16"), + "gray10le": (1, 2, "uint16"), + "gray12be": (1, 2, "uint16"), + "gray12le": (1, 2, "uint16"), + "gray14be": (1, 2, "uint16"), + "gray14le": (1, 2, "uint16"), + "gray16be": (1, 2, "uint16"), + "gray16le": (1, 2, "uint16"), + "gray8": (1, 1, "uint8"), + "gray9be": (1, 2, "uint16"), + "gray9le": (1, 2, "uint16"), + "grayf32be": (1, 4, "float32"), + "grayf32le": (1, 4, "float32"), + "rgb8": (1, 1, "uint8"), + "yuv444p": (3, 1, "uint8"), + "yuv444p16be": (3, 2, "uint16"), + "yuv444p16le": (3, 2, "uint16"), + "yuva444p16be": (4, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + "yuvj444p": (3, 1, "uint8"), + }.get(format, (None, None, None)) + if channels is not None: + if array.ndim == 2: # (height, width) -> (height, width, 1) + array = array[:, :, None] + check_ndarray(array, dtype, 3) + if not channel_last and format in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 0, 2) # (channels, h, w) -> (h, w, channels) + check_ndarray_shape(array, array.shape[2] == channels) + array = byteswap_array(array, format.endswith("be")) + frame = VideoFrame(array.shape[1], array.shape[0], format) + if frame.format.name.startswith("gbr"): # rgb -> gbr + array = np.concatenate( + [ # not inplace to avoid bad surprises + array[:, :, 1:3], + array[:, :, 0:1], + array[:, :, 3:], + ], + axis=2, + ) + for i in range(channels): + copy_array_to_plane(array[:, :, i], frame.planes[i], itemsize) + return frame + + # other cases + if format == "pal8": + array, palette = array + check_ndarray(array, "uint8", 2) + check_ndarray(palette, "uint8", 2) + check_ndarray_shape(palette, palette.shape == (256, 4)) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(array, frame.planes[0], 1) + frame.planes[1].update(palette.view(">i4").astype("i4").tobytes()) + return frame + elif format in {"yuv420p", "yuvj420p"}: + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + u_start = frame.width * frame.height + v_start = 5 * u_start // 4 + flat = array.reshape(-1) + copy_array_to_plane(flat[0:u_start], frame.planes[0], 1) + copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) + copy_array_to_plane(flat[v_start:], frame.planes[2], 1) + return frame + elif format == "yuv422p10le": + if not isinstance(array, np.ndarray) or array.dtype != np.uint16: + raise ValueError("Array must be uint16 type") + + # Convert to channel-first if needed + if channel_last and array.shape[2] == 3: + array = np.moveaxis(array, 2, 0) + elif not (array.shape[0] == 3): + raise ValueError( + "Array must have shape (3, height, width) or (height, width, 3)" + ) + + height, width = array.shape[1:] + if width % 2 != 0 or height % 2 != 0: + raise ValueError("Width and height must be even") + + frame = VideoFrame(width, height, format) + copy_array_to_plane(array[0], frame.planes[0], 2) + # Subsample U and V by taking every other column + u = array[1, :, ::2].copy() # Need copy to ensure C-contiguous + v = array[2, :, ::2].copy() # Need copy to ensure C-contiguous + copy_array_to_plane(u, frame.planes[1], 2) + copy_array_to_plane(v, frame.planes[2], 2) + return frame + elif format == "yuyv422": + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[0] % 2 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + check_ndarray_shape(array, array.shape[2] == 2) + elif format in {"rgb24", "bgr24"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + elif format in {"argb", "rgba", "abgr", "bgra"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + elif format in {"rgb48be", "rgb48le", "bgr48be", "bgr48le"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 6 + ) + return frame + elif format in {"rgbf32be", "rgbf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 12 + ) + return frame + elif format in {"rgba64be", "rgba64le", "bgra64be", "bgra64le"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf16be", "rgbaf16le"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf32be", "rgbaf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 16 + ) + return frame + elif format == "nv12": + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + uv_start = frame.width * frame.height + flat = array.reshape(-1) + copy_array_to_plane(flat[:uv_start], frame.planes[0], 1) + copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) + return frame + else: + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2] + ) + + return frame + + @staticmethod + def from_bytes( + img_bytes: bytes, + width: int, + height: int, + format="rgba", + flip_horizontal=False, + flip_vertical=False, + ): + frame = VideoFrame(width, height, format) + if format == "rgba": + copy_bytes_to_plane( + img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical + ) + elif format in { + "bayer_bggr8", + "bayer_rggb8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_bggr16le", + "bayer_rggb16le", + "bayer_gbrg16le", + "bayer_grbg16le", + "bayer_bggr16be", + "bayer_rggb16be", + "bayer_gbrg16be", + "bayer_grbg16be", + }: + copy_bytes_to_plane( + img_bytes, + frame.planes[0], + 1 if format.endswith("8") else 2, + flip_horizontal, + flip_vertical, + ) + else: + raise NotImplementedError(f"Format '{format}' is not supported.") + return frame diff --git a/lib/python3.10/site-packages/av/video/frame.pyi b/lib/python3.10/site-packages/av/video/frame.pyi new file mode 100644 index 0000000000000000000000000000000000000000..c6868aabc6a0339f3666a9e82a0e152d04ce5a27 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/frame.pyi @@ -0,0 +1,87 @@ +from enum import IntEnum +from pathlib import Path +from typing import Any, ClassVar, Union + +import numpy as np + +from av.frame import Frame + +from .format import VideoFormat +from .plane import VideoPlane + +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.uint8]], + np.ndarray[Any, np.dtype[np.uint16]], + np.ndarray[Any, np.dtype[np.float16]], + np.ndarray[Any, np.dtype[np.float32]], +] + +supported_np_pix_fmts: set[str] + +class PictureType(IntEnum): + NONE = 0 + I = 1 + P = 2 + B = 3 + S = 4 + SI = 5 + SP = 6 + BI = 7 + +class VideoFrame(Frame): + format: VideoFormat + pts: int + planes: tuple[VideoPlane, ...] + pict_type: int + colorspace: int + color_range: int + + @property + def time(self) -> float: ... + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def interlaced_frame(self) -> bool: ... + @property + def rotation(self) -> int: ... + def __init__( + self, width: int = 0, height: int = 0, format: str = "yuv420p" + ) -> None: ... + def reformat( + self, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace: str | int | None = None, + dst_colorspace: str | int | None = None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... + def to_rgb(self, **kwargs: Any) -> VideoFrame: ... + def save(self, filepath: str | Path) -> None: ... + def to_image(self, **kwargs): ... + def to_ndarray( + self, channel_last: bool = False, **kwargs: Any + ) -> _SupportedNDarray: ... + @staticmethod + def from_image(img): ... + @staticmethod + def from_numpy_buffer( + array: _SupportedNDarray, format: str = "rgb24", width: int = 0 + ) -> VideoFrame: ... + @staticmethod + def from_ndarray( + array: _SupportedNDarray, format: str = "rgb24", channel_last: bool = False + ) -> VideoFrame: ... + @staticmethod + def from_bytes( + data: bytes, + width: int, + height: int, + format: str = "rgba", + flip_horizontal: bool = False, + flip_vertical: bool = False, + ) -> VideoFrame: ... diff --git a/lib/python3.10/site-packages/av/video/plane.pxd b/lib/python3.10/site-packages/av/video/plane.pxd new file mode 100644 index 0000000000000000000000000000000000000000..f9abf22b61866d29f45e69a3651b394d8af00a08 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/plane.pxd @@ -0,0 +1,8 @@ +from av.plane cimport Plane +from av.video.format cimport VideoFormatComponent + + +cdef class VideoPlane(Plane): + + cdef readonly size_t buffer_size + cdef readonly unsigned int width, height diff --git a/lib/python3.10/site-packages/av/video/plane.pyi b/lib/python3.10/site-packages/av/video/plane.pyi new file mode 100644 index 0000000000000000000000000000000000000000..e4a0a206cd1135120ba813677523a15bffdbf59a --- /dev/null +++ b/lib/python3.10/site-packages/av/video/plane.pyi @@ -0,0 +1,11 @@ +from av.plane import Plane + +from .frame import VideoFrame + +class VideoPlane(Plane): + line_size: int + width: int + height: int + buffer_size: int + + def __init__(self, frame: VideoFrame, index: int) -> None: ... diff --git a/lib/python3.10/site-packages/av/video/plane.pyx b/lib/python3.10/site-packages/av/video/plane.pyx new file mode 100644 index 0000000000000000000000000000000000000000..908b48716bd5b6f0e7ea1bfa2284b8e000b79412 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/plane.pyx @@ -0,0 +1,37 @@ +from av.video.frame cimport VideoFrame + + +cdef class VideoPlane(Plane): + def __cinit__(self, VideoFrame frame, int index): + # The palette plane has no associated component or linesize; set fields manually + if frame.format.name == "pal8" and index == 1: + self.width = 256 + self.height = 1 + self.buffer_size = 256 * 4 + return + + for i in range(frame.format.ptr.nb_components): + if frame.format.ptr.comp[i].plane == index: + component = frame.format.components[i] + self.width = component.width + self.height = component.height + break + else: + raise RuntimeError(f"could not find plane {index} of {frame.format!r}") + + # Sometimes, linesize is negative (and that is meaningful). We are only + # insisting that the buffer size be based on the extent of linesize, and + # ignore it's direction. + self.buffer_size = abs(self.frame.ptr.linesize[self.index]) * self.height + + cdef size_t _buffer_size(self): + return self.buffer_size + + @property + def line_size(self): + """ + Bytes per horizontal line in this plane. + + :type: int + """ + return self.frame.ptr.linesize[self.index] diff --git a/lib/python3.10/site-packages/av/video/reformatter.pxd b/lib/python3.10/site-packages/av/video/reformatter.pxd new file mode 100644 index 0000000000000000000000000000000000000000..7682fab6d48355ff82932f9f204ff5f05b4392b2 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/reformatter.pxd @@ -0,0 +1,13 @@ +cimport libav as lib + +from av.video.frame cimport VideoFrame + + +cdef class VideoReformatter: + + cdef lib.SwsContext *ptr + + cdef _reformat(self, VideoFrame frame, int width, int height, + lib.AVPixelFormat format, int src_colorspace, + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range) diff --git a/lib/python3.10/site-packages/av/video/reformatter.pyi b/lib/python3.10/site-packages/av/video/reformatter.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5d83fcbe3b290945a3d54d16999d90ab142f5258 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/reformatter.pyi @@ -0,0 +1,53 @@ +from enum import IntEnum +from typing import cast + +from .frame import VideoFrame + +class Interpolation(IntEnum): + FAST_BILINEAER = cast(int, ...) + BILINEAR = cast(int, ...) + BICUBIC = cast(int, ...) + X = cast(int, ...) + POINT = cast(int, ...) + AREA = cast(int, ...) + BICUBLIN = cast(int, ...) + GAUSS = cast(int, ...) + SINC = cast(int, ...) + LANCZOS = cast(int, ...) + SPLINE = cast(int, ...) + +class Colorspace(IntEnum): + ITU709 = cast(int, ...) + FCC = cast(int, ...) + ITU601 = cast(int, ...) + ITU624 = cast(int, ...) + SMPTE170M = cast(int, ...) + SMPTE240M = cast(int, ...) + DEFAULT = cast(int, ...) + itu709 = cast(int, ...) + fcc = cast(int, ...) + itu601 = cast(int, ...) + itu624 = cast(int, ...) + smpte170m = cast(int, ...) + smpte240m = cast(int, ...) + default = cast(int, ...) + +class ColorRange(IntEnum): + UNSPECIFIED = 0 + MPEG = 1 + JPEG = 2 + NB = 3 + +class VideoReformatter: + def reformat( + self, + frame: VideoFrame, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace: int | None = None, + dst_colorspace: int | None = None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... diff --git a/lib/python3.10/site-packages/av/video/reformatter.pyx b/lib/python3.10/site-packages/av/video/reformatter.pyx new file mode 100644 index 0000000000000000000000000000000000000000..a0c576d1244b01a7307c0417f472c942b907a787 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/reformatter.pyx @@ -0,0 +1,222 @@ +cimport libav as lib +from libc.stdint cimport uint8_t + +from av.error cimport err_check +from av.video.format cimport VideoFormat +from av.video.frame cimport alloc_video_frame + +from enum import IntEnum + + +class Interpolation(IntEnum): + FAST_BILINEAR: "Fast bilinear" = lib.SWS_FAST_BILINEAR + BILINEAR: "Bilinear" = lib.SWS_BILINEAR + BICUBIC: "Bicubic" = lib.SWS_BICUBIC + X: "Experimental" = lib.SWS_X + POINT: "Nearest neighbor / point" = lib.SWS_POINT + AREA: "Area averaging" = lib.SWS_AREA + BICUBLIN: "Luma bicubic / chroma bilinear" = lib.SWS_BICUBLIN + GAUSS: "Gaussian" = lib.SWS_GAUSS + SINC: "Sinc" = lib.SWS_SINC + LANCZOS: "Bicubic spline" = lib.SWS_LANCZOS + + +class Colorspace(IntEnum): + ITU709 = lib.SWS_CS_ITU709 + FCC = lib.SWS_CS_FCC + ITU601 = lib.SWS_CS_ITU601 + ITU624 = lib.SWS_CS_ITU624 + SMPTE170M = lib.SWS_CS_SMPTE170M + SMPTE240M = lib.SWS_CS_SMPTE240M + DEFAULT = lib.SWS_CS_DEFAULT + # Lowercase for b/c. + itu709 = lib.SWS_CS_ITU709 + fcc = lib.SWS_CS_FCC + itu601 = lib.SWS_CS_ITU601 + itu624 = lib.SWS_CS_ITU624 + smpte170m = lib.SWS_CS_SMPTE170M + smpte240m = lib.SWS_CS_SMPTE240M + default = lib.SWS_CS_DEFAULT + +class ColorRange(IntEnum): + UNSPECIFIED: "Unspecified" = lib.AVCOL_RANGE_UNSPECIFIED + MPEG: "MPEG (limited) YUV range, 219*2^(n-8)" = lib.AVCOL_RANGE_MPEG + JPEG: "JPEG (full) YUV range, 2^n-1" = lib.AVCOL_RANGE_JPEG + NB: "Not part of ABI" = lib.AVCOL_RANGE_NB + + +def _resolve_enum_value(value, enum_class, default): + # Helper function to resolve enum values from different input types. + if value is None: + return default + if isinstance(value, enum_class): + return value.value + if isinstance(value, int): + return value + if isinstance(value, str): + return enum_class[value].value + raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") + + +cdef class VideoReformatter: + """An object for reformatting size and pixel format of :class:`.VideoFrame`. + + It is most efficient to have a reformatter object for each set of parameters + you will use as calling :meth:`reformat` will reconfigure the internal object. + + """ + + def __dealloc__(self): + with nogil: + lib.sws_freeContext(self.ptr) + + def reformat(self, VideoFrame frame not None, width=None, height=None, + format=None, src_colorspace=None, dst_colorspace=None, + interpolation=None, src_color_range=None, + dst_color_range=None): + """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. + + Returns the same frame untouched if nothing needs to be done to it. + + :param int width: New width, or ``None`` for the same width. + :param int height: New height, or ``None`` for the same height. + :param format: New format, or ``None`` for the same format. + :type format: :class:`.VideoFormat` or ``str`` + :param src_colorspace: Current colorspace, or ``None`` for the frame colorspace. + :type src_colorspace: :class:`Colorspace` or ``str`` + :param dst_colorspace: Desired colorspace, or ``None`` for the frame colorspace. + :type dst_colorspace: :class:`Colorspace` or ``str`` + :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. + :type interpolation: :class:`Interpolation` or ``str`` + :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. + :type src_color_range: :class:`color range` or ``str`` + :param dst_color_range: Desired color range, or ``None`` for the ``UNSPECIFIED``. + :type dst_color_range: :class:`color range` or ``str`` + + """ + + cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) + + cdef int c_src_colorspace = _resolve_enum_value(src_colorspace, Colorspace, frame.colorspace) + cdef int c_dst_colorspace = _resolve_enum_value(dst_colorspace, Colorspace, frame.colorspace) + cdef int c_interpolation = _resolve_enum_value(interpolation, Interpolation, int(Interpolation.BILINEAR)) + cdef int c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) + cdef int c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) + + return self._reformat( + frame, + width or frame.ptr.width, + height or frame.ptr.height, + video_format.pix_fmt, + c_src_colorspace, + c_dst_colorspace, + c_interpolation, + c_src_color_range, + c_dst_color_range, + ) + + cdef _reformat(self, VideoFrame frame, int width, int height, + lib.AVPixelFormat dst_format, int src_colorspace, + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range): + + if frame.ptr.format < 0: + raise ValueError("Frame does not have format set.") + + # The definition of color range in pixfmt.h and swscale.h is different. + src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 + dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 + + cdef lib.AVPixelFormat src_format = frame.ptr.format + + # Shortcut! + if ( + dst_format == src_format and + width == frame.ptr.width and + height == frame.ptr.height and + dst_colorspace == src_colorspace and + src_color_range == dst_color_range + ): + return frame + + with nogil: + self.ptr = lib.sws_getCachedContext( + self.ptr, + frame.ptr.width, + frame.ptr.height, + src_format, + width, + height, + dst_format, + interpolation, + NULL, + NULL, + NULL + ) + + # We want to change the colorspace/color_range transforms. + # We do that by grabbing all of the current settings, changing a + # couple, and setting them all. We need a lot of state here. + cdef const int *inv_tbl + cdef const int *tbl + cdef int src_colorspace_range, dst_colorspace_range + cdef int brightness, contrast, saturation + cdef int ret + + if src_colorspace != dst_colorspace or src_color_range != dst_color_range: + with nogil: + # Casts for const-ness, because Cython isn't expressive enough. + ret = lib.sws_getColorspaceDetails( + self.ptr, + &inv_tbl, + &src_colorspace_range, + &tbl, + &dst_colorspace_range, + &brightness, + &contrast, + &saturation + ) + + err_check(ret) + + with nogil: + # Grab the coefficients for the requested transforms. + # The inv_table brings us to linear, and `tbl` to the new space. + if src_colorspace != lib.SWS_CS_DEFAULT: + inv_tbl = lib.sws_getCoefficients(src_colorspace) + if dst_colorspace != lib.SWS_CS_DEFAULT: + tbl = lib.sws_getCoefficients(dst_colorspace) + + # Apply! + ret = lib.sws_setColorspaceDetails( + self.ptr, + inv_tbl, + src_color_range, + tbl, + dst_color_range, + brightness, + contrast, + saturation + ) + + err_check(ret) + + # Create a new VideoFrame. + cdef VideoFrame new_frame = alloc_video_frame() + new_frame._copy_internal_attributes(frame) + new_frame._init(dst_format, width, height) + + # Finally, scale the image. + with nogil: + lib.sws_scale( + self.ptr, + # Cast for const-ness, because Cython isn't expressive enough. + frame.ptr.data, + frame.ptr.linesize, + 0, # slice Y + frame.ptr.height, + new_frame.ptr.data, + new_frame.ptr.linesize, + ) + + return new_frame diff --git a/lib/python3.10/site-packages/av/video/stream.pxd b/lib/python3.10/site-packages/av/video/stream.pxd new file mode 100644 index 0000000000000000000000000000000000000000..f0dcfb9b2ca64283b36675e4470f39fd2e1c514b --- /dev/null +++ b/lib/python3.10/site-packages/av/video/stream.pxd @@ -0,0 +1,9 @@ +from av.packet cimport Packet +from av.stream cimport Stream + +from .frame cimport VideoFrame + + +cdef class VideoStream(Stream): + cpdef encode(self, VideoFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/lib/python3.10/site-packages/av/video/stream.py b/lib/python3.10/site-packages/av/video/stream.py new file mode 100644 index 0000000000000000000000000000000000000000..0a7032b0997835f5b05bf5f17f70c0e739121dc8 --- /dev/null +++ b/lib/python3.10/site-packages/av/video/stream.py @@ -0,0 +1,123 @@ +import cython +from cython.cimports import libav as lib +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.video.frame import VideoFrame + + +@cython.cclass +class VideoStream(Stream): + def __repr__(self): + return ( + f"" + ) + + def __getattr__(self, name): + if name in ("framerate", "rate"): + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + return getattr(self.codec_context, name) + + @cython.ccall + def encode(self, frame: VideoFrame | None = None): + """ + Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. + + :rtype: list[Packet] + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + packet: Packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + return packets + + @cython.ccall + def decode(self, packet: Packet | None = None): + """ + Decode a :class:`.Packet` and return a list of :class:`.VideoFrame`. + + :rtype: list[VideoFrame] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + return self.codec_context.decode(packet) + + @property + def average_rate(self): + """ + The average frame rate of this video stream. + + This is calculated when the file is opened by looking at the first + few frames and averaging their rate. + + :type: fractions.Fraction | None + """ + return avrational_to_fraction(cython.address(self.ptr.avg_frame_rate)) + + @property + def base_rate(self): + """ + The base frame rate of this stream. + + This is calculated as the lowest framerate at which the timestamps of + frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` + for more. + + :type: fractions.Fraction | None + """ + return avrational_to_fraction(cython.address(self.ptr.r_frame_rate)) + + @property + def guessed_rate(self): + """The guessed frame rate of this stream. + + This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple + heuristics to decide what is "the" frame rate. + + :type: fractions.Fraction | None + """ + val: lib.AVRational = lib.av_guess_frame_rate( + cython.NULL, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(val)) + + @property + def sample_aspect_ratio(self): + """The guessed sample aspect ratio (SAR) of this stream. + + This is a wrapper around :ffmpeg:`av_guess_sample_aspect_ratio`, and uses multiple + heuristics to decide what is "the" sample aspect ratio. + + :type: fractions.Fraction | None + """ + sar: lib.AVRational = lib.av_guess_sample_aspect_ratio( + self.container.ptr, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(sar)) + + @property + def display_aspect_ratio(self): + """The guessed display aspect ratio (DAR) of this stream. + + This is calculated from :meth:`.VideoStream.guessed_sample_aspect_ratio`. + + :type: fractions.Fraction | None + """ + dar = cython.declare(lib.AVRational) + lib.av_reduce( + cython.address(dar.num), + cython.address(dar.den), + self.format.width * self.sample_aspect_ratio.num, + self.format.height * self.sample_aspect_ratio.den, + 1024 * 1024, + ) + + return avrational_to_fraction(cython.address(dar)) diff --git a/lib/python3.10/site-packages/av/video/stream.pyi b/lib/python3.10/site-packages/av/video/stream.pyi new file mode 100644 index 0000000000000000000000000000000000000000..dd670d3cf11c5db9989d85dec7ee55be0b5f1b9e --- /dev/null +++ b/lib/python3.10/site-packages/av/video/stream.pyi @@ -0,0 +1,43 @@ +from fractions import Fraction +from typing import Iterator, Literal + +from av.codec.context import ThreadType +from av.packet import Packet +from av.stream import Stream + +from .codeccontext import VideoCodecContext +from .format import VideoFormat +from .frame import VideoFrame + +class VideoStream(Stream): + bit_rate: int | None + max_bit_rate: int | None + bit_rate_tolerance: int + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None + codec_context: VideoCodecContext + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + + # from codec context + format: VideoFormat + thread_count: int + thread_type: ThreadType + width: int + height: int + bits_per_coded_sample: int + pix_fmt: str | None + framerate: Fraction + rate: Fraction + gop_size: int + has_b_frames: bool + max_b_frames: int + coded_width: int + coded_height: int + color_range: int + color_primaries: int + color_trc: int + colorspace: int + type: Literal["video"] diff --git a/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..79c9825adbacb5d8c6eaee51863b8a40051d97c8 --- /dev/null +++ b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright 2010 Jason Kirtland + +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/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..6d343f57186f8f33f9fd6db264448a753de6e980 --- /dev/null +++ b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.3 +Name: blinker +Version: 1.9.0 +Summary: Fast, simple object-to-object and broadcast signaling +Author: Jason Kirtland +Maintainer-email: Pallets Ecosystem +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://blinker.readthedocs.io +Project-URL: Source, https://github.com/pallets-eco/blinker/ + +# Blinker + +Blinker provides a fast dispatching system that allows any number of +interested parties to subscribe to events, or "signals". + + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + +Signal receivers can subscribe to specific senders or receive signals +sent by any sender. + +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` + diff --git a/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..3f9f399a9dde735850ea8162390698cd6135bdcf --- /dev/null +++ b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD @@ -0,0 +1,12 @@ +blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054 +blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633 +blinker-1.9.0.dist-info/RECORD,, +blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317 +blinker/__pycache__/__init__.cpython-310.pyc,, +blinker/__pycache__/_utilities.cpython-310.pyc,, +blinker/__pycache__/base.cpython-310.pyc,, +blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675 +blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132 +blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..e3c6feefa22927866e3fd5575379ea972b432aaf --- /dev/null +++ b/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.10.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/blinker/__init__.py b/lib/python3.10/site-packages/blinker/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1772fa4a543b0288f03d60050f222ed64a83ece0 --- /dev/null +++ b/lib/python3.10/site-packages/blinker/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .base import ANY +from .base import default_namespace +from .base import NamedSignal +from .base import Namespace +from .base import Signal +from .base import signal + +__all__ = [ + "ANY", + "default_namespace", + "NamedSignal", + "Namespace", + "Signal", + "signal", +] diff --git a/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bae41a37c8c615ffa5ac2c82cf7ad45da0a802b Binary files /dev/null and b/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc b/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0188c081aa3a71a1f25aab5f5b35ba375eb76379 Binary files /dev/null and b/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc b/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59c98b9a62e984bdcf4a39baf5750c74e882f18f Binary files /dev/null and b/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/blinker/_utilities.py b/lib/python3.10/site-packages/blinker/_utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..000c902a2564d2d4a551edb646a9d654d9e7041b --- /dev/null +++ b/lib/python3.10/site-packages/blinker/_utilities.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import collections.abc as c +import inspect +import typing as t +from weakref import ref +from weakref import WeakMethod + +T = t.TypeVar("T") + + +class Symbol: + """A constant symbol, nicer than ``object()``. Repeated calls return the + same instance. + + >>> Symbol('foo') is Symbol('foo') + True + >>> Symbol('foo') + foo + """ + + symbols: t.ClassVar[dict[str, Symbol]] = {} + + def __new__(cls, name: str) -> Symbol: + if name in cls.symbols: + return cls.symbols[name] + + obj = super().__new__(cls) + cls.symbols[name] = obj + return obj + + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return self.name + + def __getnewargs__(self) -> tuple[t.Any, ...]: + return (self.name,) + + +def make_id(obj: object) -> c.Hashable: + """Get a stable identifier for a receiver or sender, to be used as a dict + key or in a set. + """ + if inspect.ismethod(obj): + # The id of a bound method is not stable, but the id of the unbound + # function and instance are. + return id(obj.__func__), id(obj.__self__) + + if isinstance(obj, (str, int)): + # Instances with the same value always compare equal and have the same + # hash, even if the id may change. + return obj + + # Assume other types are not hashable but will always be the same instance. + return id(obj) + + +def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]: + if inspect.ismethod(obj): + return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] + + return ref(obj, callback) diff --git a/lib/python3.10/site-packages/blinker/base.py b/lib/python3.10/site-packages/blinker/base.py new file mode 100644 index 0000000000000000000000000000000000000000..d051b94a32f5eecd9f32853d6eaebcca6c7133f6 --- /dev/null +++ b/lib/python3.10/site-packages/blinker/base.py @@ -0,0 +1,512 @@ +from __future__ import annotations + +import collections.abc as c +import sys +import typing as t +import weakref +from collections import defaultdict +from contextlib import contextmanager +from functools import cached_property +from inspect import iscoroutinefunction + +from ._utilities import make_id +from ._utilities import make_ref +from ._utilities import Symbol + +F = t.TypeVar("F", bound=c.Callable[..., t.Any]) + +ANY = Symbol("ANY") +"""Symbol for "any sender".""" + +ANY_ID = 0 + + +class Signal: + """A notification emitter. + + :param doc: The docstring for the signal. + """ + + ANY = ANY + """An alias for the :data:`~blinker.ANY` sender symbol.""" + + set_class: type[set[t.Any]] = set + """The set class to use for tracking connected receivers and senders. + Python's ``set`` is unordered. If receivers must be dispatched in the order + they were connected, an ordered set implementation can be used. + + .. versionadded:: 1.7 + """ + + @cached_property + def receiver_connected(self) -> Signal: + """Emitted at the end of each :meth:`connect` call. + + The signal sender is the signal instance, and the :meth:`connect` + arguments are passed through: ``receiver``, ``sender``, and ``weak``. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver connects.") + + @cached_property + def receiver_disconnected(self) -> Signal: + """Emitted at the end of each :meth:`disconnect` call. + + The sender is the signal instance, and the :meth:`disconnect` arguments + are passed through: ``receiver`` and ``sender``. + + This signal is emitted **only** when :meth:`disconnect` is called + explicitly. This signal cannot be emitted by an automatic disconnect + when a weakly referenced receiver or sender goes out of scope, as the + instance is no longer be available to be used as the sender for this + signal. + + An alternative approach is available by subscribing to + :attr:`receiver_connected` and setting up a custom weakref cleanup + callback on weak receivers and senders. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver disconnects.") + + def __init__(self, doc: str | None = None) -> None: + if doc: + self.__doc__ = doc + + self.receivers: dict[ + t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] + ] = {} + """The map of connected receivers. Useful to quickly check if any + receivers are connected to the signal: ``if s.receivers:``. The + structure and data is not part of the public API, but checking its + boolean value is. + """ + + self.is_muted: bool = False + self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} + + def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: + """Connect ``receiver`` to be called when the signal is sent by + ``sender``. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends. + """ + receiver_id = make_id(receiver) + sender_id = ANY_ID if sender is ANY else make_id(sender) + + if weak: + self.receivers[receiver_id] = make_ref( + receiver, self._make_cleanup_receiver(receiver_id) + ) + else: + self.receivers[receiver_id] = receiver + + self._by_sender[sender_id].add(receiver_id) + self._by_receiver[receiver_id].add(sender_id) + + if sender is not ANY and sender_id not in self._weak_senders: + # store a cleanup for weakref-able senders + try: + self._weak_senders[sender_id] = make_ref( + sender, self._make_cleanup_sender(sender_id) + ) + except TypeError: + pass + + if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: + try: + self.receiver_connected.send( + self, receiver=receiver, sender=sender, weak=weak + ) + except TypeError: + # TODO no explanation or test for this + self.disconnect(receiver, sender) + raise + + return receiver + + def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: + """Connect the decorated function to be called when the signal is sent + by ``sender``. + + The decorated function will be called when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument along + with any extra keyword arguments. + + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends.= + + .. versionadded:: 1.1 + """ + + def decorator(fn: F) -> F: + self.connect(fn, sender, weak) + return fn + + return decorator + + @contextmanager + def connected_to( + self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY + ) -> c.Generator[None, None, None]: + """A context manager that temporarily connects ``receiver`` to the + signal while a ``with`` block executes. When the block exits, the + receiver is disconnected. Useful for tests. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. + + .. versionadded:: 1.1 + """ + self.connect(receiver, sender=sender, weak=False) + + try: + yield None + finally: + self.disconnect(receiver) + + @contextmanager + def muted(self) -> c.Generator[None, None, None]: + """A context manager that temporarily disables the signal. No receivers + will be called if the signal is sent, until the ``with`` block exits. + Useful for tests. + """ + self.is_muted = True + + try: + yield None + finally: + self.is_muted = False + + def send( + self, + sender: t.Any | None = None, + /, + *, + _async_wrapper: c.Callable[ + [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Call all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _async_wrapper: Will be called on any receivers that are async + coroutines to turn them into sync callables. For example, could run + the receiver with an event loop. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionchanged:: 1.7 + Added the ``_async_wrapper`` argument. + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if iscoroutinefunction(receiver): + if _async_wrapper is None: + raise RuntimeError("Cannot send to a coroutine function.") + + result = _async_wrapper(receiver)(sender, **kwargs) + else: + result = receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + async def send_async( + self, + sender: t.Any | None = None, + /, + *, + _sync_wrapper: c.Callable[ + [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Await all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _sync_wrapper: Will be called on any receivers that are sync + callables to turn them into async coroutines. For example, + could call the receiver in a thread. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionadded:: 1.7 + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if not iscoroutinefunction(receiver): + if _sync_wrapper is None: + raise RuntimeError("Cannot send to a non-coroutine function.") + + result = await _sync_wrapper(receiver)(sender, **kwargs) + else: + result = await receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + def has_receivers_for(self, sender: t.Any) -> bool: + """Check if there is at least one receiver that will be called with the + given ``sender``. A receiver connected to :data:`ANY` will always be + called, regardless of sender. Does not check if weakly referenced + receivers are still live. See :meth:`receivers_for` for a stronger + search. + + :param sender: Check for receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + if not self.receivers: + return False + + if self._by_sender[ANY_ID]: + return True + + if sender is ANY: + return False + + return make_id(sender) in self._by_sender + + def receivers_for( + self, sender: t.Any + ) -> c.Generator[c.Callable[..., t.Any], None, None]: + """Yield each receiver to be called for ``sender``, in addition to those + to be called for :data:`ANY`. Weakly referenced receivers that are not + live will be disconnected and skipped. + + :param sender: Yield receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + # TODO: test receivers_for(ANY) + if not self.receivers: + return + + sender_id = make_id(sender) + + if sender_id in self._by_sender: + ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] + else: + ids = self._by_sender[ANY_ID].copy() + + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + + if receiver is None: + continue + + if isinstance(receiver, weakref.ref): + strong = receiver() + + if strong is None: + self._disconnect(receiver_id, ANY_ID) + continue + + yield strong + else: + yield receiver + + def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: + """Disconnect ``receiver`` from being called when the signal is sent by + ``sender``. + + :param receiver: A connected receiver callable. + :param sender: Disconnect from only this sender. By default, disconnect + from all senders. + """ + sender_id: c.Hashable + + if sender is ANY: + sender_id = ANY_ID + else: + sender_id = make_id(sender) + + receiver_id = make_id(receiver) + self._disconnect(receiver_id, sender_id) + + if ( + "receiver_disconnected" in self.__dict__ + and self.receiver_disconnected.receivers + ): + self.receiver_disconnected.send(self, receiver=receiver, sender=sender) + + def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: + if sender_id == ANY_ID: + if self._by_receiver.pop(receiver_id, None) is not None: + for bucket in self._by_sender.values(): + bucket.discard(receiver_id) + + self.receivers.pop(receiver_id, None) + else: + self._by_sender[sender_id].discard(receiver_id) + self._by_receiver[receiver_id].discard(sender_id) + + def _make_cleanup_receiver( + self, receiver_id: c.Hashable + ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: + """Create a callback function to disconnect a weakly referenced + receiver when it is garbage collected. + """ + + def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: + # If the interpreter is shutting down, disconnecting can result in a + # weird ignored exception. Don't call it in that case. + if not sys.is_finalizing(): + self._disconnect(receiver_id, ANY_ID) + + return cleanup + + def _make_cleanup_sender( + self, sender_id: c.Hashable + ) -> c.Callable[[weakref.ref[t.Any]], None]: + """Create a callback function to disconnect all receivers for a weakly + referenced sender when it is garbage collected. + """ + assert sender_id != ANY_ID + + def cleanup(ref: weakref.ref[t.Any]) -> None: + self._weak_senders.pop(sender_id, None) + + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + return cleanup + + def _cleanup_bookkeeping(self) -> None: + """Prune unused sender/receiver bookkeeping. Not threadsafe. + + Connecting & disconnecting leaves behind a small amount of bookkeeping + data. Typical workloads using Blinker, for example in most web apps, + Flask, CLI scripts, etc., are not adversely affected by this + bookkeeping. + + With a long-running process performing dynamic signal routing with high + volume, e.g. connecting to function closures, senders are all unique + object instances. Doing all of this over and over may cause memory usage + to grow due to extraneous bookkeeping. (An empty ``set`` for each stale + sender/receiver pair.) + + This method will prune that bookkeeping away, with the caveat that such + pruning is not threadsafe. The risk is that cleanup of a fully + disconnected receiver/sender pair occurs while another thread is + connecting that same pair. If you are in the highly dynamic, unique + receiver/sender situation that has lead you to this method, that failure + mode is perhaps not a big deal for you. + """ + for mapping in (self._by_sender, self._by_receiver): + for ident, bucket in list(mapping.items()): + if not bucket: + mapping.pop(ident, None) + + def _clear_state(self) -> None: + """Disconnect all receivers and senders. Useful for tests.""" + self._weak_senders.clear() + self.receivers.clear() + self._by_sender.clear() + self._by_receiver.clear() + + +class NamedSignal(Signal): + """A named generic notification emitter. The name is not used by the signal + itself, but matches the key in the :class:`Namespace` that it belongs to. + + :param name: The name of the signal within the namespace. + :param doc: The docstring for the signal. + """ + + def __init__(self, name: str, doc: str | None = None) -> None: + super().__init__(doc) + + #: The name of this signal. + self.name: str = name + + def __repr__(self) -> str: + base = super().__repr__() + return f"{base[:-1]}; {self.name!r}>" # noqa: E702 + + +class Namespace(dict[str, NamedSignal]): + """A dict mapping names to signals.""" + + def signal(self, name: str, doc: str | None = None) -> NamedSignal: + """Return the :class:`NamedSignal` for the given ``name``, creating it + if required. Repeated calls with the same name return the same signal. + + :param name: The name of the signal. + :param doc: The docstring of the signal. + """ + if name not in self: + self[name] = NamedSignal(name, doc) + + return self[name] + + +class _PNamespaceSignal(t.Protocol): + def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... + + +default_namespace: Namespace = Namespace() +"""A default :class:`Namespace` for creating named signals. :func:`signal` +creates a :class:`NamedSignal` in this namespace. +""" + +signal: _PNamespaceSignal = default_namespace.signal +"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given +``name``, creating it if required. Repeated calls with the same name return the +same signal. +""" diff --git a/lib/python3.10/site-packages/blinker/py.typed b/lib/python3.10/site-packages/blinker/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/INSTALLER b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/LICENSE b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..d6466b602473a2683ccf84a9f6c6dce140a058e3 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Cory Benfield + +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. \ No newline at end of file diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/METADATA b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..1061999bf17a8ae011423f8128b0ce6993a6ba97 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/METADATA @@ -0,0 +1,190 @@ +Metadata-Version: 2.1 +Name: brotlicffi +Version: 1.0.9.2 +Summary: Python CFFI bindings to the Brotli library +Home-page: https://github.com/python-hyper/brotlicffi +Author: Cory Benfield +Author-email: cory@lukasa.co.uk +License: MIT +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +License-File: LICENSE +Requires-Dist: cffi>=1.0.0 +Requires-Dist: enum34; python_version < "3.4" + +BrotliCFFI +========== + +.. image:: https://img.shields.io/pypi/v/brotlicffi + :alt: Version + :target: https://pypi.org/project/brotlicffi + +.. image:: https://pepy.tech/badge/brotlicffi + :alt: Downloads + :target: https://pepy.tech/project/brotlicffi + +.. image:: https://img.shields.io/github/workflow/status/python-hyper/brotlicffi/CI/master + :alt: CI Status + :target: https://github.com/python-hyper/brotlicffi/actions + +This library contains Python CFFI bindings for the reference Brotli encoder/decoder, +`available here`_. This allows Python software to use the Brotli compression +algorithm directly from Python code. + +Install from PyPI: + +.. code-block:: + + $ python -m pip install brotlicffi + +To use it simply, try this: + +.. code-block:: python + + import brotlicffi + data = brotlicffi.decompress(compressed_data) + +More information can be found `in the documentation`_. + +.. _available here: https://github.com/google/brotli +.. _in the documentation: https://brotlipy.readthedocs.org + +Using BrotliCFFI in Projects +---------------------------- + +The API is 100% compatible with the `Brotli Python C bindings`_. +We recommend installing the C bindings on CPython and the CFFI +bindings everywhere else (PyPy, etc) + +Essentially you use requirements like this: + + .. code-block:: python + + install_requires=[ + "brotli; platform_python_implementation == 'CPython'", + "brotlicffi; platform_python_implementation != 'CPython'" + ] + +and then import the correct Brotli library like so: + + .. code-block:: python + + try: + import brotlicffi as brotli + except ImportError: + import brotli + +We provide an `example project`_ that shows how to use both +libraries together to support Brotli with multiple Python implementations. + +.. _Brotli Python C bindings: https://pypi.org/project/Brotli +.. _example project: https://github.com/python-hyper/brotlipy/tree/master/example + +License +------- + +The source code of BrotliCFFI is available under the MIT license. Brotli itself +is made available under the Version 2.0 of the Apache Software License. See the +LICENSE and libbrotli/LICENSE files for more information. + +Authors +------- + +BrotliCFFI/brotlipy was authored by Cory Benfield and +is currently maintained by Seth Michael Larson. + + +Changelog +========= + +1.0.9.2 (2021-04-06) +-------------------- + +- Added ``manylinux_aarch64`` wheels + + +1.0.9.1 (2021-01-27) +-------------------- + +- Avoid byte/string comparison warning in error message construction + + +1.0.9.0 (2021-01-20) +-------------------- + +- Updated to v1.0.9 of the Brotli library +- Library version now follows Brotli version +- Removed the ``dictionary`` parameter from ``compress`` and ``Compressor`` +- **NOTE:** Python 2.7 wheels for Windows likely won't work until + `google/brotli#848`_ is resolved + +.. _google/brotli#848: https://github.com/google/brotli/issues/848 + +0.8.0 (2020-11-30) +------------------ + +- Renamed the package on PyPI to ``brotlicffi``, all further updates will be + published to the new package. Using the ``brotlipy`` is deprecated. +- Changed the importable namespace from ``brotli`` to ``brotlicffi`` to no longer + conflict with the ``Brotli`` PyPI package. +- Added ``process()`` method to ``Compressor`` and ``Decompressor``. +- Added ``is_finished()`` method to ``Decompressor``. + +0.7.0 (2017-05-30) +------------------ + +- Update to v0.6.0 of the Brotli library. + +0.6.0 (2016-09-08) +------------------ + +- Resolved a bug where ``decompress()`` would return an empty bytestring + instead of erroring if the provided bytestring was small enough. +- Added the ``finish()`` method to the streaming decompressor. + +0.5.1 (2016-08-17) +------------------ + +- Update to v0.5.2 of the Brotli library. +- Add new exception type (``Error``). +- Add compatibility with C++ brotli library by aliasing ``Error`` to ``error``. +- Extra error checking of input parameters to the compressor. + +0.5.0 (2016-08-16) +------------------ + +- Update to v0.5.0 of the Brotli library. +- Extend one-shot compression API to include all control parameters. +- Added streaming/incremental compression API. +- Added flags to control compression mode. + +0.4.0 (2016-08-01) +------------------ + +Update to v0.4.0 of the Brotli library. + +0.3.0 (2016-05-11) +------------------ + +Update to v0.3.0 of the Brotli library. + +0.2.0 (2015-10-05) +------------------ + +Fix broken ``brotli.compress`` support on Windows. + +0.1.3 (2015-10-05) +------------------ + +- Added basic for ``brotli.compress`` through a C wrapper included in this + library. diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/RECORD b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..e81c1107d98a702939cb869a9e9b1b40786c5349 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/RECORD @@ -0,0 +1,15 @@ +brotlicffi-1.0.9.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +brotlicffi-1.0.9.2.dist-info/LICENSE,sha256=8-nCnj2zbcMFpL5oqKIl-TlJhSzi0ZtbCjwJnu5lCcU,1056 +brotlicffi-1.0.9.2.dist-info/METADATA,sha256=yX74KnZJury1PeRe3hOVOGfdXvPWZGQcXeYx2JbJaMw,5466 +brotlicffi-1.0.9.2.dist-info/RECORD,, +brotlicffi-1.0.9.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +brotlicffi-1.0.9.2.dist-info/WHEEL,sha256=iJbXRLRLVat1Ro4zBCxytjzOfS-Ax9MlJ1Aw8pc52Bo,104 +brotlicffi-1.0.9.2.dist-info/direct_url.json,sha256=F6XEYIQi6zVHx-VD9sKqHhCshbFGWzpXJJhYYMDInx8,70 +brotlicffi-1.0.9.2.dist-info/top_level.txt,sha256=QNsXaBb1b7FFDoROgMBoHTDwfsWia7dX5GLRhUDPyXM,23 +brotlicffi/__init__.py,sha256=j7DTgBBGsuviIq6HyllnlueB5w2ks42NxBFSr098UoQ,224 +brotlicffi/__pycache__/__init__.cpython-310.pyc,, +brotlicffi/__pycache__/_api.cpython-310.pyc,, +brotlicffi/__pycache__/_build.cpython-310.pyc,, +brotlicffi/_api.py,sha256=nC5lhyWId2PfV_xTd4eyk_NkRZk-NRCvvlXdhx4f4gA,15507 +brotlicffi/_brotlicffi.abi3.so,sha256=ykCSYlJ1UbDTYUl77hcR6XKiwIdtirg8cE6kNXFjSVs,795224 +brotlicffi/_build.py,sha256=U0nclRQLyOmjw6wj8q4xZwS3F6zGxyzYrytiJ28aKj0,9320 diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/REQUESTED b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/WHEEL b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..5a58f1ed9e3722044e5fa4a7e9e8261ffb876bd6 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.44.0) +Root-Is-Purelib: false +Tag: cp310-abi3-linux_x86_64 + diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/direct_url.json b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..7e6343b46e277e92ccb00d7fa2171789524336eb --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///croot/brotlicffi_1736182461069/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/top_level.txt b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..697e753b0e8f0effe0b3ff3b52a22225f93e5b05 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi-1.0.9.2.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_brotlicffi +brotlicffi diff --git a/lib/python3.10/site-packages/brotlicffi/__init__.py b/lib/python3.10/site-packages/brotlicffi/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7f07c9e14c6829383b57f8ac2b69d42a662cdf96 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# flake8: noqa +from ._api import ( + decompress, Decompressor, compress, BrotliEncoderMode, DEFAULT_MODE, + Compressor, MODE_GENERIC, MODE_TEXT, MODE_FONT, error, Error +) + +__version__ = "1.0.9.2" diff --git a/lib/python3.10/site-packages/brotlicffi/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/brotlicffi/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6048244d3001822c3e9818b3b0fd3b17999608f6 Binary files /dev/null and b/lib/python3.10/site-packages/brotlicffi/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/brotlicffi/__pycache__/_api.cpython-310.pyc b/lib/python3.10/site-packages/brotlicffi/__pycache__/_api.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8386124a5fdf400f0a5d4cb24c8366cae2f33a3f Binary files /dev/null and b/lib/python3.10/site-packages/brotlicffi/__pycache__/_api.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/brotlicffi/__pycache__/_build.cpython-310.pyc b/lib/python3.10/site-packages/brotlicffi/__pycache__/_build.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2e4d82267211712aaf3e4a16a8337c9a591b136 Binary files /dev/null and b/lib/python3.10/site-packages/brotlicffi/__pycache__/_build.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/brotlicffi/_api.py b/lib/python3.10/site-packages/brotlicffi/_api.py new file mode 100644 index 0000000000000000000000000000000000000000..b80b370530e6dbe1fe74000a557d52da3d7ee316 --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi/_api.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +import math +import enum + +from ._brotlicffi import ffi, lib + + +class error(Exception): + """ + Raised whenever an error is encountered with compressing or decompressing + data using brotlicffi. + + .. versionadded:: 0.5.1 + """ + pass + + +#: An alias of :class:`error ` that +#: exists for compatibility with the original CFFI brotli module. +#: +#: .. versionadded: 0.8.0 +Error = error + + +class BrotliEncoderMode(enum.IntEnum): + """ + Compression modes for the Brotli encoder. + + .. versionadded:: 0.5.0 + """ + #: Default compression mode. The compressor does not know anything in + #: advance about the properties of the input. + GENERIC = lib.BROTLI_MODE_GENERIC + + #: Compression mode for UTF-8 format text input. + TEXT = lib.BROTLI_MODE_TEXT + + #: Compression mode used in WOFF 2.0 + FONT = lib.BROTLI_MODE_FONT + + +# Define some names for compatibility with the C module. + +#: The default compression mode for brotli. +DEFAULT_MODE = BrotliEncoderMode(lib.BROTLI_DEFAULT_MODE) + + +#: A compression mode where the compressor does not know anything in advance +#: about the properties of the input. +#: +#: .. note:: This name is defined for compatibility with the Brotli C +#: extension. If you're not interested in that compatibility, it is +#: recommended that you use :class:`BrotliEncoderMode +#: ` instead. +#: +#: .. versionadded:: 0.5.0 +MODE_GENERIC = BrotliEncoderMode.GENERIC + + +#: A compression mode for UTF-8 format text input. +#: +#: .. note:: This name is defined for compatibility with the Brotli C +#: extension. If you're not interested in that compatibility, it is +#: recommended that you use :class:`BrotliEncoderMode +#: ` instead. +#: +#: .. versionadded:: 0.5.0 +MODE_TEXT = BrotliEncoderMode.TEXT + + +#: The compression mode used in WOFF 2.0. +#: +#: .. note:: This name is defined for compatibility with the Brotli C +#: extension. If you're not interested in that compatibility, it is +#: recommended that you use :class:`BrotliEncoderMode +#: ` instead. +#: +#: .. versionadded:: 0.5.0 +MODE_FONT = BrotliEncoderMode.FONT + + +def decompress(data): + """ + Decompress a complete Brotli-compressed string. + + :param data: A bytestring containing Brotli-compressed data. + """ + d = Decompressor() + data = d.decompress(data) + d.finish() + return data + + +def compress(data, + mode=DEFAULT_MODE, + quality=lib.BROTLI_DEFAULT_QUALITY, + lgwin=lib.BROTLI_DEFAULT_WINDOW, + lgblock=0): + """ + Compress a string using Brotli. + + .. versionchanged:: 0.5.0 + Added ``mode``, ``quality``, `lgwin``, ``lgblock``, and ``dictionary`` + parameters. + + :param data: A bytestring containing the data to compress. + :type data: ``bytes`` + + :param mode: The encoder mode. + :type mode: :class:`BrotliEncoderMode` or ``int`` + + :param quality: Controls the compression-speed vs compression-density + tradeoffs. The higher the quality, the slower the compression. The + range of this value is 0 to 11. + :type quality: ``int`` + + :param lgwin: The base-2 logarithm of the sliding window size. The range of + this value is 10 to 24. + :type lgwin: ``int`` + + :param lgblock: The base-2 logarithm of the maximum input block size. The + range of this value is 16 to 24. If set to 0, the value will be set + based on ``quality``. + :type lgblock: ``int`` + + :returns: The compressed bytestring. + :rtype: ``bytes`` + """ + # This method uses private variables on the Compressor object, and + # generally does a whole lot of stuff that's not supported by the public + # API. The goal here is to minimise the number of allocations and copies + # we have to do. Users should prefer this method over the Compressor if + # they know they have single-shot data. + compressor = Compressor( + mode=mode, + quality=quality, + lgwin=lgwin, + lgblock=lgblock + ) + compressed_data = compressor._compress(data, lib.BROTLI_OPERATION_FINISH) + assert lib.BrotliEncoderIsFinished(compressor._encoder) == lib.BROTLI_TRUE + assert ( + lib.BrotliEncoderHasMoreOutput(compressor._encoder) == lib.BROTLI_FALSE + ) + return compressed_data + + +def _validate_mode(val): + """ + Validate that the mode is valid. + """ + try: + val = BrotliEncoderMode(val) + except ValueError: + raise error("%s is not a valid encoder mode" % val) + + +def _validate_quality(val): + """ + Validate that the quality setting is valid. + """ + if not (0 <= val <= 11): + raise error( + "%d is not a valid quality, must be between 0 and 11" % val + ) + + +def _validate_lgwin(val): + """ + Validate that the lgwin setting is valid. + """ + if not (10 <= val <= 24): + raise error("%d is not a valid lgwin, must be between 10 and 24" % val) + + +def _validate_lgblock(val): + """ + Validate that the lgblock setting is valid. + """ + if (val != 0) and not (16 <= val <= 24): + raise error( + "%d is not a valid lgblock, must be either 0 or between 16 and 24" + % val + ) + + +def _set_parameter(encoder, parameter, parameter_name, val): + """ + This helper function sets a specific Brotli encoder parameter, checking + the return code and raising :class:`Error ` if it is + invalid. + """ + rc = lib.BrotliEncoderSetParameter(encoder, parameter, val) + + if parameter == lib.BROTLI_PARAM_MODE: + _validate_mode(val) + elif parameter == lib.BROTLI_PARAM_QUALITY: + _validate_quality(val) + elif parameter == lib.BROTLI_PARAM_LGWIN: + _validate_lgwin(val) + elif parameter == lib.BROTLI_PARAM_LGBLOCK: + _validate_lgblock(val) + else: # pragma: no cover + raise RuntimeError("Unexpected parameter!") + + # This block is defensive: I see no way to hit it, but as long as the + # function returns a value we can live in hope that the brotli folks will + # enforce their own constraints. + if rc != lib.BROTLI_TRUE: # pragma: no cover + raise error( + "Error setting parameter %s: %d" % (parameter_name, val) + ) + + +class Compressor(object): + """ + An object that allows for streaming compression of data using the Brotli + compression algorithm. + + .. versionadded:: 0.5.0 + + :param mode: The encoder mode. + :type mode: :class:`BrotliEncoderMode` or ``int`` + + :param quality: Controls the compression-speed vs compression-density + tradeoffs. The higher the quality, the slower the compression. The + range of this value is 0 to 11. + :type quality: ``int`` + + :param lgwin: The base-2 logarithm of the sliding window size. The range of + this value is 10 to 24. + :type lgwin: ``int`` + + :param lgblock: The base-2 logarithm of the maximum input block size. The + range of this value is 16 to 24. If set to 0, the value will be set + based on ``quality``. + :type lgblock: ``int`` + + :param dictionary: A pre-set dictionary for LZ77. Please use this with + caution: if a dictionary is used for compression, the same dictionary + **must** be used for decompression! + :type dictionary: ``bytes`` + """ + _dictionary = None + _dictionary_size = None + + def __init__(self, + mode=DEFAULT_MODE, + quality=lib.BROTLI_DEFAULT_QUALITY, + lgwin=lib.BROTLI_DEFAULT_WINDOW, + lgblock=0): + enc = lib.BrotliEncoderCreateInstance( + ffi.NULL, ffi.NULL, ffi.NULL + ) + if not enc: # pragma: no cover + raise RuntimeError("Unable to allocate Brotli encoder!") + + enc = ffi.gc(enc, lib.BrotliEncoderDestroyInstance) + + # Configure the encoder appropriately. + _set_parameter(enc, lib.BROTLI_PARAM_MODE, "mode", mode) + _set_parameter(enc, lib.BROTLI_PARAM_QUALITY, "quality", quality) + _set_parameter(enc, lib.BROTLI_PARAM_LGWIN, "lgwin", lgwin) + _set_parameter(enc, lib.BROTLI_PARAM_LGBLOCK, "lgblock", lgblock) + + self._encoder = enc + + def _compress(self, data, operation): + """ + This private method compresses some data in a given mode. This is used + because almost all of the code uses the exact same setup. It wouldn't + have to, but it doesn't hurt at all. + """ + # The 'algorithm' for working out how big to make this buffer is from + # the Brotli source code, brotlimodule.cc. + original_output_size = int( + math.ceil(len(data) + (len(data) >> 2) + 10240) + ) + available_out = ffi.new("size_t *") + available_out[0] = original_output_size + output_buffer = ffi.new("uint8_t []", available_out[0]) + ptr_to_output_buffer = ffi.new("uint8_t **", output_buffer) + input_size = ffi.new("size_t *", len(data)) + input_buffer = ffi.new("uint8_t []", data) + ptr_to_input_buffer = ffi.new("uint8_t **", input_buffer) + + rc = lib.BrotliEncoderCompressStream( + self._encoder, + operation, + input_size, + ptr_to_input_buffer, + available_out, + ptr_to_output_buffer, + ffi.NULL + ) + if rc != lib.BROTLI_TRUE: # pragma: no cover + raise error("Error encountered compressing data.") + + assert not input_size[0] + + size_of_output = original_output_size - available_out[0] + return ffi.buffer(output_buffer, size_of_output)[:] + + def compress(self, data): + """ + Incrementally compress more data. + + :param data: A bytestring containing data to compress. + :returns: A bytestring containing some compressed data. May return the + empty bytestring if not enough data has been inserted into the + compressor to create the output yet. + """ + return self._compress(data, lib.BROTLI_OPERATION_PROCESS) + + process = compress + + def flush(self): + """ + Flush the compressor. This will emit the remaining output data, but + will not destroy the compressor. It can be used, for example, to ensure + that given chunks of content will decompress immediately. + """ + chunks = [self._compress(b'', lib.BROTLI_OPERATION_FLUSH)] + + while lib.BrotliEncoderHasMoreOutput(self._encoder) == lib.BROTLI_TRUE: + chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FLUSH)) + + return b''.join(chunks) + + def finish(self): + """ + Finish the compressor. This will emit the remaining output data and + transition the compressor to a completed state. The compressor cannot + be used again after this point, and must be replaced. + """ + chunks = [] + while lib.BrotliEncoderIsFinished(self._encoder) == lib.BROTLI_FALSE: + chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FINISH)) + + return b''.join(chunks) + + +class Decompressor(object): + """ + An object that allows for streaming decompression of Brotli-compressed + data. + + .. versionchanged:: 0.5.0 + Added ``dictionary`` parameter. + + :param dictionary: A pre-set dictionary for LZ77. Please use this with + caution: if a dictionary is used for compression, the same dictionary + **must** be used for decompression! + :type dictionary: ``bytes`` + """ + _dictionary = None + _dictionary_size = None + + def __init__(self, dictionary=b''): + dec = lib.BrotliDecoderCreateInstance(ffi.NULL, ffi.NULL, ffi.NULL) + self._decoder = ffi.gc(dec, lib.BrotliDecoderDestroyInstance) + + if dictionary: + self._dictionary = ffi.new("uint8_t []", dictionary) + self._dictionary_size = len(dictionary) + lib.BrotliDecoderSetCustomDictionary( + self._decoder, + self._dictionary_size, + self._dictionary + ) + + def decompress(self, data): + """ + Decompress part of a complete Brotli-compressed string. + + :param data: A bytestring containing Brotli-compressed data. + :returns: A bytestring containing the decompressed data. + """ + chunks = [] + + available_in = ffi.new("size_t *", len(data)) + in_buffer = ffi.new("uint8_t[]", data) + next_in = ffi.new("uint8_t **", in_buffer) + + while True: + # Allocate a buffer that's hopefully overlarge, but if it's not we + # don't mind: we'll spin around again. + buffer_size = 5 * len(data) + available_out = ffi.new("size_t *", buffer_size) + out_buffer = ffi.new("uint8_t[]", buffer_size) + next_out = ffi.new("uint8_t **", out_buffer) + + rc = lib.BrotliDecoderDecompressStream(self._decoder, + available_in, + next_in, + available_out, + next_out, + ffi.NULL) + + # First, check for errors. + if rc == lib.BROTLI_DECODER_RESULT_ERROR: + error_code = lib.BrotliDecoderGetErrorCode(self._decoder) + error_message = lib.BrotliDecoderErrorString(error_code) + raise error( + b"Decompression error: %s" % ffi.string(error_message) + ) + + # Next, copy the result out. + chunk = ffi.buffer(out_buffer, buffer_size - available_out[0])[:] + chunks.append(chunk) + + if rc == lib.BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: + assert available_in[0] == 0 + break + elif rc == lib.BROTLI_DECODER_RESULT_SUCCESS: + break + else: + # It's cool if we need more output, we just loop again. + assert rc == lib.BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT + + return b''.join(chunks) + + process = decompress + + def flush(self): + """ + Complete the decompression, return whatever data is remaining to be + decompressed. + + .. deprecated:: 0.4.0 + + This method is no longer required, as decompress() will now + decompress eagerly. + + :returns: A bytestring containing the remaining decompressed data. + """ + return b'' + + def finish(self): + """ + Finish the decompressor. As the decompressor decompresses eagerly, this + will never actually emit any data. However, it will potentially throw + errors if a truncated or damaged data stream has been used. + + Note that, once this method is called, the decompressor is no longer + safe for further use and must be thrown away. + """ + assert ( + lib.BrotliDecoderHasMoreOutput(self._decoder) == lib.BROTLI_FALSE + ) + if not self.is_finished(): + raise error("Decompression error: incomplete compressed stream.") + + return b'' + + def is_finished(self): + """ + Returns ``True`` if the decompression stream + is complete, ``False`` otherwise + """ + return lib.BrotliDecoderIsFinished(self._decoder) == lib.BROTLI_TRUE diff --git a/lib/python3.10/site-packages/brotlicffi/_build.py b/lib/python3.10/site-packages/brotlicffi/_build.py new file mode 100644 index 0000000000000000000000000000000000000000..45dccb6fff767a2801708242eeb967f31314972c --- /dev/null +++ b/lib/python3.10/site-packages/brotlicffi/_build.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +import os +import sys + +from cffi import FFI +ffi = FFI() + +USE_SHARED_BROTLI = os.environ.get("USE_SHARED_BROTLI") +if USE_SHARED_BROTLI != "1": + libraries = ['libbrotli'] +else: + libraries = ['brotlienc', 'brotlidec'] + +if 'win32' not in str(sys.platform).lower(): + libraries.append('stdc++') + + +ffi.set_source( + "_brotlicffi", + """#include + #include + """, + libraries=libraries, + include_dirs=["libbrotli/c", "libbrotli/c/include", "libbrotli/c/common"] +) + +ffi.cdef(""" + /* common/types.h */ + typedef bool BROTLI_BOOL; + #define BROTLI_TRUE ... + #define BROTLI_FALSE ... + + /* dec/state.h */ + /* Allocating function pointer. Function MUST return 0 in the case of + failure. Otherwise it MUST return a valid pointer to a memory region of + at least size length. Neither items nor size are allowed to be 0. + opaque argument is a pointer provided by client and could be used to + bind function to specific object (memory pool). */ + typedef void* (*brotli_alloc_func)(void* opaque, size_t size); + + /* Deallocating function pointer. Function SHOULD be no-op in the case the + address is 0. */ + typedef void (*brotli_free_func)(void* opaque, void* address); + + /* dec/decode.h */ + + typedef enum { + /* Decoding error, e.g. corrupt input or memory allocation problem */ + BROTLI_DECODER_RESULT_ERROR = 0, + /* Decoding successfully completed */ + BROTLI_DECODER_RESULT_SUCCESS = 1, + /* Partially done; should be called again with more input */ + BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT = 2, + /* Partially done; should be called again with more output */ + BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT = 3 + } BrotliDecoderResult; + + typedef enum {...} BrotliDecoderErrorCode; + typedef ... BrotliDecoderState; + + /* Creates the instance of BrotliDecoderState and initializes it. + |alloc_func| and |free_func| MUST be both zero or both non-zero. In the + case they are both zero, default memory allocators are used. |opaque| is + passed to |alloc_func| and |free_func| when they are called. */ + BrotliDecoderState* BrotliDecoderCreateInstance(brotli_alloc_func, + brotli_free_func, + void *); + + /* Deinitializes and frees BrotliDecoderState instance. */ + void BrotliDecoderDestroyInstance(BrotliDecoderState* state); + + /* Decompresses the data. Supports partial input and output. + + Must be called with an allocated input buffer in |*next_in| and an + allocated output buffer in |*next_out|. The values |*available_in| and + |*available_out| must specify the allocated size in |*next_in| and + |*next_out| respectively. + + After each call, |*available_in| will be decremented by the amount of + input bytes consumed, and the |*next_in| pointer will be incremented by + that amount. Similarly, |*available_out| will be decremented by the + amount of output bytes written, and the |*next_out| pointer will be + incremented by that amount. |total_out|, if it is not a null-pointer, + will be set to the number of bytes decompressed since the last state + initialization. + + Input is never overconsumed, so |next_in| and |available_in| could be + passed to the next consumer after decoding is complete. */ + BrotliDecoderResult BrotliDecoderDecompressStream(BrotliDecoderState* s, + size_t* available_in, + const uint8_t** next_in, + size_t* available_out, + uint8_t** next_out, + size_t* total_out); + + /* Returns true, if decoder has some unconsumed output. + Otherwise returns false. */ + BROTLI_BOOL BrotliDecoderHasMoreOutput(const BrotliDecoderState* s); + + /* Returns true, if decoder has already received some input bytes. + Otherwise returns false. */ + BROTLI_BOOL BrotliDecoderIsUsed(const BrotliDecoderState* s); + + /* Returns true, if decoder is in a state where we reached the end of the + input and produced all of the output; returns false otherwise. */ + BROTLI_BOOL BrotliDecoderIsFinished(const BrotliDecoderState* s); + + /* Returns detailed error code after BrotliDecompressStream returns + BROTLI_DECODER_RESULT_ERROR. */ + BrotliDecoderErrorCode BrotliDecoderGetErrorCode( + const BrotliDecoderState* s); + + const char* BrotliDecoderErrorString(BrotliDecoderErrorCode c); + + /* enc/encode.h */ + typedef ... BrotliEncoderState; + + typedef enum BrotliEncoderParameter { + BROTLI_PARAM_MODE = 0, + /* Controls the compression-speed vs compression-density tradeoffs. The + higher the quality, the slower the compression. Range is 0 to 11. */ + BROTLI_PARAM_QUALITY = 1, + /* Base 2 logarithm of the sliding window size. Range is 10 to 24. */ + BROTLI_PARAM_LGWIN = 2, + /* Base 2 logarithm of the maximum input block size. Range is 16 to 24. + If set to 0, the value will be set based on the quality. */ + BROTLI_PARAM_LGBLOCK = 3 + } BrotliEncoderParameter; + + typedef enum BrotliEncoderMode { + /* Default compression mode. The compressor does not know anything in + advance about the properties of the input. */ + BROTLI_MODE_GENERIC = 0, + /* Compression mode for UTF-8 format text input. */ + BROTLI_MODE_TEXT = 1, + /* Compression mode used in WOFF 2.0. */ + BROTLI_MODE_FONT = 2 + } BrotliEncoderMode; + + int BROTLI_DEFAULT_QUALITY = 11; + int BROTLI_DEFAULT_WINDOW = 22; + #define BROTLI_DEFAULT_MODE ... + + typedef enum BrotliEncoderOperation { + BROTLI_OPERATION_PROCESS = 0, + /* Request output stream to flush. Performed when input stream is + depleted and there is enough space in output stream. */ + BROTLI_OPERATION_FLUSH = 1, + /* Request output stream to finish. Performed when input stream is + depleted and there is enough space in output stream. */ + BROTLI_OPERATION_FINISH = 2 + } BrotliEncoderOperation; + + /* Creates the instance of BrotliEncoderState and initializes it. + |alloc_func| and |free_func| MUST be both zero or both non-zero. In the + case they are both zero, default memory allocators are used. |opaque| is + passed to |alloc_func| and |free_func| when they are called. */ + BrotliEncoderState* BrotliEncoderCreateInstance(brotli_alloc_func, + brotli_free_func, + void *); + + /* Deinitializes and frees BrotliEncoderState instance. */ + void BrotliEncoderDestroyInstance(BrotliEncoderState* state); + + /* Compresses the data in |input_buffer| into |encoded_buffer|, and sets + |*encoded_size| to the compressed length. + BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW and BROTLI_DEFAULT_MODE + should be used as |quality|, |lgwin| and |mode| if there are no specific + requirements to encoder speed and compression ratio. + If compression fails, |*encoded_size| is set to 0. + If BrotliEncoderMaxCompressedSize(|input_size|) is not zero, then + |*encoded_size| is never set to the bigger value. + Returns false if there was an error and true otherwise. */ + BROTLI_BOOL BrotliEncoderCompress(int quality, + int lgwin, + BrotliEncoderMode mode, + size_t input_size, + const uint8_t* input_buffer, + size_t* encoded_size, + uint8_t* encoded_buffer); + + BROTLI_BOOL BrotliEncoderCompressStream(BrotliEncoderState* s, + BrotliEncoderOperation op, + size_t* available_in, + const uint8_t** next_in, + size_t* available_out, + uint8_t** next_out, + size_t* total_out); + + BROTLI_BOOL BrotliEncoderSetParameter(BrotliEncoderState* state, + BrotliEncoderParameter p, + uint32_t value); + + /* Check if encoder is in "finished" state, i.e. no more input is + acceptable and no more output will be produced. + Works only with BrotliEncoderCompressStream workflow. + Returns 1 if stream is finished and 0 otherwise. */ + BROTLI_BOOL BrotliEncoderIsFinished(BrotliEncoderState* s); + + /* Check if encoder has more output bytes in internal buffer. + Works only with BrotliEncoderCompressStream workflow. + Returns 1 if has more output (in internal buffer) and 0 otherwise. */ + BROTLI_BOOL BrotliEncoderHasMoreOutput(BrotliEncoderState* s); +""") + +if __name__ == '__main__': + ffi.compile() diff --git a/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/INSTALLER b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/LICENSE b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..762a2f0bad65e525408b787f5b95925273734d49 --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014-2025 Thomas Kemmer + +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/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/METADATA b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..b8fe3d6b383a3f2a8ac8553968ad989979d3049d --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/METADATA @@ -0,0 +1,151 @@ +Metadata-Version: 2.2 +Name: cachetools +Version: 5.5.2 +Summary: Extensible memoizing collections and decorators +Home-page: https://github.com/tkem/cachetools/ +Author: Thomas Kemmer +Author-email: tkemmer@computer.org +License: MIT +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Other Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.7 +License-File: LICENSE + +cachetools +======================================================================== + +.. image:: https://img.shields.io/pypi/v/cachetools + :target: https://pypi.org/project/cachetools/ + :alt: Latest PyPI version + +.. image:: https://img.shields.io/github/actions/workflow/status/tkem/cachetools/ci.yml + :target: https://github.com/tkem/cachetools/actions/workflows/ci.yml + :alt: CI build status + +.. image:: https://img.shields.io/readthedocs/cachetools + :target: https://cachetools.readthedocs.io/ + :alt: Documentation build status + +.. image:: https://img.shields.io/codecov/c/github/tkem/cachetools/master.svg + :target: https://codecov.io/gh/tkem/cachetools + :alt: Test coverage + +.. image:: https://img.shields.io/librariesio/sourcerank/pypi/cachetools + :target: https://libraries.io/pypi/cachetools + :alt: Libraries.io SourceRank + +.. image:: https://img.shields.io/github/license/tkem/cachetools + :target: https://raw.github.com/tkem/cachetools/master/LICENSE + :alt: License + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: black + + +This module provides various memoizing collections and decorators, +including variants of the Python Standard Library's `@lru_cache`_ +function decorator. + +.. code-block:: python + + from cachetools import cached, LRUCache, TTLCache + + # speed up calculating Fibonacci numbers with dynamic programming + @cached(cache={}) + def fib(n): + return n if n < 2 else fib(n - 1) + fib(n - 2) + + # cache least recently used Python Enhancement Proposals + @cached(cache=LRUCache(maxsize=32)) + def get_pep(num): + url = 'http://www.python.org/dev/peps/pep-%04d/' % num + with urllib.request.urlopen(url) as s: + return s.read() + + # cache weather data for no longer than ten minutes + @cached(cache=TTLCache(maxsize=1024, ttl=600)) + def get_weather(place): + return owm.weather_at_place(place).get_weather() + +For the purpose of this module, a *cache* is a mutable_ mapping_ of a +fixed maximum size. When the cache is full, i.e. by adding another +item the cache would exceed its maximum size, the cache must choose +which item(s) to discard based on a suitable `cache algorithm`_. + +This module provides multiple cache classes based on different cache +algorithms, as well as decorators for easily memoizing function and +method calls. + + +Installation +------------------------------------------------------------------------ + +cachetools is available from PyPI_ and can be installed by running:: + + pip install cachetools + +Typing stubs for this package are provided by typeshed_ and can be +installed by running:: + + pip install types-cachetools + + +Project Resources +------------------------------------------------------------------------ + +- `Documentation`_ +- `Issue tracker`_ +- `Source code`_ +- `Change log`_ + + +Related Projects +------------------------------------------------------------------------ + +- asyncache_: Helpers to use cachetools with async functions +- cacheing_: Pure Python Cacheing Library +- CacheToolsUtils_: Cachetools Utilities +- kids.cache_: Kids caching library +- shelved-cache_: Persistent cache for Python cachetools + + +License +------------------------------------------------------------------------ + +Copyright (c) 2014-2025 Thomas Kemmer. + +Licensed under the `MIT License`_. + + +.. _@lru_cache: https://docs.python.org/3/library/functools.html#functools.lru_cache +.. _mutable: https://docs.python.org/dev/glossary.html#term-mutable +.. _mapping: https://docs.python.org/dev/glossary.html#term-mapping +.. _cache algorithm: https://en.wikipedia.org/wiki/Cache_algorithms + +.. _PyPI: https://pypi.org/project/cachetools/ +.. _typeshed: https://github.com/python/typeshed/ +.. _Documentation: https://cachetools.readthedocs.io/ +.. _Issue tracker: https://github.com/tkem/cachetools/issues/ +.. _Source code: https://github.com/tkem/cachetools/ +.. _Change log: https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst +.. _MIT License: https://raw.github.com/tkem/cachetools/master/LICENSE + +.. _asyncache: https://pypi.org/project/asyncache/ +.. _cacheing: https://github.com/breid48/cacheing +.. _CacheToolsUtils: https://pypi.org/project/CacheToolsUtils/ +.. _kids.cache: https://pypi.org/project/kids.cache/ +.. _shelved-cache: https://pypi.org/project/shelved-cache/ diff --git a/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/RECORD b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..bbec7f8700f64bd2372bd370a41c8896fb09c563 --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/RECORD @@ -0,0 +1,14 @@ +cachetools-5.5.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +cachetools-5.5.2.dist-info/LICENSE,sha256=I8Tv96HAJ6l3oLecRJfhdYLDNMXxfvasjKC1LR59hBc,1085 +cachetools-5.5.2.dist-info/METADATA,sha256=YY8fmEiV8he5oa9hC4S6sjLQKrDuoQhx2mQTI7Iqf5Y,5379 +cachetools-5.5.2.dist-info/RECORD,, +cachetools-5.5.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91 +cachetools-5.5.2.dist-info/top_level.txt,sha256=ai2FH78TGwoBcCgVfoqbzk5IQCtnDukdSs4zKuVPvDs,11 +cachetools/__init__.py,sha256=cutUU6fB1bIMih0ro_TVCPKJTPwM-qP4fS_PyNfQlWs,21803 +cachetools/__pycache__/__init__.cpython-310.pyc,, +cachetools/__pycache__/_decorators.cpython-310.pyc,, +cachetools/__pycache__/func.cpython-310.pyc,, +cachetools/__pycache__/keys.cpython-310.pyc,, +cachetools/_decorators.py,sha256=4_u0GL89t2BOLGwnK8CueiFtyHKK2zydoHj9aqnsMM4,3832 +cachetools/func.py,sha256=aOVfSkuNWMRADpkHZGK7LeJ_VZ8wljzbRwIAliOuhAg,3719 +cachetools/keys.py,sha256=AOgfoi-oioBOnEEk115_9qs0HKISrYnbcV4F0hyZ1yk,1777 diff --git a/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/WHEEL b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..505164bc02d63fe6b0b3299f849a77c5f1beeb41 --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/top_level.txt b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..50d14084a9bc37250ae243ccae4a536f85f3ca2f --- /dev/null +++ b/lib/python3.10/site-packages/cachetools-5.5.2.dist-info/top_level.txt @@ -0,0 +1 @@ +cachetools diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/INSTALLER b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/LICENSE b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..62b076cdee58ec8f34034141ba0befd9015b0c7e --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/METADATA b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..37515b8c6590995ec46fd170cef85f8daa485748 --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/METADATA @@ -0,0 +1,76 @@ +Metadata-Version: 2.2 +Name: certifi +Version: 2025.8.3 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/master/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/RECORD b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..0b089b306fde6b1cba45fa834e7387c7500c4f7d --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/RECORD @@ -0,0 +1,16 @@ +certifi-2025.8.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +certifi-2025.8.3.dist-info/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2025.8.3.dist-info/METADATA,sha256=d5oZImG1TSBcz2A3XeHiO9duY2mgQj86DJa9-ja4h04,2400 +certifi-2025.8.3.dist-info/RECORD,, +certifi-2025.8.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +certifi-2025.8.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91 +certifi-2025.8.3.dist-info/direct_url.json,sha256=noiTDZxfUPkuVwERcG1aMK5Aoi_sziZEw2yq7KzYWNk,111 +certifi-2025.8.3.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=0a5ro4KTYep37Oo0Z8TycCPXaDlOEtvuj2pNWZ_1t8Y,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/__pycache__/__init__.cpython-39.pyc,, +certifi/__pycache__/__main__.cpython-39.pyc,, +certifi/__pycache__/core.cpython-39.pyc,, +certifi/cacert.pem,sha256=kQLmo2RKBxumzb1KU2mPKRxKZLGEUKCLwEZUi221zIs,287634 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/REQUESTED b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/WHEEL b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..505164bc02d63fe6b0b3299f849a77c5f1beeb41 --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/direct_url.json b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..75734cf209fa1b11da2f402045a820f97197316d --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///home/conda/feedstock_root/build_artifacts/certifi_1754231422783/work/certifi"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/top_level.txt b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..963eac530b9bc28d704d1bc410299c68e3216d4d --- /dev/null +++ b/lib/python3.10/site-packages/certifi-2025.8.3.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/lib/python3.10/site-packages/certifi/__init__.py b/lib/python3.10/site-packages/certifi/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fe2a89167ed665f76adad9980f4a2ec0c10f7da3 --- /dev/null +++ b/lib/python3.10/site-packages/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2025.08.03" diff --git a/lib/python3.10/site-packages/certifi/__main__.py b/lib/python3.10/site-packages/certifi/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..8945b5da857f4a7dec2b84f1225f012f6098418c --- /dev/null +++ b/lib/python3.10/site-packages/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/lib/python3.10/site-packages/certifi/cacert.pem b/lib/python3.10/site-packages/certifi/cacert.pem new file mode 100644 index 0000000000000000000000000000000000000000..2077a1e0a3936d4b82937ae4f74e6d2699354136 --- /dev/null +++ b/lib/python3.10/site-packages/certifi/cacert.pem @@ -0,0 +1,4738 @@ + +# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. +# Label: "Entrust Root Certification Authority" +# Serial: 1164660820 +# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4 +# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9 +# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 +Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW +KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw +NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw +NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy +ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV +BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo +Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 +4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 +KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI +rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi +94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB +sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi +gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo +kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE +vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t +O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua +AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP +9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ +eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m +0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2" +# Serial: 1289 +# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b +# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 +# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3" +# Serial: 1478 +# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf +# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 +# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Label: "SwissSign Gold CA - G2" +# Serial: 13492815561806991280 +# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 +# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 +# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# Issuer: CN=SecureTrust CA O=SecureTrust Corporation +# Subject: CN=SecureTrust CA O=SecureTrust Corporation +# Label: "SecureTrust CA" +# Serial: 17199774589125277788362757014266862032 +# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 +# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 +# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Issuer: CN=Secure Global CA O=SecureTrust Corporation +# Subject: CN=Secure Global CA O=SecureTrust Corporation +# Label: "Secure Global CA" +# Serial: 9751836167731051554232119481456978597 +# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de +# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b +# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=Certigna O=Dhimyotis +# Subject: CN=Certigna O=Dhimyotis +# Label: "Certigna" +# Serial: 18364802974209362175 +# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff +# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 +# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Label: "ePKI Root Certification Authority" +# Serial: 28956088682735189655030529057352760477 +# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 +# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 +# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# Issuer: O=certSIGN OU=certSIGN ROOT CA +# Subject: O=certSIGN OU=certSIGN ROOT CA +# Label: "certSIGN ROOT CA" +# Serial: 35210227249154 +# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 +# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b +# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Commercial O=AffirmTrust +# Subject: CN=AffirmTrust Commercial O=AffirmTrust +# Label: "AffirmTrust Commercial" +# Serial: 8608355977964138876 +# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7 +# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7 +# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7 +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP +Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr +ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL +MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1 +yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr +VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/ +nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG +XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj +vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt +Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g +N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC +nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Networking O=AffirmTrust +# Subject: CN=AffirmTrust Networking O=AffirmTrust +# Label: "AffirmTrust Networking" +# Serial: 8957382827206547757 +# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f +# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f +# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b +-----BEGIN CERTIFICATE----- +MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz +dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL +MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp +cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y +YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua +kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL +QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp +6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG +yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i +QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ +KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO +tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu +QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ +Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u +olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48 +x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium O=AffirmTrust +# Subject: CN=AffirmTrust Premium O=AffirmTrust +# Label: "AffirmTrust Premium" +# Serial: 7893706540734352110 +# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57 +# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27 +# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE +BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz +dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG +A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U +cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf +qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ +JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ ++jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS +s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5 +HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7 +70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG +V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S +qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S +5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia +C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX +OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE +FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2 +KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg +Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B +8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ +MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc +0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ +u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF +u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH +YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8 +GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO +RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e +KeC2uAloGRwYQw== +-----END CERTIFICATE----- + +# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust +# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust +# Label: "AffirmTrust Premium ECC" +# Serial: 8401224907861490260 +# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d +# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb +# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23 +-----BEGIN CERTIFICATE----- +MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC +VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ +cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ +BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt +VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D +0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9 +ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G +A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs +aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I +flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Label: "TeliaSonera Root CA v1" +# Serial: 199041966741090107964904287217786801558 +# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c +# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 +# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G2" +# Serial: 1246989352 +# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2 +# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4 +# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39 +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC +VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50 +cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs +IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz +dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy +NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu +dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt +dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0 +aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T +RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN +cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW +wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1 +U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0 +jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN +BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/ +jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v +1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R +nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH +VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g== +-----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - EC1" +# Serial: 51543124481930649114116133369 +# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc +# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47 +# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5 +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG +A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3 +d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu +dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq +RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy +MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD +VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 +L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g +Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD +ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi +A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt +ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH +Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC +R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX +hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global Certification Authority" +# Serial: 1846098327275375458322922162 +# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e +# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5 +# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8 +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P256 Certification Authority" +# Serial: 4151900041497450638097112925 +# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54 +# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf +# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4 +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P384 Certification Authority" +# Serial: 2704997926503831671788816187 +# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6 +# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2 +# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97 +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R2 O=Google Trust Services LLC +# Subject: CN=GTS Root R2 O=Google Trust Services LLC +# Label: "GTS Root R2" +# Serial: 159662449406622349769042896298 +# MD5 Fingerprint: 1e:39:c0:53:e6:1e:29:82:0b:ca:52:55:36:5d:57:dc +# SHA1 Fingerprint: 9a:44:49:76:32:db:de:fa:d0:bc:fb:5a:7b:17:bd:9e:56:09:24:94 +# SHA256 Fingerprint: 8d:25:cd:97:22:9d:bf:70:35:6b:da:4e:b3:cc:73:40:31:e2:4c:f0:0f:af:cf:d3:2d:c7:6e:b5:84:1c:7e:a8 +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=CommScope Public Trust ECC Root-01 O=CommScope +# Subject: CN=CommScope Public Trust ECC Root-01 O=CommScope +# Label: "CommScope Public Trust ECC Root-01" +# Serial: 385011430473757362783587124273108818652468453534 +# MD5 Fingerprint: 3a:40:a7:fc:03:8c:9c:38:79:2f:3a:a2:6c:b6:0a:16 +# SHA1 Fingerprint: 07:86:c0:d8:dd:8e:c0:80:98:06:98:d0:58:7a:ef:de:a6:cc:a2:5d +# SHA256 Fingerprint: 11:43:7c:da:7b:b4:5e:41:36:5f:45:b3:9a:38:98:6b:0d:e0:0d:ef:34:8e:0c:7b:b0:87:36:33:80:0b:c3:8b +-----BEGIN CERTIFICATE----- +MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMw +TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t +bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNa +Fw00NjA0MjgxNzM1NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv +cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16ocNfQj3Rid8NeeqrltqLxeP0C +flfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOcw5tjnSCDPgYLpkJE +hRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq +hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg +2NED3W3ROT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uS +Um9poIyNStDuiw7LR47QjRE= +-----END CERTIFICATE----- + +# Issuer: CN=CommScope Public Trust ECC Root-02 O=CommScope +# Subject: CN=CommScope Public Trust ECC Root-02 O=CommScope +# Label: "CommScope Public Trust ECC Root-02" +# Serial: 234015080301808452132356021271193974922492992893 +# MD5 Fingerprint: 59:b0:44:d5:65:4d:b8:5c:55:19:92:02:b6:d1:94:b2 +# SHA1 Fingerprint: 3c:3f:ef:57:0f:fe:65:93:86:9e:a0:fe:b0:f6:ed:8e:d1:13:c7:e5 +# SHA256 Fingerprint: 2f:fb:7f:81:3b:bb:b3:c8:9a:b4:e8:16:2d:0f:16:d7:15:09:a8:30:cc:9d:73:c2:62:e5:14:08:75:d1:ad:4a +-----BEGIN CERTIFICATE----- +MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMw +TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t +bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRa +Fw00NjA0MjgxNzQ0NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv +cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l63FRD/cHB8o5mXxO1Q/MMDAL +j2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/ubCK1sK9IRQq9qEmU +v4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD +VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq +hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/n +ich/m35rChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AV +mkzw5l4lIhVtwodZ0LKOag== +-----END CERTIFICATE----- + +# Issuer: CN=CommScope Public Trust RSA Root-01 O=CommScope +# Subject: CN=CommScope Public Trust RSA Root-01 O=CommScope +# Label: "CommScope Public Trust RSA Root-01" +# Serial: 354030733275608256394402989253558293562031411421 +# MD5 Fingerprint: 0e:b4:15:bc:87:63:5d:5d:02:73:d4:26:38:68:73:d8 +# SHA1 Fingerprint: 6d:0a:5f:f7:b4:23:06:b4:85:b3:b7:97:64:fc:ac:75:f5:33:f2:93 +# SHA256 Fingerprint: 02:bd:f9:6e:2a:45:dd:9b:f1:8f:c7:e1:db:df:21:a0:37:9b:a3:c9:c2:61:03:44:cf:d8:d6:06:fe:c1:ed:81 +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi +Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1 +NTRaFw00NjA0MjgxNjQ1NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t +U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt +MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwSGWjDR1C45FtnYSk +YZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2bKOx7dAvnQmtVzslh +suitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBWBB0HW0al +DrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj +WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFl +P8GMicWWMJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547 +KI2DAg+pn3LiLCuz3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7p +UcZsBSjBAg/pGG3svZwG1KdJ9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/ +kQO9lLvkuI6cMmPNn7togbGEW682v3fuHX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JO +Hg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcmAyejOQoBqsCyMWCDIqFPEgkB +Ea801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8eKg1mp9BFM6ltM6U +CBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ +KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6 +NWPxzIHIxgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQ +nmhUQo8mUuJM3y+Xpi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+ +QgvfKNmwrZggvkN80V4aCRckjXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2v +trV0KnahP/t1MJ+UXjulYPPLXAziDslg+MkfFoom3ecnf+slpoq9uC02EJqxWE2a +aE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0DLwAHb/WNyVntHKLr4W96ioD +j8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/xBpMu95yo9GA+o/E4 +Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054A5U+1C0w +lREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn +YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVoc +icCMb3SgazNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw +-----END CERTIFICATE----- + +# Issuer: CN=CommScope Public Trust RSA Root-02 O=CommScope +# Subject: CN=CommScope Public Trust RSA Root-02 O=CommScope +# Label: "CommScope Public Trust RSA Root-02" +# Serial: 480062499834624527752716769107743131258796508494 +# MD5 Fingerprint: e1:29:f9:62:7b:76:e2:96:6d:f3:d4:d7:0f:ae:1f:aa +# SHA1 Fingerprint: ea:b0:e2:52:1b:89:93:4c:11:68:f2:d8:9a:ac:22:4c:a3:8a:57:ae +# SHA256 Fingerprint: ff:e9:43:d7:93:42:4b:4f:7c:44:0c:1c:3d:64:8d:53:63:f3:4b:82:dc:87:aa:7a:9f:11:8f:c5:de:e1:01:f1 +-----BEGIN CERTIFICATE----- +MIIFbDCCA1SgAwIBAgIUVBa/O345lXGN0aoApYYNK496BU4wDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi +Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMjAeFw0yMTA0MjgxNzE2 +NDNaFw00NjA0MjgxNzE2NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t +U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt +MDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDh+g77aAASyE3VrCLE +NQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mnG2I81lDnNJUDMrG0 +kyI9p+Kx7eZ7Ti6Hmw0zdQreqjXnfuU2mKKuJZ6VszKWpCtYHu8//mI0SFHRtI1C +rWDaSWqVcN3SAOLMV2MCe5bdSZdbkk6V0/nLKR8YSvgBKtJjCW4k6YnS5cciTNxz +hkcAqg2Ijq6FfUrpuzNPDlJwnZXjfG2WWy09X6GDRl224yW4fKcZgBzqZUPckXk2 +LHR88mcGyYnJ27/aaL8j7dxrrSiDeS/sOKUNNwFnJ5rpM9kzXzehxfCrPfp4sOcs +n/Y+n2Dg70jpkEUeBVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIfhs1w/tku +FT0du7jyU1fbzMZ0KZwYszZ1OC4PVKH4kh+Jlk+71O6d6Ts2QrUKOyrUZHk2EOH5 +kQMreyBUzQ0ZGshBMjTRsJnhkB4BQDa1t/qp5Xd1pCKBXbCL5CcSD1SIxtuFdOa3 +wNemKfrb3vOTlycEVS8KbzfFPROvCgCpLIscgSjX74Yxqa7ybrjKaixUR9gqiC6v +wQcQeKwRoi9C8DfF8rhW3Q5iLc4tVn5V8qdE9isy9COoR+jUKgF4z2rDN6ieZdIs +5fq6M8EGRPbmz6UNp2YINIos8wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUR9DnsSL/nSz12Vdgs7GxcJXvYXowDQYJ +KoZIhvcNAQELBQADggIBAIZpsU0v6Z9PIpNojuQhmaPORVMbc0RTAIFhzTHjCLqB +KCh6krm2qMhDnscTJk3C2OVVnJJdUNjCK9v+5qiXz1I6JMNlZFxHMaNlNRPDk7n3 ++VGXu6TwYofF1gbTl4MgqX67tiHCpQ2EAOHyJxCDut0DgdXdaMNmEMjRdrSzbyme +APnCKfWxkxlSaRosTKCL4BWaMS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3Nyq +pgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv41xdgSGn2rtO/+YHqP65DSdsu3BaVXoT +6fEqSWnHX4dXTEN5bTpl6TBcQe7rd6VzEojov32u5cSoHw2OHG1QAk8mGEPej1WF +sQs3BWDJVTkSBKEqz3EWnzZRSb9wO55nnPt7eck5HHisd5FUmrh1CoFSl+NmYWvt +PjgelmFV4ZFUjO2MJB+ByRCac5krFk5yAD9UG/iNuovnFNa2RU9g7Jauwy8CTl2d +lklyALKrdVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670 +v64fG9PiO/yzcnMcmyiQiRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17O +rg3bhzjlP1v9mxnhMUF6cKojawHhRUzNlM47ni3niAIi9G7oyOzWPPO5std3eqx7 +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Label: "FIRMAPROFESIONAL CA ROOT-A WEB" +# Serial: 65916896770016886708751106294915943533 +# MD5 Fingerprint: 82:b2:ad:45:00:82:b0:66:63:f8:5f:c3:67:4e:ce:a3 +# SHA1 Fingerprint: a8:31:11:74:a6:14:15:0d:ca:77:dd:0e:e4:0c:5d:58:fc:a0:72:a5 +# SHA256 Fingerprint: be:f2:56:da:f2:6e:9c:69:bd:ec:16:02:35:97:98:f3:ca:f7:18:21:a0:3e:01:82:57:c5:3c:65:61:7f:3d:4a +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA12" +# Serial: 587887345431707215246142177076162061960426065942 +# MD5 Fingerprint: c6:89:ca:64:42:9b:62:08:49:0b:1e:7f:e9:07:3d:e8 +# SHA1 Fingerprint: 7a:22:1e:3d:de:1b:06:ac:9e:c8:47:70:16:8e:3c:e5:f7:6b:06:f4 +# SHA256 Fingerprint: 3f:03:4b:b5:70:4d:44:b2:d0:85:45:a0:20:57:de:93:eb:f3:90:5f:ce:72:1a:cb:c7:30:c0:6d:da:ee:90:4e +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- diff --git a/lib/python3.10/site-packages/certifi/core.py b/lib/python3.10/site-packages/certifi/core.py new file mode 100644 index 0000000000000000000000000000000000000000..1c9661cc7c2f6917c2506a30b2710002f81ab23a --- /dev/null +++ b/lib/python3.10/site-packages/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/lib/python3.10/site-packages/certifi/py.typed b/lib/python3.10/site-packages/certifi/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/INSTALLER b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/LICENSE b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..29225eee9edcd72c6a354550a5a3bedf1932b2ef --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/LICENSE @@ -0,0 +1,26 @@ + +Except when otherwise stated (look for LICENSE files in directories or +information at the beginning of each file) all software and +documentation is licensed as follows: + + 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/lib/python3.10/site-packages/cffi-1.15.1.dist-info/METADATA b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..62f691305a9152d5fe71fbddb5596eca2746e924 --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/METADATA @@ -0,0 +1,37 @@ +Metadata-Version: 2.1 +Name: cffi +Version: 1.15.1 +Summary: Foreign Function Interface for Python calling C code. +Home-page: http://cffi.readthedocs.org +Author: Armin Rigo, Maciej Fijalkowski +Author-email: python-cffi@googlegroups.com +License: MIT +Platform: UNKNOWN +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: License :: OSI Approved :: MIT License +License-File: LICENSE +Requires-Dist: pycparser + + +CFFI +==== + +Foreign Function Interface for Python calling C code. +Please see the `Documentation `_. + +Contact +------- + +`Mailing list `_ + + diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/RECORD b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..a9c38d95580ef82b88c784ee9185682ced7b804f --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/RECORD @@ -0,0 +1,46 @@ +_cffi_backend.cpython-310-x86_64-linux-gnu.so,sha256=_EAqkaLs7kPGuMVNS2woFxVnInM6CuL2ZoT9noHPcBU,639720 +cffi-1.15.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +cffi-1.15.1.dist-info/LICENSE,sha256=BLgPWwd7vtaICM_rreteNSPyqMmpZJXFh72W3x6sKjM,1294 +cffi-1.15.1.dist-info/METADATA,sha256=xBSCMCgS_caWnWssw5bIzJ7igwA267fvCiO1W6iotYA,1164 +cffi-1.15.1.dist-info/RECORD,, +cffi-1.15.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +cffi-1.15.1.dist-info/WHEEL,sha256=nTy_Z8ivGEB6qFwVLg7_h6rq0Jt0JU5Pz2cL2_FjSMQ,105 +cffi-1.15.1.dist-info/direct_url.json,sha256=wpdJVBQDaBd0EKkKUXnLprFQyqhWMmpgQZjdtVlRy0c,91 +cffi-1.15.1.dist-info/entry_points.txt,sha256=y6jTxnyeuLnL-XJcDv8uML3n6wyYiGRg8MTp_QGJ9Ho,75 +cffi-1.15.1.dist-info/top_level.txt,sha256=rE7WR3rZfNKxWI9-jn6hsHCAl7MDkB-FmuQbxWjFehQ,19 +cffi/__init__.py,sha256=6xB_tafGvhhM5Xvj0Ova3oPC2SEhVlLTEObVLnazeiM,513 +cffi/__pycache__/__init__.cpython-310.pyc,, +cffi/__pycache__/api.cpython-310.pyc,, +cffi/__pycache__/backend_ctypes.cpython-310.pyc,, +cffi/__pycache__/cffi_opcode.cpython-310.pyc,, +cffi/__pycache__/commontypes.cpython-310.pyc,, +cffi/__pycache__/cparser.cpython-310.pyc,, +cffi/__pycache__/error.cpython-310.pyc,, +cffi/__pycache__/ffiplatform.cpython-310.pyc,, +cffi/__pycache__/lock.cpython-310.pyc,, +cffi/__pycache__/model.cpython-310.pyc,, +cffi/__pycache__/pkgconfig.cpython-310.pyc,, +cffi/__pycache__/recompiler.cpython-310.pyc,, +cffi/__pycache__/setuptools_ext.cpython-310.pyc,, +cffi/__pycache__/vengine_cpy.cpython-310.pyc,, +cffi/__pycache__/vengine_gen.cpython-310.pyc,, +cffi/__pycache__/verifier.cpython-310.pyc,, +cffi/_cffi_errors.h,sha256=zQXt7uR_m8gUW-fI2hJg0KoSkJFwXv8RGUkEDZ177dQ,3908 +cffi/_cffi_include.h,sha256=tKnA1rdSoPHp23FnDL1mDGwFo-Uj6fXfA6vA6kcoEUc,14800 +cffi/_embedding.h,sha256=9tnjF44QRobR8z0FGqAmAZY-wMSBOae1SUPqHccowqc,17680 +cffi/api.py,sha256=yxJalIePbr1mz_WxAHokSwyP5CVYde44m-nolHnbJNo,42064 +cffi/backend_ctypes.py,sha256=h5ZIzLc6BFVXnGyc9xPqZWUS7qGy7yFSDqXe68Sa8z4,42454 +cffi/cffi_opcode.py,sha256=v9RdD_ovA8rCtqsC95Ivki5V667rAOhGgs3fb2q9xpM,5724 +cffi/commontypes.py,sha256=QS4uxCDI7JhtTyjh1hlnCA-gynmaszWxJaRRLGkJa1A,2689 +cffi/cparser.py,sha256=rO_1pELRw1gI1DE1m4gi2ik5JMfpxouAACLXpRPlVEA,44231 +cffi/error.py,sha256=v6xTiS4U0kvDcy4h_BDRo5v39ZQuj-IMRYLv5ETddZs,877 +cffi/ffiplatform.py,sha256=HMXqR8ks2wtdsNxGaWpQ_PyqIvtiuos_vf1qKCy-cwg,4046 +cffi/lock.py,sha256=l9TTdwMIMpi6jDkJGnQgE9cvTIR7CAntIJr8EGHt3pY,747 +cffi/model.py,sha256=_GH_UF1Rn9vC4AvmgJm6qj7RUXXG3eqKPc8bPxxyBKE,21768 +cffi/parse_c_type.h,sha256=OdwQfwM9ktq6vlCB43exFQmxDBtj2MBNdK8LYl15tjw,5976 +cffi/pkgconfig.py,sha256=LP1w7vmWvmKwyqLaU1Z243FOWGNQMrgMUZrvgFuOlco,4374 +cffi/recompiler.py,sha256=YgVYTh2CrXIobo-vMk7_K9mwAXdd_LqB4-IbYABQ488,64598 +cffi/setuptools_ext.py,sha256=RUR17N5f8gpiQBBlXL34P9FtOu1mhHIaAf3WJlg5S4I,8931 +cffi/vengine_cpy.py,sha256=YglN8YS-UaHEv2k2cxgotNWE87dHX20-68EyKoiKUYA,43320 +cffi/vengine_gen.py,sha256=5dX7s1DU6pTBOMI6oTVn_8Bnmru_lj932B6b4v29Hlg,26684 +cffi/verifier.py,sha256=ESwuXWXtXrKEagCKveLRDjFzLNCyaKdqAgAlKREcyhY,11253 diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/REQUESTED b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/WHEEL b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..3f78a79a0f2d50333e0936e383d5fd80de48c119 --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: false +Tag: cp310-cp310-linux_x86_64 + diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/direct_url.json b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..d2f1f175ee1bcae408730b321340a1602d4b9ec4 --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///tmp/abs_98z5h56wf8/croots/recipe/cffi_1659598650955/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/entry_points.txt b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..4b0274f2333a8cfadbe2d13922c47d0138e48141 --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[distutils.setup_keywords] +cffi_modules = cffi.setuptools_ext:cffi_modules diff --git a/lib/python3.10/site-packages/cffi-1.15.1.dist-info/top_level.txt b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..f64577957eb0d893196994ae517759f3fa8e48dd --- /dev/null +++ b/lib/python3.10/site-packages/cffi-1.15.1.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_cffi_backend +cffi diff --git a/lib/python3.10/site-packages/cffi/__init__.py b/lib/python3.10/site-packages/cffi/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..90e2e6559da7b0e973285198d676f643e03baa69 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/__init__.py @@ -0,0 +1,14 @@ +__all__ = ['FFI', 'VerificationError', 'VerificationMissing', 'CDefError', + 'FFIError'] + +from .api import FFI +from .error import CDefError, FFIError, VerificationError, VerificationMissing +from .error import PkgConfigError + +__version__ = "1.15.1" +__version_info__ = (1, 15, 1) + +# The verifier module file names are based on the CRC32 of a string that +# contains the following version number. It may be older than __version__ +# if nothing is clearly incompatible. +__version_verifier_modules__ = "0.8.6" diff --git a/lib/python3.10/site-packages/cffi/_cffi_errors.h b/lib/python3.10/site-packages/cffi/_cffi_errors.h new file mode 100644 index 0000000000000000000000000000000000000000..158e0590346a9a8b2ab047ac1bd23bcb3af21398 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/_cffi_errors.h @@ -0,0 +1,149 @@ +#ifndef CFFI_MESSAGEBOX +# ifdef _MSC_VER +# define CFFI_MESSAGEBOX 1 +# else +# define CFFI_MESSAGEBOX 0 +# endif +#endif + + +#if CFFI_MESSAGEBOX +/* Windows only: logic to take the Python-CFFI embedding logic + initialization errors and display them in a background thread + with MessageBox. The idea is that if the whole program closes + as a result of this problem, then likely it is already a console + program and you can read the stderr output in the console too. + If it is not a console program, then it will likely show its own + dialog to complain, or generally not abruptly close, and for this + case the background thread should stay alive. +*/ +static void *volatile _cffi_bootstrap_text; + +static PyObject *_cffi_start_error_capture(void) +{ + PyObject *result = NULL; + PyObject *x, *m, *bi; + + if (InterlockedCompareExchangePointer(&_cffi_bootstrap_text, + (void *)1, NULL) != NULL) + return (PyObject *)1; + + m = PyImport_AddModule("_cffi_error_capture"); + if (m == NULL) + goto error; + + result = PyModule_GetDict(m); + if (result == NULL) + goto error; + +#if PY_MAJOR_VERSION >= 3 + bi = PyImport_ImportModule("builtins"); +#else + bi = PyImport_ImportModule("__builtin__"); +#endif + if (bi == NULL) + goto error; + PyDict_SetItemString(result, "__builtins__", bi); + Py_DECREF(bi); + + x = PyRun_String( + "import sys\n" + "class FileLike:\n" + " def write(self, x):\n" + " try:\n" + " of.write(x)\n" + " except: pass\n" + " self.buf += x\n" + " def flush(self):\n" + " pass\n" + "fl = FileLike()\n" + "fl.buf = ''\n" + "of = sys.stderr\n" + "sys.stderr = fl\n" + "def done():\n" + " sys.stderr = of\n" + " return fl.buf\n", /* make sure the returned value stays alive */ + Py_file_input, + result, result); + Py_XDECREF(x); + + error: + if (PyErr_Occurred()) + { + PyErr_WriteUnraisable(Py_None); + PyErr_Clear(); + } + return result; +} + +#pragma comment(lib, "user32.lib") + +static DWORD WINAPI _cffi_bootstrap_dialog(LPVOID ignored) +{ + Sleep(666); /* may be interrupted if the whole process is closing */ +#if PY_MAJOR_VERSION >= 3 + MessageBoxW(NULL, (wchar_t *)_cffi_bootstrap_text, + L"Python-CFFI error", + MB_OK | MB_ICONERROR); +#else + MessageBoxA(NULL, (char *)_cffi_bootstrap_text, + "Python-CFFI error", + MB_OK | MB_ICONERROR); +#endif + _cffi_bootstrap_text = NULL; + return 0; +} + +static void _cffi_stop_error_capture(PyObject *ecap) +{ + PyObject *s; + void *text; + + if (ecap == (PyObject *)1) + return; + + if (ecap == NULL) + goto error; + + s = PyRun_String("done()", Py_eval_input, ecap, ecap); + if (s == NULL) + goto error; + + /* Show a dialog box, but in a background thread, and + never show multiple dialog boxes at once. */ +#if PY_MAJOR_VERSION >= 3 + text = PyUnicode_AsWideCharString(s, NULL); +#else + text = PyString_AsString(s); +#endif + + _cffi_bootstrap_text = text; + + if (text != NULL) + { + HANDLE h; + h = CreateThread(NULL, 0, _cffi_bootstrap_dialog, + NULL, 0, NULL); + if (h != NULL) + CloseHandle(h); + } + /* decref the string, but it should stay alive as 'fl.buf' + in the small module above. It will really be freed only if + we later get another similar error. So it's a leak of at + most one copy of the small module. That's fine for this + situation which is usually a "fatal error" anyway. */ + Py_DECREF(s); + PyErr_Clear(); + return; + + error: + _cffi_bootstrap_text = NULL; + PyErr_Clear(); +} + +#else + +static PyObject *_cffi_start_error_capture(void) { return NULL; } +static void _cffi_stop_error_capture(PyObject *ecap) { } + +#endif diff --git a/lib/python3.10/site-packages/cffi/_cffi_include.h b/lib/python3.10/site-packages/cffi/_cffi_include.h new file mode 100644 index 0000000000000000000000000000000000000000..e4c0a672405298ddb3dcb2e2ca6da9eea3d2e162 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/_cffi_include.h @@ -0,0 +1,385 @@ +#define _CFFI_ + +/* We try to define Py_LIMITED_API before including Python.h. + + Mess: we can only define it if Py_DEBUG, Py_TRACE_REFS and + Py_REF_DEBUG are not defined. This is a best-effort approximation: + we can learn about Py_DEBUG from pyconfig.h, but it is unclear if + the same works for the other two macros. Py_DEBUG implies them, + but not the other way around. + + The implementation is messy (issue #350): on Windows, with _MSC_VER, + we have to define Py_LIMITED_API even before including pyconfig.h. + In that case, we guess what pyconfig.h will do to the macros above, + and check our guess after the #include. + + Note that on Windows, with CPython 3.x, you need >= 3.5 and virtualenv + version >= 16.0.0. With older versions of either, you don't get a + copy of PYTHON3.DLL in the virtualenv. We can't check the version of + CPython *before* we even include pyconfig.h. ffi.set_source() puts + a ``#define _CFFI_NO_LIMITED_API'' at the start of this file if it is + running on Windows < 3.5, as an attempt at fixing it, but that's + arguably wrong because it may not be the target version of Python. + Still better than nothing I guess. As another workaround, you can + remove the definition of Py_LIMITED_API here. + + See also 'py_limited_api' in cffi/setuptools_ext.py. +*/ +#if !defined(_CFFI_USE_EMBEDDING) && !defined(Py_LIMITED_API) +# ifdef _MSC_VER +# if !defined(_DEBUG) && !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) +# define Py_LIMITED_API +# endif +# include + /* sanity-check: Py_LIMITED_API will cause crashes if any of these + are also defined. Normally, the Python file PC/pyconfig.h does not + cause any of these to be defined, with the exception that _DEBUG + causes Py_DEBUG. Double-check that. */ +# ifdef Py_LIMITED_API +# if defined(Py_DEBUG) +# error "pyconfig.h unexpectedly defines Py_DEBUG, but Py_LIMITED_API is set" +# endif +# if defined(Py_TRACE_REFS) +# error "pyconfig.h unexpectedly defines Py_TRACE_REFS, but Py_LIMITED_API is set" +# endif +# if defined(Py_REF_DEBUG) +# error "pyconfig.h unexpectedly defines Py_REF_DEBUG, but Py_LIMITED_API is set" +# endif +# endif +# else +# include +# if !defined(Py_DEBUG) && !defined(Py_TRACE_REFS) && !defined(Py_REF_DEBUG) && !defined(_CFFI_NO_LIMITED_API) +# define Py_LIMITED_API +# endif +# endif +#endif + +#include +#ifdef __cplusplus +extern "C" { +#endif +#include +#include "parse_c_type.h" + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +#endif + +#ifdef __GNUC__ +# define _CFFI_UNUSED_FN __attribute__((unused)) +#else +# define _CFFI_UNUSED_FN /* nothing */ +#endif + +#ifdef __cplusplus +# ifndef _Bool + typedef bool _Bool; /* semi-hackish: C++ has no _Bool; bool is builtin */ +# endif +#endif + +/********** CPython-specific section **********/ +#ifndef PYPY_VERSION + + +#if PY_MAJOR_VERSION >= 3 +# define PyInt_FromLong PyLong_FromLong +#endif + +#define _cffi_from_c_double PyFloat_FromDouble +#define _cffi_from_c_float PyFloat_FromDouble +#define _cffi_from_c_long PyInt_FromLong +#define _cffi_from_c_ulong PyLong_FromUnsignedLong +#define _cffi_from_c_longlong PyLong_FromLongLong +#define _cffi_from_c_ulonglong PyLong_FromUnsignedLongLong +#define _cffi_from_c__Bool PyBool_FromLong + +#define _cffi_to_c_double PyFloat_AsDouble +#define _cffi_to_c_float PyFloat_AsDouble + +#define _cffi_from_c_int(x, type) \ + (((type)-1) > 0 ? /* unsigned */ \ + (sizeof(type) < sizeof(long) ? \ + PyInt_FromLong((long)x) : \ + sizeof(type) == sizeof(long) ? \ + PyLong_FromUnsignedLong((unsigned long)x) : \ + PyLong_FromUnsignedLongLong((unsigned long long)x)) : \ + (sizeof(type) <= sizeof(long) ? \ + PyInt_FromLong((long)x) : \ + PyLong_FromLongLong((long long)x))) + +#define _cffi_to_c_int(o, type) \ + ((type)( \ + sizeof(type) == 1 ? (((type)-1) > 0 ? (type)_cffi_to_c_u8(o) \ + : (type)_cffi_to_c_i8(o)) : \ + sizeof(type) == 2 ? (((type)-1) > 0 ? (type)_cffi_to_c_u16(o) \ + : (type)_cffi_to_c_i16(o)) : \ + sizeof(type) == 4 ? (((type)-1) > 0 ? (type)_cffi_to_c_u32(o) \ + : (type)_cffi_to_c_i32(o)) : \ + sizeof(type) == 8 ? (((type)-1) > 0 ? (type)_cffi_to_c_u64(o) \ + : (type)_cffi_to_c_i64(o)) : \ + (Py_FatalError("unsupported size for type " #type), (type)0))) + +#define _cffi_to_c_i8 \ + ((int(*)(PyObject *))_cffi_exports[1]) +#define _cffi_to_c_u8 \ + ((int(*)(PyObject *))_cffi_exports[2]) +#define _cffi_to_c_i16 \ + ((int(*)(PyObject *))_cffi_exports[3]) +#define _cffi_to_c_u16 \ + ((int(*)(PyObject *))_cffi_exports[4]) +#define _cffi_to_c_i32 \ + ((int(*)(PyObject *))_cffi_exports[5]) +#define _cffi_to_c_u32 \ + ((unsigned int(*)(PyObject *))_cffi_exports[6]) +#define _cffi_to_c_i64 \ + ((long long(*)(PyObject *))_cffi_exports[7]) +#define _cffi_to_c_u64 \ + ((unsigned long long(*)(PyObject *))_cffi_exports[8]) +#define _cffi_to_c_char \ + ((int(*)(PyObject *))_cffi_exports[9]) +#define _cffi_from_c_pointer \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[10]) +#define _cffi_to_c_pointer \ + ((char *(*)(PyObject *, struct _cffi_ctypedescr *))_cffi_exports[11]) +#define _cffi_get_struct_layout \ + not used any more +#define _cffi_restore_errno \ + ((void(*)(void))_cffi_exports[13]) +#define _cffi_save_errno \ + ((void(*)(void))_cffi_exports[14]) +#define _cffi_from_c_char \ + ((PyObject *(*)(char))_cffi_exports[15]) +#define _cffi_from_c_deref \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[16]) +#define _cffi_to_c \ + ((int(*)(char *, struct _cffi_ctypedescr *, PyObject *))_cffi_exports[17]) +#define _cffi_from_c_struct \ + ((PyObject *(*)(char *, struct _cffi_ctypedescr *))_cffi_exports[18]) +#define _cffi_to_c_wchar_t \ + ((_cffi_wchar_t(*)(PyObject *))_cffi_exports[19]) +#define _cffi_from_c_wchar_t \ + ((PyObject *(*)(_cffi_wchar_t))_cffi_exports[20]) +#define _cffi_to_c_long_double \ + ((long double(*)(PyObject *))_cffi_exports[21]) +#define _cffi_to_c__Bool \ + ((_Bool(*)(PyObject *))_cffi_exports[22]) +#define _cffi_prepare_pointer_call_argument \ + ((Py_ssize_t(*)(struct _cffi_ctypedescr *, \ + PyObject *, char **))_cffi_exports[23]) +#define _cffi_convert_array_from_object \ + ((int(*)(char *, struct _cffi_ctypedescr *, PyObject *))_cffi_exports[24]) +#define _CFFI_CPIDX 25 +#define _cffi_call_python \ + ((void(*)(struct _cffi_externpy_s *, char *))_cffi_exports[_CFFI_CPIDX]) +#define _cffi_to_c_wchar3216_t \ + ((int(*)(PyObject *))_cffi_exports[26]) +#define _cffi_from_c_wchar3216_t \ + ((PyObject *(*)(int))_cffi_exports[27]) +#define _CFFI_NUM_EXPORTS 28 + +struct _cffi_ctypedescr; + +static void *_cffi_exports[_CFFI_NUM_EXPORTS]; + +#define _cffi_type(index) ( \ + assert((((uintptr_t)_cffi_types[index]) & 1) == 0), \ + (struct _cffi_ctypedescr *)_cffi_types[index]) + +static PyObject *_cffi_init(const char *module_name, Py_ssize_t version, + const struct _cffi_type_context_s *ctx) +{ + PyObject *module, *o_arg, *new_module; + void *raw[] = { + (void *)module_name, + (void *)version, + (void *)_cffi_exports, + (void *)ctx, + }; + + module = PyImport_ImportModule("_cffi_backend"); + if (module == NULL) + goto failure; + + o_arg = PyLong_FromVoidPtr((void *)raw); + if (o_arg == NULL) + goto failure; + + new_module = PyObject_CallMethod( + module, (char *)"_init_cffi_1_0_external_module", (char *)"O", o_arg); + + Py_DECREF(o_arg); + Py_DECREF(module); + return new_module; + + failure: + Py_XDECREF(module); + return NULL; +} + + +#ifdef HAVE_WCHAR_H +typedef wchar_t _cffi_wchar_t; +#else +typedef uint16_t _cffi_wchar_t; /* same random pick as _cffi_backend.c */ +#endif + +_CFFI_UNUSED_FN static uint16_t _cffi_to_c_char16_t(PyObject *o) +{ + if (sizeof(_cffi_wchar_t) == 2) + return (uint16_t)_cffi_to_c_wchar_t(o); + else + return (uint16_t)_cffi_to_c_wchar3216_t(o); +} + +_CFFI_UNUSED_FN static PyObject *_cffi_from_c_char16_t(uint16_t x) +{ + if (sizeof(_cffi_wchar_t) == 2) + return _cffi_from_c_wchar_t((_cffi_wchar_t)x); + else + return _cffi_from_c_wchar3216_t((int)x); +} + +_CFFI_UNUSED_FN static int _cffi_to_c_char32_t(PyObject *o) +{ + if (sizeof(_cffi_wchar_t) == 4) + return (int)_cffi_to_c_wchar_t(o); + else + return (int)_cffi_to_c_wchar3216_t(o); +} + +_CFFI_UNUSED_FN static PyObject *_cffi_from_c_char32_t(unsigned int x) +{ + if (sizeof(_cffi_wchar_t) == 4) + return _cffi_from_c_wchar_t((_cffi_wchar_t)x); + else + return _cffi_from_c_wchar3216_t((int)x); +} + +union _cffi_union_alignment_u { + unsigned char m_char; + unsigned short m_short; + unsigned int m_int; + unsigned long m_long; + unsigned long long m_longlong; + float m_float; + double m_double; + long double m_longdouble; +}; + +struct _cffi_freeme_s { + struct _cffi_freeme_s *next; + union _cffi_union_alignment_u alignment; +}; + +_CFFI_UNUSED_FN static int +_cffi_convert_array_argument(struct _cffi_ctypedescr *ctptr, PyObject *arg, + char **output_data, Py_ssize_t datasize, + struct _cffi_freeme_s **freeme) +{ + char *p; + if (datasize < 0) + return -1; + + p = *output_data; + if (p == NULL) { + struct _cffi_freeme_s *fp = (struct _cffi_freeme_s *)PyObject_Malloc( + offsetof(struct _cffi_freeme_s, alignment) + (size_t)datasize); + if (fp == NULL) + return -1; + fp->next = *freeme; + *freeme = fp; + p = *output_data = (char *)&fp->alignment; + } + memset((void *)p, 0, (size_t)datasize); + return _cffi_convert_array_from_object(p, ctptr, arg); +} + +_CFFI_UNUSED_FN static void +_cffi_free_array_arguments(struct _cffi_freeme_s *freeme) +{ + do { + void *p = (void *)freeme; + freeme = freeme->next; + PyObject_Free(p); + } while (freeme != NULL); +} + +/********** end CPython-specific section **********/ +#else +_CFFI_UNUSED_FN +static void (*_cffi_call_python_org)(struct _cffi_externpy_s *, char *); +# define _cffi_call_python _cffi_call_python_org +#endif + + +#define _cffi_array_len(array) (sizeof(array) / sizeof((array)[0])) + +#define _cffi_prim_int(size, sign) \ + ((size) == 1 ? ((sign) ? _CFFI_PRIM_INT8 : _CFFI_PRIM_UINT8) : \ + (size) == 2 ? ((sign) ? _CFFI_PRIM_INT16 : _CFFI_PRIM_UINT16) : \ + (size) == 4 ? ((sign) ? _CFFI_PRIM_INT32 : _CFFI_PRIM_UINT32) : \ + (size) == 8 ? ((sign) ? _CFFI_PRIM_INT64 : _CFFI_PRIM_UINT64) : \ + _CFFI__UNKNOWN_PRIM) + +#define _cffi_prim_float(size) \ + ((size) == sizeof(float) ? _CFFI_PRIM_FLOAT : \ + (size) == sizeof(double) ? _CFFI_PRIM_DOUBLE : \ + (size) == sizeof(long double) ? _CFFI__UNKNOWN_LONG_DOUBLE : \ + _CFFI__UNKNOWN_FLOAT_PRIM) + +#define _cffi_check_int(got, got_nonpos, expected) \ + ((got_nonpos) == (expected <= 0) && \ + (got) == (unsigned long long)expected) + +#ifdef MS_WIN32 +# define _cffi_stdcall __stdcall +#else +# define _cffi_stdcall /* nothing */ +#endif + +#ifdef __cplusplus +} +#endif diff --git a/lib/python3.10/site-packages/cffi/_embedding.h b/lib/python3.10/site-packages/cffi/_embedding.h new file mode 100644 index 0000000000000000000000000000000000000000..8e8df882d475b3672af183044602ce564ce0720c --- /dev/null +++ b/lib/python3.10/site-packages/cffi/_embedding.h @@ -0,0 +1,528 @@ + +/***** Support code for embedding *****/ + +#ifdef __cplusplus +extern "C" { +#endif + + +#if defined(_WIN32) +# define CFFI_DLLEXPORT __declspec(dllexport) +#elif defined(__GNUC__) +# define CFFI_DLLEXPORT __attribute__((visibility("default"))) +#else +# define CFFI_DLLEXPORT /* nothing */ +#endif + + +/* There are two global variables of type _cffi_call_python_fnptr: + + * _cffi_call_python, which we declare just below, is the one called + by ``extern "Python"`` implementations. + + * _cffi_call_python_org, which on CPython is actually part of the + _cffi_exports[] array, is the function pointer copied from + _cffi_backend. If _cffi_start_python() fails, then this is set + to NULL; otherwise, it should never be NULL. + + After initialization is complete, both are equal. However, the + first one remains equal to &_cffi_start_and_call_python until the + very end of initialization, when we are (or should be) sure that + concurrent threads also see a completely initialized world, and + only then is it changed. +*/ +#undef _cffi_call_python +typedef void (*_cffi_call_python_fnptr)(struct _cffi_externpy_s *, char *); +static void _cffi_start_and_call_python(struct _cffi_externpy_s *, char *); +static _cffi_call_python_fnptr _cffi_call_python = &_cffi_start_and_call_python; + + +#ifndef _MSC_VER + /* --- Assuming a GCC not infinitely old --- */ +# define cffi_compare_and_swap(l,o,n) __sync_bool_compare_and_swap(l,o,n) +# define cffi_write_barrier() __sync_synchronize() +# if !defined(__amd64__) && !defined(__x86_64__) && \ + !defined(__i386__) && !defined(__i386) +# define cffi_read_barrier() __sync_synchronize() +# else +# define cffi_read_barrier() (void)0 +# endif +#else + /* --- Windows threads version --- */ +# include +# define cffi_compare_and_swap(l,o,n) \ + (InterlockedCompareExchangePointer(l,n,o) == (o)) +# define cffi_write_barrier() InterlockedCompareExchange(&_cffi_dummy,0,0) +# define cffi_read_barrier() (void)0 +static volatile LONG _cffi_dummy; +#endif + +#ifdef WITH_THREAD +# ifndef _MSC_VER +# include + static pthread_mutex_t _cffi_embed_startup_lock; +# else + static CRITICAL_SECTION _cffi_embed_startup_lock; +# endif + static char _cffi_embed_startup_lock_ready = 0; +#endif + +static void _cffi_acquire_reentrant_mutex(void) +{ + static void *volatile lock = NULL; + + while (!cffi_compare_and_swap(&lock, NULL, (void *)1)) { + /* should ideally do a spin loop instruction here, but + hard to do it portably and doesn't really matter I + think: pthread_mutex_init() should be very fast, and + this is only run at start-up anyway. */ + } + +#ifdef WITH_THREAD + if (!_cffi_embed_startup_lock_ready) { +# ifndef _MSC_VER + pthread_mutexattr_t attr; + pthread_mutexattr_init(&attr); + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init(&_cffi_embed_startup_lock, &attr); +# else + InitializeCriticalSection(&_cffi_embed_startup_lock); +# endif + _cffi_embed_startup_lock_ready = 1; + } +#endif + + while (!cffi_compare_and_swap(&lock, (void *)1, NULL)) + ; + +#ifndef _MSC_VER + pthread_mutex_lock(&_cffi_embed_startup_lock); +#else + EnterCriticalSection(&_cffi_embed_startup_lock); +#endif +} + +static void _cffi_release_reentrant_mutex(void) +{ +#ifndef _MSC_VER + pthread_mutex_unlock(&_cffi_embed_startup_lock); +#else + LeaveCriticalSection(&_cffi_embed_startup_lock); +#endif +} + + +/********** CPython-specific section **********/ +#ifndef PYPY_VERSION + +#include "_cffi_errors.h" + + +#define _cffi_call_python_org _cffi_exports[_CFFI_CPIDX] + +PyMODINIT_FUNC _CFFI_PYTHON_STARTUP_FUNC(void); /* forward */ + +static void _cffi_py_initialize(void) +{ + /* XXX use initsigs=0, which "skips initialization registration of + signal handlers, which might be useful when Python is + embedded" according to the Python docs. But review and think + if it should be a user-controllable setting. + + XXX we should also give a way to write errors to a buffer + instead of to stderr. + + XXX if importing 'site' fails, CPython (any version) calls + exit(). Should we try to work around this behavior here? + */ + Py_InitializeEx(0); +} + +static int _cffi_initialize_python(void) +{ + /* This initializes Python, imports _cffi_backend, and then the + present .dll/.so is set up as a CPython C extension module. + */ + int result; + PyGILState_STATE state; + PyObject *pycode=NULL, *global_dict=NULL, *x; + PyObject *builtins; + + state = PyGILState_Ensure(); + + /* Call the initxxx() function from the present module. It will + create and initialize us as a CPython extension module, instead + of letting the startup Python code do it---it might reimport + the same .dll/.so and get maybe confused on some platforms. + It might also have troubles locating the .dll/.so again for all + I know. + */ + (void)_CFFI_PYTHON_STARTUP_FUNC(); + if (PyErr_Occurred()) + goto error; + + /* Now run the Python code provided to ffi.embedding_init_code(). + */ + pycode = Py_CompileString(_CFFI_PYTHON_STARTUP_CODE, + "", + Py_file_input); + if (pycode == NULL) + goto error; + global_dict = PyDict_New(); + if (global_dict == NULL) + goto error; + builtins = PyEval_GetBuiltins(); + if (builtins == NULL) + goto error; + if (PyDict_SetItemString(global_dict, "__builtins__", builtins) < 0) + goto error; + x = PyEval_EvalCode( +#if PY_MAJOR_VERSION < 3 + (PyCodeObject *) +#endif + pycode, global_dict, global_dict); + if (x == NULL) + goto error; + Py_DECREF(x); + + /* Done! Now if we've been called from + _cffi_start_and_call_python() in an ``extern "Python"``, we can + only hope that the Python code did correctly set up the + corresponding @ffi.def_extern() function. Otherwise, the + general logic of ``extern "Python"`` functions (inside the + _cffi_backend module) will find that the reference is still + missing and print an error. + */ + result = 0; + done: + Py_XDECREF(pycode); + Py_XDECREF(global_dict); + PyGILState_Release(state); + return result; + + error:; + { + /* Print as much information as potentially useful. + Debugging load-time failures with embedding is not fun + */ + PyObject *ecap; + PyObject *exception, *v, *tb, *f, *modules, *mod; + PyErr_Fetch(&exception, &v, &tb); + ecap = _cffi_start_error_capture(); + f = PySys_GetObject((char *)"stderr"); + if (f != NULL && f != Py_None) { + PyFile_WriteString( + "Failed to initialize the Python-CFFI embedding logic:\n\n", f); + } + + if (exception != NULL) { + PyErr_NormalizeException(&exception, &v, &tb); + PyErr_Display(exception, v, tb); + } + Py_XDECREF(exception); + Py_XDECREF(v); + Py_XDECREF(tb); + + if (f != NULL && f != Py_None) { + PyFile_WriteString("\nFrom: " _CFFI_MODULE_NAME + "\ncompiled with cffi version: 1.15.1" + "\n_cffi_backend module: ", f); + modules = PyImport_GetModuleDict(); + mod = PyDict_GetItemString(modules, "_cffi_backend"); + if (mod == NULL) { + PyFile_WriteString("not loaded", f); + } + else { + v = PyObject_GetAttrString(mod, "__file__"); + PyFile_WriteObject(v, f, 0); + Py_XDECREF(v); + } + PyFile_WriteString("\nsys.path: ", f); + PyFile_WriteObject(PySys_GetObject((char *)"path"), f, 0); + PyFile_WriteString("\n\n", f); + } + _cffi_stop_error_capture(ecap); + } + result = -1; + goto done; +} + +#if PY_VERSION_HEX < 0x03080000 +PyAPI_DATA(char *) _PyParser_TokenNames[]; /* from CPython */ +#endif + +static int _cffi_carefully_make_gil(void) +{ + /* This does the basic initialization of Python. It can be called + completely concurrently from unrelated threads. It assumes + that we don't hold the GIL before (if it exists), and we don't + hold it afterwards. + + (What it really does used to be completely different in Python 2 + and Python 3, with the Python 2 solution avoiding the spin-lock + around the Py_InitializeEx() call. However, after recent changes + to CPython 2.7 (issue #358) it no longer works. So we use the + Python 3 solution everywhere.) + + This initializes Python by calling Py_InitializeEx(). + Important: this must not be called concurrently at all. + So we use a global variable as a simple spin lock. This global + variable must be from 'libpythonX.Y.so', not from this + cffi-based extension module, because it must be shared from + different cffi-based extension modules. + + In Python < 3.8, we choose + _PyParser_TokenNames[0] as a completely arbitrary pointer value + that is never written to. The default is to point to the + string "ENDMARKER". We change it temporarily to point to the + next character in that string. (Yes, I know it's REALLY + obscure.) + + In Python >= 3.8, this string array is no longer writable, so + instead we pick PyCapsuleType.tp_version_tag. We can't change + Python < 3.8 because someone might use a mixture of cffi + embedded modules, some of which were compiled before this file + changed. + */ + +#ifdef WITH_THREAD +# if PY_VERSION_HEX < 0x03080000 + char *volatile *lock = (char *volatile *)_PyParser_TokenNames; + char *old_value, *locked_value; + + while (1) { /* spin loop */ + old_value = *lock; + locked_value = old_value + 1; + if (old_value[0] == 'E') { + assert(old_value[1] == 'N'); + if (cffi_compare_and_swap(lock, old_value, locked_value)) + break; + } + else { + assert(old_value[0] == 'N'); + /* should ideally do a spin loop instruction here, but + hard to do it portably and doesn't really matter I + think: PyEval_InitThreads() should be very fast, and + this is only run at start-up anyway. */ + } + } +# else + int volatile *lock = (int volatile *)&PyCapsule_Type.tp_version_tag; + int old_value, locked_value; + assert(!(PyCapsule_Type.tp_flags & Py_TPFLAGS_HAVE_VERSION_TAG)); + + while (1) { /* spin loop */ + old_value = *lock; + locked_value = -42; + if (old_value == 0) { + if (cffi_compare_and_swap(lock, old_value, locked_value)) + break; + } + else { + assert(old_value == locked_value); + /* should ideally do a spin loop instruction here, but + hard to do it portably and doesn't really matter I + think: PyEval_InitThreads() should be very fast, and + this is only run at start-up anyway. */ + } + } +# endif +#endif + + /* call Py_InitializeEx() */ + if (!Py_IsInitialized()) { + _cffi_py_initialize(); +#if PY_VERSION_HEX < 0x03070000 + PyEval_InitThreads(); +#endif + PyEval_SaveThread(); /* release the GIL */ + /* the returned tstate must be the one that has been stored into the + autoTLSkey by _PyGILState_Init() called from Py_Initialize(). */ + } + else { +#if PY_VERSION_HEX < 0x03070000 + /* PyEval_InitThreads() is always a no-op from CPython 3.7 */ + PyGILState_STATE state = PyGILState_Ensure(); + PyEval_InitThreads(); + PyGILState_Release(state); +#endif + } + +#ifdef WITH_THREAD + /* release the lock */ + while (!cffi_compare_and_swap(lock, locked_value, old_value)) + ; +#endif + + return 0; +} + +/********** end CPython-specific section **********/ + + +#else + + +/********** PyPy-specific section **********/ + +PyMODINIT_FUNC _CFFI_PYTHON_STARTUP_FUNC(const void *[]); /* forward */ + +static struct _cffi_pypy_init_s { + const char *name; + void *func; /* function pointer */ + const char *code; +} _cffi_pypy_init = { + _CFFI_MODULE_NAME, + _CFFI_PYTHON_STARTUP_FUNC, + _CFFI_PYTHON_STARTUP_CODE, +}; + +extern int pypy_carefully_make_gil(const char *); +extern int pypy_init_embedded_cffi_module(int, struct _cffi_pypy_init_s *); + +static int _cffi_carefully_make_gil(void) +{ + return pypy_carefully_make_gil(_CFFI_MODULE_NAME); +} + +static int _cffi_initialize_python(void) +{ + return pypy_init_embedded_cffi_module(0xB011, &_cffi_pypy_init); +} + +/********** end PyPy-specific section **********/ + + +#endif + + +#ifdef __GNUC__ +__attribute__((noinline)) +#endif +static _cffi_call_python_fnptr _cffi_start_python(void) +{ + /* Delicate logic to initialize Python. This function can be + called multiple times concurrently, e.g. when the process calls + its first ``extern "Python"`` functions in multiple threads at + once. It can also be called recursively, in which case we must + ignore it. We also have to consider what occurs if several + different cffi-based extensions reach this code in parallel + threads---it is a different copy of the code, then, and we + can't have any shared global variable unless it comes from + 'libpythonX.Y.so'. + + Idea: + + * _cffi_carefully_make_gil(): "carefully" call + PyEval_InitThreads() (possibly with Py_InitializeEx() first). + + * then we use a (local) custom lock to make sure that a call to this + cffi-based extension will wait if another call to the *same* + extension is running the initialization in another thread. + It is reentrant, so that a recursive call will not block, but + only one from a different thread. + + * then we grab the GIL and (Python 2) we call Py_InitializeEx(). + At this point, concurrent calls to Py_InitializeEx() are not + possible: we have the GIL. + + * do the rest of the specific initialization, which may + temporarily release the GIL but not the custom lock. + Only release the custom lock when we are done. + */ + static char called = 0; + + if (_cffi_carefully_make_gil() != 0) + return NULL; + + _cffi_acquire_reentrant_mutex(); + + /* Here the GIL exists, but we don't have it. We're only protected + from concurrency by the reentrant mutex. */ + + /* This file only initializes the embedded module once, the first + time this is called, even if there are subinterpreters. */ + if (!called) { + called = 1; /* invoke _cffi_initialize_python() only once, + but don't set '_cffi_call_python' right now, + otherwise concurrent threads won't call + this function at all (we need them to wait) */ + if (_cffi_initialize_python() == 0) { + /* now initialization is finished. Switch to the fast-path. */ + + /* We would like nobody to see the new value of + '_cffi_call_python' without also seeing the rest of the + data initialized. However, this is not possible. But + the new value of '_cffi_call_python' is the function + 'cffi_call_python()' from _cffi_backend. So: */ + cffi_write_barrier(); + /* ^^^ we put a write barrier here, and a corresponding + read barrier at the start of cffi_call_python(). This + ensures that after that read barrier, we see everything + done here before the write barrier. + */ + + assert(_cffi_call_python_org != NULL); + _cffi_call_python = (_cffi_call_python_fnptr)_cffi_call_python_org; + } + else { + /* initialization failed. Reset this to NULL, even if it was + already set to some other value. Future calls to + _cffi_start_python() are still forced to occur, and will + always return NULL from now on. */ + _cffi_call_python_org = NULL; + } + } + + _cffi_release_reentrant_mutex(); + + return (_cffi_call_python_fnptr)_cffi_call_python_org; +} + +static +void _cffi_start_and_call_python(struct _cffi_externpy_s *externpy, char *args) +{ + _cffi_call_python_fnptr fnptr; + int current_err = errno; +#ifdef _MSC_VER + int current_lasterr = GetLastError(); +#endif + fnptr = _cffi_start_python(); + if (fnptr == NULL) { + fprintf(stderr, "function %s() called, but initialization code " + "failed. Returning 0.\n", externpy->name); + memset(args, 0, externpy->size_of_result); + } +#ifdef _MSC_VER + SetLastError(current_lasterr); +#endif + errno = current_err; + + if (fnptr != NULL) + fnptr(externpy, args); +} + + +/* The cffi_start_python() function makes sure Python is initialized + and our cffi module is set up. It can be called manually from the + user C code. The same effect is obtained automatically from any + dll-exported ``extern "Python"`` function. This function returns + -1 if initialization failed, 0 if all is OK. */ +_CFFI_UNUSED_FN +static int cffi_start_python(void) +{ + if (_cffi_call_python == &_cffi_start_and_call_python) { + if (_cffi_start_python() == NULL) + return -1; + } + cffi_read_barrier(); + return 0; +} + +#undef cffi_compare_and_swap +#undef cffi_write_barrier +#undef cffi_read_barrier + +#ifdef __cplusplus +} +#endif diff --git a/lib/python3.10/site-packages/cffi/api.py b/lib/python3.10/site-packages/cffi/api.py new file mode 100644 index 0000000000000000000000000000000000000000..999a8aefc4af0b27120823116212b0afe8484aad --- /dev/null +++ b/lib/python3.10/site-packages/cffi/api.py @@ -0,0 +1,965 @@ +import sys, types +from .lock import allocate_lock +from .error import CDefError +from . import model + +try: + callable +except NameError: + # Python 3.1 + from collections import Callable + callable = lambda x: isinstance(x, Callable) + +try: + basestring +except NameError: + # Python 3.x + basestring = str + +_unspecified = object() + + + +class FFI(object): + r''' + The main top-level class that you instantiate once, or once per module. + + Example usage: + + ffi = FFI() + ffi.cdef(""" + int printf(const char *, ...); + """) + + C = ffi.dlopen(None) # standard library + -or- + C = ffi.verify() # use a C compiler: verify the decl above is right + + C.printf("hello, %s!\n", ffi.new("char[]", "world")) + ''' + + def __init__(self, backend=None): + """Create an FFI instance. The 'backend' argument is used to + select a non-default backend, mostly for tests. + """ + if backend is None: + # You need PyPy (>= 2.0 beta), or a CPython (>= 2.6) with + # _cffi_backend.so compiled. + import _cffi_backend as backend + from . import __version__ + if backend.__version__ != __version__: + # bad version! Try to be as explicit as possible. + if hasattr(backend, '__file__'): + # CPython + raise Exception("Version mismatch: this is the 'cffi' package version %s, located in %r. When we import the top-level '_cffi_backend' extension module, we get version %s, located in %r. The two versions should be equal; check your installation." % ( + __version__, __file__, + backend.__version__, backend.__file__)) + else: + # PyPy + raise Exception("Version mismatch: this is the 'cffi' package version %s, located in %r. This interpreter comes with a built-in '_cffi_backend' module, which is version %s. The two versions should be equal; check your installation." % ( + __version__, __file__, backend.__version__)) + # (If you insist you can also try to pass the option + # 'backend=backend_ctypes.CTypesBackend()', but don't + # rely on it! It's probably not going to work well.) + + from . import cparser + self._backend = backend + self._lock = allocate_lock() + self._parser = cparser.Parser() + self._cached_btypes = {} + self._parsed_types = types.ModuleType('parsed_types').__dict__ + self._new_types = types.ModuleType('new_types').__dict__ + self._function_caches = [] + self._libraries = [] + self._cdefsources = [] + self._included_ffis = [] + self._windows_unicode = None + self._init_once_cache = {} + self._cdef_version = None + self._embedding = None + self._typecache = model.get_typecache(backend) + if hasattr(backend, 'set_ffi'): + backend.set_ffi(self) + for name in list(backend.__dict__): + if name.startswith('RTLD_'): + setattr(self, name, getattr(backend, name)) + # + with self._lock: + self.BVoidP = self._get_cached_btype(model.voidp_type) + self.BCharA = self._get_cached_btype(model.char_array_type) + if isinstance(backend, types.ModuleType): + # _cffi_backend: attach these constants to the class + if not hasattr(FFI, 'NULL'): + FFI.NULL = self.cast(self.BVoidP, 0) + FFI.CData, FFI.CType = backend._get_types() + else: + # ctypes backend: attach these constants to the instance + self.NULL = self.cast(self.BVoidP, 0) + self.CData, self.CType = backend._get_types() + self.buffer = backend.buffer + + def cdef(self, csource, override=False, packed=False, pack=None): + """Parse the given C source. This registers all declared functions, + types, and global variables. The functions and global variables can + then be accessed via either 'ffi.dlopen()' or 'ffi.verify()'. + The types can be used in 'ffi.new()' and other functions. + If 'packed' is specified as True, all structs declared inside this + cdef are packed, i.e. laid out without any field alignment at all. + Alternatively, 'pack' can be a small integer, and requests for + alignment greater than that are ignored (pack=1 is equivalent to + packed=True). + """ + self._cdef(csource, override=override, packed=packed, pack=pack) + + def embedding_api(self, csource, packed=False, pack=None): + self._cdef(csource, packed=packed, pack=pack, dllexport=True) + if self._embedding is None: + self._embedding = '' + + def _cdef(self, csource, override=False, **options): + if not isinstance(csource, str): # unicode, on Python 2 + if not isinstance(csource, basestring): + raise TypeError("cdef() argument must be a string") + csource = csource.encode('ascii') + with self._lock: + self._cdef_version = object() + self._parser.parse(csource, override=override, **options) + self._cdefsources.append(csource) + if override: + for cache in self._function_caches: + cache.clear() + finishlist = self._parser._recomplete + if finishlist: + self._parser._recomplete = [] + for tp in finishlist: + tp.finish_backend_type(self, finishlist) + + def dlopen(self, name, flags=0): + """Load and return a dynamic library identified by 'name'. + The standard C library can be loaded by passing None. + Note that functions and types declared by 'ffi.cdef()' are not + linked to a particular library, just like C headers; in the + library we only look for the actual (untyped) symbols. + """ + if not (isinstance(name, basestring) or + name is None or + isinstance(name, self.CData)): + raise TypeError("dlopen(name): name must be a file name, None, " + "or an already-opened 'void *' handle") + with self._lock: + lib, function_cache = _make_ffi_library(self, name, flags) + self._function_caches.append(function_cache) + self._libraries.append(lib) + return lib + + def dlclose(self, lib): + """Close a library obtained with ffi.dlopen(). After this call, + access to functions or variables from the library will fail + (possibly with a segmentation fault). + """ + type(lib).__cffi_close__(lib) + + def _typeof_locked(self, cdecl): + # call me with the lock! + key = cdecl + if key in self._parsed_types: + return self._parsed_types[key] + # + if not isinstance(cdecl, str): # unicode, on Python 2 + cdecl = cdecl.encode('ascii') + # + type = self._parser.parse_type(cdecl) + really_a_function_type = type.is_raw_function + if really_a_function_type: + type = type.as_function_pointer() + btype = self._get_cached_btype(type) + result = btype, really_a_function_type + self._parsed_types[key] = result + return result + + def _typeof(self, cdecl, consider_function_as_funcptr=False): + # string -> ctype object + try: + result = self._parsed_types[cdecl] + except KeyError: + with self._lock: + result = self._typeof_locked(cdecl) + # + btype, really_a_function_type = result + if really_a_function_type and not consider_function_as_funcptr: + raise CDefError("the type %r is a function type, not a " + "pointer-to-function type" % (cdecl,)) + return btype + + def typeof(self, cdecl): + """Parse the C type given as a string and return the + corresponding object. + It can also be used on 'cdata' instance to get its C type. + """ + if isinstance(cdecl, basestring): + return self._typeof(cdecl) + if isinstance(cdecl, self.CData): + return self._backend.typeof(cdecl) + if isinstance(cdecl, types.BuiltinFunctionType): + res = _builtin_function_type(cdecl) + if res is not None: + return res + if (isinstance(cdecl, types.FunctionType) + and hasattr(cdecl, '_cffi_base_type')): + with self._lock: + return self._get_cached_btype(cdecl._cffi_base_type) + raise TypeError(type(cdecl)) + + def sizeof(self, cdecl): + """Return the size in bytes of the argument. It can be a + string naming a C type, or a 'cdata' instance. + """ + if isinstance(cdecl, basestring): + BType = self._typeof(cdecl) + return self._backend.sizeof(BType) + else: + return self._backend.sizeof(cdecl) + + def alignof(self, cdecl): + """Return the natural alignment size in bytes of the C type + given as a string. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.alignof(cdecl) + + def offsetof(self, cdecl, *fields_or_indexes): + """Return the offset of the named field inside the given + structure or array, which must be given as a C type name. + You can give several field names in case of nested structures. + You can also give numeric values which correspond to array + items, in case of an array type. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._typeoffsetof(cdecl, *fields_or_indexes)[1] + + def new(self, cdecl, init=None): + """Allocate an instance according to the specified C type and + return a pointer to it. The specified C type must be either a + pointer or an array: ``new('X *')`` allocates an X and returns + a pointer to it, whereas ``new('X[n]')`` allocates an array of + n X'es and returns an array referencing it (which works + mostly like a pointer, like in C). You can also use + ``new('X[]', n)`` to allocate an array of a non-constant + length n. + + The memory is initialized following the rules of declaring a + global variable in C: by default it is zero-initialized, but + an explicit initializer can be given which can be used to + fill all or part of the memory. + + When the returned object goes out of scope, the memory + is freed. In other words the returned object has + ownership of the value of type 'cdecl' that it points to. This + means that the raw data can be used as long as this object is + kept alive, but must not be used for a longer time. Be careful + about that when copying the pointer to the memory somewhere + else, e.g. into another structure. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.newp(cdecl, init) + + def new_allocator(self, alloc=None, free=None, + should_clear_after_alloc=True): + """Return a new allocator, i.e. a function that behaves like ffi.new() + but uses the provided low-level 'alloc' and 'free' functions. + + 'alloc' is called with the size as argument. If it returns NULL, a + MemoryError is raised. 'free' is called with the result of 'alloc' + as argument. Both can be either Python function or directly C + functions. If 'free' is None, then no free function is called. + If both 'alloc' and 'free' are None, the default is used. + + If 'should_clear_after_alloc' is set to False, then the memory + returned by 'alloc' is assumed to be already cleared (or you are + fine with garbage); otherwise CFFI will clear it. + """ + compiled_ffi = self._backend.FFI() + allocator = compiled_ffi.new_allocator(alloc, free, + should_clear_after_alloc) + def allocate(cdecl, init=None): + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return allocator(cdecl, init) + return allocate + + def cast(self, cdecl, source): + """Similar to a C cast: returns an instance of the named C + type initialized with the given 'source'. The source is + casted between integers or pointers of any type. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.cast(cdecl, source) + + def string(self, cdata, maxlen=-1): + """Return a Python string (or unicode string) from the 'cdata'. + If 'cdata' is a pointer or array of characters or bytes, returns + the null-terminated string. The returned string extends until + the first null character, or at most 'maxlen' characters. If + 'cdata' is an array then 'maxlen' defaults to its length. + + If 'cdata' is a pointer or array of wchar_t, returns a unicode + string following the same rules. + + If 'cdata' is a single character or byte or a wchar_t, returns + it as a string or unicode string. + + If 'cdata' is an enum, returns the value of the enumerator as a + string, or 'NUMBER' if the value is out of range. + """ + return self._backend.string(cdata, maxlen) + + def unpack(self, cdata, length): + """Unpack an array of C data of the given length, + returning a Python string/unicode/list. + + If 'cdata' is a pointer to 'char', returns a byte string. + It does not stop at the first null. This is equivalent to: + ffi.buffer(cdata, length)[:] + + If 'cdata' is a pointer to 'wchar_t', returns a unicode string. + 'length' is measured in wchar_t's; it is not the size in bytes. + + If 'cdata' is a pointer to anything else, returns a list of + 'length' items. This is a faster equivalent to: + [cdata[i] for i in range(length)] + """ + return self._backend.unpack(cdata, length) + + #def buffer(self, cdata, size=-1): + # """Return a read-write buffer object that references the raw C data + # pointed to by the given 'cdata'. The 'cdata' must be a pointer or + # an array. Can be passed to functions expecting a buffer, or directly + # manipulated with: + # + # buf[:] get a copy of it in a regular string, or + # buf[idx] as a single character + # buf[:] = ... + # buf[idx] = ... change the content + # """ + # note that 'buffer' is a type, set on this instance by __init__ + + def from_buffer(self, cdecl, python_buffer=_unspecified, + require_writable=False): + """Return a cdata of the given type pointing to the data of the + given Python object, which must support the buffer interface. + Note that this is not meant to be used on the built-in types + str or unicode (you can build 'char[]' arrays explicitly) + but only on objects containing large quantities of raw data + in some other format, like 'array.array' or numpy arrays. + + The first argument is optional and default to 'char[]'. + """ + if python_buffer is _unspecified: + cdecl, python_buffer = self.BCharA, cdecl + elif isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + return self._backend.from_buffer(cdecl, python_buffer, + require_writable) + + def memmove(self, dest, src, n): + """ffi.memmove(dest, src, n) copies n bytes of memory from src to dest. + + Like the C function memmove(), the memory areas may overlap; + apart from that it behaves like the C function memcpy(). + + 'src' can be any cdata ptr or array, or any Python buffer object. + 'dest' can be any cdata ptr or array, or a writable Python buffer + object. The size to copy, 'n', is always measured in bytes. + + Unlike other methods, this one supports all Python buffer including + byte strings and bytearrays---but it still does not support + non-contiguous buffers. + """ + return self._backend.memmove(dest, src, n) + + def callback(self, cdecl, python_callable=None, error=None, onerror=None): + """Return a callback object or a decorator making such a + callback object. 'cdecl' must name a C function pointer type. + The callback invokes the specified 'python_callable' (which may + be provided either directly or via a decorator). Important: the + callback object must be manually kept alive for as long as the + callback may be invoked from the C level. + """ + def callback_decorator_wrap(python_callable): + if not callable(python_callable): + raise TypeError("the 'python_callable' argument " + "is not callable") + return self._backend.callback(cdecl, python_callable, + error, onerror) + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl, consider_function_as_funcptr=True) + if python_callable is None: + return callback_decorator_wrap # decorator mode + else: + return callback_decorator_wrap(python_callable) # direct mode + + def getctype(self, cdecl, replace_with=''): + """Return a string giving the C type 'cdecl', which may be itself + a string or a object. If 'replace_with' is given, it gives + extra text to append (or insert for more complicated C types), like + a variable name, or '*' to get actually the C type 'pointer-to-cdecl'. + """ + if isinstance(cdecl, basestring): + cdecl = self._typeof(cdecl) + replace_with = replace_with.strip() + if (replace_with.startswith('*') + and '&[' in self._backend.getcname(cdecl, '&')): + replace_with = '(%s)' % replace_with + elif replace_with and not replace_with[0] in '[(': + replace_with = ' ' + replace_with + return self._backend.getcname(cdecl, replace_with) + + def gc(self, cdata, destructor, size=0): + """Return a new cdata object that points to the same + data. Later, when this new cdata object is garbage-collected, + 'destructor(old_cdata_object)' will be called. + + The optional 'size' gives an estimate of the size, used to + trigger the garbage collection more eagerly. So far only used + on PyPy. It tells the GC that the returned object keeps alive + roughly 'size' bytes of external memory. + """ + return self._backend.gcp(cdata, destructor, size) + + def _get_cached_btype(self, type): + assert self._lock.acquire(False) is False + # call me with the lock! + try: + BType = self._cached_btypes[type] + except KeyError: + finishlist = [] + BType = type.get_cached_btype(self, finishlist) + for type in finishlist: + type.finish_backend_type(self, finishlist) + return BType + + def verify(self, source='', tmpdir=None, **kwargs): + """Verify that the current ffi signatures compile on this + machine, and return a dynamic library object. The dynamic + library can be used to call functions and access global + variables declared in this 'ffi'. The library is compiled + by the C compiler: it gives you C-level API compatibility + (including calling macros). This is unlike 'ffi.dlopen()', + which requires binary compatibility in the signatures. + """ + from .verifier import Verifier, _caller_dir_pycache + # + # If set_unicode(True) was called, insert the UNICODE and + # _UNICODE macro declarations + if self._windows_unicode: + self._apply_windows_unicode(kwargs) + # + # Set the tmpdir here, and not in Verifier.__init__: it picks + # up the caller's directory, which we want to be the caller of + # ffi.verify(), as opposed to the caller of Veritier(). + tmpdir = tmpdir or _caller_dir_pycache() + # + # Make a Verifier() and use it to load the library. + self.verifier = Verifier(self, source, tmpdir, **kwargs) + lib = self.verifier.load_library() + # + # Save the loaded library for keep-alive purposes, even + # if the caller doesn't keep it alive itself (it should). + self._libraries.append(lib) + return lib + + def _get_errno(self): + return self._backend.get_errno() + def _set_errno(self, errno): + self._backend.set_errno(errno) + errno = property(_get_errno, _set_errno, None, + "the value of 'errno' from/to the C calls") + + def getwinerror(self, code=-1): + return self._backend.getwinerror(code) + + def _pointer_to(self, ctype): + with self._lock: + return model.pointer_cache(self, ctype) + + def addressof(self, cdata, *fields_or_indexes): + """Return the address of a . + If 'fields_or_indexes' are given, returns the address of that + field or array item in the structure or array, recursively in + case of nested structures. + """ + try: + ctype = self._backend.typeof(cdata) + except TypeError: + if '__addressof__' in type(cdata).__dict__: + return type(cdata).__addressof__(cdata, *fields_or_indexes) + raise + if fields_or_indexes: + ctype, offset = self._typeoffsetof(ctype, *fields_or_indexes) + else: + if ctype.kind == "pointer": + raise TypeError("addressof(pointer)") + offset = 0 + ctypeptr = self._pointer_to(ctype) + return self._backend.rawaddressof(ctypeptr, cdata, offset) + + def _typeoffsetof(self, ctype, field_or_index, *fields_or_indexes): + ctype, offset = self._backend.typeoffsetof(ctype, field_or_index) + for field1 in fields_or_indexes: + ctype, offset1 = self._backend.typeoffsetof(ctype, field1, 1) + offset += offset1 + return ctype, offset + + def include(self, ffi_to_include): + """Includes the typedefs, structs, unions and enums defined + in another FFI instance. Usage is similar to a #include in C, + where a part of the program might include types defined in + another part for its own usage. Note that the include() + method has no effect on functions, constants and global + variables, which must anyway be accessed directly from the + lib object returned by the original FFI instance. + """ + if not isinstance(ffi_to_include, FFI): + raise TypeError("ffi.include() expects an argument that is also of" + " type cffi.FFI, not %r" % ( + type(ffi_to_include).__name__,)) + if ffi_to_include is self: + raise ValueError("self.include(self)") + with ffi_to_include._lock: + with self._lock: + self._parser.include(ffi_to_include._parser) + self._cdefsources.append('[') + self._cdefsources.extend(ffi_to_include._cdefsources) + self._cdefsources.append(']') + self._included_ffis.append(ffi_to_include) + + def new_handle(self, x): + return self._backend.newp_handle(self.BVoidP, x) + + def from_handle(self, x): + return self._backend.from_handle(x) + + def release(self, x): + self._backend.release(x) + + def set_unicode(self, enabled_flag): + """Windows: if 'enabled_flag' is True, enable the UNICODE and + _UNICODE defines in C, and declare the types like TCHAR and LPTCSTR + to be (pointers to) wchar_t. If 'enabled_flag' is False, + declare these types to be (pointers to) plain 8-bit characters. + This is mostly for backward compatibility; you usually want True. + """ + if self._windows_unicode is not None: + raise ValueError("set_unicode() can only be called once") + enabled_flag = bool(enabled_flag) + if enabled_flag: + self.cdef("typedef wchar_t TBYTE;" + "typedef wchar_t TCHAR;" + "typedef const wchar_t *LPCTSTR;" + "typedef const wchar_t *PCTSTR;" + "typedef wchar_t *LPTSTR;" + "typedef wchar_t *PTSTR;" + "typedef TBYTE *PTBYTE;" + "typedef TCHAR *PTCHAR;") + else: + self.cdef("typedef char TBYTE;" + "typedef char TCHAR;" + "typedef const char *LPCTSTR;" + "typedef const char *PCTSTR;" + "typedef char *LPTSTR;" + "typedef char *PTSTR;" + "typedef TBYTE *PTBYTE;" + "typedef TCHAR *PTCHAR;") + self._windows_unicode = enabled_flag + + def _apply_windows_unicode(self, kwds): + defmacros = kwds.get('define_macros', ()) + if not isinstance(defmacros, (list, tuple)): + raise TypeError("'define_macros' must be a list or tuple") + defmacros = list(defmacros) + [('UNICODE', '1'), + ('_UNICODE', '1')] + kwds['define_macros'] = defmacros + + def _apply_embedding_fix(self, kwds): + # must include an argument like "-lpython2.7" for the compiler + def ensure(key, value): + lst = kwds.setdefault(key, []) + if value not in lst: + lst.append(value) + # + if '__pypy__' in sys.builtin_module_names: + import os + if sys.platform == "win32": + # we need 'libpypy-c.lib'. Current distributions of + # pypy (>= 4.1) contain it as 'libs/python27.lib'. + pythonlib = "python{0[0]}{0[1]}".format(sys.version_info) + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'libs')) + else: + # we need 'libpypy-c.{so,dylib}', which should be by + # default located in 'sys.prefix/bin' for installed + # systems. + if sys.version_info < (3,): + pythonlib = "pypy-c" + else: + pythonlib = "pypy3-c" + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'bin')) + # On uninstalled pypy's, the libpypy-c is typically found in + # .../pypy/goal/. + if hasattr(sys, 'prefix'): + ensure('library_dirs', os.path.join(sys.prefix, 'pypy', 'goal')) + else: + if sys.platform == "win32": + template = "python%d%d" + if hasattr(sys, 'gettotalrefcount'): + template += '_d' + else: + try: + import sysconfig + except ImportError: # 2.6 + from distutils import sysconfig + template = "python%d.%d" + if sysconfig.get_config_var('DEBUG_EXT'): + template += sysconfig.get_config_var('DEBUG_EXT') + pythonlib = (template % + (sys.hexversion >> 24, (sys.hexversion >> 16) & 0xff)) + if hasattr(sys, 'abiflags'): + pythonlib += sys.abiflags + ensure('libraries', pythonlib) + if sys.platform == "win32": + ensure('extra_link_args', '/MANIFEST') + + def set_source(self, module_name, source, source_extension='.c', **kwds): + import os + if hasattr(self, '_assigned_source'): + raise ValueError("set_source() cannot be called several times " + "per ffi object") + if not isinstance(module_name, basestring): + raise TypeError("'module_name' must be a string") + if os.sep in module_name or (os.altsep and os.altsep in module_name): + raise ValueError("'module_name' must not contain '/': use a dotted " + "name to make a 'package.module' location") + self._assigned_source = (str(module_name), source, + source_extension, kwds) + + def set_source_pkgconfig(self, module_name, pkgconfig_libs, source, + source_extension='.c', **kwds): + from . import pkgconfig + if not isinstance(pkgconfig_libs, list): + raise TypeError("the pkgconfig_libs argument must be a list " + "of package names") + kwds2 = pkgconfig.flags_from_pkgconfig(pkgconfig_libs) + pkgconfig.merge_flags(kwds, kwds2) + self.set_source(module_name, source, source_extension, **kwds) + + def distutils_extension(self, tmpdir='build', verbose=True): + from distutils.dir_util import mkpath + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + if hasattr(self, 'verifier'): # fallback, 'tmpdir' ignored + return self.verifier.get_extension() + raise ValueError("set_source() must be called before" + " distutils_extension()") + module_name, source, source_extension, kwds = self._assigned_source + if source is None: + raise TypeError("distutils_extension() is only for C extension " + "modules, not for dlopen()-style pure Python " + "modules") + mkpath(tmpdir) + ext, updated = recompile(self, module_name, + source, tmpdir=tmpdir, extradir=tmpdir, + source_extension=source_extension, + call_c_compiler=False, **kwds) + if verbose: + if updated: + sys.stderr.write("regenerated: %r\n" % (ext.sources[0],)) + else: + sys.stderr.write("not modified: %r\n" % (ext.sources[0],)) + return ext + + def emit_c_code(self, filename): + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before emit_c_code()") + module_name, source, source_extension, kwds = self._assigned_source + if source is None: + raise TypeError("emit_c_code() is only for C extension modules, " + "not for dlopen()-style pure Python modules") + recompile(self, module_name, source, + c_file=filename, call_c_compiler=False, **kwds) + + def emit_python_code(self, filename): + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before emit_c_code()") + module_name, source, source_extension, kwds = self._assigned_source + if source is not None: + raise TypeError("emit_python_code() is only for dlopen()-style " + "pure Python modules, not for C extension modules") + recompile(self, module_name, source, + c_file=filename, call_c_compiler=False, **kwds) + + def compile(self, tmpdir='.', verbose=0, target=None, debug=None): + """The 'target' argument gives the final file name of the + compiled DLL. Use '*' to force distutils' choice, suitable for + regular CPython C API modules. Use a file name ending in '.*' + to ask for the system's default extension for dynamic libraries + (.so/.dll/.dylib). + + The default is '*' when building a non-embedded C API extension, + and (module_name + '.*') when building an embedded library. + """ + from .recompiler import recompile + # + if not hasattr(self, '_assigned_source'): + raise ValueError("set_source() must be called before compile()") + module_name, source, source_extension, kwds = self._assigned_source + return recompile(self, module_name, source, tmpdir=tmpdir, + target=target, source_extension=source_extension, + compiler_verbose=verbose, debug=debug, **kwds) + + def init_once(self, func, tag): + # Read _init_once_cache[tag], which is either (False, lock) if + # we're calling the function now in some thread, or (True, result). + # Don't call setdefault() in most cases, to avoid allocating and + # immediately freeing a lock; but still use setdefaut() to avoid + # races. + try: + x = self._init_once_cache[tag] + except KeyError: + x = self._init_once_cache.setdefault(tag, (False, allocate_lock())) + # Common case: we got (True, result), so we return the result. + if x[0]: + return x[1] + # Else, it's a lock. Acquire it to serialize the following tests. + with x[1]: + # Read again from _init_once_cache the current status. + x = self._init_once_cache[tag] + if x[0]: + return x[1] + # Call the function and store the result back. + result = func() + self._init_once_cache[tag] = (True, result) + return result + + def embedding_init_code(self, pysource): + if self._embedding: + raise ValueError("embedding_init_code() can only be called once") + # fix 'pysource' before it gets dumped into the C file: + # - remove empty lines at the beginning, so it starts at "line 1" + # - dedent, if all non-empty lines are indented + # - check for SyntaxErrors + import re + match = re.match(r'\s*\n', pysource) + if match: + pysource = pysource[match.end():] + lines = pysource.splitlines() or [''] + prefix = re.match(r'\s*', lines[0]).group() + for i in range(1, len(lines)): + line = lines[i] + if line.rstrip(): + while not line.startswith(prefix): + prefix = prefix[:-1] + i = len(prefix) + lines = [line[i:]+'\n' for line in lines] + pysource = ''.join(lines) + # + compile(pysource, "cffi_init", "exec") + # + self._embedding = pysource + + def def_extern(self, *args, **kwds): + raise ValueError("ffi.def_extern() is only available on API-mode FFI " + "objects") + + def list_types(self): + """Returns the user type names known to this FFI instance. + This returns a tuple containing three lists of names: + (typedef_names, names_of_structs, names_of_unions) + """ + typedefs = [] + structs = [] + unions = [] + for key in self._parser._declarations: + if key.startswith('typedef '): + typedefs.append(key[8:]) + elif key.startswith('struct '): + structs.append(key[7:]) + elif key.startswith('union '): + unions.append(key[6:]) + typedefs.sort() + structs.sort() + unions.sort() + return (typedefs, structs, unions) + + +def _load_backend_lib(backend, name, flags): + import os + if not isinstance(name, basestring): + if sys.platform != "win32" or name is not None: + return backend.load_library(name, flags) + name = "c" # Windows: load_library(None) fails, but this works + # on Python 2 (backward compatibility hack only) + first_error = None + if '.' in name or '/' in name or os.sep in name: + try: + return backend.load_library(name, flags) + except OSError as e: + first_error = e + import ctypes.util + path = ctypes.util.find_library(name) + if path is None: + if name == "c" and sys.platform == "win32" and sys.version_info >= (3,): + raise OSError("dlopen(None) cannot work on Windows for Python 3 " + "(see http://bugs.python.org/issue23606)") + msg = ("ctypes.util.find_library() did not manage " + "to locate a library called %r" % (name,)) + if first_error is not None: + msg = "%s. Additionally, %s" % (first_error, msg) + raise OSError(msg) + return backend.load_library(path, flags) + +def _make_ffi_library(ffi, libname, flags): + backend = ffi._backend + backendlib = _load_backend_lib(backend, libname, flags) + # + def accessor_function(name): + key = 'function ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + value = backendlib.load_function(BType, name) + library.__dict__[name] = value + # + def accessor_variable(name): + key = 'variable ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + read_variable = backendlib.read_variable + write_variable = backendlib.write_variable + setattr(FFILibrary, name, property( + lambda self: read_variable(BType, name), + lambda self, value: write_variable(BType, name, value))) + # + def addressof_var(name): + try: + return addr_variables[name] + except KeyError: + with ffi._lock: + if name not in addr_variables: + key = 'variable ' + name + tp, _ = ffi._parser._declarations[key] + BType = ffi._get_cached_btype(tp) + if BType.kind != 'array': + BType = model.pointer_cache(ffi, BType) + p = backendlib.load_function(BType, name) + addr_variables[name] = p + return addr_variables[name] + # + def accessor_constant(name): + raise NotImplementedError("non-integer constant '%s' cannot be " + "accessed from a dlopen() library" % (name,)) + # + def accessor_int_constant(name): + library.__dict__[name] = ffi._parser._int_constants[name] + # + accessors = {} + accessors_version = [False] + addr_variables = {} + # + def update_accessors(): + if accessors_version[0] is ffi._cdef_version: + return + # + for key, (tp, _) in ffi._parser._declarations.items(): + if not isinstance(tp, model.EnumType): + tag, name = key.split(' ', 1) + if tag == 'function': + accessors[name] = accessor_function + elif tag == 'variable': + accessors[name] = accessor_variable + elif tag == 'constant': + accessors[name] = accessor_constant + else: + for i, enumname in enumerate(tp.enumerators): + def accessor_enum(name, tp=tp, i=i): + tp.check_not_partial() + library.__dict__[name] = tp.enumvalues[i] + accessors[enumname] = accessor_enum + for name in ffi._parser._int_constants: + accessors.setdefault(name, accessor_int_constant) + accessors_version[0] = ffi._cdef_version + # + def make_accessor(name): + with ffi._lock: + if name in library.__dict__ or name in FFILibrary.__dict__: + return # added by another thread while waiting for the lock + if name not in accessors: + update_accessors() + if name not in accessors: + raise AttributeError(name) + accessors[name](name) + # + class FFILibrary(object): + def __getattr__(self, name): + make_accessor(name) + return getattr(self, name) + def __setattr__(self, name, value): + try: + property = getattr(self.__class__, name) + except AttributeError: + make_accessor(name) + setattr(self, name, value) + else: + property.__set__(self, value) + def __dir__(self): + with ffi._lock: + update_accessors() + return accessors.keys() + def __addressof__(self, name): + if name in library.__dict__: + return library.__dict__[name] + if name in FFILibrary.__dict__: + return addressof_var(name) + make_accessor(name) + if name in library.__dict__: + return library.__dict__[name] + if name in FFILibrary.__dict__: + return addressof_var(name) + raise AttributeError("cffi library has no function or " + "global variable named '%s'" % (name,)) + def __cffi_close__(self): + backendlib.close_lib() + self.__dict__.clear() + # + if isinstance(libname, basestring): + try: + if not isinstance(libname, str): # unicode, on Python 2 + libname = libname.encode('utf-8') + FFILibrary.__name__ = 'FFILibrary_%s' % libname + except UnicodeError: + pass + library = FFILibrary() + return library, library.__dict__ + +def _builtin_function_type(func): + # a hack to make at least ffi.typeof(builtin_function) work, + # if the builtin function was obtained by 'vengine_cpy'. + import sys + try: + module = sys.modules[func.__module__] + ffi = module._cffi_original_ffi + types_of_builtin_funcs = module._cffi_types_of_builtin_funcs + tp = types_of_builtin_funcs[func] + except (KeyError, AttributeError, TypeError): + return None + else: + with ffi._lock: + return ffi._get_cached_btype(tp) diff --git a/lib/python3.10/site-packages/cffi/backend_ctypes.py b/lib/python3.10/site-packages/cffi/backend_ctypes.py new file mode 100644 index 0000000000000000000000000000000000000000..e7956a79cfb1c3d28a2ad22a40b261ae7dbbbb5f --- /dev/null +++ b/lib/python3.10/site-packages/cffi/backend_ctypes.py @@ -0,0 +1,1121 @@ +import ctypes, ctypes.util, operator, sys +from . import model + +if sys.version_info < (3,): + bytechr = chr +else: + unicode = str + long = int + xrange = range + bytechr = lambda num: bytes([num]) + +class CTypesType(type): + pass + +class CTypesData(object): + __metaclass__ = CTypesType + __slots__ = ['__weakref__'] + __name__ = '' + + def __init__(self, *args): + raise TypeError("cannot instantiate %r" % (self.__class__,)) + + @classmethod + def _newp(cls, init): + raise TypeError("expected a pointer or array ctype, got '%s'" + % (cls._get_c_name(),)) + + @staticmethod + def _to_ctypes(value): + raise TypeError + + @classmethod + def _arg_to_ctypes(cls, *value): + try: + ctype = cls._ctype + except AttributeError: + raise TypeError("cannot create an instance of %r" % (cls,)) + if value: + res = cls._to_ctypes(*value) + if not isinstance(res, ctype): + res = cls._ctype(res) + else: + res = cls._ctype() + return res + + @classmethod + def _create_ctype_obj(cls, init): + if init is None: + return cls._arg_to_ctypes() + else: + return cls._arg_to_ctypes(init) + + @staticmethod + def _from_ctypes(ctypes_value): + raise TypeError + + @classmethod + def _get_c_name(cls, replace_with=''): + return cls._reftypename.replace(' &', replace_with) + + @classmethod + def _fix_class(cls): + cls.__name__ = 'CData<%s>' % (cls._get_c_name(),) + cls.__qualname__ = 'CData<%s>' % (cls._get_c_name(),) + cls.__module__ = 'ffi' + + def _get_own_repr(self): + raise NotImplementedError + + def _addr_repr(self, address): + if address == 0: + return 'NULL' + else: + if address < 0: + address += 1 << (8*ctypes.sizeof(ctypes.c_void_p)) + return '0x%x' % address + + def __repr__(self, c_name=None): + own = self._get_own_repr() + return '' % (c_name or self._get_c_name(), own) + + def _convert_to_address(self, BClass): + if BClass is None: + raise TypeError("cannot convert %r to an address" % ( + self._get_c_name(),)) + else: + raise TypeError("cannot convert %r to %r" % ( + self._get_c_name(), BClass._get_c_name())) + + @classmethod + def _get_size(cls): + return ctypes.sizeof(cls._ctype) + + def _get_size_of_instance(self): + return ctypes.sizeof(self._ctype) + + @classmethod + def _cast_from(cls, source): + raise TypeError("cannot cast to %r" % (cls._get_c_name(),)) + + def _cast_to_integer(self): + return self._convert_to_address(None) + + @classmethod + def _alignment(cls): + return ctypes.alignment(cls._ctype) + + def __iter__(self): + raise TypeError("cdata %r does not support iteration" % ( + self._get_c_name()),) + + def _make_cmp(name): + cmpfunc = getattr(operator, name) + def cmp(self, other): + v_is_ptr = not isinstance(self, CTypesGenericPrimitive) + w_is_ptr = (isinstance(other, CTypesData) and + not isinstance(other, CTypesGenericPrimitive)) + if v_is_ptr and w_is_ptr: + return cmpfunc(self._convert_to_address(None), + other._convert_to_address(None)) + elif v_is_ptr or w_is_ptr: + return NotImplemented + else: + if isinstance(self, CTypesGenericPrimitive): + self = self._value + if isinstance(other, CTypesGenericPrimitive): + other = other._value + return cmpfunc(self, other) + cmp.func_name = name + return cmp + + __eq__ = _make_cmp('__eq__') + __ne__ = _make_cmp('__ne__') + __lt__ = _make_cmp('__lt__') + __le__ = _make_cmp('__le__') + __gt__ = _make_cmp('__gt__') + __ge__ = _make_cmp('__ge__') + + def __hash__(self): + return hash(self._convert_to_address(None)) + + def _to_string(self, maxlen): + raise TypeError("string(): %r" % (self,)) + + +class CTypesGenericPrimitive(CTypesData): + __slots__ = [] + + def __hash__(self): + return hash(self._value) + + def _get_own_repr(self): + return repr(self._from_ctypes(self._value)) + + +class CTypesGenericArray(CTypesData): + __slots__ = [] + + @classmethod + def _newp(cls, init): + return cls(init) + + def __iter__(self): + for i in xrange(len(self)): + yield self[i] + + def _get_own_repr(self): + return self._addr_repr(ctypes.addressof(self._blob)) + + +class CTypesGenericPtr(CTypesData): + __slots__ = ['_address', '_as_ctype_ptr'] + _automatic_casts = False + kind = "pointer" + + @classmethod + def _newp(cls, init): + return cls(init) + + @classmethod + def _cast_from(cls, source): + if source is None: + address = 0 + elif isinstance(source, CTypesData): + address = source._cast_to_integer() + elif isinstance(source, (int, long)): + address = source + else: + raise TypeError("bad type for cast to %r: %r" % + (cls, type(source).__name__)) + return cls._new_pointer_at(address) + + @classmethod + def _new_pointer_at(cls, address): + self = cls.__new__(cls) + self._address = address + self._as_ctype_ptr = ctypes.cast(address, cls._ctype) + return self + + def _get_own_repr(self): + try: + return self._addr_repr(self._address) + except AttributeError: + return '???' + + def _cast_to_integer(self): + return self._address + + def __nonzero__(self): + return bool(self._address) + __bool__ = __nonzero__ + + @classmethod + def _to_ctypes(cls, value): + if not isinstance(value, CTypesData): + raise TypeError("unexpected %s object" % type(value).__name__) + address = value._convert_to_address(cls) + return ctypes.cast(address, cls._ctype) + + @classmethod + def _from_ctypes(cls, ctypes_ptr): + address = ctypes.cast(ctypes_ptr, ctypes.c_void_p).value or 0 + return cls._new_pointer_at(address) + + @classmethod + def _initialize(cls, ctypes_ptr, value): + if value: + ctypes_ptr.contents = cls._to_ctypes(value).contents + + def _convert_to_address(self, BClass): + if (BClass in (self.__class__, None) or BClass._automatic_casts + or self._automatic_casts): + return self._address + else: + return CTypesData._convert_to_address(self, BClass) + + +class CTypesBaseStructOrUnion(CTypesData): + __slots__ = ['_blob'] + + @classmethod + def _create_ctype_obj(cls, init): + # may be overridden + raise TypeError("cannot instantiate opaque type %s" % (cls,)) + + def _get_own_repr(self): + return self._addr_repr(ctypes.addressof(self._blob)) + + @classmethod + def _offsetof(cls, fieldname): + return getattr(cls._ctype, fieldname).offset + + def _convert_to_address(self, BClass): + if getattr(BClass, '_BItem', None) is self.__class__: + return ctypes.addressof(self._blob) + else: + return CTypesData._convert_to_address(self, BClass) + + @classmethod + def _from_ctypes(cls, ctypes_struct_or_union): + self = cls.__new__(cls) + self._blob = ctypes_struct_or_union + return self + + @classmethod + def _to_ctypes(cls, value): + return value._blob + + def __repr__(self, c_name=None): + return CTypesData.__repr__(self, c_name or self._get_c_name(' &')) + + +class CTypesBackend(object): + + PRIMITIVE_TYPES = { + 'char': ctypes.c_char, + 'short': ctypes.c_short, + 'int': ctypes.c_int, + 'long': ctypes.c_long, + 'long long': ctypes.c_longlong, + 'signed char': ctypes.c_byte, + 'unsigned char': ctypes.c_ubyte, + 'unsigned short': ctypes.c_ushort, + 'unsigned int': ctypes.c_uint, + 'unsigned long': ctypes.c_ulong, + 'unsigned long long': ctypes.c_ulonglong, + 'float': ctypes.c_float, + 'double': ctypes.c_double, + '_Bool': ctypes.c_bool, + } + + for _name in ['unsigned long long', 'unsigned long', + 'unsigned int', 'unsigned short', 'unsigned char']: + _size = ctypes.sizeof(PRIMITIVE_TYPES[_name]) + PRIMITIVE_TYPES['uint%d_t' % (8*_size)] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_void_p): + PRIMITIVE_TYPES['uintptr_t'] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_size_t): + PRIMITIVE_TYPES['size_t'] = PRIMITIVE_TYPES[_name] + + for _name in ['long long', 'long', 'int', 'short', 'signed char']: + _size = ctypes.sizeof(PRIMITIVE_TYPES[_name]) + PRIMITIVE_TYPES['int%d_t' % (8*_size)] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_void_p): + PRIMITIVE_TYPES['intptr_t'] = PRIMITIVE_TYPES[_name] + PRIMITIVE_TYPES['ptrdiff_t'] = PRIMITIVE_TYPES[_name] + if _size == ctypes.sizeof(ctypes.c_size_t): + PRIMITIVE_TYPES['ssize_t'] = PRIMITIVE_TYPES[_name] + + + def __init__(self): + self.RTLD_LAZY = 0 # not supported anyway by ctypes + self.RTLD_NOW = 0 + self.RTLD_GLOBAL = ctypes.RTLD_GLOBAL + self.RTLD_LOCAL = ctypes.RTLD_LOCAL + + def set_ffi(self, ffi): + self.ffi = ffi + + def _get_types(self): + return CTypesData, CTypesType + + def load_library(self, path, flags=0): + cdll = ctypes.CDLL(path, flags) + return CTypesLibrary(self, cdll) + + def new_void_type(self): + class CTypesVoid(CTypesData): + __slots__ = [] + _reftypename = 'void &' + @staticmethod + def _from_ctypes(novalue): + return None + @staticmethod + def _to_ctypes(novalue): + if novalue is not None: + raise TypeError("None expected, got %s object" % + (type(novalue).__name__,)) + return None + CTypesVoid._fix_class() + return CTypesVoid + + def new_primitive_type(self, name): + if name == 'wchar_t': + raise NotImplementedError(name) + ctype = self.PRIMITIVE_TYPES[name] + if name == 'char': + kind = 'char' + elif name in ('float', 'double'): + kind = 'float' + else: + if name in ('signed char', 'unsigned char'): + kind = 'byte' + elif name == '_Bool': + kind = 'bool' + else: + kind = 'int' + is_signed = (ctype(-1).value == -1) + # + def _cast_source_to_int(source): + if isinstance(source, (int, long, float)): + source = int(source) + elif isinstance(source, CTypesData): + source = source._cast_to_integer() + elif isinstance(source, bytes): + source = ord(source) + elif source is None: + source = 0 + else: + raise TypeError("bad type for cast to %r: %r" % + (CTypesPrimitive, type(source).__name__)) + return source + # + kind1 = kind + class CTypesPrimitive(CTypesGenericPrimitive): + __slots__ = ['_value'] + _ctype = ctype + _reftypename = '%s &' % name + kind = kind1 + + def __init__(self, value): + self._value = value + + @staticmethod + def _create_ctype_obj(init): + if init is None: + return ctype() + return ctype(CTypesPrimitive._to_ctypes(init)) + + if kind == 'int' or kind == 'byte': + @classmethod + def _cast_from(cls, source): + source = _cast_source_to_int(source) + source = ctype(source).value # cast within range + return cls(source) + def __int__(self): + return self._value + + if kind == 'bool': + @classmethod + def _cast_from(cls, source): + if not isinstance(source, (int, long, float)): + source = _cast_source_to_int(source) + return cls(bool(source)) + def __int__(self): + return int(self._value) + + if kind == 'char': + @classmethod + def _cast_from(cls, source): + source = _cast_source_to_int(source) + source = bytechr(source & 0xFF) + return cls(source) + def __int__(self): + return ord(self._value) + + if kind == 'float': + @classmethod + def _cast_from(cls, source): + if isinstance(source, float): + pass + elif isinstance(source, CTypesGenericPrimitive): + if hasattr(source, '__float__'): + source = float(source) + else: + source = int(source) + else: + source = _cast_source_to_int(source) + source = ctype(source).value # fix precision + return cls(source) + def __int__(self): + return int(self._value) + def __float__(self): + return self._value + + _cast_to_integer = __int__ + + if kind == 'int' or kind == 'byte' or kind == 'bool': + @staticmethod + def _to_ctypes(x): + if not isinstance(x, (int, long)): + if isinstance(x, CTypesData): + x = int(x) + else: + raise TypeError("integer expected, got %s" % + type(x).__name__) + if ctype(x).value != x: + if not is_signed and x < 0: + raise OverflowError("%s: negative integer" % name) + else: + raise OverflowError("%s: integer out of bounds" + % name) + return x + + if kind == 'char': + @staticmethod + def _to_ctypes(x): + if isinstance(x, bytes) and len(x) == 1: + return x + if isinstance(x, CTypesPrimitive): # > + return x._value + raise TypeError("character expected, got %s" % + type(x).__name__) + def __nonzero__(self): + return ord(self._value) != 0 + else: + def __nonzero__(self): + return self._value != 0 + __bool__ = __nonzero__ + + if kind == 'float': + @staticmethod + def _to_ctypes(x): + if not isinstance(x, (int, long, float, CTypesData)): + raise TypeError("float expected, got %s" % + type(x).__name__) + return ctype(x).value + + @staticmethod + def _from_ctypes(value): + return getattr(value, 'value', value) + + @staticmethod + def _initialize(blob, init): + blob.value = CTypesPrimitive._to_ctypes(init) + + if kind == 'char': + def _to_string(self, maxlen): + return self._value + if kind == 'byte': + def _to_string(self, maxlen): + return chr(self._value & 0xff) + # + CTypesPrimitive._fix_class() + return CTypesPrimitive + + def new_pointer_type(self, BItem): + getbtype = self.ffi._get_cached_btype + if BItem is getbtype(model.PrimitiveType('char')): + kind = 'charp' + elif BItem in (getbtype(model.PrimitiveType('signed char')), + getbtype(model.PrimitiveType('unsigned char'))): + kind = 'bytep' + elif BItem is getbtype(model.void_type): + kind = 'voidp' + else: + kind = 'generic' + # + class CTypesPtr(CTypesGenericPtr): + __slots__ = ['_own'] + if kind == 'charp': + __slots__ += ['__as_strbuf'] + _BItem = BItem + if hasattr(BItem, '_ctype'): + _ctype = ctypes.POINTER(BItem._ctype) + _bitem_size = ctypes.sizeof(BItem._ctype) + else: + _ctype = ctypes.c_void_p + if issubclass(BItem, CTypesGenericArray): + _reftypename = BItem._get_c_name('(* &)') + else: + _reftypename = BItem._get_c_name(' * &') + + def __init__(self, init): + ctypeobj = BItem._create_ctype_obj(init) + if kind == 'charp': + self.__as_strbuf = ctypes.create_string_buffer( + ctypeobj.value + b'\x00') + self._as_ctype_ptr = ctypes.cast( + self.__as_strbuf, self._ctype) + else: + self._as_ctype_ptr = ctypes.pointer(ctypeobj) + self._address = ctypes.cast(self._as_ctype_ptr, + ctypes.c_void_p).value + self._own = True + + def __add__(self, other): + if isinstance(other, (int, long)): + return self._new_pointer_at(self._address + + other * self._bitem_size) + else: + return NotImplemented + + def __sub__(self, other): + if isinstance(other, (int, long)): + return self._new_pointer_at(self._address - + other * self._bitem_size) + elif type(self) is type(other): + return (self._address - other._address) // self._bitem_size + else: + return NotImplemented + + def __getitem__(self, index): + if getattr(self, '_own', False) and index != 0: + raise IndexError + return BItem._from_ctypes(self._as_ctype_ptr[index]) + + def __setitem__(self, index, value): + self._as_ctype_ptr[index] = BItem._to_ctypes(value) + + if kind == 'charp' or kind == 'voidp': + @classmethod + def _arg_to_ctypes(cls, *value): + if value and isinstance(value[0], bytes): + return ctypes.c_char_p(value[0]) + else: + return super(CTypesPtr, cls)._arg_to_ctypes(*value) + + if kind == 'charp' or kind == 'bytep': + def _to_string(self, maxlen): + if maxlen < 0: + maxlen = sys.maxsize + p = ctypes.cast(self._as_ctype_ptr, + ctypes.POINTER(ctypes.c_char)) + n = 0 + while n < maxlen and p[n] != b'\x00': + n += 1 + return b''.join([p[i] for i in range(n)]) + + def _get_own_repr(self): + if getattr(self, '_own', False): + return 'owning %d bytes' % ( + ctypes.sizeof(self._as_ctype_ptr.contents),) + return super(CTypesPtr, self)._get_own_repr() + # + if (BItem is self.ffi._get_cached_btype(model.void_type) or + BItem is self.ffi._get_cached_btype(model.PrimitiveType('char'))): + CTypesPtr._automatic_casts = True + # + CTypesPtr._fix_class() + return CTypesPtr + + def new_array_type(self, CTypesPtr, length): + if length is None: + brackets = ' &[]' + else: + brackets = ' &[%d]' % length + BItem = CTypesPtr._BItem + getbtype = self.ffi._get_cached_btype + if BItem is getbtype(model.PrimitiveType('char')): + kind = 'char' + elif BItem in (getbtype(model.PrimitiveType('signed char')), + getbtype(model.PrimitiveType('unsigned char'))): + kind = 'byte' + else: + kind = 'generic' + # + class CTypesArray(CTypesGenericArray): + __slots__ = ['_blob', '_own'] + if length is not None: + _ctype = BItem._ctype * length + else: + __slots__.append('_ctype') + _reftypename = BItem._get_c_name(brackets) + _declared_length = length + _CTPtr = CTypesPtr + + def __init__(self, init): + if length is None: + if isinstance(init, (int, long)): + len1 = init + init = None + elif kind == 'char' and isinstance(init, bytes): + len1 = len(init) + 1 # extra null + else: + init = tuple(init) + len1 = len(init) + self._ctype = BItem._ctype * len1 + self._blob = self._ctype() + self._own = True + if init is not None: + self._initialize(self._blob, init) + + @staticmethod + def _initialize(blob, init): + if isinstance(init, bytes): + init = [init[i:i+1] for i in range(len(init))] + else: + if isinstance(init, CTypesGenericArray): + if (len(init) != len(blob) or + not isinstance(init, CTypesArray)): + raise TypeError("length/type mismatch: %s" % (init,)) + init = tuple(init) + if len(init) > len(blob): + raise IndexError("too many initializers") + addr = ctypes.cast(blob, ctypes.c_void_p).value + PTR = ctypes.POINTER(BItem._ctype) + itemsize = ctypes.sizeof(BItem._ctype) + for i, value in enumerate(init): + p = ctypes.cast(addr + i * itemsize, PTR) + BItem._initialize(p.contents, value) + + def __len__(self): + return len(self._blob) + + def __getitem__(self, index): + if not (0 <= index < len(self._blob)): + raise IndexError + return BItem._from_ctypes(self._blob[index]) + + def __setitem__(self, index, value): + if not (0 <= index < len(self._blob)): + raise IndexError + self._blob[index] = BItem._to_ctypes(value) + + if kind == 'char' or kind == 'byte': + def _to_string(self, maxlen): + if maxlen < 0: + maxlen = len(self._blob) + p = ctypes.cast(self._blob, + ctypes.POINTER(ctypes.c_char)) + n = 0 + while n < maxlen and p[n] != b'\x00': + n += 1 + return b''.join([p[i] for i in range(n)]) + + def _get_own_repr(self): + if getattr(self, '_own', False): + return 'owning %d bytes' % (ctypes.sizeof(self._blob),) + return super(CTypesArray, self)._get_own_repr() + + def _convert_to_address(self, BClass): + if BClass in (CTypesPtr, None) or BClass._automatic_casts: + return ctypes.addressof(self._blob) + else: + return CTypesData._convert_to_address(self, BClass) + + @staticmethod + def _from_ctypes(ctypes_array): + self = CTypesArray.__new__(CTypesArray) + self._blob = ctypes_array + return self + + @staticmethod + def _arg_to_ctypes(value): + return CTypesPtr._arg_to_ctypes(value) + + def __add__(self, other): + if isinstance(other, (int, long)): + return CTypesPtr._new_pointer_at( + ctypes.addressof(self._blob) + + other * ctypes.sizeof(BItem._ctype)) + else: + return NotImplemented + + @classmethod + def _cast_from(cls, source): + raise NotImplementedError("casting to %r" % ( + cls._get_c_name(),)) + # + CTypesArray._fix_class() + return CTypesArray + + def _new_struct_or_union(self, kind, name, base_ctypes_class): + # + class struct_or_union(base_ctypes_class): + pass + struct_or_union.__name__ = '%s_%s' % (kind, name) + kind1 = kind + # + class CTypesStructOrUnion(CTypesBaseStructOrUnion): + __slots__ = ['_blob'] + _ctype = struct_or_union + _reftypename = '%s &' % (name,) + _kind = kind = kind1 + # + CTypesStructOrUnion._fix_class() + return CTypesStructOrUnion + + def new_struct_type(self, name): + return self._new_struct_or_union('struct', name, ctypes.Structure) + + def new_union_type(self, name): + return self._new_struct_or_union('union', name, ctypes.Union) + + def complete_struct_or_union(self, CTypesStructOrUnion, fields, tp, + totalsize=-1, totalalignment=-1, sflags=0, + pack=0): + if totalsize >= 0 or totalalignment >= 0: + raise NotImplementedError("the ctypes backend of CFFI does not support " + "structures completed by verify(); please " + "compile and install the _cffi_backend module.") + struct_or_union = CTypesStructOrUnion._ctype + fnames = [fname for (fname, BField, bitsize) in fields] + btypes = [BField for (fname, BField, bitsize) in fields] + bitfields = [bitsize for (fname, BField, bitsize) in fields] + # + bfield_types = {} + cfields = [] + for (fname, BField, bitsize) in fields: + if bitsize < 0: + cfields.append((fname, BField._ctype)) + bfield_types[fname] = BField + else: + cfields.append((fname, BField._ctype, bitsize)) + bfield_types[fname] = Ellipsis + if sflags & 8: + struct_or_union._pack_ = 1 + elif pack: + struct_or_union._pack_ = pack + struct_or_union._fields_ = cfields + CTypesStructOrUnion._bfield_types = bfield_types + # + @staticmethod + def _create_ctype_obj(init): + result = struct_or_union() + if init is not None: + initialize(result, init) + return result + CTypesStructOrUnion._create_ctype_obj = _create_ctype_obj + # + def initialize(blob, init): + if is_union: + if len(init) > 1: + raise ValueError("union initializer: %d items given, but " + "only one supported (use a dict if needed)" + % (len(init),)) + if not isinstance(init, dict): + if isinstance(init, (bytes, unicode)): + raise TypeError("union initializer: got a str") + init = tuple(init) + if len(init) > len(fnames): + raise ValueError("too many values for %s initializer" % + CTypesStructOrUnion._get_c_name()) + init = dict(zip(fnames, init)) + addr = ctypes.addressof(blob) + for fname, value in init.items(): + BField, bitsize = name2fieldtype[fname] + assert bitsize < 0, \ + "not implemented: initializer with bit fields" + offset = CTypesStructOrUnion._offsetof(fname) + PTR = ctypes.POINTER(BField._ctype) + p = ctypes.cast(addr + offset, PTR) + BField._initialize(p.contents, value) + is_union = CTypesStructOrUnion._kind == 'union' + name2fieldtype = dict(zip(fnames, zip(btypes, bitfields))) + # + for fname, BField, bitsize in fields: + if fname == '': + raise NotImplementedError("nested anonymous structs/unions") + if hasattr(CTypesStructOrUnion, fname): + raise ValueError("the field name %r conflicts in " + "the ctypes backend" % fname) + if bitsize < 0: + def getter(self, fname=fname, BField=BField, + offset=CTypesStructOrUnion._offsetof(fname), + PTR=ctypes.POINTER(BField._ctype)): + addr = ctypes.addressof(self._blob) + p = ctypes.cast(addr + offset, PTR) + return BField._from_ctypes(p.contents) + def setter(self, value, fname=fname, BField=BField): + setattr(self._blob, fname, BField._to_ctypes(value)) + # + if issubclass(BField, CTypesGenericArray): + setter = None + if BField._declared_length == 0: + def getter(self, fname=fname, BFieldPtr=BField._CTPtr, + offset=CTypesStructOrUnion._offsetof(fname), + PTR=ctypes.POINTER(BField._ctype)): + addr = ctypes.addressof(self._blob) + p = ctypes.cast(addr + offset, PTR) + return BFieldPtr._from_ctypes(p) + # + else: + def getter(self, fname=fname, BField=BField): + return BField._from_ctypes(getattr(self._blob, fname)) + def setter(self, value, fname=fname, BField=BField): + # xxx obscure workaround + value = BField._to_ctypes(value) + oldvalue = getattr(self._blob, fname) + setattr(self._blob, fname, value) + if value != getattr(self._blob, fname): + setattr(self._blob, fname, oldvalue) + raise OverflowError("value too large for bitfield") + setattr(CTypesStructOrUnion, fname, property(getter, setter)) + # + CTypesPtr = self.ffi._get_cached_btype(model.PointerType(tp)) + for fname in fnames: + if hasattr(CTypesPtr, fname): + raise ValueError("the field name %r conflicts in " + "the ctypes backend" % fname) + def getter(self, fname=fname): + return getattr(self[0], fname) + def setter(self, value, fname=fname): + setattr(self[0], fname, value) + setattr(CTypesPtr, fname, property(getter, setter)) + + def new_function_type(self, BArgs, BResult, has_varargs): + nameargs = [BArg._get_c_name() for BArg in BArgs] + if has_varargs: + nameargs.append('...') + nameargs = ', '.join(nameargs) + # + class CTypesFunctionPtr(CTypesGenericPtr): + __slots__ = ['_own_callback', '_name'] + _ctype = ctypes.CFUNCTYPE(getattr(BResult, '_ctype', None), + *[BArg._ctype for BArg in BArgs], + use_errno=True) + _reftypename = BResult._get_c_name('(* &)(%s)' % (nameargs,)) + + def __init__(self, init, error=None): + # create a callback to the Python callable init() + import traceback + assert not has_varargs, "varargs not supported for callbacks" + if getattr(BResult, '_ctype', None) is not None: + error = BResult._from_ctypes( + BResult._create_ctype_obj(error)) + else: + error = None + def callback(*args): + args2 = [] + for arg, BArg in zip(args, BArgs): + args2.append(BArg._from_ctypes(arg)) + try: + res2 = init(*args2) + res2 = BResult._to_ctypes(res2) + except: + traceback.print_exc() + res2 = error + if issubclass(BResult, CTypesGenericPtr): + if res2: + res2 = ctypes.cast(res2, ctypes.c_void_p).value + # .value: http://bugs.python.org/issue1574593 + else: + res2 = None + #print repr(res2) + return res2 + if issubclass(BResult, CTypesGenericPtr): + # The only pointers callbacks can return are void*s: + # http://bugs.python.org/issue5710 + callback_ctype = ctypes.CFUNCTYPE( + ctypes.c_void_p, + *[BArg._ctype for BArg in BArgs], + use_errno=True) + else: + callback_ctype = CTypesFunctionPtr._ctype + self._as_ctype_ptr = callback_ctype(callback) + self._address = ctypes.cast(self._as_ctype_ptr, + ctypes.c_void_p).value + self._own_callback = init + + @staticmethod + def _initialize(ctypes_ptr, value): + if value: + raise NotImplementedError("ctypes backend: not supported: " + "initializers for function pointers") + + def __repr__(self): + c_name = getattr(self, '_name', None) + if c_name: + i = self._reftypename.index('(* &)') + if self._reftypename[i-1] not in ' )*': + c_name = ' ' + c_name + c_name = self._reftypename.replace('(* &)', c_name) + return CTypesData.__repr__(self, c_name) + + def _get_own_repr(self): + if getattr(self, '_own_callback', None) is not None: + return 'calling %r' % (self._own_callback,) + return super(CTypesFunctionPtr, self)._get_own_repr() + + def __call__(self, *args): + if has_varargs: + assert len(args) >= len(BArgs) + extraargs = args[len(BArgs):] + args = args[:len(BArgs)] + else: + assert len(args) == len(BArgs) + ctypes_args = [] + for arg, BArg in zip(args, BArgs): + ctypes_args.append(BArg._arg_to_ctypes(arg)) + if has_varargs: + for i, arg in enumerate(extraargs): + if arg is None: + ctypes_args.append(ctypes.c_void_p(0)) # NULL + continue + if not isinstance(arg, CTypesData): + raise TypeError( + "argument %d passed in the variadic part " + "needs to be a cdata object (got %s)" % + (1 + len(BArgs) + i, type(arg).__name__)) + ctypes_args.append(arg._arg_to_ctypes(arg)) + result = self._as_ctype_ptr(*ctypes_args) + return BResult._from_ctypes(result) + # + CTypesFunctionPtr._fix_class() + return CTypesFunctionPtr + + def new_enum_type(self, name, enumerators, enumvalues, CTypesInt): + assert isinstance(name, str) + reverse_mapping = dict(zip(reversed(enumvalues), + reversed(enumerators))) + # + class CTypesEnum(CTypesInt): + __slots__ = [] + _reftypename = '%s &' % name + + def _get_own_repr(self): + value = self._value + try: + return '%d: %s' % (value, reverse_mapping[value]) + except KeyError: + return str(value) + + def _to_string(self, maxlen): + value = self._value + try: + return reverse_mapping[value] + except KeyError: + return str(value) + # + CTypesEnum._fix_class() + return CTypesEnum + + def get_errno(self): + return ctypes.get_errno() + + def set_errno(self, value): + ctypes.set_errno(value) + + def string(self, b, maxlen=-1): + return b._to_string(maxlen) + + def buffer(self, bptr, size=-1): + raise NotImplementedError("buffer() with ctypes backend") + + def sizeof(self, cdata_or_BType): + if isinstance(cdata_or_BType, CTypesData): + return cdata_or_BType._get_size_of_instance() + else: + assert issubclass(cdata_or_BType, CTypesData) + return cdata_or_BType._get_size() + + def alignof(self, BType): + assert issubclass(BType, CTypesData) + return BType._alignment() + + def newp(self, BType, source): + if not issubclass(BType, CTypesData): + raise TypeError + return BType._newp(source) + + def cast(self, BType, source): + return BType._cast_from(source) + + def callback(self, BType, source, error, onerror): + assert onerror is None # XXX not implemented + return BType(source, error) + + _weakref_cache_ref = None + + def gcp(self, cdata, destructor, size=0): + if self._weakref_cache_ref is None: + import weakref + class MyRef(weakref.ref): + def __eq__(self, other): + myref = self() + return self is other or ( + myref is not None and myref is other()) + def __ne__(self, other): + return not (self == other) + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(self()) + return self._hash + self._weakref_cache_ref = {}, MyRef + weak_cache, MyRef = self._weakref_cache_ref + + if destructor is None: + try: + del weak_cache[MyRef(cdata)] + except KeyError: + raise TypeError("Can remove destructor only on a object " + "previously returned by ffi.gc()") + return None + + def remove(k): + cdata, destructor = weak_cache.pop(k, (None, None)) + if destructor is not None: + destructor(cdata) + + new_cdata = self.cast(self.typeof(cdata), cdata) + assert new_cdata is not cdata + weak_cache[MyRef(new_cdata, remove)] = (cdata, destructor) + return new_cdata + + typeof = type + + def getcname(self, BType, replace_with): + return BType._get_c_name(replace_with) + + def typeoffsetof(self, BType, fieldname, num=0): + if isinstance(fieldname, str): + if num == 0 and issubclass(BType, CTypesGenericPtr): + BType = BType._BItem + if not issubclass(BType, CTypesBaseStructOrUnion): + raise TypeError("expected a struct or union ctype") + BField = BType._bfield_types[fieldname] + if BField is Ellipsis: + raise TypeError("not supported for bitfields") + return (BField, BType._offsetof(fieldname)) + elif isinstance(fieldname, (int, long)): + if issubclass(BType, CTypesGenericArray): + BType = BType._CTPtr + if not issubclass(BType, CTypesGenericPtr): + raise TypeError("expected an array or ptr ctype") + BItem = BType._BItem + offset = BItem._get_size() * fieldname + if offset > sys.maxsize: + raise OverflowError + return (BItem, offset) + else: + raise TypeError(type(fieldname)) + + def rawaddressof(self, BTypePtr, cdata, offset=None): + if isinstance(cdata, CTypesBaseStructOrUnion): + ptr = ctypes.pointer(type(cdata)._to_ctypes(cdata)) + elif isinstance(cdata, CTypesGenericPtr): + if offset is None or not issubclass(type(cdata)._BItem, + CTypesBaseStructOrUnion): + raise TypeError("unexpected cdata type") + ptr = type(cdata)._to_ctypes(cdata) + elif isinstance(cdata, CTypesGenericArray): + ptr = type(cdata)._to_ctypes(cdata) + else: + raise TypeError("expected a ") + if offset: + ptr = ctypes.cast( + ctypes.c_void_p( + ctypes.cast(ptr, ctypes.c_void_p).value + offset), + type(ptr)) + return BTypePtr._from_ctypes(ptr) + + +class CTypesLibrary(object): + + def __init__(self, backend, cdll): + self.backend = backend + self.cdll = cdll + + def load_function(self, BType, name): + c_func = getattr(self.cdll, name) + funcobj = BType._from_ctypes(c_func) + funcobj._name = name + return funcobj + + def read_variable(self, BType, name): + try: + ctypes_obj = BType._ctype.in_dll(self.cdll, name) + except AttributeError as e: + raise NotImplementedError(e) + return BType._from_ctypes(ctypes_obj) + + def write_variable(self, BType, name, value): + new_ctypes_obj = BType._to_ctypes(value) + ctypes_obj = BType._ctype.in_dll(self.cdll, name) + ctypes.memmove(ctypes.addressof(ctypes_obj), + ctypes.addressof(new_ctypes_obj), + ctypes.sizeof(BType._ctype)) diff --git a/lib/python3.10/site-packages/cffi/cffi_opcode.py b/lib/python3.10/site-packages/cffi/cffi_opcode.py new file mode 100644 index 0000000000000000000000000000000000000000..a0df98d1c743790f4047672abcae0d00f993a2ce --- /dev/null +++ b/lib/python3.10/site-packages/cffi/cffi_opcode.py @@ -0,0 +1,187 @@ +from .error import VerificationError + +class CffiOp(object): + def __init__(self, op, arg): + self.op = op + self.arg = arg + + def as_c_expr(self): + if self.op is None: + assert isinstance(self.arg, str) + return '(_cffi_opcode_t)(%s)' % (self.arg,) + classname = CLASS_NAME[self.op] + return '_CFFI_OP(_CFFI_OP_%s, %s)' % (classname, self.arg) + + def as_python_bytes(self): + if self.op is None and self.arg.isdigit(): + value = int(self.arg) # non-negative: '-' not in self.arg + if value >= 2**31: + raise OverflowError("cannot emit %r: limited to 2**31-1" + % (self.arg,)) + return format_four_bytes(value) + if isinstance(self.arg, str): + raise VerificationError("cannot emit to Python: %r" % (self.arg,)) + return format_four_bytes((self.arg << 8) | self.op) + + def __str__(self): + classname = CLASS_NAME.get(self.op, self.op) + return '(%s %s)' % (classname, self.arg) + +def format_four_bytes(num): + return '\\x%02X\\x%02X\\x%02X\\x%02X' % ( + (num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + (num ) & 0xFF) + +OP_PRIMITIVE = 1 +OP_POINTER = 3 +OP_ARRAY = 5 +OP_OPEN_ARRAY = 7 +OP_STRUCT_UNION = 9 +OP_ENUM = 11 +OP_FUNCTION = 13 +OP_FUNCTION_END = 15 +OP_NOOP = 17 +OP_BITFIELD = 19 +OP_TYPENAME = 21 +OP_CPYTHON_BLTN_V = 23 # varargs +OP_CPYTHON_BLTN_N = 25 # noargs +OP_CPYTHON_BLTN_O = 27 # O (i.e. a single arg) +OP_CONSTANT = 29 +OP_CONSTANT_INT = 31 +OP_GLOBAL_VAR = 33 +OP_DLOPEN_FUNC = 35 +OP_DLOPEN_CONST = 37 +OP_GLOBAL_VAR_F = 39 +OP_EXTERN_PYTHON = 41 + +PRIM_VOID = 0 +PRIM_BOOL = 1 +PRIM_CHAR = 2 +PRIM_SCHAR = 3 +PRIM_UCHAR = 4 +PRIM_SHORT = 5 +PRIM_USHORT = 6 +PRIM_INT = 7 +PRIM_UINT = 8 +PRIM_LONG = 9 +PRIM_ULONG = 10 +PRIM_LONGLONG = 11 +PRIM_ULONGLONG = 12 +PRIM_FLOAT = 13 +PRIM_DOUBLE = 14 +PRIM_LONGDOUBLE = 15 + +PRIM_WCHAR = 16 +PRIM_INT8 = 17 +PRIM_UINT8 = 18 +PRIM_INT16 = 19 +PRIM_UINT16 = 20 +PRIM_INT32 = 21 +PRIM_UINT32 = 22 +PRIM_INT64 = 23 +PRIM_UINT64 = 24 +PRIM_INTPTR = 25 +PRIM_UINTPTR = 26 +PRIM_PTRDIFF = 27 +PRIM_SIZE = 28 +PRIM_SSIZE = 29 +PRIM_INT_LEAST8 = 30 +PRIM_UINT_LEAST8 = 31 +PRIM_INT_LEAST16 = 32 +PRIM_UINT_LEAST16 = 33 +PRIM_INT_LEAST32 = 34 +PRIM_UINT_LEAST32 = 35 +PRIM_INT_LEAST64 = 36 +PRIM_UINT_LEAST64 = 37 +PRIM_INT_FAST8 = 38 +PRIM_UINT_FAST8 = 39 +PRIM_INT_FAST16 = 40 +PRIM_UINT_FAST16 = 41 +PRIM_INT_FAST32 = 42 +PRIM_UINT_FAST32 = 43 +PRIM_INT_FAST64 = 44 +PRIM_UINT_FAST64 = 45 +PRIM_INTMAX = 46 +PRIM_UINTMAX = 47 +PRIM_FLOATCOMPLEX = 48 +PRIM_DOUBLECOMPLEX = 49 +PRIM_CHAR16 = 50 +PRIM_CHAR32 = 51 + +_NUM_PRIM = 52 +_UNKNOWN_PRIM = -1 +_UNKNOWN_FLOAT_PRIM = -2 +_UNKNOWN_LONG_DOUBLE = -3 + +_IO_FILE_STRUCT = -1 + +PRIMITIVE_TO_INDEX = { + 'char': PRIM_CHAR, + 'short': PRIM_SHORT, + 'int': PRIM_INT, + 'long': PRIM_LONG, + 'long long': PRIM_LONGLONG, + 'signed char': PRIM_SCHAR, + 'unsigned char': PRIM_UCHAR, + 'unsigned short': PRIM_USHORT, + 'unsigned int': PRIM_UINT, + 'unsigned long': PRIM_ULONG, + 'unsigned long long': PRIM_ULONGLONG, + 'float': PRIM_FLOAT, + 'double': PRIM_DOUBLE, + 'long double': PRIM_LONGDOUBLE, + 'float _Complex': PRIM_FLOATCOMPLEX, + 'double _Complex': PRIM_DOUBLECOMPLEX, + '_Bool': PRIM_BOOL, + 'wchar_t': PRIM_WCHAR, + 'char16_t': PRIM_CHAR16, + 'char32_t': PRIM_CHAR32, + 'int8_t': PRIM_INT8, + 'uint8_t': PRIM_UINT8, + 'int16_t': PRIM_INT16, + 'uint16_t': PRIM_UINT16, + 'int32_t': PRIM_INT32, + 'uint32_t': PRIM_UINT32, + 'int64_t': PRIM_INT64, + 'uint64_t': PRIM_UINT64, + 'intptr_t': PRIM_INTPTR, + 'uintptr_t': PRIM_UINTPTR, + 'ptrdiff_t': PRIM_PTRDIFF, + 'size_t': PRIM_SIZE, + 'ssize_t': PRIM_SSIZE, + 'int_least8_t': PRIM_INT_LEAST8, + 'uint_least8_t': PRIM_UINT_LEAST8, + 'int_least16_t': PRIM_INT_LEAST16, + 'uint_least16_t': PRIM_UINT_LEAST16, + 'int_least32_t': PRIM_INT_LEAST32, + 'uint_least32_t': PRIM_UINT_LEAST32, + 'int_least64_t': PRIM_INT_LEAST64, + 'uint_least64_t': PRIM_UINT_LEAST64, + 'int_fast8_t': PRIM_INT_FAST8, + 'uint_fast8_t': PRIM_UINT_FAST8, + 'int_fast16_t': PRIM_INT_FAST16, + 'uint_fast16_t': PRIM_UINT_FAST16, + 'int_fast32_t': PRIM_INT_FAST32, + 'uint_fast32_t': PRIM_UINT_FAST32, + 'int_fast64_t': PRIM_INT_FAST64, + 'uint_fast64_t': PRIM_UINT_FAST64, + 'intmax_t': PRIM_INTMAX, + 'uintmax_t': PRIM_UINTMAX, + } + +F_UNION = 0x01 +F_CHECK_FIELDS = 0x02 +F_PACKED = 0x04 +F_EXTERNAL = 0x08 +F_OPAQUE = 0x10 + +G_FLAGS = dict([('_CFFI_' + _key, globals()[_key]) + for _key in ['F_UNION', 'F_CHECK_FIELDS', 'F_PACKED', + 'F_EXTERNAL', 'F_OPAQUE']]) + +CLASS_NAME = {} +for _name, _value in list(globals().items()): + if _name.startswith('OP_') and isinstance(_value, int): + CLASS_NAME[_value] = _name[3:] diff --git a/lib/python3.10/site-packages/cffi/commontypes.py b/lib/python3.10/site-packages/cffi/commontypes.py new file mode 100644 index 0000000000000000000000000000000000000000..8ec97c756a4b1023fd3963dd39b706f7c0e34373 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/commontypes.py @@ -0,0 +1,80 @@ +import sys +from . import model +from .error import FFIError + + +COMMON_TYPES = {} + +try: + # fetch "bool" and all simple Windows types + from _cffi_backend import _get_common_types + _get_common_types(COMMON_TYPES) +except ImportError: + pass + +COMMON_TYPES['FILE'] = model.unknown_type('FILE', '_IO_FILE') +COMMON_TYPES['bool'] = '_Bool' # in case we got ImportError above + +for _type in model.PrimitiveType.ALL_PRIMITIVE_TYPES: + if _type.endswith('_t'): + COMMON_TYPES[_type] = _type +del _type + +_CACHE = {} + +def resolve_common_type(parser, commontype): + try: + return _CACHE[commontype] + except KeyError: + cdecl = COMMON_TYPES.get(commontype, commontype) + if not isinstance(cdecl, str): + result, quals = cdecl, 0 # cdecl is already a BaseType + elif cdecl in model.PrimitiveType.ALL_PRIMITIVE_TYPES: + result, quals = model.PrimitiveType(cdecl), 0 + elif cdecl == 'set-unicode-needed': + raise FFIError("The Windows type %r is only available after " + "you call ffi.set_unicode()" % (commontype,)) + else: + if commontype == cdecl: + raise FFIError( + "Unsupported type: %r. Please look at " + "http://cffi.readthedocs.io/en/latest/cdef.html#ffi-cdef-limitations " + "and file an issue if you think this type should really " + "be supported." % (commontype,)) + result, quals = parser.parse_type_and_quals(cdecl) # recursive + + assert isinstance(result, model.BaseTypeByIdentity) + _CACHE[commontype] = result, quals + return result, quals + + +# ____________________________________________________________ +# extra types for Windows (most of them are in commontypes.c) + + +def win_common_types(): + return { + "UNICODE_STRING": model.StructType( + "_UNICODE_STRING", + ["Length", + "MaximumLength", + "Buffer"], + [model.PrimitiveType("unsigned short"), + model.PrimitiveType("unsigned short"), + model.PointerType(model.PrimitiveType("wchar_t"))], + [-1, -1, -1]), + "PUNICODE_STRING": "UNICODE_STRING *", + "PCUNICODE_STRING": "const UNICODE_STRING *", + + "TBYTE": "set-unicode-needed", + "TCHAR": "set-unicode-needed", + "LPCTSTR": "set-unicode-needed", + "PCTSTR": "set-unicode-needed", + "LPTSTR": "set-unicode-needed", + "PTSTR": "set-unicode-needed", + "PTBYTE": "set-unicode-needed", + "PTCHAR": "set-unicode-needed", + } + +if sys.platform == 'win32': + COMMON_TYPES.update(win_common_types()) diff --git a/lib/python3.10/site-packages/cffi/cparser.py b/lib/python3.10/site-packages/cffi/cparser.py new file mode 100644 index 0000000000000000000000000000000000000000..74830e913f21409f536febddae7769d0364cd24b --- /dev/null +++ b/lib/python3.10/site-packages/cffi/cparser.py @@ -0,0 +1,1006 @@ +from . import model +from .commontypes import COMMON_TYPES, resolve_common_type +from .error import FFIError, CDefError +try: + from . import _pycparser as pycparser +except ImportError: + import pycparser +import weakref, re, sys + +try: + if sys.version_info < (3,): + import thread as _thread + else: + import _thread + lock = _thread.allocate_lock() +except ImportError: + lock = None + +def _workaround_for_static_import_finders(): + # Issue #392: packaging tools like cx_Freeze can not find these + # because pycparser uses exec dynamic import. This is an obscure + # workaround. This function is never called. + import pycparser.yacctab + import pycparser.lextab + +CDEF_SOURCE_STRING = "" +_r_comment = re.compile(r"/\*.*?\*/|//([^\n\\]|\\.)*?$", + re.DOTALL | re.MULTILINE) +_r_define = re.compile(r"^\s*#\s*define\s+([A-Za-z_][A-Za-z_0-9]*)" + r"\b((?:[^\n\\]|\\.)*?)$", + re.DOTALL | re.MULTILINE) +_r_line_directive = re.compile(r"^[ \t]*#[ \t]*(?:line|\d+)\b.*$", re.MULTILINE) +_r_partial_enum = re.compile(r"=\s*\.\.\.\s*[,}]|\.\.\.\s*\}") +_r_enum_dotdotdot = re.compile(r"__dotdotdot\d+__$") +_r_partial_array = re.compile(r"\[\s*\.\.\.\s*\]") +_r_words = re.compile(r"\w+|\S") +_parser_cache = None +_r_int_literal = re.compile(r"-?0?x?[0-9a-f]+[lu]*$", re.IGNORECASE) +_r_stdcall1 = re.compile(r"\b(__stdcall|WINAPI)\b") +_r_stdcall2 = re.compile(r"[(]\s*(__stdcall|WINAPI)\b") +_r_cdecl = re.compile(r"\b__cdecl\b") +_r_extern_python = re.compile(r'\bextern\s*"' + r'(Python|Python\s*\+\s*C|C\s*\+\s*Python)"\s*.') +_r_star_const_space = re.compile( # matches "* const " + r"[*]\s*((const|volatile|restrict)\b\s*)+") +_r_int_dotdotdot = re.compile(r"(\b(int|long|short|signed|unsigned|char)\s*)+" + r"\.\.\.") +_r_float_dotdotdot = re.compile(r"\b(double|float)\s*\.\.\.") + +def _get_parser(): + global _parser_cache + if _parser_cache is None: + _parser_cache = pycparser.CParser() + return _parser_cache + +def _workaround_for_old_pycparser(csource): + # Workaround for a pycparser issue (fixed between pycparser 2.10 and + # 2.14): "char*const***" gives us a wrong syntax tree, the same as + # for "char***(*const)". This means we can't tell the difference + # afterwards. But "char(*const(***))" gives us the right syntax + # tree. The issue only occurs if there are several stars in + # sequence with no parenthesis inbetween, just possibly qualifiers. + # Attempt to fix it by adding some parentheses in the source: each + # time we see "* const" or "* const *", we add an opening + # parenthesis before each star---the hard part is figuring out where + # to close them. + parts = [] + while True: + match = _r_star_const_space.search(csource) + if not match: + break + #print repr(''.join(parts)+csource), '=>', + parts.append(csource[:match.start()]) + parts.append('('); closing = ')' + parts.append(match.group()) # e.g. "* const " + endpos = match.end() + if csource.startswith('*', endpos): + parts.append('('); closing += ')' + level = 0 + i = endpos + while i < len(csource): + c = csource[i] + if c == '(': + level += 1 + elif c == ')': + if level == 0: + break + level -= 1 + elif c in ',;=': + if level == 0: + break + i += 1 + csource = csource[endpos:i] + closing + csource[i:] + #print repr(''.join(parts)+csource) + parts.append(csource) + return ''.join(parts) + +def _preprocess_extern_python(csource): + # input: `extern "Python" int foo(int);` or + # `extern "Python" { int foo(int); }` + # output: + # void __cffi_extern_python_start; + # int foo(int); + # void __cffi_extern_python_stop; + # + # input: `extern "Python+C" int foo(int);` + # output: + # void __cffi_extern_python_plus_c_start; + # int foo(int); + # void __cffi_extern_python_stop; + parts = [] + while True: + match = _r_extern_python.search(csource) + if not match: + break + endpos = match.end() - 1 + #print + #print ''.join(parts)+csource + #print '=>' + parts.append(csource[:match.start()]) + if 'C' in match.group(1): + parts.append('void __cffi_extern_python_plus_c_start; ') + else: + parts.append('void __cffi_extern_python_start; ') + if csource[endpos] == '{': + # grouping variant + closing = csource.find('}', endpos) + if closing < 0: + raise CDefError("'extern \"Python\" {': no '}' found") + if csource.find('{', endpos + 1, closing) >= 0: + raise NotImplementedError("cannot use { } inside a block " + "'extern \"Python\" { ... }'") + parts.append(csource[endpos+1:closing]) + csource = csource[closing+1:] + else: + # non-grouping variant + semicolon = csource.find(';', endpos) + if semicolon < 0: + raise CDefError("'extern \"Python\": no ';' found") + parts.append(csource[endpos:semicolon+1]) + csource = csource[semicolon+1:] + parts.append(' void __cffi_extern_python_stop;') + #print ''.join(parts)+csource + #print + parts.append(csource) + return ''.join(parts) + +def _warn_for_string_literal(csource): + if '"' not in csource: + return + for line in csource.splitlines(): + if '"' in line and not line.lstrip().startswith('#'): + import warnings + warnings.warn("String literal found in cdef() or type source. " + "String literals are ignored here, but you should " + "remove them anyway because some character sequences " + "confuse pre-parsing.") + break + +def _warn_for_non_extern_non_static_global_variable(decl): + if not decl.storage: + import warnings + warnings.warn("Global variable '%s' in cdef(): for consistency " + "with C it should have a storage class specifier " + "(usually 'extern')" % (decl.name,)) + +def _remove_line_directives(csource): + # _r_line_directive matches whole lines, without the final \n, if they + # start with '#line' with some spacing allowed, or '#NUMBER'. This + # function stores them away and replaces them with exactly the string + # '#line@N', where N is the index in the list 'line_directives'. + line_directives = [] + def replace(m): + i = len(line_directives) + line_directives.append(m.group()) + return '#line@%d' % i + csource = _r_line_directive.sub(replace, csource) + return csource, line_directives + +def _put_back_line_directives(csource, line_directives): + def replace(m): + s = m.group() + if not s.startswith('#line@'): + raise AssertionError("unexpected #line directive " + "(should have been processed and removed") + return line_directives[int(s[6:])] + return _r_line_directive.sub(replace, csource) + +def _preprocess(csource): + # First, remove the lines of the form '#line N "filename"' because + # the "filename" part could confuse the rest + csource, line_directives = _remove_line_directives(csource) + # Remove comments. NOTE: this only work because the cdef() section + # should not contain any string literals (except in line directives)! + def replace_keeping_newlines(m): + return ' ' + m.group().count('\n') * '\n' + csource = _r_comment.sub(replace_keeping_newlines, csource) + # Remove the "#define FOO x" lines + macros = {} + for match in _r_define.finditer(csource): + macroname, macrovalue = match.groups() + macrovalue = macrovalue.replace('\\\n', '').strip() + macros[macroname] = macrovalue + csource = _r_define.sub('', csource) + # + if pycparser.__version__ < '2.14': + csource = _workaround_for_old_pycparser(csource) + # + # BIG HACK: replace WINAPI or __stdcall with "volatile const". + # It doesn't make sense for the return type of a function to be + # "volatile volatile const", so we abuse it to detect __stdcall... + # Hack number 2 is that "int(volatile *fptr)();" is not valid C + # syntax, so we place the "volatile" before the opening parenthesis. + csource = _r_stdcall2.sub(' volatile volatile const(', csource) + csource = _r_stdcall1.sub(' volatile volatile const ', csource) + csource = _r_cdecl.sub(' ', csource) + # + # Replace `extern "Python"` with start/end markers + csource = _preprocess_extern_python(csource) + # + # Now there should not be any string literal left; warn if we get one + _warn_for_string_literal(csource) + # + # Replace "[...]" with "[__dotdotdotarray__]" + csource = _r_partial_array.sub('[__dotdotdotarray__]', csource) + # + # Replace "...}" with "__dotdotdotNUM__}". This construction should + # occur only at the end of enums; at the end of structs we have "...;}" + # and at the end of vararg functions "...);". Also replace "=...[,}]" + # with ",__dotdotdotNUM__[,}]": this occurs in the enums too, when + # giving an unknown value. + matches = list(_r_partial_enum.finditer(csource)) + for number, match in enumerate(reversed(matches)): + p = match.start() + if csource[p] == '=': + p2 = csource.find('...', p, match.end()) + assert p2 > p + csource = '%s,__dotdotdot%d__ %s' % (csource[:p], number, + csource[p2+3:]) + else: + assert csource[p:p+3] == '...' + csource = '%s __dotdotdot%d__ %s' % (csource[:p], number, + csource[p+3:]) + # Replace "int ..." or "unsigned long int..." with "__dotdotdotint__" + csource = _r_int_dotdotdot.sub(' __dotdotdotint__ ', csource) + # Replace "float ..." or "double..." with "__dotdotdotfloat__" + csource = _r_float_dotdotdot.sub(' __dotdotdotfloat__ ', csource) + # Replace all remaining "..." with the same name, "__dotdotdot__", + # which is declared with a typedef for the purpose of C parsing. + csource = csource.replace('...', ' __dotdotdot__ ') + # Finally, put back the line directives + csource = _put_back_line_directives(csource, line_directives) + return csource, macros + +def _common_type_names(csource): + # Look in the source for what looks like usages of types from the + # list of common types. A "usage" is approximated here as the + # appearance of the word, minus a "definition" of the type, which + # is the last word in a "typedef" statement. Approximative only + # but should be fine for all the common types. + look_for_words = set(COMMON_TYPES) + look_for_words.add(';') + look_for_words.add(',') + look_for_words.add('(') + look_for_words.add(')') + look_for_words.add('typedef') + words_used = set() + is_typedef = False + paren = 0 + previous_word = '' + for word in _r_words.findall(csource): + if word in look_for_words: + if word == ';': + if is_typedef: + words_used.discard(previous_word) + look_for_words.discard(previous_word) + is_typedef = False + elif word == 'typedef': + is_typedef = True + paren = 0 + elif word == '(': + paren += 1 + elif word == ')': + paren -= 1 + elif word == ',': + if is_typedef and paren == 0: + words_used.discard(previous_word) + look_for_words.discard(previous_word) + else: # word in COMMON_TYPES + words_used.add(word) + previous_word = word + return words_used + + +class Parser(object): + + def __init__(self): + self._declarations = {} + self._included_declarations = set() + self._anonymous_counter = 0 + self._structnode2type = weakref.WeakKeyDictionary() + self._options = {} + self._int_constants = {} + self._recomplete = [] + self._uses_new_feature = None + + def _parse(self, csource): + csource, macros = _preprocess(csource) + # XXX: for more efficiency we would need to poke into the + # internals of CParser... the following registers the + # typedefs, because their presence or absence influences the + # parsing itself (but what they are typedef'ed to plays no role) + ctn = _common_type_names(csource) + typenames = [] + for name in sorted(self._declarations): + if name.startswith('typedef '): + name = name[8:] + typenames.append(name) + ctn.discard(name) + typenames += sorted(ctn) + # + csourcelines = [] + csourcelines.append('# 1 ""') + for typename in typenames: + csourcelines.append('typedef int %s;' % typename) + csourcelines.append('typedef int __dotdotdotint__, __dotdotdotfloat__,' + ' __dotdotdot__;') + # this forces pycparser to consider the following in the file + # called from line 1 + csourcelines.append('# 1 "%s"' % (CDEF_SOURCE_STRING,)) + csourcelines.append(csource) + fullcsource = '\n'.join(csourcelines) + if lock is not None: + lock.acquire() # pycparser is not thread-safe... + try: + ast = _get_parser().parse(fullcsource) + except pycparser.c_parser.ParseError as e: + self.convert_pycparser_error(e, csource) + finally: + if lock is not None: + lock.release() + # csource will be used to find buggy source text + return ast, macros, csource + + def _convert_pycparser_error(self, e, csource): + # xxx look for ":NUM:" at the start of str(e) + # and interpret that as a line number. This will not work if + # the user gives explicit ``# NUM "FILE"`` directives. + line = None + msg = str(e) + match = re.match(r"%s:(\d+):" % (CDEF_SOURCE_STRING,), msg) + if match: + linenum = int(match.group(1), 10) + csourcelines = csource.splitlines() + if 1 <= linenum <= len(csourcelines): + line = csourcelines[linenum-1] + return line + + def convert_pycparser_error(self, e, csource): + line = self._convert_pycparser_error(e, csource) + + msg = str(e) + if line: + msg = 'cannot parse "%s"\n%s' % (line.strip(), msg) + else: + msg = 'parse error\n%s' % (msg,) + raise CDefError(msg) + + def parse(self, csource, override=False, packed=False, pack=None, + dllexport=False): + if packed: + if packed != True: + raise ValueError("'packed' should be False or True; use " + "'pack' to give another value") + if pack: + raise ValueError("cannot give both 'pack' and 'packed'") + pack = 1 + elif pack: + if pack & (pack - 1): + raise ValueError("'pack' must be a power of two, not %r" % + (pack,)) + else: + pack = 0 + prev_options = self._options + try: + self._options = {'override': override, + 'packed': pack, + 'dllexport': dllexport} + self._internal_parse(csource) + finally: + self._options = prev_options + + def _internal_parse(self, csource): + ast, macros, csource = self._parse(csource) + # add the macros + self._process_macros(macros) + # find the first "__dotdotdot__" and use that as a separator + # between the repeated typedefs and the real csource + iterator = iter(ast.ext) + for decl in iterator: + if decl.name == '__dotdotdot__': + break + else: + assert 0 + current_decl = None + # + try: + self._inside_extern_python = '__cffi_extern_python_stop' + for decl in iterator: + current_decl = decl + if isinstance(decl, pycparser.c_ast.Decl): + self._parse_decl(decl) + elif isinstance(decl, pycparser.c_ast.Typedef): + if not decl.name: + raise CDefError("typedef does not declare any name", + decl) + quals = 0 + if (isinstance(decl.type.type, pycparser.c_ast.IdentifierType) and + decl.type.type.names[-1].startswith('__dotdotdot')): + realtype = self._get_unknown_type(decl) + elif (isinstance(decl.type, pycparser.c_ast.PtrDecl) and + isinstance(decl.type.type, pycparser.c_ast.TypeDecl) and + isinstance(decl.type.type.type, + pycparser.c_ast.IdentifierType) and + decl.type.type.type.names[-1].startswith('__dotdotdot')): + realtype = self._get_unknown_ptr_type(decl) + else: + realtype, quals = self._get_type_and_quals( + decl.type, name=decl.name, partial_length_ok=True, + typedef_example="*(%s *)0" % (decl.name,)) + self._declare('typedef ' + decl.name, realtype, quals=quals) + elif decl.__class__.__name__ == 'Pragma': + pass # skip pragma, only in pycparser 2.15 + else: + raise CDefError("unexpected <%s>: this construct is valid " + "C but not valid in cdef()" % + decl.__class__.__name__, decl) + except CDefError as e: + if len(e.args) == 1: + e.args = e.args + (current_decl,) + raise + except FFIError as e: + msg = self._convert_pycparser_error(e, csource) + if msg: + e.args = (e.args[0] + "\n *** Err: %s" % msg,) + raise + + def _add_constants(self, key, val): + if key in self._int_constants: + if self._int_constants[key] == val: + return # ignore identical double declarations + raise FFIError( + "multiple declarations of constant: %s" % (key,)) + self._int_constants[key] = val + + def _add_integer_constant(self, name, int_str): + int_str = int_str.lower().rstrip("ul") + neg = int_str.startswith('-') + if neg: + int_str = int_str[1:] + # "010" is not valid oct in py3 + if (int_str.startswith("0") and int_str != '0' + and not int_str.startswith("0x")): + int_str = "0o" + int_str[1:] + pyvalue = int(int_str, 0) + if neg: + pyvalue = -pyvalue + self._add_constants(name, pyvalue) + self._declare('macro ' + name, pyvalue) + + def _process_macros(self, macros): + for key, value in macros.items(): + value = value.strip() + if _r_int_literal.match(value): + self._add_integer_constant(key, value) + elif value == '...': + self._declare('macro ' + key, value) + else: + raise CDefError( + 'only supports one of the following syntax:\n' + ' #define %s ... (literally dot-dot-dot)\n' + ' #define %s NUMBER (with NUMBER an integer' + ' constant, decimal/hex/octal)\n' + 'got:\n' + ' #define %s %s' + % (key, key, key, value)) + + def _declare_function(self, tp, quals, decl): + tp = self._get_type_pointer(tp, quals) + if self._options.get('dllexport'): + tag = 'dllexport_python ' + elif self._inside_extern_python == '__cffi_extern_python_start': + tag = 'extern_python ' + elif self._inside_extern_python == '__cffi_extern_python_plus_c_start': + tag = 'extern_python_plus_c ' + else: + tag = 'function ' + self._declare(tag + decl.name, tp) + + def _parse_decl(self, decl): + node = decl.type + if isinstance(node, pycparser.c_ast.FuncDecl): + tp, quals = self._get_type_and_quals(node, name=decl.name) + assert isinstance(tp, model.RawFunctionType) + self._declare_function(tp, quals, decl) + else: + if isinstance(node, pycparser.c_ast.Struct): + self._get_struct_union_enum_type('struct', node) + elif isinstance(node, pycparser.c_ast.Union): + self._get_struct_union_enum_type('union', node) + elif isinstance(node, pycparser.c_ast.Enum): + self._get_struct_union_enum_type('enum', node) + elif not decl.name: + raise CDefError("construct does not declare any variable", + decl) + # + if decl.name: + tp, quals = self._get_type_and_quals(node, + partial_length_ok=True) + if tp.is_raw_function: + self._declare_function(tp, quals, decl) + elif (tp.is_integer_type() and + hasattr(decl, 'init') and + hasattr(decl.init, 'value') and + _r_int_literal.match(decl.init.value)): + self._add_integer_constant(decl.name, decl.init.value) + elif (tp.is_integer_type() and + isinstance(decl.init, pycparser.c_ast.UnaryOp) and + decl.init.op == '-' and + hasattr(decl.init.expr, 'value') and + _r_int_literal.match(decl.init.expr.value)): + self._add_integer_constant(decl.name, + '-' + decl.init.expr.value) + elif (tp is model.void_type and + decl.name.startswith('__cffi_extern_python_')): + # hack: `extern "Python"` in the C source is replaced + # with "void __cffi_extern_python_start;" and + # "void __cffi_extern_python_stop;" + self._inside_extern_python = decl.name + else: + if self._inside_extern_python !='__cffi_extern_python_stop': + raise CDefError( + "cannot declare constants or " + "variables with 'extern \"Python\"'") + if (quals & model.Q_CONST) and not tp.is_array_type: + self._declare('constant ' + decl.name, tp, quals=quals) + else: + _warn_for_non_extern_non_static_global_variable(decl) + self._declare('variable ' + decl.name, tp, quals=quals) + + def parse_type(self, cdecl): + return self.parse_type_and_quals(cdecl)[0] + + def parse_type_and_quals(self, cdecl): + ast, macros = self._parse('void __dummy(\n%s\n);' % cdecl)[:2] + assert not macros + exprnode = ast.ext[-1].type.args.params[0] + if isinstance(exprnode, pycparser.c_ast.ID): + raise CDefError("unknown identifier '%s'" % (exprnode.name,)) + return self._get_type_and_quals(exprnode.type) + + def _declare(self, name, obj, included=False, quals=0): + if name in self._declarations: + prevobj, prevquals = self._declarations[name] + if prevobj is obj and prevquals == quals: + return + if not self._options.get('override'): + raise FFIError( + "multiple declarations of %s (for interactive usage, " + "try cdef(xx, override=True))" % (name,)) + assert '__dotdotdot__' not in name.split() + self._declarations[name] = (obj, quals) + if included: + self._included_declarations.add(obj) + + def _extract_quals(self, type): + quals = 0 + if isinstance(type, (pycparser.c_ast.TypeDecl, + pycparser.c_ast.PtrDecl)): + if 'const' in type.quals: + quals |= model.Q_CONST + if 'volatile' in type.quals: + quals |= model.Q_VOLATILE + if 'restrict' in type.quals: + quals |= model.Q_RESTRICT + return quals + + def _get_type_pointer(self, type, quals, declname=None): + if isinstance(type, model.RawFunctionType): + return type.as_function_pointer() + if (isinstance(type, model.StructOrUnionOrEnum) and + type.name.startswith('$') and type.name[1:].isdigit() and + type.forcename is None and declname is not None): + return model.NamedPointerType(type, declname, quals) + return model.PointerType(type, quals) + + def _get_type_and_quals(self, typenode, name=None, partial_length_ok=False, + typedef_example=None): + # first, dereference typedefs, if we have it already parsed, we're good + if (isinstance(typenode, pycparser.c_ast.TypeDecl) and + isinstance(typenode.type, pycparser.c_ast.IdentifierType) and + len(typenode.type.names) == 1 and + ('typedef ' + typenode.type.names[0]) in self._declarations): + tp, quals = self._declarations['typedef ' + typenode.type.names[0]] + quals |= self._extract_quals(typenode) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.ArrayDecl): + # array type + if typenode.dim is None: + length = None + else: + length = self._parse_constant( + typenode.dim, partial_length_ok=partial_length_ok) + # a hack: in 'typedef int foo_t[...][...];', don't use '...' as + # the length but use directly the C expression that would be + # generated by recompiler.py. This lets the typedef be used in + # many more places within recompiler.py + if typedef_example is not None: + if length == '...': + length = '_cffi_array_len(%s)' % (typedef_example,) + typedef_example = "*" + typedef_example + # + tp, quals = self._get_type_and_quals(typenode.type, + partial_length_ok=partial_length_ok, + typedef_example=typedef_example) + return model.ArrayType(tp, length), quals + # + if isinstance(typenode, pycparser.c_ast.PtrDecl): + # pointer type + itemtype, itemquals = self._get_type_and_quals(typenode.type) + tp = self._get_type_pointer(itemtype, itemquals, declname=name) + quals = self._extract_quals(typenode) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.TypeDecl): + quals = self._extract_quals(typenode) + type = typenode.type + if isinstance(type, pycparser.c_ast.IdentifierType): + # assume a primitive type. get it from .names, but reduce + # synonyms to a single chosen combination + names = list(type.names) + if names != ['signed', 'char']: # keep this unmodified + prefixes = {} + while names: + name = names[0] + if name in ('short', 'long', 'signed', 'unsigned'): + prefixes[name] = prefixes.get(name, 0) + 1 + del names[0] + else: + break + # ignore the 'signed' prefix below, and reorder the others + newnames = [] + for prefix in ('unsigned', 'short', 'long'): + for i in range(prefixes.get(prefix, 0)): + newnames.append(prefix) + if not names: + names = ['int'] # implicitly + if names == ['int']: # but kill it if 'short' or 'long' + if 'short' in prefixes or 'long' in prefixes: + names = [] + names = newnames + names + ident = ' '.join(names) + if ident == 'void': + return model.void_type, quals + if ident == '__dotdotdot__': + raise FFIError(':%d: bad usage of "..."' % + typenode.coord.line) + tp0, quals0 = resolve_common_type(self, ident) + return tp0, (quals | quals0) + # + if isinstance(type, pycparser.c_ast.Struct): + # 'struct foobar' + tp = self._get_struct_union_enum_type('struct', type, name) + return tp, quals + # + if isinstance(type, pycparser.c_ast.Union): + # 'union foobar' + tp = self._get_struct_union_enum_type('union', type, name) + return tp, quals + # + if isinstance(type, pycparser.c_ast.Enum): + # 'enum foobar' + tp = self._get_struct_union_enum_type('enum', type, name) + return tp, quals + # + if isinstance(typenode, pycparser.c_ast.FuncDecl): + # a function type + return self._parse_function_type(typenode, name), 0 + # + # nested anonymous structs or unions end up here + if isinstance(typenode, pycparser.c_ast.Struct): + return self._get_struct_union_enum_type('struct', typenode, name, + nested=True), 0 + if isinstance(typenode, pycparser.c_ast.Union): + return self._get_struct_union_enum_type('union', typenode, name, + nested=True), 0 + # + raise FFIError(":%d: bad or unsupported type declaration" % + typenode.coord.line) + + def _parse_function_type(self, typenode, funcname=None): + params = list(getattr(typenode.args, 'params', [])) + for i, arg in enumerate(params): + if not hasattr(arg, 'type'): + raise CDefError("%s arg %d: unknown type '%s'" + " (if you meant to use the old C syntax of giving" + " untyped arguments, it is not supported)" + % (funcname or 'in expression', i + 1, + getattr(arg, 'name', '?'))) + ellipsis = ( + len(params) > 0 and + isinstance(params[-1].type, pycparser.c_ast.TypeDecl) and + isinstance(params[-1].type.type, + pycparser.c_ast.IdentifierType) and + params[-1].type.type.names == ['__dotdotdot__']) + if ellipsis: + params.pop() + if not params: + raise CDefError( + "%s: a function with only '(...)' as argument" + " is not correct C" % (funcname or 'in expression')) + args = [self._as_func_arg(*self._get_type_and_quals(argdeclnode.type)) + for argdeclnode in params] + if not ellipsis and args == [model.void_type]: + args = [] + result, quals = self._get_type_and_quals(typenode.type) + # the 'quals' on the result type are ignored. HACK: we absure them + # to detect __stdcall functions: we textually replace "__stdcall" + # with "volatile volatile const" above. + abi = None + if hasattr(typenode.type, 'quals'): # else, probable syntax error anyway + if typenode.type.quals[-3:] == ['volatile', 'volatile', 'const']: + abi = '__stdcall' + return model.RawFunctionType(tuple(args), result, ellipsis, abi) + + def _as_func_arg(self, type, quals): + if isinstance(type, model.ArrayType): + return model.PointerType(type.item, quals) + elif isinstance(type, model.RawFunctionType): + return type.as_function_pointer() + else: + return type + + def _get_struct_union_enum_type(self, kind, type, name=None, nested=False): + # First, a level of caching on the exact 'type' node of the AST. + # This is obscure, but needed because pycparser "unrolls" declarations + # such as "typedef struct { } foo_t, *foo_p" and we end up with + # an AST that is not a tree, but a DAG, with the "type" node of the + # two branches foo_t and foo_p of the trees being the same node. + # It's a bit silly but detecting "DAG-ness" in the AST tree seems + # to be the only way to distinguish this case from two independent + # structs. See test_struct_with_two_usages. + try: + return self._structnode2type[type] + except KeyError: + pass + # + # Note that this must handle parsing "struct foo" any number of + # times and always return the same StructType object. Additionally, + # one of these times (not necessarily the first), the fields of + # the struct can be specified with "struct foo { ...fields... }". + # If no name is given, then we have to create a new anonymous struct + # with no caching; in this case, the fields are either specified + # right now or never. + # + force_name = name + name = type.name + # + # get the type or create it if needed + if name is None: + # 'force_name' is used to guess a more readable name for + # anonymous structs, for the common case "typedef struct { } foo". + if force_name is not None: + explicit_name = '$%s' % force_name + else: + self._anonymous_counter += 1 + explicit_name = '$%d' % self._anonymous_counter + tp = None + else: + explicit_name = name + key = '%s %s' % (kind, name) + tp, _ = self._declarations.get(key, (None, None)) + # + if tp is None: + if kind == 'struct': + tp = model.StructType(explicit_name, None, None, None) + elif kind == 'union': + tp = model.UnionType(explicit_name, None, None, None) + elif kind == 'enum': + if explicit_name == '__dotdotdot__': + raise CDefError("Enums cannot be declared with ...") + tp = self._build_enum_type(explicit_name, type.values) + else: + raise AssertionError("kind = %r" % (kind,)) + if name is not None: + self._declare(key, tp) + else: + if kind == 'enum' and type.values is not None: + raise NotImplementedError( + "enum %s: the '{}' declaration should appear on the first " + "time the enum is mentioned, not later" % explicit_name) + if not tp.forcename: + tp.force_the_name(force_name) + if tp.forcename and '$' in tp.name: + self._declare('anonymous %s' % tp.forcename, tp) + # + self._structnode2type[type] = tp + # + # enums: done here + if kind == 'enum': + return tp + # + # is there a 'type.decls'? If yes, then this is the place in the + # C sources that declare the fields. If no, then just return the + # existing type, possibly still incomplete. + if type.decls is None: + return tp + # + if tp.fldnames is not None: + raise CDefError("duplicate declaration of struct %s" % name) + fldnames = [] + fldtypes = [] + fldbitsize = [] + fldquals = [] + for decl in type.decls: + if (isinstance(decl.type, pycparser.c_ast.IdentifierType) and + ''.join(decl.type.names) == '__dotdotdot__'): + # XXX pycparser is inconsistent: 'names' should be a list + # of strings, but is sometimes just one string. Use + # str.join() as a way to cope with both. + self._make_partial(tp, nested) + continue + if decl.bitsize is None: + bitsize = -1 + else: + bitsize = self._parse_constant(decl.bitsize) + self._partial_length = False + type, fqual = self._get_type_and_quals(decl.type, + partial_length_ok=True) + if self._partial_length: + self._make_partial(tp, nested) + if isinstance(type, model.StructType) and type.partial: + self._make_partial(tp, nested) + fldnames.append(decl.name or '') + fldtypes.append(type) + fldbitsize.append(bitsize) + fldquals.append(fqual) + tp.fldnames = tuple(fldnames) + tp.fldtypes = tuple(fldtypes) + tp.fldbitsize = tuple(fldbitsize) + tp.fldquals = tuple(fldquals) + if fldbitsize != [-1] * len(fldbitsize): + if isinstance(tp, model.StructType) and tp.partial: + raise NotImplementedError("%s: using both bitfields and '...;'" + % (tp,)) + tp.packed = self._options.get('packed') + if tp.completed: # must be re-completed: it is not opaque any more + tp.completed = 0 + self._recomplete.append(tp) + return tp + + def _make_partial(self, tp, nested): + if not isinstance(tp, model.StructOrUnion): + raise CDefError("%s cannot be partial" % (tp,)) + if not tp.has_c_name() and not nested: + raise NotImplementedError("%s is partial but has no C name" %(tp,)) + tp.partial = True + + def _parse_constant(self, exprnode, partial_length_ok=False): + # for now, limited to expressions that are an immediate number + # or positive/negative number + if isinstance(exprnode, pycparser.c_ast.Constant): + s = exprnode.value + if '0' <= s[0] <= '9': + s = s.rstrip('uUlL') + try: + if s.startswith('0'): + return int(s, 8) + else: + return int(s, 10) + except ValueError: + if len(s) > 1: + if s.lower()[0:2] == '0x': + return int(s, 16) + elif s.lower()[0:2] == '0b': + return int(s, 2) + raise CDefError("invalid constant %r" % (s,)) + elif s[0] == "'" and s[-1] == "'" and ( + len(s) == 3 or (len(s) == 4 and s[1] == "\\")): + return ord(s[-2]) + else: + raise CDefError("invalid constant %r" % (s,)) + # + if (isinstance(exprnode, pycparser.c_ast.UnaryOp) and + exprnode.op == '+'): + return self._parse_constant(exprnode.expr) + # + if (isinstance(exprnode, pycparser.c_ast.UnaryOp) and + exprnode.op == '-'): + return -self._parse_constant(exprnode.expr) + # load previously defined int constant + if (isinstance(exprnode, pycparser.c_ast.ID) and + exprnode.name in self._int_constants): + return self._int_constants[exprnode.name] + # + if (isinstance(exprnode, pycparser.c_ast.ID) and + exprnode.name == '__dotdotdotarray__'): + if partial_length_ok: + self._partial_length = True + return '...' + raise FFIError(":%d: unsupported '[...]' here, cannot derive " + "the actual array length in this context" + % exprnode.coord.line) + # + if isinstance(exprnode, pycparser.c_ast.BinaryOp): + left = self._parse_constant(exprnode.left) + right = self._parse_constant(exprnode.right) + if exprnode.op == '+': + return left + right + elif exprnode.op == '-': + return left - right + elif exprnode.op == '*': + return left * right + elif exprnode.op == '/': + return self._c_div(left, right) + elif exprnode.op == '%': + return left - self._c_div(left, right) * right + elif exprnode.op == '<<': + return left << right + elif exprnode.op == '>>': + return left >> right + elif exprnode.op == '&': + return left & right + elif exprnode.op == '|': + return left | right + elif exprnode.op == '^': + return left ^ right + # + raise FFIError(":%d: unsupported expression: expected a " + "simple numeric constant" % exprnode.coord.line) + + def _c_div(self, a, b): + result = a // b + if ((a < 0) ^ (b < 0)) and (a % b) != 0: + result += 1 + return result + + def _build_enum_type(self, explicit_name, decls): + if decls is not None: + partial = False + enumerators = [] + enumvalues = [] + nextenumvalue = 0 + for enum in decls.enumerators: + if _r_enum_dotdotdot.match(enum.name): + partial = True + continue + if enum.value is not None: + nextenumvalue = self._parse_constant(enum.value) + enumerators.append(enum.name) + enumvalues.append(nextenumvalue) + self._add_constants(enum.name, nextenumvalue) + nextenumvalue += 1 + enumerators = tuple(enumerators) + enumvalues = tuple(enumvalues) + tp = model.EnumType(explicit_name, enumerators, enumvalues) + tp.partial = partial + else: # opaque enum + tp = model.EnumType(explicit_name, (), ()) + return tp + + def include(self, other): + for name, (tp, quals) in other._declarations.items(): + if name.startswith('anonymous $enum_$'): + continue # fix for test_anonymous_enum_include + kind = name.split(' ', 1)[0] + if kind in ('struct', 'union', 'enum', 'anonymous', 'typedef'): + self._declare(name, tp, included=True, quals=quals) + for k, v in other._int_constants.items(): + self._add_constants(k, v) + + def _get_unknown_type(self, decl): + typenames = decl.type.type.names + if typenames == ['__dotdotdot__']: + return model.unknown_type(decl.name) + + if typenames == ['__dotdotdotint__']: + if self._uses_new_feature is None: + self._uses_new_feature = "'typedef int... %s'" % decl.name + return model.UnknownIntegerType(decl.name) + + if typenames == ['__dotdotdotfloat__']: + # note: not for 'long double' so far + if self._uses_new_feature is None: + self._uses_new_feature = "'typedef float... %s'" % decl.name + return model.UnknownFloatType(decl.name) + + raise FFIError(':%d: unsupported usage of "..." in typedef' + % decl.coord.line) + + def _get_unknown_ptr_type(self, decl): + if decl.type.type.type.names == ['__dotdotdot__']: + return model.unknown_ptr_type(decl.name) + raise FFIError(':%d: unsupported usage of "..." in typedef' + % decl.coord.line) diff --git a/lib/python3.10/site-packages/cffi/error.py b/lib/python3.10/site-packages/cffi/error.py new file mode 100644 index 0000000000000000000000000000000000000000..0a27247c32a381ab7cecedd0f985b781619c1ea5 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/error.py @@ -0,0 +1,31 @@ + +class FFIError(Exception): + __module__ = 'cffi' + +class CDefError(Exception): + __module__ = 'cffi' + def __str__(self): + try: + current_decl = self.args[1] + filename = current_decl.coord.file + linenum = current_decl.coord.line + prefix = '%s:%d: ' % (filename, linenum) + except (AttributeError, TypeError, IndexError): + prefix = '' + return '%s%s' % (prefix, self.args[0]) + +class VerificationError(Exception): + """ An error raised when verification fails + """ + __module__ = 'cffi' + +class VerificationMissing(Exception): + """ An error raised when incomplete structures are passed into + cdef, but no verification has been done + """ + __module__ = 'cffi' + +class PkgConfigError(Exception): + """ An error raised for missing modules in pkg-config + """ + __module__ = 'cffi' diff --git a/lib/python3.10/site-packages/cffi/ffiplatform.py b/lib/python3.10/site-packages/cffi/ffiplatform.py new file mode 100644 index 0000000000000000000000000000000000000000..85313460a69477513c8e00f4df430925f2c4ecc9 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/ffiplatform.py @@ -0,0 +1,127 @@ +import sys, os +from .error import VerificationError + + +LIST_OF_FILE_NAMES = ['sources', 'include_dirs', 'library_dirs', + 'extra_objects', 'depends'] + +def get_extension(srcfilename, modname, sources=(), **kwds): + _hack_at_distutils() + from distutils.core import Extension + allsources = [srcfilename] + for src in sources: + allsources.append(os.path.normpath(src)) + return Extension(name=modname, sources=allsources, **kwds) + +def compile(tmpdir, ext, compiler_verbose=0, debug=None): + """Compile a C extension module using distutils.""" + + _hack_at_distutils() + saved_environ = os.environ.copy() + try: + outputfilename = _build(tmpdir, ext, compiler_verbose, debug) + outputfilename = os.path.abspath(outputfilename) + finally: + # workaround for a distutils bugs where some env vars can + # become longer and longer every time it is used + for key, value in saved_environ.items(): + if os.environ.get(key) != value: + os.environ[key] = value + return outputfilename + +def _build(tmpdir, ext, compiler_verbose=0, debug=None): + # XXX compact but horrible :-( + from distutils.core import Distribution + import distutils.errors, distutils.log + # + dist = Distribution({'ext_modules': [ext]}) + dist.parse_config_files() + options = dist.get_option_dict('build_ext') + if debug is None: + debug = sys.flags.debug + options['debug'] = ('ffiplatform', debug) + options['force'] = ('ffiplatform', True) + options['build_lib'] = ('ffiplatform', tmpdir) + options['build_temp'] = ('ffiplatform', tmpdir) + # + try: + old_level = distutils.log.set_threshold(0) or 0 + try: + distutils.log.set_verbosity(compiler_verbose) + dist.run_command('build_ext') + cmd_obj = dist.get_command_obj('build_ext') + [soname] = cmd_obj.get_outputs() + finally: + distutils.log.set_threshold(old_level) + except (distutils.errors.CompileError, + distutils.errors.LinkError) as e: + raise VerificationError('%s: %s' % (e.__class__.__name__, e)) + # + return soname + +try: + from os.path import samefile +except ImportError: + def samefile(f1, f2): + return os.path.abspath(f1) == os.path.abspath(f2) + +def maybe_relative_path(path): + if not os.path.isabs(path): + return path # already relative + dir = path + names = [] + while True: + prevdir = dir + dir, name = os.path.split(prevdir) + if dir == prevdir or not dir: + return path # failed to make it relative + names.append(name) + try: + if samefile(dir, os.curdir): + names.reverse() + return os.path.join(*names) + except OSError: + pass + +# ____________________________________________________________ + +try: + int_or_long = (int, long) + import cStringIO +except NameError: + int_or_long = int # Python 3 + import io as cStringIO + +def _flatten(x, f): + if isinstance(x, str): + f.write('%ds%s' % (len(x), x)) + elif isinstance(x, dict): + keys = sorted(x.keys()) + f.write('%dd' % len(keys)) + for key in keys: + _flatten(key, f) + _flatten(x[key], f) + elif isinstance(x, (list, tuple)): + f.write('%dl' % len(x)) + for value in x: + _flatten(value, f) + elif isinstance(x, int_or_long): + f.write('%di' % (x,)) + else: + raise TypeError( + "the keywords to verify() contains unsupported object %r" % (x,)) + +def flatten(x): + f = cStringIO.StringIO() + _flatten(x, f) + return f.getvalue() + +def _hack_at_distutils(): + # Windows-only workaround for some configurations: see + # https://bugs.python.org/issue23246 (Python 2.7 with + # a specific MS compiler suite download) + if sys.platform == "win32": + try: + import setuptools # for side-effects, patches distutils + except ImportError: + pass diff --git a/lib/python3.10/site-packages/cffi/lock.py b/lib/python3.10/site-packages/cffi/lock.py new file mode 100644 index 0000000000000000000000000000000000000000..db91b7158c4ee9aa653462fe38e79ed1b553db87 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/lock.py @@ -0,0 +1,30 @@ +import sys + +if sys.version_info < (3,): + try: + from thread import allocate_lock + except ImportError: + from dummy_thread import allocate_lock +else: + try: + from _thread import allocate_lock + except ImportError: + from _dummy_thread import allocate_lock + + +##import sys +##l1 = allocate_lock + +##class allocate_lock(object): +## def __init__(self): +## self._real = l1() +## def __enter__(self): +## for i in range(4, 0, -1): +## print sys._getframe(i).f_code +## print +## return self._real.__enter__() +## def __exit__(self, *args): +## return self._real.__exit__(*args) +## def acquire(self, f): +## assert f is False +## return self._real.acquire(f) diff --git a/lib/python3.10/site-packages/cffi/model.py b/lib/python3.10/site-packages/cffi/model.py new file mode 100644 index 0000000000000000000000000000000000000000..ad1c1764893d0257c0e75eeb61b0a359e89adf0f --- /dev/null +++ b/lib/python3.10/site-packages/cffi/model.py @@ -0,0 +1,617 @@ +import types +import weakref + +from .lock import allocate_lock +from .error import CDefError, VerificationError, VerificationMissing + +# type qualifiers +Q_CONST = 0x01 +Q_RESTRICT = 0x02 +Q_VOLATILE = 0x04 + +def qualify(quals, replace_with): + if quals & Q_CONST: + replace_with = ' const ' + replace_with.lstrip() + if quals & Q_VOLATILE: + replace_with = ' volatile ' + replace_with.lstrip() + if quals & Q_RESTRICT: + # It seems that __restrict is supported by gcc and msvc. + # If you hit some different compiler, add a #define in + # _cffi_include.h for it (and in its copies, documented there) + replace_with = ' __restrict ' + replace_with.lstrip() + return replace_with + + +class BaseTypeByIdentity(object): + is_array_type = False + is_raw_function = False + + def get_c_name(self, replace_with='', context='a C file', quals=0): + result = self.c_name_with_marker + assert result.count('&') == 1 + # some logic duplication with ffi.getctype()... :-( + replace_with = replace_with.strip() + if replace_with: + if replace_with.startswith('*') and '&[' in result: + replace_with = '(%s)' % replace_with + elif not replace_with[0] in '[(': + replace_with = ' ' + replace_with + replace_with = qualify(quals, replace_with) + result = result.replace('&', replace_with) + if '$' in result: + raise VerificationError( + "cannot generate '%s' in %s: unknown type name" + % (self._get_c_name(), context)) + return result + + def _get_c_name(self): + return self.c_name_with_marker.replace('&', '') + + def has_c_name(self): + return '$' not in self._get_c_name() + + def is_integer_type(self): + return False + + def get_cached_btype(self, ffi, finishlist, can_delay=False): + try: + BType = ffi._cached_btypes[self] + except KeyError: + BType = self.build_backend_type(ffi, finishlist) + BType2 = ffi._cached_btypes.setdefault(self, BType) + assert BType2 is BType + return BType + + def __repr__(self): + return '<%s>' % (self._get_c_name(),) + + def _get_items(self): + return [(name, getattr(self, name)) for name in self._attrs_] + + +class BaseType(BaseTypeByIdentity): + + def __eq__(self, other): + return (self.__class__ == other.__class__ and + self._get_items() == other._get_items()) + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.__class__, tuple(self._get_items()))) + + +class VoidType(BaseType): + _attrs_ = () + + def __init__(self): + self.c_name_with_marker = 'void&' + + def build_backend_type(self, ffi, finishlist): + return global_cache(self, ffi, 'new_void_type') + +void_type = VoidType() + + +class BasePrimitiveType(BaseType): + def is_complex_type(self): + return False + + +class PrimitiveType(BasePrimitiveType): + _attrs_ = ('name',) + + ALL_PRIMITIVE_TYPES = { + 'char': 'c', + 'short': 'i', + 'int': 'i', + 'long': 'i', + 'long long': 'i', + 'signed char': 'i', + 'unsigned char': 'i', + 'unsigned short': 'i', + 'unsigned int': 'i', + 'unsigned long': 'i', + 'unsigned long long': 'i', + 'float': 'f', + 'double': 'f', + 'long double': 'f', + 'float _Complex': 'j', + 'double _Complex': 'j', + '_Bool': 'i', + # the following types are not primitive in the C sense + 'wchar_t': 'c', + 'char16_t': 'c', + 'char32_t': 'c', + 'int8_t': 'i', + 'uint8_t': 'i', + 'int16_t': 'i', + 'uint16_t': 'i', + 'int32_t': 'i', + 'uint32_t': 'i', + 'int64_t': 'i', + 'uint64_t': 'i', + 'int_least8_t': 'i', + 'uint_least8_t': 'i', + 'int_least16_t': 'i', + 'uint_least16_t': 'i', + 'int_least32_t': 'i', + 'uint_least32_t': 'i', + 'int_least64_t': 'i', + 'uint_least64_t': 'i', + 'int_fast8_t': 'i', + 'uint_fast8_t': 'i', + 'int_fast16_t': 'i', + 'uint_fast16_t': 'i', + 'int_fast32_t': 'i', + 'uint_fast32_t': 'i', + 'int_fast64_t': 'i', + 'uint_fast64_t': 'i', + 'intptr_t': 'i', + 'uintptr_t': 'i', + 'intmax_t': 'i', + 'uintmax_t': 'i', + 'ptrdiff_t': 'i', + 'size_t': 'i', + 'ssize_t': 'i', + } + + def __init__(self, name): + assert name in self.ALL_PRIMITIVE_TYPES + self.name = name + self.c_name_with_marker = name + '&' + + def is_char_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'c' + def is_integer_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'i' + def is_float_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'f' + def is_complex_type(self): + return self.ALL_PRIMITIVE_TYPES[self.name] == 'j' + + def build_backend_type(self, ffi, finishlist): + return global_cache(self, ffi, 'new_primitive_type', self.name) + + +class UnknownIntegerType(BasePrimitiveType): + _attrs_ = ('name',) + + def __init__(self, name): + self.name = name + self.c_name_with_marker = name + '&' + + def is_integer_type(self): + return True + + def build_backend_type(self, ffi, finishlist): + raise NotImplementedError("integer type '%s' can only be used after " + "compilation" % self.name) + +class UnknownFloatType(BasePrimitiveType): + _attrs_ = ('name', ) + + def __init__(self, name): + self.name = name + self.c_name_with_marker = name + '&' + + def build_backend_type(self, ffi, finishlist): + raise NotImplementedError("float type '%s' can only be used after " + "compilation" % self.name) + + +class BaseFunctionType(BaseType): + _attrs_ = ('args', 'result', 'ellipsis', 'abi') + + def __init__(self, args, result, ellipsis, abi=None): + self.args = args + self.result = result + self.ellipsis = ellipsis + self.abi = abi + # + reprargs = [arg._get_c_name() for arg in self.args] + if self.ellipsis: + reprargs.append('...') + reprargs = reprargs or ['void'] + replace_with = self._base_pattern % (', '.join(reprargs),) + if abi is not None: + replace_with = replace_with[:1] + abi + ' ' + replace_with[1:] + self.c_name_with_marker = ( + self.result.c_name_with_marker.replace('&', replace_with)) + + +class RawFunctionType(BaseFunctionType): + # Corresponds to a C type like 'int(int)', which is the C type of + # a function, but not a pointer-to-function. The backend has no + # notion of such a type; it's used temporarily by parsing. + _base_pattern = '(&)(%s)' + is_raw_function = True + + def build_backend_type(self, ffi, finishlist): + raise CDefError("cannot render the type %r: it is a function " + "type, not a pointer-to-function type" % (self,)) + + def as_function_pointer(self): + return FunctionPtrType(self.args, self.result, self.ellipsis, self.abi) + + +class FunctionPtrType(BaseFunctionType): + _base_pattern = '(*&)(%s)' + + def build_backend_type(self, ffi, finishlist): + result = self.result.get_cached_btype(ffi, finishlist) + args = [] + for tp in self.args: + args.append(tp.get_cached_btype(ffi, finishlist)) + abi_args = () + if self.abi == "__stdcall": + if not self.ellipsis: # __stdcall ignored for variadic funcs + try: + abi_args = (ffi._backend.FFI_STDCALL,) + except AttributeError: + pass + return global_cache(self, ffi, 'new_function_type', + tuple(args), result, self.ellipsis, *abi_args) + + def as_raw_function(self): + return RawFunctionType(self.args, self.result, self.ellipsis, self.abi) + + +class PointerType(BaseType): + _attrs_ = ('totype', 'quals') + + def __init__(self, totype, quals=0): + self.totype = totype + self.quals = quals + extra = qualify(quals, " *&") + if totype.is_array_type: + extra = "(%s)" % (extra.lstrip(),) + self.c_name_with_marker = totype.c_name_with_marker.replace('&', extra) + + def build_backend_type(self, ffi, finishlist): + BItem = self.totype.get_cached_btype(ffi, finishlist, can_delay=True) + return global_cache(self, ffi, 'new_pointer_type', BItem) + +voidp_type = PointerType(void_type) + +def ConstPointerType(totype): + return PointerType(totype, Q_CONST) + +const_voidp_type = ConstPointerType(void_type) + + +class NamedPointerType(PointerType): + _attrs_ = ('totype', 'name') + + def __init__(self, totype, name, quals=0): + PointerType.__init__(self, totype, quals) + self.name = name + self.c_name_with_marker = name + '&' + + +class ArrayType(BaseType): + _attrs_ = ('item', 'length') + is_array_type = True + + def __init__(self, item, length): + self.item = item + self.length = length + # + if length is None: + brackets = '&[]' + elif length == '...': + brackets = '&[/*...*/]' + else: + brackets = '&[%s]' % length + self.c_name_with_marker = ( + self.item.c_name_with_marker.replace('&', brackets)) + + def length_is_unknown(self): + return isinstance(self.length, str) + + def resolve_length(self, newlength): + return ArrayType(self.item, newlength) + + def build_backend_type(self, ffi, finishlist): + if self.length_is_unknown(): + raise CDefError("cannot render the type %r: unknown length" % + (self,)) + self.item.get_cached_btype(ffi, finishlist) # force the item BType + BPtrItem = PointerType(self.item).get_cached_btype(ffi, finishlist) + return global_cache(self, ffi, 'new_array_type', BPtrItem, self.length) + +char_array_type = ArrayType(PrimitiveType('char'), None) + + +class StructOrUnionOrEnum(BaseTypeByIdentity): + _attrs_ = ('name',) + forcename = None + + def build_c_name_with_marker(self): + name = self.forcename or '%s %s' % (self.kind, self.name) + self.c_name_with_marker = name + '&' + + def force_the_name(self, forcename): + self.forcename = forcename + self.build_c_name_with_marker() + + def get_official_name(self): + assert self.c_name_with_marker.endswith('&') + return self.c_name_with_marker[:-1] + + +class StructOrUnion(StructOrUnionOrEnum): + fixedlayout = None + completed = 0 + partial = False + packed = 0 + + def __init__(self, name, fldnames, fldtypes, fldbitsize, fldquals=None): + self.name = name + self.fldnames = fldnames + self.fldtypes = fldtypes + self.fldbitsize = fldbitsize + self.fldquals = fldquals + self.build_c_name_with_marker() + + def anonymous_struct_fields(self): + if self.fldtypes is not None: + for name, type in zip(self.fldnames, self.fldtypes): + if name == '' and isinstance(type, StructOrUnion): + yield type + + def enumfields(self, expand_anonymous_struct_union=True): + fldquals = self.fldquals + if fldquals is None: + fldquals = (0,) * len(self.fldnames) + for name, type, bitsize, quals in zip(self.fldnames, self.fldtypes, + self.fldbitsize, fldquals): + if (name == '' and isinstance(type, StructOrUnion) + and expand_anonymous_struct_union): + # nested anonymous struct/union + for result in type.enumfields(): + yield result + else: + yield (name, type, bitsize, quals) + + def force_flatten(self): + # force the struct or union to have a declaration that lists + # directly all fields returned by enumfields(), flattening + # nested anonymous structs/unions. + names = [] + types = [] + bitsizes = [] + fldquals = [] + for name, type, bitsize, quals in self.enumfields(): + names.append(name) + types.append(type) + bitsizes.append(bitsize) + fldquals.append(quals) + self.fldnames = tuple(names) + self.fldtypes = tuple(types) + self.fldbitsize = tuple(bitsizes) + self.fldquals = tuple(fldquals) + + def get_cached_btype(self, ffi, finishlist, can_delay=False): + BType = StructOrUnionOrEnum.get_cached_btype(self, ffi, finishlist, + can_delay) + if not can_delay: + self.finish_backend_type(ffi, finishlist) + return BType + + def finish_backend_type(self, ffi, finishlist): + if self.completed: + if self.completed != 2: + raise NotImplementedError("recursive structure declaration " + "for '%s'" % (self.name,)) + return + BType = ffi._cached_btypes[self] + # + self.completed = 1 + # + if self.fldtypes is None: + pass # not completing it: it's an opaque struct + # + elif self.fixedlayout is None: + fldtypes = [tp.get_cached_btype(ffi, finishlist) + for tp in self.fldtypes] + lst = list(zip(self.fldnames, fldtypes, self.fldbitsize)) + extra_flags = () + if self.packed: + if self.packed == 1: + extra_flags = (8,) # SF_PACKED + else: + extra_flags = (0, self.packed) + ffi._backend.complete_struct_or_union(BType, lst, self, + -1, -1, *extra_flags) + # + else: + fldtypes = [] + fieldofs, fieldsize, totalsize, totalalignment = self.fixedlayout + for i in range(len(self.fldnames)): + fsize = fieldsize[i] + ftype = self.fldtypes[i] + # + if isinstance(ftype, ArrayType) and ftype.length_is_unknown(): + # fix the length to match the total size + BItemType = ftype.item.get_cached_btype(ffi, finishlist) + nlen, nrest = divmod(fsize, ffi.sizeof(BItemType)) + if nrest != 0: + self._verification_error( + "field '%s.%s' has a bogus size?" % ( + self.name, self.fldnames[i] or '{}')) + ftype = ftype.resolve_length(nlen) + self.fldtypes = (self.fldtypes[:i] + (ftype,) + + self.fldtypes[i+1:]) + # + BFieldType = ftype.get_cached_btype(ffi, finishlist) + if isinstance(ftype, ArrayType) and ftype.length is None: + assert fsize == 0 + else: + bitemsize = ffi.sizeof(BFieldType) + if bitemsize != fsize: + self._verification_error( + "field '%s.%s' is declared as %d bytes, but is " + "really %d bytes" % (self.name, + self.fldnames[i] or '{}', + bitemsize, fsize)) + fldtypes.append(BFieldType) + # + lst = list(zip(self.fldnames, fldtypes, self.fldbitsize, fieldofs)) + ffi._backend.complete_struct_or_union(BType, lst, self, + totalsize, totalalignment) + self.completed = 2 + + def _verification_error(self, msg): + raise VerificationError(msg) + + def check_not_partial(self): + if self.partial and self.fixedlayout is None: + raise VerificationMissing(self._get_c_name()) + + def build_backend_type(self, ffi, finishlist): + self.check_not_partial() + finishlist.append(self) + # + return global_cache(self, ffi, 'new_%s_type' % self.kind, + self.get_official_name(), key=self) + + +class StructType(StructOrUnion): + kind = 'struct' + + +class UnionType(StructOrUnion): + kind = 'union' + + +class EnumType(StructOrUnionOrEnum): + kind = 'enum' + partial = False + partial_resolved = False + + def __init__(self, name, enumerators, enumvalues, baseinttype=None): + self.name = name + self.enumerators = enumerators + self.enumvalues = enumvalues + self.baseinttype = baseinttype + self.build_c_name_with_marker() + + def force_the_name(self, forcename): + StructOrUnionOrEnum.force_the_name(self, forcename) + if self.forcename is None: + name = self.get_official_name() + self.forcename = '$' + name.replace(' ', '_') + + def check_not_partial(self): + if self.partial and not self.partial_resolved: + raise VerificationMissing(self._get_c_name()) + + def build_backend_type(self, ffi, finishlist): + self.check_not_partial() + base_btype = self.build_baseinttype(ffi, finishlist) + return global_cache(self, ffi, 'new_enum_type', + self.get_official_name(), + self.enumerators, self.enumvalues, + base_btype, key=self) + + def build_baseinttype(self, ffi, finishlist): + if self.baseinttype is not None: + return self.baseinttype.get_cached_btype(ffi, finishlist) + # + if self.enumvalues: + smallest_value = min(self.enumvalues) + largest_value = max(self.enumvalues) + else: + import warnings + try: + # XXX! The goal is to ensure that the warnings.warn() + # will not suppress the warning. We want to get it + # several times if we reach this point several times. + __warningregistry__.clear() + except NameError: + pass + warnings.warn("%r has no values explicitly defined; " + "guessing that it is equivalent to 'unsigned int'" + % self._get_c_name()) + smallest_value = largest_value = 0 + if smallest_value < 0: # needs a signed type + sign = 1 + candidate1 = PrimitiveType("int") + candidate2 = PrimitiveType("long") + else: + sign = 0 + candidate1 = PrimitiveType("unsigned int") + candidate2 = PrimitiveType("unsigned long") + btype1 = candidate1.get_cached_btype(ffi, finishlist) + btype2 = candidate2.get_cached_btype(ffi, finishlist) + size1 = ffi.sizeof(btype1) + size2 = ffi.sizeof(btype2) + if (smallest_value >= ((-1) << (8*size1-1)) and + largest_value < (1 << (8*size1-sign))): + return btype1 + if (smallest_value >= ((-1) << (8*size2-1)) and + largest_value < (1 << (8*size2-sign))): + return btype2 + raise CDefError("%s values don't all fit into either 'long' " + "or 'unsigned long'" % self._get_c_name()) + +def unknown_type(name, structname=None): + if structname is None: + structname = '$%s' % name + tp = StructType(structname, None, None, None) + tp.force_the_name(name) + tp.origin = "unknown_type" + return tp + +def unknown_ptr_type(name, structname=None): + if structname is None: + structname = '$$%s' % name + tp = StructType(structname, None, None, None) + return NamedPointerType(tp, name) + + +global_lock = allocate_lock() +_typecache_cffi_backend = weakref.WeakValueDictionary() + +def get_typecache(backend): + # returns _typecache_cffi_backend if backend is the _cffi_backend + # module, or type(backend).__typecache if backend is an instance of + # CTypesBackend (or some FakeBackend class during tests) + if isinstance(backend, types.ModuleType): + return _typecache_cffi_backend + with global_lock: + if not hasattr(type(backend), '__typecache'): + type(backend).__typecache = weakref.WeakValueDictionary() + return type(backend).__typecache + +def global_cache(srctype, ffi, funcname, *args, **kwds): + key = kwds.pop('key', (funcname, args)) + assert not kwds + try: + return ffi._typecache[key] + except KeyError: + pass + try: + res = getattr(ffi._backend, funcname)(*args) + except NotImplementedError as e: + raise NotImplementedError("%s: %r: %s" % (funcname, srctype, e)) + # note that setdefault() on WeakValueDictionary is not atomic + # and contains a rare bug (http://bugs.python.org/issue19542); + # we have to use a lock and do it ourselves + cache = ffi._typecache + with global_lock: + res1 = cache.get(key) + if res1 is None: + cache[key] = res + return res + else: + return res1 + +def pointer_cache(ffi, BType): + return global_cache('?', ffi, 'new_pointer_type', BType) + +def attach_exception_info(e, name): + if e.args and type(e.args[0]) is str: + e.args = ('%s: %s' % (name, e.args[0]),) + e.args[1:] diff --git a/lib/python3.10/site-packages/cffi/parse_c_type.h b/lib/python3.10/site-packages/cffi/parse_c_type.h new file mode 100644 index 0000000000000000000000000000000000000000..84e4ef85659eb63e6453d8af9f024f1866182342 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/parse_c_type.h @@ -0,0 +1,181 @@ + +/* This part is from file 'cffi/parse_c_type.h'. It is copied at the + beginning of C sources generated by CFFI's ffi.set_source(). */ + +typedef void *_cffi_opcode_t; + +#define _CFFI_OP(opcode, arg) (_cffi_opcode_t)(opcode | (((uintptr_t)(arg)) << 8)) +#define _CFFI_GETOP(cffi_opcode) ((unsigned char)(uintptr_t)cffi_opcode) +#define _CFFI_GETARG(cffi_opcode) (((intptr_t)cffi_opcode) >> 8) + +#define _CFFI_OP_PRIMITIVE 1 +#define _CFFI_OP_POINTER 3 +#define _CFFI_OP_ARRAY 5 +#define _CFFI_OP_OPEN_ARRAY 7 +#define _CFFI_OP_STRUCT_UNION 9 +#define _CFFI_OP_ENUM 11 +#define _CFFI_OP_FUNCTION 13 +#define _CFFI_OP_FUNCTION_END 15 +#define _CFFI_OP_NOOP 17 +#define _CFFI_OP_BITFIELD 19 +#define _CFFI_OP_TYPENAME 21 +#define _CFFI_OP_CPYTHON_BLTN_V 23 // varargs +#define _CFFI_OP_CPYTHON_BLTN_N 25 // noargs +#define _CFFI_OP_CPYTHON_BLTN_O 27 // O (i.e. a single arg) +#define _CFFI_OP_CONSTANT 29 +#define _CFFI_OP_CONSTANT_INT 31 +#define _CFFI_OP_GLOBAL_VAR 33 +#define _CFFI_OP_DLOPEN_FUNC 35 +#define _CFFI_OP_DLOPEN_CONST 37 +#define _CFFI_OP_GLOBAL_VAR_F 39 +#define _CFFI_OP_EXTERN_PYTHON 41 + +#define _CFFI_PRIM_VOID 0 +#define _CFFI_PRIM_BOOL 1 +#define _CFFI_PRIM_CHAR 2 +#define _CFFI_PRIM_SCHAR 3 +#define _CFFI_PRIM_UCHAR 4 +#define _CFFI_PRIM_SHORT 5 +#define _CFFI_PRIM_USHORT 6 +#define _CFFI_PRIM_INT 7 +#define _CFFI_PRIM_UINT 8 +#define _CFFI_PRIM_LONG 9 +#define _CFFI_PRIM_ULONG 10 +#define _CFFI_PRIM_LONGLONG 11 +#define _CFFI_PRIM_ULONGLONG 12 +#define _CFFI_PRIM_FLOAT 13 +#define _CFFI_PRIM_DOUBLE 14 +#define _CFFI_PRIM_LONGDOUBLE 15 + +#define _CFFI_PRIM_WCHAR 16 +#define _CFFI_PRIM_INT8 17 +#define _CFFI_PRIM_UINT8 18 +#define _CFFI_PRIM_INT16 19 +#define _CFFI_PRIM_UINT16 20 +#define _CFFI_PRIM_INT32 21 +#define _CFFI_PRIM_UINT32 22 +#define _CFFI_PRIM_INT64 23 +#define _CFFI_PRIM_UINT64 24 +#define _CFFI_PRIM_INTPTR 25 +#define _CFFI_PRIM_UINTPTR 26 +#define _CFFI_PRIM_PTRDIFF 27 +#define _CFFI_PRIM_SIZE 28 +#define _CFFI_PRIM_SSIZE 29 +#define _CFFI_PRIM_INT_LEAST8 30 +#define _CFFI_PRIM_UINT_LEAST8 31 +#define _CFFI_PRIM_INT_LEAST16 32 +#define _CFFI_PRIM_UINT_LEAST16 33 +#define _CFFI_PRIM_INT_LEAST32 34 +#define _CFFI_PRIM_UINT_LEAST32 35 +#define _CFFI_PRIM_INT_LEAST64 36 +#define _CFFI_PRIM_UINT_LEAST64 37 +#define _CFFI_PRIM_INT_FAST8 38 +#define _CFFI_PRIM_UINT_FAST8 39 +#define _CFFI_PRIM_INT_FAST16 40 +#define _CFFI_PRIM_UINT_FAST16 41 +#define _CFFI_PRIM_INT_FAST32 42 +#define _CFFI_PRIM_UINT_FAST32 43 +#define _CFFI_PRIM_INT_FAST64 44 +#define _CFFI_PRIM_UINT_FAST64 45 +#define _CFFI_PRIM_INTMAX 46 +#define _CFFI_PRIM_UINTMAX 47 +#define _CFFI_PRIM_FLOATCOMPLEX 48 +#define _CFFI_PRIM_DOUBLECOMPLEX 49 +#define _CFFI_PRIM_CHAR16 50 +#define _CFFI_PRIM_CHAR32 51 + +#define _CFFI__NUM_PRIM 52 +#define _CFFI__UNKNOWN_PRIM (-1) +#define _CFFI__UNKNOWN_FLOAT_PRIM (-2) +#define _CFFI__UNKNOWN_LONG_DOUBLE (-3) + +#define _CFFI__IO_FILE_STRUCT (-1) + + +struct _cffi_global_s { + const char *name; + void *address; + _cffi_opcode_t type_op; + void *size_or_direct_fn; // OP_GLOBAL_VAR: size, or 0 if unknown + // OP_CPYTHON_BLTN_*: addr of direct function +}; + +struct _cffi_getconst_s { + unsigned long long value; + const struct _cffi_type_context_s *ctx; + int gindex; +}; + +struct _cffi_struct_union_s { + const char *name; + int type_index; // -> _cffi_types, on a OP_STRUCT_UNION + int flags; // _CFFI_F_* flags below + size_t size; + int alignment; + int first_field_index; // -> _cffi_fields array + int num_fields; +}; +#define _CFFI_F_UNION 0x01 // is a union, not a struct +#define _CFFI_F_CHECK_FIELDS 0x02 // complain if fields are not in the + // "standard layout" or if some are missing +#define _CFFI_F_PACKED 0x04 // for CHECK_FIELDS, assume a packed struct +#define _CFFI_F_EXTERNAL 0x08 // in some other ffi.include() +#define _CFFI_F_OPAQUE 0x10 // opaque + +struct _cffi_field_s { + const char *name; + size_t field_offset; + size_t field_size; + _cffi_opcode_t field_type_op; +}; + +struct _cffi_enum_s { + const char *name; + int type_index; // -> _cffi_types, on a OP_ENUM + int type_prim; // _CFFI_PRIM_xxx + const char *enumerators; // comma-delimited string +}; + +struct _cffi_typename_s { + const char *name; + int type_index; /* if opaque, points to a possibly artificial + OP_STRUCT which is itself opaque */ +}; + +struct _cffi_type_context_s { + _cffi_opcode_t *types; + const struct _cffi_global_s *globals; + const struct _cffi_field_s *fields; + const struct _cffi_struct_union_s *struct_unions; + const struct _cffi_enum_s *enums; + const struct _cffi_typename_s *typenames; + int num_globals; + int num_struct_unions; + int num_enums; + int num_typenames; + const char *const *includes; + int num_types; + int flags; /* future extension */ +}; + +struct _cffi_parse_info_s { + const struct _cffi_type_context_s *ctx; + _cffi_opcode_t *output; + unsigned int output_size; + size_t error_location; + const char *error_message; +}; + +struct _cffi_externpy_s { + const char *name; + size_t size_of_result; + void *reserved1, *reserved2; +}; + +#ifdef _CFFI_INTERNAL +static int parse_c_type(struct _cffi_parse_info_s *info, const char *input); +static int search_in_globals(const struct _cffi_type_context_s *ctx, + const char *search, size_t search_len); +static int search_in_struct_unions(const struct _cffi_type_context_s *ctx, + const char *search, size_t search_len); +#endif diff --git a/lib/python3.10/site-packages/cffi/pkgconfig.py b/lib/python3.10/site-packages/cffi/pkgconfig.py new file mode 100644 index 0000000000000000000000000000000000000000..5c93f15a60e6f904b2dd108d6e22044a5890bcb4 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/pkgconfig.py @@ -0,0 +1,121 @@ +# pkg-config, https://www.freedesktop.org/wiki/Software/pkg-config/ integration for cffi +import sys, os, subprocess + +from .error import PkgConfigError + + +def merge_flags(cfg1, cfg2): + """Merge values from cffi config flags cfg2 to cf1 + + Example: + merge_flags({"libraries": ["one"]}, {"libraries": ["two"]}) + {"libraries": ["one", "two"]} + """ + for key, value in cfg2.items(): + if key not in cfg1: + cfg1[key] = value + else: + if not isinstance(cfg1[key], list): + raise TypeError("cfg1[%r] should be a list of strings" % (key,)) + if not isinstance(value, list): + raise TypeError("cfg2[%r] should be a list of strings" % (key,)) + cfg1[key].extend(value) + return cfg1 + + +def call(libname, flag, encoding=sys.getfilesystemencoding()): + """Calls pkg-config and returns the output if found + """ + a = ["pkg-config", "--print-errors"] + a.append(flag) + a.append(libname) + try: + pc = subprocess.Popen(a, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except EnvironmentError as e: + raise PkgConfigError("cannot run pkg-config: %s" % (str(e).strip(),)) + + bout, berr = pc.communicate() + if pc.returncode != 0: + try: + berr = berr.decode(encoding) + except Exception: + pass + raise PkgConfigError(berr.strip()) + + if sys.version_info >= (3,) and not isinstance(bout, str): # Python 3.x + try: + bout = bout.decode(encoding) + except UnicodeDecodeError: + raise PkgConfigError("pkg-config %s %s returned bytes that cannot " + "be decoded with encoding %r:\n%r" % + (flag, libname, encoding, bout)) + + if os.altsep != '\\' and '\\' in bout: + raise PkgConfigError("pkg-config %s %s returned an unsupported " + "backslash-escaped output:\n%r" % + (flag, libname, bout)) + return bout + + +def flags_from_pkgconfig(libs): + r"""Return compiler line flags for FFI.set_source based on pkg-config output + + Usage + ... + ffibuilder.set_source("_foo", pkgconfig = ["libfoo", "libbar >= 1.8.3"]) + + If pkg-config is installed on build machine, then arguments include_dirs, + library_dirs, libraries, define_macros, extra_compile_args and + extra_link_args are extended with an output of pkg-config for libfoo and + libbar. + + Raises PkgConfigError in case the pkg-config call fails. + """ + + def get_include_dirs(string): + return [x[2:] for x in string.split() if x.startswith("-I")] + + def get_library_dirs(string): + return [x[2:] for x in string.split() if x.startswith("-L")] + + def get_libraries(string): + return [x[2:] for x in string.split() if x.startswith("-l")] + + # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by distutils + def get_macros(string): + def _macro(x): + x = x[2:] # drop "-D" + if '=' in x: + return tuple(x.split("=", 1)) # "-Dfoo=bar" => ("foo", "bar") + else: + return (x, None) # "-Dfoo" => ("foo", None) + return [_macro(x) for x in string.split() if x.startswith("-D")] + + def get_other_cflags(string): + return [x for x in string.split() if not x.startswith("-I") and + not x.startswith("-D")] + + def get_other_libs(string): + return [x for x in string.split() if not x.startswith("-L") and + not x.startswith("-l")] + + # return kwargs for given libname + def kwargs(libname): + fse = sys.getfilesystemencoding() + all_cflags = call(libname, "--cflags") + all_libs = call(libname, "--libs") + return { + "include_dirs": get_include_dirs(all_cflags), + "library_dirs": get_library_dirs(all_libs), + "libraries": get_libraries(all_libs), + "define_macros": get_macros(all_cflags), + "extra_compile_args": get_other_cflags(all_cflags), + "extra_link_args": get_other_libs(all_libs), + } + + # merge all arguments together + ret = {} + for libname in libs: + lib_flags = kwargs(libname) + merge_flags(ret, lib_flags) + return ret diff --git a/lib/python3.10/site-packages/cffi/recompiler.py b/lib/python3.10/site-packages/cffi/recompiler.py new file mode 100644 index 0000000000000000000000000000000000000000..5d9d32d7132027562c5a29405d625899611bc977 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/recompiler.py @@ -0,0 +1,1581 @@ +import os, sys, io +from . import ffiplatform, model +from .error import VerificationError +from .cffi_opcode import * + +VERSION_BASE = 0x2601 +VERSION_EMBEDDED = 0x2701 +VERSION_CHAR16CHAR32 = 0x2801 + +USE_LIMITED_API = (sys.platform != 'win32' or sys.version_info < (3, 0) or + sys.version_info >= (3, 5)) + + +class GlobalExpr: + def __init__(self, name, address, type_op, size=0, check_value=0): + self.name = name + self.address = address + self.type_op = type_op + self.size = size + self.check_value = check_value + + def as_c_expr(self): + return ' { "%s", (void *)%s, %s, (void *)%s },' % ( + self.name, self.address, self.type_op.as_c_expr(), self.size) + + def as_python_expr(self): + return "b'%s%s',%d" % (self.type_op.as_python_bytes(), self.name, + self.check_value) + +class FieldExpr: + def __init__(self, name, field_offset, field_size, fbitsize, field_type_op): + self.name = name + self.field_offset = field_offset + self.field_size = field_size + self.fbitsize = fbitsize + self.field_type_op = field_type_op + + def as_c_expr(self): + spaces = " " * len(self.name) + return (' { "%s", %s,\n' % (self.name, self.field_offset) + + ' %s %s,\n' % (spaces, self.field_size) + + ' %s %s },' % (spaces, self.field_type_op.as_c_expr())) + + def as_python_expr(self): + raise NotImplementedError + + def as_field_python_expr(self): + if self.field_type_op.op == OP_NOOP: + size_expr = '' + elif self.field_type_op.op == OP_BITFIELD: + size_expr = format_four_bytes(self.fbitsize) + else: + raise NotImplementedError + return "b'%s%s%s'" % (self.field_type_op.as_python_bytes(), + size_expr, + self.name) + +class StructUnionExpr: + def __init__(self, name, type_index, flags, size, alignment, comment, + first_field_index, c_fields): + self.name = name + self.type_index = type_index + self.flags = flags + self.size = size + self.alignment = alignment + self.comment = comment + self.first_field_index = first_field_index + self.c_fields = c_fields + + def as_c_expr(self): + return (' { "%s", %d, %s,' % (self.name, self.type_index, self.flags) + + '\n %s, %s, ' % (self.size, self.alignment) + + '%d, %d ' % (self.first_field_index, len(self.c_fields)) + + ('/* %s */ ' % self.comment if self.comment else '') + + '},') + + def as_python_expr(self): + flags = eval(self.flags, G_FLAGS) + fields_expr = [c_field.as_field_python_expr() + for c_field in self.c_fields] + return "(b'%s%s%s',%s)" % ( + format_four_bytes(self.type_index), + format_four_bytes(flags), + self.name, + ','.join(fields_expr)) + +class EnumExpr: + def __init__(self, name, type_index, size, signed, allenums): + self.name = name + self.type_index = type_index + self.size = size + self.signed = signed + self.allenums = allenums + + def as_c_expr(self): + return (' { "%s", %d, _cffi_prim_int(%s, %s),\n' + ' "%s" },' % (self.name, self.type_index, + self.size, self.signed, self.allenums)) + + def as_python_expr(self): + prim_index = { + (1, 0): PRIM_UINT8, (1, 1): PRIM_INT8, + (2, 0): PRIM_UINT16, (2, 1): PRIM_INT16, + (4, 0): PRIM_UINT32, (4, 1): PRIM_INT32, + (8, 0): PRIM_UINT64, (8, 1): PRIM_INT64, + }[self.size, self.signed] + return "b'%s%s%s\\x00%s'" % (format_four_bytes(self.type_index), + format_four_bytes(prim_index), + self.name, self.allenums) + +class TypenameExpr: + def __init__(self, name, type_index): + self.name = name + self.type_index = type_index + + def as_c_expr(self): + return ' { "%s", %d },' % (self.name, self.type_index) + + def as_python_expr(self): + return "b'%s%s'" % (format_four_bytes(self.type_index), self.name) + + +# ____________________________________________________________ + + +class Recompiler: + _num_externpy = 0 + + def __init__(self, ffi, module_name, target_is_python=False): + self.ffi = ffi + self.module_name = module_name + self.target_is_python = target_is_python + self._version = VERSION_BASE + + def needs_version(self, ver): + self._version = max(self._version, ver) + + def collect_type_table(self): + self._typesdict = {} + self._generate("collecttype") + # + all_decls = sorted(self._typesdict, key=str) + # + # prepare all FUNCTION bytecode sequences first + self.cffi_types = [] + for tp in all_decls: + if tp.is_raw_function: + assert self._typesdict[tp] is None + self._typesdict[tp] = len(self.cffi_types) + self.cffi_types.append(tp) # placeholder + for tp1 in tp.args: + assert isinstance(tp1, (model.VoidType, + model.BasePrimitiveType, + model.PointerType, + model.StructOrUnionOrEnum, + model.FunctionPtrType)) + if self._typesdict[tp1] is None: + self._typesdict[tp1] = len(self.cffi_types) + self.cffi_types.append(tp1) # placeholder + self.cffi_types.append('END') # placeholder + # + # prepare all OTHER bytecode sequences + for tp in all_decls: + if not tp.is_raw_function and self._typesdict[tp] is None: + self._typesdict[tp] = len(self.cffi_types) + self.cffi_types.append(tp) # placeholder + if tp.is_array_type and tp.length is not None: + self.cffi_types.append('LEN') # placeholder + assert None not in self._typesdict.values() + # + # collect all structs and unions and enums + self._struct_unions = {} + self._enums = {} + for tp in all_decls: + if isinstance(tp, model.StructOrUnion): + self._struct_unions[tp] = None + elif isinstance(tp, model.EnumType): + self._enums[tp] = None + for i, tp in enumerate(sorted(self._struct_unions, + key=lambda tp: tp.name)): + self._struct_unions[tp] = i + for i, tp in enumerate(sorted(self._enums, + key=lambda tp: tp.name)): + self._enums[tp] = i + # + # emit all bytecode sequences now + for tp in all_decls: + method = getattr(self, '_emit_bytecode_' + tp.__class__.__name__) + method(tp, self._typesdict[tp]) + # + # consistency check + for op in self.cffi_types: + assert isinstance(op, CffiOp) + self.cffi_types = tuple(self.cffi_types) # don't change any more + + def _enum_fields(self, tp): + # When producing C, expand all anonymous struct/union fields. + # That's necessary to have C code checking the offsets of the + # individual fields contained in them. When producing Python, + # don't do it and instead write it like it is, with the + # corresponding fields having an empty name. Empty names are + # recognized at runtime when we import the generated Python + # file. + expand_anonymous_struct_union = not self.target_is_python + return tp.enumfields(expand_anonymous_struct_union) + + def _do_collect_type(self, tp): + if not isinstance(tp, model.BaseTypeByIdentity): + if isinstance(tp, tuple): + for x in tp: + self._do_collect_type(x) + return + if tp not in self._typesdict: + self._typesdict[tp] = None + if isinstance(tp, model.FunctionPtrType): + self._do_collect_type(tp.as_raw_function()) + elif isinstance(tp, model.StructOrUnion): + if tp.fldtypes is not None and ( + tp not in self.ffi._parser._included_declarations): + for name1, tp1, _, _ in self._enum_fields(tp): + self._do_collect_type(self._field_type(tp, name1, tp1)) + else: + for _, x in tp._get_items(): + self._do_collect_type(x) + + def _generate(self, step_name): + lst = self.ffi._parser._declarations.items() + for name, (tp, quals) in sorted(lst): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_cpy_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in recompile(): %r" % name) + try: + self._current_quals = quals + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + # ---------- + + ALL_STEPS = ["global", "field", "struct_union", "enum", "typename"] + + def collect_step_tables(self): + # collect the declarations for '_cffi_globals', '_cffi_typenames', etc. + self._lsts = {} + for step_name in self.ALL_STEPS: + self._lsts[step_name] = [] + self._seen_struct_unions = set() + self._generate("ctx") + self._add_missing_struct_unions() + # + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + if step_name != "field": + lst.sort(key=lambda entry: entry.name) + self._lsts[step_name] = tuple(lst) # don't change any more + # + # check for a possible internal inconsistency: _cffi_struct_unions + # should have been generated with exactly self._struct_unions + lst = self._lsts["struct_union"] + for tp, i in self._struct_unions.items(): + assert i < len(lst) + assert lst[i].name == tp.name + assert len(lst) == len(self._struct_unions) + # same with enums + lst = self._lsts["enum"] + for tp, i in self._enums.items(): + assert i < len(lst) + assert lst[i].name == tp.name + assert len(lst) == len(self._enums) + + # ---------- + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def write_source_to_f(self, f, preamble): + if self.target_is_python: + assert preamble is None + self.write_py_source_to_f(f) + else: + assert preamble is not None + self.write_c_source_to_f(f, preamble) + + def _rel_readlines(self, filename): + g = open(os.path.join(os.path.dirname(__file__), filename), 'r') + lines = g.readlines() + g.close() + return lines + + def write_c_source_to_f(self, f, preamble): + self._f = f + prnt = self._prnt + if self.ffi._embedding is not None: + prnt('#define _CFFI_USE_EMBEDDING') + if not USE_LIMITED_API: + prnt('#define _CFFI_NO_LIMITED_API') + # + # first the '#include' (actually done by inlining the file's content) + lines = self._rel_readlines('_cffi_include.h') + i = lines.index('#include "parse_c_type.h"\n') + lines[i:i+1] = self._rel_readlines('parse_c_type.h') + prnt(''.join(lines)) + # + # if we have ffi._embedding != None, we give it here as a macro + # and include an extra file + base_module_name = self.module_name.split('.')[-1] + if self.ffi._embedding is not None: + prnt('#define _CFFI_MODULE_NAME "%s"' % (self.module_name,)) + prnt('static const char _CFFI_PYTHON_STARTUP_CODE[] = {') + self._print_string_literal_in_array(self.ffi._embedding) + prnt('0 };') + prnt('#ifdef PYPY_VERSION') + prnt('# define _CFFI_PYTHON_STARTUP_FUNC _cffi_pypyinit_%s' % ( + base_module_name,)) + prnt('#elif PY_MAJOR_VERSION >= 3') + prnt('# define _CFFI_PYTHON_STARTUP_FUNC PyInit_%s' % ( + base_module_name,)) + prnt('#else') + prnt('# define _CFFI_PYTHON_STARTUP_FUNC init%s' % ( + base_module_name,)) + prnt('#endif') + lines = self._rel_readlines('_embedding.h') + i = lines.index('#include "_cffi_errors.h"\n') + lines[i:i+1] = self._rel_readlines('_cffi_errors.h') + prnt(''.join(lines)) + self.needs_version(VERSION_EMBEDDED) + # + # then paste the C source given by the user, verbatim. + prnt('/************************************************************/') + prnt() + prnt(preamble) + prnt() + prnt('/************************************************************/') + prnt() + # + # the declaration of '_cffi_types' + prnt('static void *_cffi_types[] = {') + typeindex2type = dict([(i, tp) for (tp, i) in self._typesdict.items()]) + for i, op in enumerate(self.cffi_types): + comment = '' + if i in typeindex2type: + comment = ' // ' + typeindex2type[i]._get_c_name() + prnt('/* %2d */ %s,%s' % (i, op.as_c_expr(), comment)) + if not self.cffi_types: + prnt(' 0') + prnt('};') + prnt() + # + # call generate_cpy_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._seen_constants = set() + self._generate("decl") + # + # the declaration of '_cffi_globals' and '_cffi_typenames' + nums = {} + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + nums[step_name] = len(lst) + if nums[step_name] > 0: + prnt('static const struct _cffi_%s_s _cffi_%ss[] = {' % ( + step_name, step_name)) + for entry in lst: + prnt(entry.as_c_expr()) + prnt('};') + prnt() + # + # the declaration of '_cffi_includes' + if self.ffi._included_ffis: + prnt('static const char * const _cffi_includes[] = {') + for ffi_to_include in self.ffi._included_ffis: + try: + included_module_name, included_source = ( + ffi_to_include._assigned_source[:2]) + except AttributeError: + raise VerificationError( + "ffi object %r includes %r, but the latter has not " + "been prepared with set_source()" % ( + self.ffi, ffi_to_include,)) + if included_source is None: + raise VerificationError( + "not implemented yet: ffi.include() of a Python-based " + "ffi inside a C-based ffi") + prnt(' "%s",' % (included_module_name,)) + prnt(' NULL') + prnt('};') + prnt() + # + # the declaration of '_cffi_type_context' + prnt('static const struct _cffi_type_context_s _cffi_type_context = {') + prnt(' _cffi_types,') + for step_name in self.ALL_STEPS: + if nums[step_name] > 0: + prnt(' _cffi_%ss,' % step_name) + else: + prnt(' NULL, /* no %ss */' % step_name) + for step_name in self.ALL_STEPS: + if step_name != "field": + prnt(' %d, /* num_%ss */' % (nums[step_name], step_name)) + if self.ffi._included_ffis: + prnt(' _cffi_includes,') + else: + prnt(' NULL, /* no includes */') + prnt(' %d, /* num_types */' % (len(self.cffi_types),)) + flags = 0 + if self._num_externpy > 0 or self.ffi._embedding is not None: + flags |= 1 # set to mean that we use extern "Python" + prnt(' %d, /* flags */' % flags) + prnt('};') + prnt() + # + # the init function + prnt('#ifdef __GNUC__') + prnt('# pragma GCC visibility push(default) /* for -fvisibility= */') + prnt('#endif') + prnt() + prnt('#ifdef PYPY_VERSION') + prnt('PyMODINIT_FUNC') + prnt('_cffi_pypyinit_%s(const void *p[])' % (base_module_name,)) + prnt('{') + if flags & 1: + prnt(' if (((intptr_t)p[0]) >= 0x0A03) {') + prnt(' _cffi_call_python_org = ' + '(void(*)(struct _cffi_externpy_s *, char *))p[1];') + prnt(' }') + prnt(' p[0] = (const void *)0x%x;' % self._version) + prnt(' p[1] = &_cffi_type_context;') + prnt('#if PY_MAJOR_VERSION >= 3') + prnt(' return NULL;') + prnt('#endif') + prnt('}') + # on Windows, distutils insists on putting init_cffi_xyz in + # 'export_symbols', so instead of fighting it, just give up and + # give it one + prnt('# ifdef _MSC_VER') + prnt(' PyMODINIT_FUNC') + prnt('# if PY_MAJOR_VERSION >= 3') + prnt(' PyInit_%s(void) { return NULL; }' % (base_module_name,)) + prnt('# else') + prnt(' init%s(void) { }' % (base_module_name,)) + prnt('# endif') + prnt('# endif') + prnt('#elif PY_MAJOR_VERSION >= 3') + prnt('PyMODINIT_FUNC') + prnt('PyInit_%s(void)' % (base_module_name,)) + prnt('{') + prnt(' return _cffi_init("%s", 0x%x, &_cffi_type_context);' % ( + self.module_name, self._version)) + prnt('}') + prnt('#else') + prnt('PyMODINIT_FUNC') + prnt('init%s(void)' % (base_module_name,)) + prnt('{') + prnt(' _cffi_init("%s", 0x%x, &_cffi_type_context);' % ( + self.module_name, self._version)) + prnt('}') + prnt('#endif') + prnt() + prnt('#ifdef __GNUC__') + prnt('# pragma GCC visibility pop') + prnt('#endif') + self._version = None + + def _to_py(self, x): + if isinstance(x, str): + return "b'%s'" % (x,) + if isinstance(x, (list, tuple)): + rep = [self._to_py(item) for item in x] + if len(rep) == 1: + rep.append('') + return "(%s)" % (','.join(rep),) + return x.as_python_expr() # Py2: unicode unexpected; Py3: bytes unexp. + + def write_py_source_to_f(self, f): + self._f = f + prnt = self._prnt + # + # header + prnt("# auto-generated file") + prnt("import _cffi_backend") + # + # the 'import' of the included ffis + num_includes = len(self.ffi._included_ffis or ()) + for i in range(num_includes): + ffi_to_include = self.ffi._included_ffis[i] + try: + included_module_name, included_source = ( + ffi_to_include._assigned_source[:2]) + except AttributeError: + raise VerificationError( + "ffi object %r includes %r, but the latter has not " + "been prepared with set_source()" % ( + self.ffi, ffi_to_include,)) + if included_source is not None: + raise VerificationError( + "not implemented yet: ffi.include() of a C-based " + "ffi inside a Python-based ffi") + prnt('from %s import ffi as _ffi%d' % (included_module_name, i)) + prnt() + prnt("ffi = _cffi_backend.FFI('%s'," % (self.module_name,)) + prnt(" _version = 0x%x," % (self._version,)) + self._version = None + # + # the '_types' keyword argument + self.cffi_types = tuple(self.cffi_types) # don't change any more + types_lst = [op.as_python_bytes() for op in self.cffi_types] + prnt(' _types = %s,' % (self._to_py(''.join(types_lst)),)) + typeindex2type = dict([(i, tp) for (tp, i) in self._typesdict.items()]) + # + # the keyword arguments from ALL_STEPS + for step_name in self.ALL_STEPS: + lst = self._lsts[step_name] + if len(lst) > 0 and step_name != "field": + prnt(' _%ss = %s,' % (step_name, self._to_py(lst))) + # + # the '_includes' keyword argument + if num_includes > 0: + prnt(' _includes = (%s,),' % ( + ', '.join(['_ffi%d' % i for i in range(num_includes)]),)) + # + # the footer + prnt(')') + + # ---------- + + def _gettypenum(self, type): + # a KeyError here is a bug. please report it! :-) + return self._typesdict[type] + + def _convert_funcarg_to_c(self, tp, fromvar, tovar, errcode): + extraarg = '' + if isinstance(tp, model.BasePrimitiveType) and not tp.is_complex_type(): + if tp.is_integer_type() and tp.name != '_Bool': + converter = '_cffi_to_c_int' + extraarg = ', %s' % tp.name + elif isinstance(tp, model.UnknownFloatType): + # don't check with is_float_type(): it may be a 'long + # double' here, and _cffi_to_c_double would loose precision + converter = '(%s)_cffi_to_c_double' % (tp.get_c_name(''),) + else: + cname = tp.get_c_name('') + converter = '(%s)_cffi_to_c_%s' % (cname, + tp.name.replace(' ', '_')) + if cname in ('char16_t', 'char32_t'): + self.needs_version(VERSION_CHAR16CHAR32) + errvalue = '-1' + # + elif isinstance(tp, model.PointerType): + self._convert_funcarg_to_c_ptr_or_array(tp, fromvar, + tovar, errcode) + return + # + elif (isinstance(tp, model.StructOrUnionOrEnum) or + isinstance(tp, model.BasePrimitiveType)): + # a struct (not a struct pointer) as a function argument; + # or, a complex (the same code works) + self._prnt(' if (_cffi_to_c((char *)&%s, _cffi_type(%d), %s) < 0)' + % (tovar, self._gettypenum(tp), fromvar)) + self._prnt(' %s;' % errcode) + return + # + elif isinstance(tp, model.FunctionPtrType): + converter = '(%s)_cffi_to_c_pointer' % tp.get_c_name('') + extraarg = ', _cffi_type(%d)' % self._gettypenum(tp) + errvalue = 'NULL' + # + else: + raise NotImplementedError(tp) + # + self._prnt(' %s = %s(%s%s);' % (tovar, converter, fromvar, extraarg)) + self._prnt(' if (%s == (%s)%s && PyErr_Occurred())' % ( + tovar, tp.get_c_name(''), errvalue)) + self._prnt(' %s;' % errcode) + + def _extra_local_variables(self, tp, localvars, freelines): + if isinstance(tp, model.PointerType): + localvars.add('Py_ssize_t datasize') + localvars.add('struct _cffi_freeme_s *large_args_free = NULL') + freelines.add('if (large_args_free != NULL)' + ' _cffi_free_array_arguments(large_args_free);') + + def _convert_funcarg_to_c_ptr_or_array(self, tp, fromvar, tovar, errcode): + self._prnt(' datasize = _cffi_prepare_pointer_call_argument(') + self._prnt(' _cffi_type(%d), %s, (char **)&%s);' % ( + self._gettypenum(tp), fromvar, tovar)) + self._prnt(' if (datasize != 0) {') + self._prnt(' %s = ((size_t)datasize) <= 640 ? ' + '(%s)alloca((size_t)datasize) : NULL;' % ( + tovar, tp.get_c_name(''))) + self._prnt(' if (_cffi_convert_array_argument(_cffi_type(%d), %s, ' + '(char **)&%s,' % (self._gettypenum(tp), fromvar, tovar)) + self._prnt(' datasize, &large_args_free) < 0)') + self._prnt(' %s;' % errcode) + self._prnt(' }') + + def _convert_expr_from_c(self, tp, var, context): + if isinstance(tp, model.BasePrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + return '_cffi_from_c_int(%s, %s)' % (var, tp.name) + elif isinstance(tp, model.UnknownFloatType): + return '_cffi_from_c_double(%s)' % (var,) + elif tp.name != 'long double' and not tp.is_complex_type(): + cname = tp.name.replace(' ', '_') + if cname in ('char16_t', 'char32_t'): + self.needs_version(VERSION_CHAR16CHAR32) + return '_cffi_from_c_%s(%s)' % (cname, var) + else: + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, (model.PointerType, model.FunctionPtrType)): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.ArrayType): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(model.PointerType(tp.item))) + elif isinstance(tp, model.StructOrUnion): + if tp.fldnames is None: + raise TypeError("'%s' is used as %s, but is opaque" % ( + tp._get_c_name(), context)) + return '_cffi_from_c_struct((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.EnumType): + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + else: + raise NotImplementedError(tp) + + # ---------- + # typedefs + + def _typedef_type(self, tp, name): + return self._global_type(tp, "(*(%s *)0)" % (name,)) + + def _generate_cpy_typedef_collecttype(self, tp, name): + self._do_collect_type(self._typedef_type(tp, name)) + + def _generate_cpy_typedef_decl(self, tp, name): + pass + + def _typedef_ctx(self, tp, name): + type_index = self._typesdict[tp] + self._lsts["typename"].append(TypenameExpr(name, type_index)) + + def _generate_cpy_typedef_ctx(self, tp, name): + tp = self._typedef_type(tp, name) + self._typedef_ctx(tp, name) + if getattr(tp, "origin", None) == "unknown_type": + self._struct_ctx(tp, tp.name, approxname=None) + elif isinstance(tp, model.NamedPointerType): + self._struct_ctx(tp.totype, tp.totype.name, approxname=tp.name, + named_ptr=tp) + + # ---------- + # function declarations + + def _generate_cpy_function_collecttype(self, tp, name): + self._do_collect_type(tp.as_raw_function()) + if tp.ellipsis and not self.target_is_python: + self._do_collect_type(tp) + + def _generate_cpy_function_decl(self, tp, name): + assert not self.target_is_python + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no CPython wrapper) + self._generate_cpy_constant_decl(tp, name) + return + prnt = self._prnt + numargs = len(tp.args) + if numargs == 0: + argname = 'noarg' + elif numargs == 1: + argname = 'arg0' + else: + argname = 'args' + # + # ------------------------------ + # the 'd' version of the function, only for addressof(lib, 'func') + arguments = [] + call_arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arguments.append(type.get_c_name(' x%d' % i, context)) + call_arguments.append('x%d' % i) + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + if tp.abi: + abi = tp.abi + ' ' + else: + abi = '' + name_and_arguments = '%s_cffi_d_%s(%s)' % (abi, name, repr_arguments) + prnt('static %s' % (tp.result.get_c_name(name_and_arguments),)) + prnt('{') + call_arguments = ', '.join(call_arguments) + result_code = 'return ' + if isinstance(tp.result, model.VoidType): + result_code = '' + prnt(' %s%s(%s);' % (result_code, name, call_arguments)) + prnt('}') + # + prnt('#ifndef PYPY_VERSION') # ------------------------------ + # + prnt('static PyObject *') + prnt('_cffi_f_%s(PyObject *self, PyObject *%s)' % (name, argname)) + prnt('{') + # + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arg = type.get_c_name(' x%d' % i, context) + prnt(' %s;' % arg) + # + localvars = set() + freelines = set() + for type in tp.args: + self._extra_local_variables(type, localvars, freelines) + for decl in sorted(localvars): + prnt(' %s;' % (decl,)) + # + if not isinstance(tp.result, model.VoidType): + result_code = 'result = ' + context = 'result of %s' % name + result_decl = ' %s;' % tp.result.get_c_name(' result', context) + prnt(result_decl) + prnt(' PyObject *pyresult;') + else: + result_decl = None + result_code = '' + # + if len(tp.args) > 1: + rng = range(len(tp.args)) + for i in rng: + prnt(' PyObject *arg%d;' % i) + prnt() + prnt(' if (!PyArg_UnpackTuple(args, "%s", %d, %d, %s))' % ( + name, len(rng), len(rng), + ', '.join(['&arg%d' % i for i in rng]))) + prnt(' return NULL;') + prnt() + # + for i, type in enumerate(tp.args): + self._convert_funcarg_to_c(type, 'arg%d' % i, 'x%d' % i, + 'return NULL') + prnt() + # + prnt(' Py_BEGIN_ALLOW_THREADS') + prnt(' _cffi_restore_errno();') + call_arguments = ['x%d' % i for i in range(len(tp.args))] + call_arguments = ', '.join(call_arguments) + prnt(' { %s%s(%s); }' % (result_code, name, call_arguments)) + prnt(' _cffi_save_errno();') + prnt(' Py_END_ALLOW_THREADS') + prnt() + # + prnt(' (void)self; /* unused */') + if numargs == 0: + prnt(' (void)noarg; /* unused */') + if result_code: + prnt(' pyresult = %s;' % + self._convert_expr_from_c(tp.result, 'result', 'result type')) + for freeline in freelines: + prnt(' ' + freeline) + prnt(' return pyresult;') + else: + for freeline in freelines: + prnt(' ' + freeline) + prnt(' Py_INCREF(Py_None);') + prnt(' return Py_None;') + prnt('}') + # + prnt('#else') # ------------------------------ + # + # the PyPy version: need to replace struct/union arguments with + # pointers, and if the result is a struct/union, insert a first + # arg that is a pointer to the result. We also do that for + # complex args and return type. + def need_indirection(type): + return (isinstance(type, model.StructOrUnion) or + (isinstance(type, model.PrimitiveType) and + type.is_complex_type())) + difference = False + arguments = [] + call_arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + indirection = '' + if need_indirection(type): + indirection = '*' + difference = True + arg = type.get_c_name(' %sx%d' % (indirection, i), context) + arguments.append(arg) + call_arguments.append('%sx%d' % (indirection, i)) + tp_result = tp.result + if need_indirection(tp_result): + context = 'result of %s' % name + arg = tp_result.get_c_name(' *result', context) + arguments.insert(0, arg) + tp_result = model.void_type + result_decl = None + result_code = '*result = ' + difference = True + if difference: + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + name_and_arguments = '%s_cffi_f_%s(%s)' % (abi, name, + repr_arguments) + prnt('static %s' % (tp_result.get_c_name(name_and_arguments),)) + prnt('{') + if result_decl: + prnt(result_decl) + call_arguments = ', '.join(call_arguments) + prnt(' { %s%s(%s); }' % (result_code, name, call_arguments)) + if result_decl: + prnt(' return result;') + prnt('}') + else: + prnt('# define _cffi_f_%s _cffi_d_%s' % (name, name)) + # + prnt('#endif') # ------------------------------ + prnt() + + def _generate_cpy_function_ctx(self, tp, name): + if tp.ellipsis and not self.target_is_python: + self._generate_cpy_constant_ctx(tp, name) + return + type_index = self._typesdict[tp.as_raw_function()] + numargs = len(tp.args) + if self.target_is_python: + meth_kind = OP_DLOPEN_FUNC + elif numargs == 0: + meth_kind = OP_CPYTHON_BLTN_N # 'METH_NOARGS' + elif numargs == 1: + meth_kind = OP_CPYTHON_BLTN_O # 'METH_O' + else: + meth_kind = OP_CPYTHON_BLTN_V # 'METH_VARARGS' + self._lsts["global"].append( + GlobalExpr(name, '_cffi_f_%s' % name, + CffiOp(meth_kind, type_index), + size='_cffi_d_%s' % name)) + + # ---------- + # named structs or unions + + def _field_type(self, tp_struct, field_name, tp_field): + if isinstance(tp_field, model.ArrayType): + actual_length = tp_field.length + if actual_length == '...': + ptr_struct_name = tp_struct.get_c_name('*') + actual_length = '_cffi_array_len(((%s)0)->%s)' % ( + ptr_struct_name, field_name) + tp_item = self._field_type(tp_struct, '%s[0]' % field_name, + tp_field.item) + tp_field = model.ArrayType(tp_item, actual_length) + return tp_field + + def _struct_collecttype(self, tp): + self._do_collect_type(tp) + if self.target_is_python: + # also requires nested anon struct/unions in ABI mode, recursively + for fldtype in tp.anonymous_struct_fields(): + self._struct_collecttype(fldtype) + + def _struct_decl(self, tp, cname, approxname): + if tp.fldtypes is None: + return + prnt = self._prnt + checkfuncname = '_cffi_checkfld_%s' % (approxname,) + prnt('_CFFI_UNUSED_FN') + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in self._enum_fields(tp): + try: + if ftype.is_integer_type() or fbitsize >= 0: + # accept all integers, but complain on float or double + if fname != '': + prnt(" (void)((p->%s) | 0); /* check that '%s.%s' is " + "an integer */" % (fname, cname, fname)) + continue + # only accept exactly the type declared, except that '[]' + # is interpreted as a '*' and so will match any array length. + # (It would also match '*', but that's harder to detect...) + while (isinstance(ftype, model.ArrayType) + and (ftype.length is None or ftype.length == '...')): + ftype = ftype.item + fname = fname + '[0]' + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + prnt('struct _cffi_align_%s { char x; %s y; };' % (approxname, cname)) + prnt() + + def _struct_ctx(self, tp, cname, approxname, named_ptr=None): + type_index = self._typesdict[tp] + reason_for_not_expanding = None + flags = [] + if isinstance(tp, model.UnionType): + flags.append("_CFFI_F_UNION") + if tp.fldtypes is None: + flags.append("_CFFI_F_OPAQUE") + reason_for_not_expanding = "opaque" + if (tp not in self.ffi._parser._included_declarations and + (named_ptr is None or + named_ptr not in self.ffi._parser._included_declarations)): + if tp.fldtypes is None: + pass # opaque + elif tp.partial or any(tp.anonymous_struct_fields()): + pass # field layout obtained silently from the C compiler + else: + flags.append("_CFFI_F_CHECK_FIELDS") + if tp.packed: + if tp.packed > 1: + raise NotImplementedError( + "%r is declared with 'pack=%r'; only 0 or 1 are " + "supported in API mode (try to use \"...;\", which " + "does not require a 'pack' declaration)" % + (tp, tp.packed)) + flags.append("_CFFI_F_PACKED") + else: + flags.append("_CFFI_F_EXTERNAL") + reason_for_not_expanding = "external" + flags = '|'.join(flags) or '0' + c_fields = [] + if reason_for_not_expanding is None: + enumfields = list(self._enum_fields(tp)) + for fldname, fldtype, fbitsize, fqual in enumfields: + fldtype = self._field_type(tp, fldname, fldtype) + self._check_not_opaque(fldtype, + "field '%s.%s'" % (tp.name, fldname)) + # cname is None for _add_missing_struct_unions() only + op = OP_NOOP + if fbitsize >= 0: + op = OP_BITFIELD + size = '%d /* bits */' % fbitsize + elif cname is None or ( + isinstance(fldtype, model.ArrayType) and + fldtype.length is None): + size = '(size_t)-1' + else: + size = 'sizeof(((%s)0)->%s)' % ( + tp.get_c_name('*') if named_ptr is None + else named_ptr.name, + fldname) + if cname is None or fbitsize >= 0: + offset = '(size_t)-1' + elif named_ptr is not None: + offset = '((char *)&((%s)0)->%s) - (char *)0' % ( + named_ptr.name, fldname) + else: + offset = 'offsetof(%s, %s)' % (tp.get_c_name(''), fldname) + c_fields.append( + FieldExpr(fldname, offset, size, fbitsize, + CffiOp(op, self._typesdict[fldtype]))) + first_field_index = len(self._lsts["field"]) + self._lsts["field"].extend(c_fields) + # + if cname is None: # unknown name, for _add_missing_struct_unions + size = '(size_t)-2' + align = -2 + comment = "unnamed" + else: + if named_ptr is not None: + size = 'sizeof(*(%s)0)' % (named_ptr.name,) + align = '-1 /* unknown alignment */' + else: + size = 'sizeof(%s)' % (cname,) + align = 'offsetof(struct _cffi_align_%s, y)' % (approxname,) + comment = None + else: + size = '(size_t)-1' + align = -1 + first_field_index = -1 + comment = reason_for_not_expanding + self._lsts["struct_union"].append( + StructUnionExpr(tp.name, type_index, flags, size, align, comment, + first_field_index, c_fields)) + self._seen_struct_unions.add(tp) + + def _check_not_opaque(self, tp, location): + while isinstance(tp, model.ArrayType): + tp = tp.item + if isinstance(tp, model.StructOrUnion) and tp.fldtypes is None: + raise TypeError( + "%s is of an opaque type (not declared in cdef())" % location) + + def _add_missing_struct_unions(self): + # not very nice, but some struct declarations might be missing + # because they don't have any known C name. Check that they are + # not partial (we can't complete or verify them!) and emit them + # anonymously. + lst = list(self._struct_unions.items()) + lst.sort(key=lambda tp_order: tp_order[1]) + for tp, order in lst: + if tp not in self._seen_struct_unions: + if tp.partial: + raise NotImplementedError("internal inconsistency: %r is " + "partial but was not seen at " + "this point" % (tp,)) + if tp.name.startswith('$') and tp.name[1:].isdigit(): + approxname = tp.name[1:] + elif tp.name == '_IO_FILE' and tp.forcename == 'FILE': + approxname = 'FILE' + self._typedef_ctx(tp, 'FILE') + else: + raise NotImplementedError("internal inconsistency: %r" % + (tp,)) + self._struct_ctx(tp, None, approxname) + + def _generate_cpy_struct_collecttype(self, tp, name): + self._struct_collecttype(tp) + _generate_cpy_union_collecttype = _generate_cpy_struct_collecttype + + def _struct_names(self, tp): + cname = tp.get_c_name('') + if ' ' in cname: + return cname, cname.replace(' ', '_') + else: + return cname, '_' + cname + + def _generate_cpy_struct_decl(self, tp, name): + self._struct_decl(tp, *self._struct_names(tp)) + _generate_cpy_union_decl = _generate_cpy_struct_decl + + def _generate_cpy_struct_ctx(self, tp, name): + self._struct_ctx(tp, *self._struct_names(tp)) + _generate_cpy_union_ctx = _generate_cpy_struct_ctx + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + def _generate_cpy_anonymous_collecttype(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_collecttype(tp, name) + else: + self._struct_collecttype(tp) + + def _generate_cpy_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_decl(tp) + else: + self._struct_decl(tp, name, 'typedef_' + name) + + def _generate_cpy_anonymous_ctx(self, tp, name): + if isinstance(tp, model.EnumType): + self._enum_ctx(tp, name) + else: + self._struct_ctx(tp, name, 'typedef_' + name) + + # ---------- + # constants, declared with "static const ..." + + def _generate_cpy_const(self, is_int, name, tp=None, category='const', + check_value=None): + if (category, name) in self._seen_constants: + raise VerificationError( + "duplicate declaration of %s '%s'" % (category, name)) + self._seen_constants.add((category, name)) + # + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + if is_int: + prnt('static int %s(unsigned long long *o)' % funcname) + prnt('{') + prnt(' int n = (%s) <= 0;' % (name,)) + prnt(' *o = (unsigned long long)((%s) | 0);' + ' /* check that %s is an integer */' % (name, name)) + if check_value is not None: + if check_value > 0: + check_value = '%dU' % (check_value,) + prnt(' if (!_cffi_check_int(*o, n, %s))' % (check_value,)) + prnt(' n |= 2;') + prnt(' return n;') + prnt('}') + else: + assert check_value is None + prnt('static void %s(char *o)' % funcname) + prnt('{') + prnt(' *(%s)o = %s;' % (tp.get_c_name('*'), name)) + prnt('}') + prnt() + + def _generate_cpy_constant_collecttype(self, tp, name): + is_int = tp.is_integer_type() + if not is_int or self.target_is_python: + self._do_collect_type(tp) + + def _generate_cpy_constant_decl(self, tp, name): + is_int = tp.is_integer_type() + self._generate_cpy_const(is_int, name, tp) + + def _generate_cpy_constant_ctx(self, tp, name): + if not self.target_is_python and tp.is_integer_type(): + type_op = CffiOp(OP_CONSTANT_INT, -1) + else: + if self.target_is_python: + const_kind = OP_DLOPEN_CONST + else: + const_kind = OP_CONSTANT + type_index = self._typesdict[tp] + type_op = CffiOp(const_kind, type_index) + self._lsts["global"].append( + GlobalExpr(name, '_cffi_const_%s' % name, type_op)) + + # ---------- + # enums + + def _generate_cpy_enum_collecttype(self, tp, name): + self._do_collect_type(tp) + + def _generate_cpy_enum_decl(self, tp, name=None): + for enumerator in tp.enumerators: + self._generate_cpy_const(True, enumerator) + + def _enum_ctx(self, tp, cname): + type_index = self._typesdict[tp] + type_op = CffiOp(OP_ENUM, -1) + if self.target_is_python: + tp.check_not_partial() + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._lsts["global"].append( + GlobalExpr(enumerator, '_cffi_const_%s' % enumerator, type_op, + check_value=enumvalue)) + # + if cname is not None and '$' not in cname and not self.target_is_python: + size = "sizeof(%s)" % cname + signed = "((%s)-1) <= 0" % cname + else: + basetp = tp.build_baseinttype(self.ffi, []) + size = self.ffi.sizeof(basetp) + signed = int(int(self.ffi.cast(basetp, -1)) < 0) + allenums = ",".join(tp.enumerators) + self._lsts["enum"].append( + EnumExpr(tp.name, type_index, size, signed, allenums)) + + def _generate_cpy_enum_ctx(self, tp, name): + self._enum_ctx(tp, tp._get_c_name()) + + # ---------- + # macros: for now only for integers + + def _generate_cpy_macro_collecttype(self, tp, name): + pass + + def _generate_cpy_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_cpy_const(True, name, check_value=check_value) + + def _generate_cpy_macro_ctx(self, tp, name): + if tp == '...': + if self.target_is_python: + raise VerificationError( + "cannot use the syntax '...' in '#define %s ...' when " + "using the ABI mode" % (name,)) + check_value = None + else: + check_value = tp # an integer + type_op = CffiOp(OP_CONSTANT_INT, -1) + self._lsts["global"].append( + GlobalExpr(name, '_cffi_const_%s' % name, type_op, + check_value=check_value)) + + # ---------- + # global variables + + def _global_type(self, tp, global_name): + if isinstance(tp, model.ArrayType): + actual_length = tp.length + if actual_length == '...': + actual_length = '_cffi_array_len(%s)' % (global_name,) + tp_item = self._global_type(tp.item, '%s[0]' % global_name) + tp = model.ArrayType(tp_item, actual_length) + return tp + + def _generate_cpy_variable_collecttype(self, tp, name): + self._do_collect_type(self._global_type(tp, name)) + + def _generate_cpy_variable_decl(self, tp, name): + prnt = self._prnt + tp = self._global_type(tp, name) + if isinstance(tp, model.ArrayType) and tp.length is None: + tp = tp.item + ampersand = '' + else: + ampersand = '&' + # This code assumes that casts from "tp *" to "void *" is a + # no-op, i.e. a function that returns a "tp *" can be called + # as if it returned a "void *". This should be generally true + # on any modern machine. The only exception to that rule (on + # uncommon architectures, and as far as I can tell) might be + # if 'tp' were a function type, but that is not possible here. + # (If 'tp' is a function _pointer_ type, then casts from "fn_t + # **" to "void *" are again no-ops, as far as I can tell.) + decl = '*_cffi_var_%s(void)' % (name,) + prnt('static ' + tp.get_c_name(decl, quals=self._current_quals)) + prnt('{') + prnt(' return %s(%s);' % (ampersand, name)) + prnt('}') + prnt() + + def _generate_cpy_variable_ctx(self, tp, name): + tp = self._global_type(tp, name) + type_index = self._typesdict[tp] + if self.target_is_python: + op = OP_GLOBAL_VAR + else: + op = OP_GLOBAL_VAR_F + self._lsts["global"].append( + GlobalExpr(name, '_cffi_var_%s' % name, CffiOp(op, type_index))) + + # ---------- + # extern "Python" + + def _generate_cpy_extern_python_collecttype(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + self._do_collect_type(tp) + _generate_cpy_dllexport_python_collecttype = \ + _generate_cpy_extern_python_plus_c_collecttype = \ + _generate_cpy_extern_python_collecttype + + def _extern_python_decl(self, tp, name, tag_and_space): + prnt = self._prnt + if isinstance(tp.result, model.VoidType): + size_of_result = '0' + else: + context = 'result of %s' % name + size_of_result = '(int)sizeof(%s)' % ( + tp.result.get_c_name('', context),) + prnt('static struct _cffi_externpy_s _cffi_externpy__%s =' % name) + prnt(' { "%s.%s", %s, 0, 0 };' % ( + self.module_name, name, size_of_result)) + prnt() + # + arguments = [] + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + arg = type.get_c_name(' a%d' % i, context) + arguments.append(arg) + # + repr_arguments = ', '.join(arguments) + repr_arguments = repr_arguments or 'void' + name_and_arguments = '%s(%s)' % (name, repr_arguments) + if tp.abi == "__stdcall": + name_and_arguments = '_cffi_stdcall ' + name_and_arguments + # + def may_need_128_bits(tp): + return (isinstance(tp, model.PrimitiveType) and + tp.name == 'long double') + # + size_of_a = max(len(tp.args)*8, 8) + if may_need_128_bits(tp.result): + size_of_a = max(size_of_a, 16) + if isinstance(tp.result, model.StructOrUnion): + size_of_a = 'sizeof(%s) > %d ? sizeof(%s) : %d' % ( + tp.result.get_c_name(''), size_of_a, + tp.result.get_c_name(''), size_of_a) + prnt('%s%s' % (tag_and_space, tp.result.get_c_name(name_and_arguments))) + prnt('{') + prnt(' char a[%s];' % size_of_a) + prnt(' char *p = a;') + for i, type in enumerate(tp.args): + arg = 'a%d' % i + if (isinstance(type, model.StructOrUnion) or + may_need_128_bits(type)): + arg = '&' + arg + type = model.PointerType(type) + prnt(' *(%s)(p + %d) = %s;' % (type.get_c_name('*'), i*8, arg)) + prnt(' _cffi_call_python(&_cffi_externpy__%s, p);' % name) + if not isinstance(tp.result, model.VoidType): + prnt(' return *(%s)p;' % (tp.result.get_c_name('*'),)) + prnt('}') + prnt() + self._num_externpy += 1 + + def _generate_cpy_extern_python_decl(self, tp, name): + self._extern_python_decl(tp, name, 'static ') + + def _generate_cpy_dllexport_python_decl(self, tp, name): + self._extern_python_decl(tp, name, 'CFFI_DLLEXPORT ') + + def _generate_cpy_extern_python_plus_c_decl(self, tp, name): + self._extern_python_decl(tp, name, '') + + def _generate_cpy_extern_python_ctx(self, tp, name): + if self.target_is_python: + raise VerificationError( + "cannot use 'extern \"Python\"' in the ABI mode") + if tp.ellipsis: + raise NotImplementedError("a vararg function is extern \"Python\"") + type_index = self._typesdict[tp] + type_op = CffiOp(OP_EXTERN_PYTHON, type_index) + self._lsts["global"].append( + GlobalExpr(name, '&_cffi_externpy__%s' % name, type_op, name)) + + _generate_cpy_dllexport_python_ctx = \ + _generate_cpy_extern_python_plus_c_ctx = \ + _generate_cpy_extern_python_ctx + + def _print_string_literal_in_array(self, s): + prnt = self._prnt + prnt('// # NB. this is not a string because of a size limit in MSVC') + if not isinstance(s, bytes): # unicode + s = s.encode('utf-8') # -> bytes + else: + s.decode('utf-8') # got bytes, check for valid utf-8 + try: + s.decode('ascii') + except UnicodeDecodeError: + s = b'# -*- encoding: utf8 -*-\n' + s + for line in s.splitlines(True): + comment = line + if type('//') is bytes: # python2 + line = map(ord, line) # make a list of integers + else: # python3 + # type(line) is bytes, which enumerates like a list of integers + comment = ascii(comment)[1:-1] + prnt(('// ' + comment).rstrip()) + printed_line = '' + for c in line: + if len(printed_line) >= 76: + prnt(printed_line) + printed_line = '' + printed_line += '%d,' % (c,) + prnt(printed_line) + + # ---------- + # emitting the opcodes for individual types + + def _emit_bytecode_VoidType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, PRIM_VOID) + + def _emit_bytecode_PrimitiveType(self, tp, index): + prim_index = PRIMITIVE_TO_INDEX[tp.name] + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, prim_index) + + def _emit_bytecode_UnknownIntegerType(self, tp, index): + s = ('_cffi_prim_int(sizeof(%s), (\n' + ' ((%s)-1) | 0 /* check that %s is an integer type */\n' + ' ) <= 0)' % (tp.name, tp.name, tp.name)) + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, s) + + def _emit_bytecode_UnknownFloatType(self, tp, index): + s = ('_cffi_prim_float(sizeof(%s) *\n' + ' (((%s)1) / 2) * 2 /* integer => 0, float => 1 */\n' + ' )' % (tp.name, tp.name)) + self.cffi_types[index] = CffiOp(OP_PRIMITIVE, s) + + def _emit_bytecode_RawFunctionType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_FUNCTION, self._typesdict[tp.result]) + index += 1 + for tp1 in tp.args: + realindex = self._typesdict[tp1] + if index != realindex: + if isinstance(tp1, model.PrimitiveType): + self._emit_bytecode_PrimitiveType(tp1, index) + else: + self.cffi_types[index] = CffiOp(OP_NOOP, realindex) + index += 1 + flags = int(tp.ellipsis) + if tp.abi is not None: + if tp.abi == '__stdcall': + flags |= 2 + else: + raise NotImplementedError("abi=%r" % (tp.abi,)) + self.cffi_types[index] = CffiOp(OP_FUNCTION_END, flags) + + def _emit_bytecode_PointerType(self, tp, index): + self.cffi_types[index] = CffiOp(OP_POINTER, self._typesdict[tp.totype]) + + _emit_bytecode_ConstPointerType = _emit_bytecode_PointerType + _emit_bytecode_NamedPointerType = _emit_bytecode_PointerType + + def _emit_bytecode_FunctionPtrType(self, tp, index): + raw = tp.as_raw_function() + self.cffi_types[index] = CffiOp(OP_POINTER, self._typesdict[raw]) + + def _emit_bytecode_ArrayType(self, tp, index): + item_index = self._typesdict[tp.item] + if tp.length is None: + self.cffi_types[index] = CffiOp(OP_OPEN_ARRAY, item_index) + elif tp.length == '...': + raise VerificationError( + "type %s badly placed: the '...' array length can only be " + "used on global arrays or on fields of structures" % ( + str(tp).replace('/*...*/', '...'),)) + else: + assert self.cffi_types[index + 1] == 'LEN' + self.cffi_types[index] = CffiOp(OP_ARRAY, item_index) + self.cffi_types[index + 1] = CffiOp(None, str(tp.length)) + + def _emit_bytecode_StructType(self, tp, index): + struct_index = self._struct_unions[tp] + self.cffi_types[index] = CffiOp(OP_STRUCT_UNION, struct_index) + _emit_bytecode_UnionType = _emit_bytecode_StructType + + def _emit_bytecode_EnumType(self, tp, index): + enum_index = self._enums[tp] + self.cffi_types[index] = CffiOp(OP_ENUM, enum_index) + + +if sys.version_info >= (3,): + NativeIO = io.StringIO +else: + class NativeIO(io.BytesIO): + def write(self, s): + if isinstance(s, unicode): + s = s.encode('ascii') + super(NativeIO, self).write(s) + +def _make_c_or_py_source(ffi, module_name, preamble, target_file, verbose): + if verbose: + print("generating %s" % (target_file,)) + recompiler = Recompiler(ffi, module_name, + target_is_python=(preamble is None)) + recompiler.collect_type_table() + recompiler.collect_step_tables() + f = NativeIO() + recompiler.write_source_to_f(f, preamble) + output = f.getvalue() + try: + with open(target_file, 'r') as f1: + if f1.read(len(output) + 1) != output: + raise IOError + if verbose: + print("(already up-to-date)") + return False # already up-to-date + except IOError: + tmp_file = '%s.~%d' % (target_file, os.getpid()) + with open(tmp_file, 'w') as f1: + f1.write(output) + try: + os.rename(tmp_file, target_file) + except OSError: + os.unlink(target_file) + os.rename(tmp_file, target_file) + return True + +def make_c_source(ffi, module_name, preamble, target_c_file, verbose=False): + assert preamble is not None + return _make_c_or_py_source(ffi, module_name, preamble, target_c_file, + verbose) + +def make_py_source(ffi, module_name, target_py_file, verbose=False): + return _make_c_or_py_source(ffi, module_name, None, target_py_file, + verbose) + +def _modname_to_file(outputdir, modname, extension): + parts = modname.split('.') + try: + os.makedirs(os.path.join(outputdir, *parts[:-1])) + except OSError: + pass + parts[-1] += extension + return os.path.join(outputdir, *parts), parts + + +# Aaargh. Distutils is not tested at all for the purpose of compiling +# DLLs that are not extension modules. Here are some hacks to work +# around that, in the _patch_for_*() functions... + +def _patch_meth(patchlist, cls, name, new_meth): + old = getattr(cls, name) + patchlist.append((cls, name, old)) + setattr(cls, name, new_meth) + return old + +def _unpatch_meths(patchlist): + for cls, name, old_meth in reversed(patchlist): + setattr(cls, name, old_meth) + +def _patch_for_embedding(patchlist): + if sys.platform == 'win32': + # we must not remove the manifest when building for embedding! + from distutils.msvc9compiler import MSVCCompiler + _patch_meth(patchlist, MSVCCompiler, '_remove_visual_c_ref', + lambda self, manifest_file: manifest_file) + + if sys.platform == 'darwin': + # we must not make a '-bundle', but a '-dynamiclib' instead + from distutils.ccompiler import CCompiler + def my_link_shared_object(self, *args, **kwds): + if '-bundle' in self.linker_so: + self.linker_so = list(self.linker_so) + i = self.linker_so.index('-bundle') + self.linker_so[i] = '-dynamiclib' + return old_link_shared_object(self, *args, **kwds) + old_link_shared_object = _patch_meth(patchlist, CCompiler, + 'link_shared_object', + my_link_shared_object) + +def _patch_for_target(patchlist, target): + from distutils.command.build_ext import build_ext + # if 'target' is different from '*', we need to patch some internal + # method to just return this 'target' value, instead of having it + # built from module_name + if target.endswith('.*'): + target = target[:-2] + if sys.platform == 'win32': + target += '.dll' + elif sys.platform == 'darwin': + target += '.dylib' + else: + target += '.so' + _patch_meth(patchlist, build_ext, 'get_ext_filename', + lambda self, ext_name: target) + + +def recompile(ffi, module_name, preamble, tmpdir='.', call_c_compiler=True, + c_file=None, source_extension='.c', extradir=None, + compiler_verbose=1, target=None, debug=None, **kwds): + if not isinstance(module_name, str): + module_name = module_name.encode('ascii') + if ffi._windows_unicode: + ffi._apply_windows_unicode(kwds) + if preamble is not None: + embedding = (ffi._embedding is not None) + if embedding: + ffi._apply_embedding_fix(kwds) + if c_file is None: + c_file, parts = _modname_to_file(tmpdir, module_name, + source_extension) + if extradir: + parts = [extradir] + parts + ext_c_file = os.path.join(*parts) + else: + ext_c_file = c_file + # + if target is None: + if embedding: + target = '%s.*' % module_name + else: + target = '*' + # + ext = ffiplatform.get_extension(ext_c_file, module_name, **kwds) + updated = make_c_source(ffi, module_name, preamble, c_file, + verbose=compiler_verbose) + if call_c_compiler: + patchlist = [] + cwd = os.getcwd() + try: + if embedding: + _patch_for_embedding(patchlist) + if target != '*': + _patch_for_target(patchlist, target) + if compiler_verbose: + if tmpdir == '.': + msg = 'the current directory is' + else: + msg = 'setting the current directory to' + print('%s %r' % (msg, os.path.abspath(tmpdir))) + os.chdir(tmpdir) + outputfilename = ffiplatform.compile('.', ext, + compiler_verbose, debug) + finally: + os.chdir(cwd) + _unpatch_meths(patchlist) + return outputfilename + else: + return ext, updated + else: + if c_file is None: + c_file, _ = _modname_to_file(tmpdir, module_name, '.py') + updated = make_py_source(ffi, module_name, c_file, + verbose=compiler_verbose) + if call_c_compiler: + return c_file + else: + return None, updated + diff --git a/lib/python3.10/site-packages/cffi/setuptools_ext.py b/lib/python3.10/site-packages/cffi/setuptools_ext.py new file mode 100644 index 0000000000000000000000000000000000000000..8fe361487e469b3a87b80ddec1c5585b3801c587 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/setuptools_ext.py @@ -0,0 +1,219 @@ +import os +import sys + +try: + basestring +except NameError: + # Python 3.x + basestring = str + +def error(msg): + from distutils.errors import DistutilsSetupError + raise DistutilsSetupError(msg) + + +def execfile(filename, glob): + # We use execfile() (here rewritten for Python 3) instead of + # __import__() to load the build script. The problem with + # a normal import is that in some packages, the intermediate + # __init__.py files may already try to import the file that + # we are generating. + with open(filename) as f: + src = f.read() + src += '\n' # Python 2.6 compatibility + code = compile(src, filename, 'exec') + exec(code, glob, glob) + + +def add_cffi_module(dist, mod_spec): + from cffi.api import FFI + + if not isinstance(mod_spec, basestring): + error("argument to 'cffi_modules=...' must be a str or a list of str," + " not %r" % (type(mod_spec).__name__,)) + mod_spec = str(mod_spec) + try: + build_file_name, ffi_var_name = mod_spec.split(':') + except ValueError: + error("%r must be of the form 'path/build.py:ffi_variable'" % + (mod_spec,)) + if not os.path.exists(build_file_name): + ext = '' + rewritten = build_file_name.replace('.', '/') + '.py' + if os.path.exists(rewritten): + ext = ' (rewrite cffi_modules to [%r])' % ( + rewritten + ':' + ffi_var_name,) + error("%r does not name an existing file%s" % (build_file_name, ext)) + + mod_vars = {'__name__': '__cffi__', '__file__': build_file_name} + execfile(build_file_name, mod_vars) + + try: + ffi = mod_vars[ffi_var_name] + except KeyError: + error("%r: object %r not found in module" % (mod_spec, + ffi_var_name)) + if not isinstance(ffi, FFI): + ffi = ffi() # maybe it's a function instead of directly an ffi + if not isinstance(ffi, FFI): + error("%r is not an FFI instance (got %r)" % (mod_spec, + type(ffi).__name__)) + if not hasattr(ffi, '_assigned_source'): + error("%r: the set_source() method was not called" % (mod_spec,)) + module_name, source, source_extension, kwds = ffi._assigned_source + if ffi._windows_unicode: + kwds = kwds.copy() + ffi._apply_windows_unicode(kwds) + + if source is None: + _add_py_module(dist, ffi, module_name) + else: + _add_c_module(dist, ffi, module_name, source, source_extension, kwds) + +def _set_py_limited_api(Extension, kwds): + """ + Add py_limited_api to kwds if setuptools >= 26 is in use. + Do not alter the setting if it already exists. + Setuptools takes care of ignoring the flag on Python 2 and PyPy. + + CPython itself should ignore the flag in a debugging version + (by not listing .abi3.so in the extensions it supports), but + it doesn't so far, creating troubles. That's why we check + for "not hasattr(sys, 'gettotalrefcount')" (the 2.7 compatible equivalent + of 'd' not in sys.abiflags). (http://bugs.python.org/issue28401) + + On Windows, with CPython <= 3.4, it's better not to use py_limited_api + because virtualenv *still* doesn't copy PYTHON3.DLL on these versions. + Recently (2020) we started shipping only >= 3.5 wheels, though. So + we'll give it another try and set py_limited_api on Windows >= 3.5. + """ + from cffi import recompiler + + if ('py_limited_api' not in kwds and not hasattr(sys, 'gettotalrefcount') + and recompiler.USE_LIMITED_API): + import setuptools + try: + setuptools_major_version = int(setuptools.__version__.partition('.')[0]) + if setuptools_major_version >= 26: + kwds['py_limited_api'] = True + except ValueError: # certain development versions of setuptools + # If we don't know the version number of setuptools, we + # try to set 'py_limited_api' anyway. At worst, we get a + # warning. + kwds['py_limited_api'] = True + return kwds + +def _add_c_module(dist, ffi, module_name, source, source_extension, kwds): + from distutils.core import Extension + # We are a setuptools extension. Need this build_ext for py_limited_api. + from setuptools.command.build_ext import build_ext + from distutils.dir_util import mkpath + from distutils import log + from cffi import recompiler + + allsources = ['$PLACEHOLDER'] + allsources.extend(kwds.pop('sources', [])) + kwds = _set_py_limited_api(Extension, kwds) + ext = Extension(name=module_name, sources=allsources, **kwds) + + def make_mod(tmpdir, pre_run=None): + c_file = os.path.join(tmpdir, module_name + source_extension) + log.info("generating cffi module %r" % c_file) + mkpath(tmpdir) + # a setuptools-only, API-only hook: called with the "ext" and "ffi" + # arguments just before we turn the ffi into C code. To use it, + # subclass the 'distutils.command.build_ext.build_ext' class and + # add a method 'def pre_run(self, ext, ffi)'. + if pre_run is not None: + pre_run(ext, ffi) + updated = recompiler.make_c_source(ffi, module_name, source, c_file) + if not updated: + log.info("already up-to-date") + return c_file + + if dist.ext_modules is None: + dist.ext_modules = [] + dist.ext_modules.append(ext) + + base_class = dist.cmdclass.get('build_ext', build_ext) + class build_ext_make_mod(base_class): + def run(self): + if ext.sources[0] == '$PLACEHOLDER': + pre_run = getattr(self, 'pre_run', None) + ext.sources[0] = make_mod(self.build_temp, pre_run) + base_class.run(self) + dist.cmdclass['build_ext'] = build_ext_make_mod + # NB. multiple runs here will create multiple 'build_ext_make_mod' + # classes. Even in this case the 'build_ext' command should be + # run once; but just in case, the logic above does nothing if + # called again. + + +def _add_py_module(dist, ffi, module_name): + from distutils.dir_util import mkpath + from setuptools.command.build_py import build_py + from setuptools.command.build_ext import build_ext + from distutils import log + from cffi import recompiler + + def generate_mod(py_file): + log.info("generating cffi module %r" % py_file) + mkpath(os.path.dirname(py_file)) + updated = recompiler.make_py_source(ffi, module_name, py_file) + if not updated: + log.info("already up-to-date") + + base_class = dist.cmdclass.get('build_py', build_py) + class build_py_make_mod(base_class): + def run(self): + base_class.run(self) + module_path = module_name.split('.') + module_path[-1] += '.py' + generate_mod(os.path.join(self.build_lib, *module_path)) + def get_source_files(self): + # This is called from 'setup.py sdist' only. Exclude + # the generate .py module in this case. + saved_py_modules = self.py_modules + try: + if saved_py_modules: + self.py_modules = [m for m in saved_py_modules + if m != module_name] + return base_class.get_source_files(self) + finally: + self.py_modules = saved_py_modules + dist.cmdclass['build_py'] = build_py_make_mod + + # distutils and setuptools have no notion I could find of a + # generated python module. If we don't add module_name to + # dist.py_modules, then things mostly work but there are some + # combination of options (--root and --record) that will miss + # the module. So we add it here, which gives a few apparently + # harmless warnings about not finding the file outside the + # build directory. + # Then we need to hack more in get_source_files(); see above. + if dist.py_modules is None: + dist.py_modules = [] + dist.py_modules.append(module_name) + + # the following is only for "build_ext -i" + base_class_2 = dist.cmdclass.get('build_ext', build_ext) + class build_ext_make_mod(base_class_2): + def run(self): + base_class_2.run(self) + if self.inplace: + # from get_ext_fullpath() in distutils/command/build_ext.py + module_path = module_name.split('.') + package = '.'.join(module_path[:-1]) + build_py = self.get_finalized_command('build_py') + package_dir = build_py.get_package_dir(package) + file_name = module_path[-1] + '.py' + generate_mod(os.path.join(package_dir, file_name)) + dist.cmdclass['build_ext'] = build_ext_make_mod + +def cffi_modules(dist, attr, value): + assert attr == 'cffi_modules' + if isinstance(value, basestring): + value = [value] + + for cffi_module in value: + add_cffi_module(dist, cffi_module) diff --git a/lib/python3.10/site-packages/cffi/vengine_cpy.py b/lib/python3.10/site-packages/cffi/vengine_cpy.py new file mode 100644 index 0000000000000000000000000000000000000000..6de0df0ea4e1a98e65964ab61588df9abf536bac --- /dev/null +++ b/lib/python3.10/site-packages/cffi/vengine_cpy.py @@ -0,0 +1,1076 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys, imp +from . import model +from .error import VerificationError + + +class VCPythonEngine(object): + _class_key = 'x' + _gen_python_module = True + + def __init__(self, verifier): + self.verifier = verifier + self.ffi = verifier.ffi + self._struct_pending_verification = {} + self._types_of_builtin_functions = {} + + def patch_extension_kwds(self, kwds): + pass + + def find_module(self, module_name, path, so_suffixes): + try: + f, filename, descr = imp.find_module(module_name, path) + except ImportError: + return None + if f is not None: + f.close() + # Note that after a setuptools installation, there are both .py + # and .so files with the same basename. The code here relies on + # imp.find_module() locating the .so in priority. + if descr[0] not in so_suffixes: + return None + return filename + + def collect_types(self): + self._typesdict = {} + self._generate("collecttype") + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def _gettypenum(self, type): + # a KeyError here is a bug. please report it! :-) + return self._typesdict[type] + + def _do_collect_type(self, tp): + if ((not isinstance(tp, model.PrimitiveType) + or tp.name == 'long double') + and tp not in self._typesdict): + num = len(self._typesdict) + self._typesdict[tp] = num + + def write_source_to_f(self): + self.collect_types() + # + # The new module will have a _cffi_setup() function that receives + # objects from the ffi world, and that calls some setup code in + # the module. This setup code is split in several independent + # functions, e.g. one per constant. The functions are "chained" + # by ending in a tail call to each other. + # + # This is further split in two chained lists, depending on if we + # can do it at import-time or if we must wait for _cffi_setup() to + # provide us with the objects. This is needed because we + # need the values of the enum constants in order to build the + # that we may have to pass to _cffi_setup(). + # + # The following two 'chained_list_constants' items contains + # the head of these two chained lists, as a string that gives the + # call to do, if any. + self._chained_list_constants = ['((void)lib,0)', '((void)lib,0)'] + # + prnt = self._prnt + # first paste some standard set of lines that are mostly '#define' + prnt(cffimod_header) + prnt() + # then paste the C source given by the user, verbatim. + prnt(self.verifier.preamble) + prnt() + # + # call generate_cpy_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._generate("decl") + # + # implement the function _cffi_setup_custom() as calling the + # head of the chained list. + self._generate_setup_custom() + prnt() + # + # produce the method table, including the entries for the + # generated Python->C function wrappers, which are done + # by generate_cpy_function_method(). + prnt('static PyMethodDef _cffi_methods[] = {') + self._generate("method") + prnt(' {"_cffi_setup", _cffi_setup, METH_VARARGS, NULL},') + prnt(' {NULL, NULL, 0, NULL} /* Sentinel */') + prnt('};') + prnt() + # + # standard init. + modname = self.verifier.get_module_name() + constants = self._chained_list_constants[False] + prnt('#if PY_MAJOR_VERSION >= 3') + prnt() + prnt('static struct PyModuleDef _cffi_module_def = {') + prnt(' PyModuleDef_HEAD_INIT,') + prnt(' "%s",' % modname) + prnt(' NULL,') + prnt(' -1,') + prnt(' _cffi_methods,') + prnt(' NULL, NULL, NULL, NULL') + prnt('};') + prnt() + prnt('PyMODINIT_FUNC') + prnt('PyInit_%s(void)' % modname) + prnt('{') + prnt(' PyObject *lib;') + prnt(' lib = PyModule_Create(&_cffi_module_def);') + prnt(' if (lib == NULL)') + prnt(' return NULL;') + prnt(' if (%s < 0 || _cffi_init() < 0) {' % (constants,)) + prnt(' Py_DECREF(lib);') + prnt(' return NULL;') + prnt(' }') + prnt(' return lib;') + prnt('}') + prnt() + prnt('#else') + prnt() + prnt('PyMODINIT_FUNC') + prnt('init%s(void)' % modname) + prnt('{') + prnt(' PyObject *lib;') + prnt(' lib = Py_InitModule("%s", _cffi_methods);' % modname) + prnt(' if (lib == NULL)') + prnt(' return;') + prnt(' if (%s < 0 || _cffi_init() < 0)' % (constants,)) + prnt(' return;') + prnt(' return;') + prnt('}') + prnt() + prnt('#endif') + + def load_library(self, flags=None): + # XXX review all usages of 'self' here! + # import it as a new extension module + imp.acquire_lock() + try: + if hasattr(sys, "getdlopenflags"): + previous_flags = sys.getdlopenflags() + try: + if hasattr(sys, "setdlopenflags") and flags is not None: + sys.setdlopenflags(flags) + module = imp.load_dynamic(self.verifier.get_module_name(), + self.verifier.modulefilename) + except ImportError as e: + error = "importing %r: %s" % (self.verifier.modulefilename, e) + raise VerificationError(error) + finally: + if hasattr(sys, "setdlopenflags"): + sys.setdlopenflags(previous_flags) + finally: + imp.release_lock() + # + # call loading_cpy_struct() to get the struct layout inferred by + # the C compiler + self._load(module, 'loading') + # + # the C code will need the objects. Collect them in + # order in a list. + revmapping = dict([(value, key) + for (key, value) in self._typesdict.items()]) + lst = [revmapping[i] for i in range(len(revmapping))] + lst = list(map(self.ffi._get_cached_btype, lst)) + # + # build the FFILibrary class and instance and call _cffi_setup(). + # this will set up some fields like '_cffi_types', and only then + # it will invoke the chained list of functions that will really + # build (notably) the constant objects, as if they are + # pointers, and store them as attributes on the 'library' object. + class FFILibrary(object): + _cffi_python_module = module + _cffi_ffi = self.ffi + _cffi_dir = [] + def __dir__(self): + return FFILibrary._cffi_dir + list(self.__dict__) + library = FFILibrary() + if module._cffi_setup(lst, VerificationError, library): + import warnings + warnings.warn("reimporting %r might overwrite older definitions" + % (self.verifier.get_module_name())) + # + # finally, call the loaded_cpy_xxx() functions. This will perform + # the final adjustments, like copying the Python->C wrapper + # functions from the module to the 'library' object, and setting + # up the FFILibrary class with properties for the global C variables. + self._load(module, 'loaded', library=library) + module._cffi_original_ffi = self.ffi + module._cffi_types_of_builtin_funcs = self._types_of_builtin_functions + return library + + def _get_declarations(self): + lst = [(key, tp) for (key, (tp, qual)) in + self.ffi._parser._declarations.items()] + lst.sort() + return lst + + def _generate(self, step_name): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_cpy_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in verify(): %r" % name) + try: + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _load(self, module, step_name, **kwds): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + method = getattr(self, '_%s_cpy_%s' % (step_name, kind)) + try: + method(tp, realname, module, **kwds) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _generate_nothing(self, tp, name): + pass + + def _loaded_noop(self, tp, name, module, **kwds): + pass + + # ---------- + + def _convert_funcarg_to_c(self, tp, fromvar, tovar, errcode): + extraarg = '' + if isinstance(tp, model.PrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + converter = '_cffi_to_c_int' + extraarg = ', %s' % tp.name + else: + converter = '(%s)_cffi_to_c_%s' % (tp.get_c_name(''), + tp.name.replace(' ', '_')) + errvalue = '-1' + # + elif isinstance(tp, model.PointerType): + self._convert_funcarg_to_c_ptr_or_array(tp, fromvar, + tovar, errcode) + return + # + elif isinstance(tp, (model.StructOrUnion, model.EnumType)): + # a struct (not a struct pointer) as a function argument + self._prnt(' if (_cffi_to_c((char *)&%s, _cffi_type(%d), %s) < 0)' + % (tovar, self._gettypenum(tp), fromvar)) + self._prnt(' %s;' % errcode) + return + # + elif isinstance(tp, model.FunctionPtrType): + converter = '(%s)_cffi_to_c_pointer' % tp.get_c_name('') + extraarg = ', _cffi_type(%d)' % self._gettypenum(tp) + errvalue = 'NULL' + # + else: + raise NotImplementedError(tp) + # + self._prnt(' %s = %s(%s%s);' % (tovar, converter, fromvar, extraarg)) + self._prnt(' if (%s == (%s)%s && PyErr_Occurred())' % ( + tovar, tp.get_c_name(''), errvalue)) + self._prnt(' %s;' % errcode) + + def _extra_local_variables(self, tp, localvars, freelines): + if isinstance(tp, model.PointerType): + localvars.add('Py_ssize_t datasize') + localvars.add('struct _cffi_freeme_s *large_args_free = NULL') + freelines.add('if (large_args_free != NULL)' + ' _cffi_free_array_arguments(large_args_free);') + + def _convert_funcarg_to_c_ptr_or_array(self, tp, fromvar, tovar, errcode): + self._prnt(' datasize = _cffi_prepare_pointer_call_argument(') + self._prnt(' _cffi_type(%d), %s, (char **)&%s);' % ( + self._gettypenum(tp), fromvar, tovar)) + self._prnt(' if (datasize != 0) {') + self._prnt(' %s = ((size_t)datasize) <= 640 ? ' + 'alloca((size_t)datasize) : NULL;' % (tovar,)) + self._prnt(' if (_cffi_convert_array_argument(_cffi_type(%d), %s, ' + '(char **)&%s,' % (self._gettypenum(tp), fromvar, tovar)) + self._prnt(' datasize, &large_args_free) < 0)') + self._prnt(' %s;' % errcode) + self._prnt(' }') + + def _convert_expr_from_c(self, tp, var, context): + if isinstance(tp, model.PrimitiveType): + if tp.is_integer_type() and tp.name != '_Bool': + return '_cffi_from_c_int(%s, %s)' % (var, tp.name) + elif tp.name != 'long double': + return '_cffi_from_c_%s(%s)' % (tp.name.replace(' ', '_'), var) + else: + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, (model.PointerType, model.FunctionPtrType)): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.ArrayType): + return '_cffi_from_c_pointer((char *)%s, _cffi_type(%d))' % ( + var, self._gettypenum(model.PointerType(tp.item))) + elif isinstance(tp, model.StructOrUnion): + if tp.fldnames is None: + raise TypeError("'%s' is used as %s, but is opaque" % ( + tp._get_c_name(), context)) + return '_cffi_from_c_struct((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + elif isinstance(tp, model.EnumType): + return '_cffi_from_c_deref((char *)&%s, _cffi_type(%d))' % ( + var, self._gettypenum(tp)) + else: + raise NotImplementedError(tp) + + # ---------- + # typedefs: generates no code so far + + _generate_cpy_typedef_collecttype = _generate_nothing + _generate_cpy_typedef_decl = _generate_nothing + _generate_cpy_typedef_method = _generate_nothing + _loading_cpy_typedef = _loaded_noop + _loaded_cpy_typedef = _loaded_noop + + # ---------- + # function declarations + + def _generate_cpy_function_collecttype(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + self._do_collect_type(tp) + else: + # don't call _do_collect_type(tp) in this common case, + # otherwise test_autofilled_struct_as_argument fails + for type in tp.args: + self._do_collect_type(type) + self._do_collect_type(tp.result) + + def _generate_cpy_function_decl(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no CPython wrapper) + self._generate_cpy_const(False, name, tp) + return + prnt = self._prnt + numargs = len(tp.args) + if numargs == 0: + argname = 'noarg' + elif numargs == 1: + argname = 'arg0' + else: + argname = 'args' + prnt('static PyObject *') + prnt('_cffi_f_%s(PyObject *self, PyObject *%s)' % (name, argname)) + prnt('{') + # + context = 'argument of %s' % name + for i, type in enumerate(tp.args): + prnt(' %s;' % type.get_c_name(' x%d' % i, context)) + # + localvars = set() + freelines = set() + for type in tp.args: + self._extra_local_variables(type, localvars, freelines) + for decl in sorted(localvars): + prnt(' %s;' % (decl,)) + # + if not isinstance(tp.result, model.VoidType): + result_code = 'result = ' + context = 'result of %s' % name + prnt(' %s;' % tp.result.get_c_name(' result', context)) + prnt(' PyObject *pyresult;') + else: + result_code = '' + # + if len(tp.args) > 1: + rng = range(len(tp.args)) + for i in rng: + prnt(' PyObject *arg%d;' % i) + prnt() + prnt(' if (!PyArg_ParseTuple(args, "%s:%s", %s))' % ( + 'O' * numargs, name, ', '.join(['&arg%d' % i for i in rng]))) + prnt(' return NULL;') + prnt() + # + for i, type in enumerate(tp.args): + self._convert_funcarg_to_c(type, 'arg%d' % i, 'x%d' % i, + 'return NULL') + prnt() + # + prnt(' Py_BEGIN_ALLOW_THREADS') + prnt(' _cffi_restore_errno();') + prnt(' { %s%s(%s); }' % ( + result_code, name, + ', '.join(['x%d' % i for i in range(len(tp.args))]))) + prnt(' _cffi_save_errno();') + prnt(' Py_END_ALLOW_THREADS') + prnt() + # + prnt(' (void)self; /* unused */') + if numargs == 0: + prnt(' (void)noarg; /* unused */') + if result_code: + prnt(' pyresult = %s;' % + self._convert_expr_from_c(tp.result, 'result', 'result type')) + for freeline in freelines: + prnt(' ' + freeline) + prnt(' return pyresult;') + else: + for freeline in freelines: + prnt(' ' + freeline) + prnt(' Py_INCREF(Py_None);') + prnt(' return Py_None;') + prnt('}') + prnt() + + def _generate_cpy_function_method(self, tp, name): + if tp.ellipsis: + return + numargs = len(tp.args) + if numargs == 0: + meth = 'METH_NOARGS' + elif numargs == 1: + meth = 'METH_O' + else: + meth = 'METH_VARARGS' + self._prnt(' {"%s", _cffi_f_%s, %s, NULL},' % (name, name, meth)) + + _loading_cpy_function = _loaded_noop + + def _loaded_cpy_function(self, tp, name, module, library): + if tp.ellipsis: + return + func = getattr(module, name) + setattr(library, name, func) + self._types_of_builtin_functions[func] = tp + + # ---------- + # named structs + + _generate_cpy_struct_collecttype = _generate_nothing + def _generate_cpy_struct_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'struct', name) + def _generate_cpy_struct_method(self, tp, name): + self._generate_struct_or_union_method(tp, 'struct', name) + def _loading_cpy_struct(self, tp, name, module): + self._loading_struct_or_union(tp, 'struct', name, module) + def _loaded_cpy_struct(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + _generate_cpy_union_collecttype = _generate_nothing + def _generate_cpy_union_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'union', name) + def _generate_cpy_union_method(self, tp, name): + self._generate_struct_or_union_method(tp, 'union', name) + def _loading_cpy_union(self, tp, name, module): + self._loading_struct_or_union(tp, 'union', name, module) + def _loaded_cpy_union(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_struct_or_union_decl(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + checkfuncname = '_cffi_check_%s_%s' % (prefix, name) + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + cname = ('%s %s' % (prefix, name)).strip() + # + prnt = self._prnt + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if (isinstance(ftype, model.PrimitiveType) + and ftype.is_integer_type()) or fbitsize >= 0: + # accept all integers, but complain on float or double + prnt(' (void)((p->%s) << 1);' % fname) + else: + # only accept exactly the type declared. + try: + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + prnt('static PyObject *') + prnt('%s(PyObject *self, PyObject *noarg)' % (layoutfuncname,)) + prnt('{') + prnt(' struct _cffi_aligncheck { char x; %s y; };' % cname) + prnt(' static Py_ssize_t nums[] = {') + prnt(' sizeof(%s),' % cname) + prnt(' offsetof(struct _cffi_aligncheck, y),') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + prnt(' offsetof(%s, %s),' % (cname, fname)) + if isinstance(ftype, model.ArrayType) and ftype.length is None: + prnt(' 0, /* %s */' % ftype._get_c_name()) + else: + prnt(' sizeof(((%s *)0)->%s),' % (cname, fname)) + prnt(' -1') + prnt(' };') + prnt(' (void)self; /* unused */') + prnt(' (void)noarg; /* unused */') + prnt(' return _cffi_get_struct_layout(nums);') + prnt(' /* the next line is not executed, but compiled */') + prnt(' %s(0);' % (checkfuncname,)) + prnt('}') + prnt() + + def _generate_struct_or_union_method(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + self._prnt(' {"%s", %s, METH_NOARGS, NULL},' % (layoutfuncname, + layoutfuncname)) + + def _loading_struct_or_union(self, tp, prefix, name, module): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + # + function = getattr(module, layoutfuncname) + layout = function() + if isinstance(tp, model.StructOrUnion) and tp.partial: + # use the function()'s sizes and offsets to guide the + # layout of the struct + totalsize = layout[0] + totalalignment = layout[1] + fieldofs = layout[2::2] + fieldsize = layout[3::2] + tp.force_flatten() + assert len(fieldofs) == len(fieldsize) == len(tp.fldnames) + tp.fixedlayout = fieldofs, fieldsize, totalsize, totalalignment + else: + cname = ('%s %s' % (prefix, name)).strip() + self._struct_pending_verification[tp] = layout, cname + + def _loaded_struct_or_union(self, tp): + if tp.fldnames is None: + return # nothing to do with opaque structs + self.ffi._get_cached_btype(tp) # force 'fixedlayout' to be considered + + if tp in self._struct_pending_verification: + # check that the layout sizes and offsets match the real ones + def check(realvalue, expectedvalue, msg): + if realvalue != expectedvalue: + raise VerificationError( + "%s (we have %d, but C compiler says %d)" + % (msg, expectedvalue, realvalue)) + ffi = self.ffi + BStruct = ffi._get_cached_btype(tp) + layout, cname = self._struct_pending_verification.pop(tp) + check(layout[0], ffi.sizeof(BStruct), "wrong total size") + check(layout[1], ffi.alignof(BStruct), "wrong total alignment") + i = 2 + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + check(layout[i], ffi.offsetof(BStruct, fname), + "wrong offset for field %r" % (fname,)) + if layout[i+1] != 0: + BField = ffi._get_cached_btype(ftype) + check(layout[i+1], ffi.sizeof(BField), + "wrong size for field %r" % (fname,)) + i += 2 + assert i == len(layout) + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + _generate_cpy_anonymous_collecttype = _generate_nothing + + def _generate_cpy_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_cpy_enum_decl(tp, name, '') + else: + self._generate_struct_or_union_decl(tp, '', name) + + def _generate_cpy_anonymous_method(self, tp, name): + if not isinstance(tp, model.EnumType): + self._generate_struct_or_union_method(tp, '', name) + + def _loading_cpy_anonymous(self, tp, name, module): + if isinstance(tp, model.EnumType): + self._loading_cpy_enum(tp, name, module) + else: + self._loading_struct_or_union(tp, '', name, module) + + def _loaded_cpy_anonymous(self, tp, name, module, **kwds): + if isinstance(tp, model.EnumType): + self._loaded_cpy_enum(tp, name, module, **kwds) + else: + self._loaded_struct_or_union(tp) + + # ---------- + # constants, likely declared with '#define' + + def _generate_cpy_const(self, is_int, name, tp=None, category='const', + vartp=None, delayed=True, size_too=False, + check_value=None): + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + prnt('static int %s(PyObject *lib)' % funcname) + prnt('{') + prnt(' PyObject *o;') + prnt(' int res;') + if not is_int: + prnt(' %s;' % (vartp or tp).get_c_name(' i', name)) + else: + assert category == 'const' + # + if check_value is not None: + self._check_int_constant_value(name, check_value) + # + if not is_int: + if category == 'var': + realexpr = '&' + name + else: + realexpr = name + prnt(' i = (%s);' % (realexpr,)) + prnt(' o = %s;' % (self._convert_expr_from_c(tp, 'i', + 'variable type'),)) + assert delayed + else: + prnt(' o = _cffi_from_c_int_const(%s);' % name) + prnt(' if (o == NULL)') + prnt(' return -1;') + if size_too: + prnt(' {') + prnt(' PyObject *o1 = o;') + prnt(' o = Py_BuildValue("On", o1, (Py_ssize_t)sizeof(%s));' + % (name,)) + prnt(' Py_DECREF(o1);') + prnt(' if (o == NULL)') + prnt(' return -1;') + prnt(' }') + prnt(' res = PyObject_SetAttrString(lib, "%s", o);' % name) + prnt(' Py_DECREF(o);') + prnt(' if (res < 0)') + prnt(' return -1;') + prnt(' return %s;' % self._chained_list_constants[delayed]) + self._chained_list_constants[delayed] = funcname + '(lib)' + prnt('}') + prnt() + + def _generate_cpy_constant_collecttype(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + if not is_int: + self._do_collect_type(tp) + + def _generate_cpy_constant_decl(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + self._generate_cpy_const(is_int, name, tp) + + _generate_cpy_constant_method = _generate_nothing + _loading_cpy_constant = _loaded_noop + _loaded_cpy_constant = _loaded_noop + + # ---------- + # enums + + def _check_int_constant_value(self, name, value, err_prefix=''): + prnt = self._prnt + if value <= 0: + prnt(' if ((%s) > 0 || (long)(%s) != %dL) {' % ( + name, name, value)) + else: + prnt(' if ((%s) <= 0 || (unsigned long)(%s) != %dUL) {' % ( + name, name, value)) + prnt(' char buf[64];') + prnt(' if ((%s) <= 0)' % name) + prnt(' snprintf(buf, 63, "%%ld", (long)(%s));' % name) + prnt(' else') + prnt(' snprintf(buf, 63, "%%lu", (unsigned long)(%s));' % + name) + prnt(' PyErr_Format(_cffi_VerificationError,') + prnt(' "%s%s has the real value %s, not %s",') + prnt(' "%s", "%s", buf, "%d");' % ( + err_prefix, name, value)) + prnt(' return -1;') + prnt(' }') + + def _enum_funcname(self, prefix, name): + # "$enum_$1" => "___D_enum____D_1" + name = name.replace('$', '___D_') + return '_cffi_e_%s_%s' % (prefix, name) + + def _generate_cpy_enum_decl(self, tp, name, prefix='enum'): + if tp.partial: + for enumerator in tp.enumerators: + self._generate_cpy_const(True, enumerator, delayed=False) + return + # + funcname = self._enum_funcname(prefix, name) + prnt = self._prnt + prnt('static int %s(PyObject *lib)' % funcname) + prnt('{') + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._check_int_constant_value(enumerator, enumvalue, + "enum %s: " % name) + prnt(' return %s;' % self._chained_list_constants[True]) + self._chained_list_constants[True] = funcname + '(lib)' + prnt('}') + prnt() + + _generate_cpy_enum_collecttype = _generate_nothing + _generate_cpy_enum_method = _generate_nothing + + def _loading_cpy_enum(self, tp, name, module): + if tp.partial: + enumvalues = [getattr(module, enumerator) + for enumerator in tp.enumerators] + tp.enumvalues = tuple(enumvalues) + tp.partial_resolved = True + + def _loaded_cpy_enum(self, tp, name, module, library): + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + setattr(library, enumerator, enumvalue) + + # ---------- + # macros: for now only for integers + + def _generate_cpy_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_cpy_const(True, name, check_value=check_value) + + _generate_cpy_macro_collecttype = _generate_nothing + _generate_cpy_macro_method = _generate_nothing + _loading_cpy_macro = _loaded_noop + _loaded_cpy_macro = _loaded_noop + + # ---------- + # global variables + + def _generate_cpy_variable_collecttype(self, tp, name): + if isinstance(tp, model.ArrayType): + tp_ptr = model.PointerType(tp.item) + else: + tp_ptr = model.PointerType(tp) + self._do_collect_type(tp_ptr) + + def _generate_cpy_variable_decl(self, tp, name): + if isinstance(tp, model.ArrayType): + tp_ptr = model.PointerType(tp.item) + self._generate_cpy_const(False, name, tp, vartp=tp_ptr, + size_too = tp.length_is_unknown()) + else: + tp_ptr = model.PointerType(tp) + self._generate_cpy_const(False, name, tp_ptr, category='var') + + _generate_cpy_variable_method = _generate_nothing + _loading_cpy_variable = _loaded_noop + + def _loaded_cpy_variable(self, tp, name, module, library): + value = getattr(library, name) + if isinstance(tp, model.ArrayType): # int a[5] is "constant" in the + # sense that "a=..." is forbidden + if tp.length_is_unknown(): + assert isinstance(value, tuple) + (value, size) = value + BItemType = self.ffi._get_cached_btype(tp.item) + length, rest = divmod(size, self.ffi.sizeof(BItemType)) + if rest != 0: + raise VerificationError( + "bad size: %r does not seem to be an array of %s" % + (name, tp.item)) + tp = tp.resolve_length(length) + # 'value' is a which we have to replace with + # a if the N is actually known + if tp.length is not None: + BArray = self.ffi._get_cached_btype(tp) + value = self.ffi.cast(BArray, value) + setattr(library, name, value) + return + # remove ptr= from the library instance, and replace + # it by a property on the class, which reads/writes into ptr[0]. + ptr = value + delattr(library, name) + def getter(library): + return ptr[0] + def setter(library, value): + ptr[0] = value + setattr(type(library), name, property(getter, setter)) + type(library)._cffi_dir.append(name) + + # ---------- + + def _generate_setup_custom(self): + prnt = self._prnt + prnt('static int _cffi_setup_custom(PyObject *lib)') + prnt('{') + prnt(' return %s;' % self._chained_list_constants[True]) + prnt('}') + +cffimod_header = r''' +#include +#include + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +#endif + +#if PY_MAJOR_VERSION < 3 +# undef PyCapsule_CheckExact +# undef PyCapsule_GetPointer +# define PyCapsule_CheckExact(capsule) (PyCObject_Check(capsule)) +# define PyCapsule_GetPointer(capsule, name) \ + (PyCObject_AsVoidPtr(capsule)) +#endif + +#if PY_MAJOR_VERSION >= 3 +# define PyInt_FromLong PyLong_FromLong +#endif + +#define _cffi_from_c_double PyFloat_FromDouble +#define _cffi_from_c_float PyFloat_FromDouble +#define _cffi_from_c_long PyInt_FromLong +#define _cffi_from_c_ulong PyLong_FromUnsignedLong +#define _cffi_from_c_longlong PyLong_FromLongLong +#define _cffi_from_c_ulonglong PyLong_FromUnsignedLongLong +#define _cffi_from_c__Bool PyBool_FromLong + +#define _cffi_to_c_double PyFloat_AsDouble +#define _cffi_to_c_float PyFloat_AsDouble + +#define _cffi_from_c_int_const(x) \ + (((x) > 0) ? \ + ((unsigned long long)(x) <= (unsigned long long)LONG_MAX) ? \ + PyInt_FromLong((long)(x)) : \ + PyLong_FromUnsignedLongLong((unsigned long long)(x)) : \ + ((long long)(x) >= (long long)LONG_MIN) ? \ + PyInt_FromLong((long)(x)) : \ + PyLong_FromLongLong((long long)(x))) + +#define _cffi_from_c_int(x, type) \ + (((type)-1) > 0 ? /* unsigned */ \ + (sizeof(type) < sizeof(long) ? \ + PyInt_FromLong((long)x) : \ + sizeof(type) == sizeof(long) ? \ + PyLong_FromUnsignedLong((unsigned long)x) : \ + PyLong_FromUnsignedLongLong((unsigned long long)x)) : \ + (sizeof(type) <= sizeof(long) ? \ + PyInt_FromLong((long)x) : \ + PyLong_FromLongLong((long long)x))) + +#define _cffi_to_c_int(o, type) \ + ((type)( \ + sizeof(type) == 1 ? (((type)-1) > 0 ? (type)_cffi_to_c_u8(o) \ + : (type)_cffi_to_c_i8(o)) : \ + sizeof(type) == 2 ? (((type)-1) > 0 ? (type)_cffi_to_c_u16(o) \ + : (type)_cffi_to_c_i16(o)) : \ + sizeof(type) == 4 ? (((type)-1) > 0 ? (type)_cffi_to_c_u32(o) \ + : (type)_cffi_to_c_i32(o)) : \ + sizeof(type) == 8 ? (((type)-1) > 0 ? (type)_cffi_to_c_u64(o) \ + : (type)_cffi_to_c_i64(o)) : \ + (Py_FatalError("unsupported size for type " #type), (type)0))) + +#define _cffi_to_c_i8 \ + ((int(*)(PyObject *))_cffi_exports[1]) +#define _cffi_to_c_u8 \ + ((int(*)(PyObject *))_cffi_exports[2]) +#define _cffi_to_c_i16 \ + ((int(*)(PyObject *))_cffi_exports[3]) +#define _cffi_to_c_u16 \ + ((int(*)(PyObject *))_cffi_exports[4]) +#define _cffi_to_c_i32 \ + ((int(*)(PyObject *))_cffi_exports[5]) +#define _cffi_to_c_u32 \ + ((unsigned int(*)(PyObject *))_cffi_exports[6]) +#define _cffi_to_c_i64 \ + ((long long(*)(PyObject *))_cffi_exports[7]) +#define _cffi_to_c_u64 \ + ((unsigned long long(*)(PyObject *))_cffi_exports[8]) +#define _cffi_to_c_char \ + ((int(*)(PyObject *))_cffi_exports[9]) +#define _cffi_from_c_pointer \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[10]) +#define _cffi_to_c_pointer \ + ((char *(*)(PyObject *, CTypeDescrObject *))_cffi_exports[11]) +#define _cffi_get_struct_layout \ + ((PyObject *(*)(Py_ssize_t[]))_cffi_exports[12]) +#define _cffi_restore_errno \ + ((void(*)(void))_cffi_exports[13]) +#define _cffi_save_errno \ + ((void(*)(void))_cffi_exports[14]) +#define _cffi_from_c_char \ + ((PyObject *(*)(char))_cffi_exports[15]) +#define _cffi_from_c_deref \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[16]) +#define _cffi_to_c \ + ((int(*)(char *, CTypeDescrObject *, PyObject *))_cffi_exports[17]) +#define _cffi_from_c_struct \ + ((PyObject *(*)(char *, CTypeDescrObject *))_cffi_exports[18]) +#define _cffi_to_c_wchar_t \ + ((wchar_t(*)(PyObject *))_cffi_exports[19]) +#define _cffi_from_c_wchar_t \ + ((PyObject *(*)(wchar_t))_cffi_exports[20]) +#define _cffi_to_c_long_double \ + ((long double(*)(PyObject *))_cffi_exports[21]) +#define _cffi_to_c__Bool \ + ((_Bool(*)(PyObject *))_cffi_exports[22]) +#define _cffi_prepare_pointer_call_argument \ + ((Py_ssize_t(*)(CTypeDescrObject *, PyObject *, char **))_cffi_exports[23]) +#define _cffi_convert_array_from_object \ + ((int(*)(char *, CTypeDescrObject *, PyObject *))_cffi_exports[24]) +#define _CFFI_NUM_EXPORTS 25 + +typedef struct _ctypedescr CTypeDescrObject; + +static void *_cffi_exports[_CFFI_NUM_EXPORTS]; +static PyObject *_cffi_types, *_cffi_VerificationError; + +static int _cffi_setup_custom(PyObject *lib); /* forward */ + +static PyObject *_cffi_setup(PyObject *self, PyObject *args) +{ + PyObject *library; + int was_alive = (_cffi_types != NULL); + (void)self; /* unused */ + if (!PyArg_ParseTuple(args, "OOO", &_cffi_types, &_cffi_VerificationError, + &library)) + return NULL; + Py_INCREF(_cffi_types); + Py_INCREF(_cffi_VerificationError); + if (_cffi_setup_custom(library) < 0) + return NULL; + return PyBool_FromLong(was_alive); +} + +union _cffi_union_alignment_u { + unsigned char m_char; + unsigned short m_short; + unsigned int m_int; + unsigned long m_long; + unsigned long long m_longlong; + float m_float; + double m_double; + long double m_longdouble; +}; + +struct _cffi_freeme_s { + struct _cffi_freeme_s *next; + union _cffi_union_alignment_u alignment; +}; + +#ifdef __GNUC__ + __attribute__((unused)) +#endif +static int _cffi_convert_array_argument(CTypeDescrObject *ctptr, PyObject *arg, + char **output_data, Py_ssize_t datasize, + struct _cffi_freeme_s **freeme) +{ + char *p; + if (datasize < 0) + return -1; + + p = *output_data; + if (p == NULL) { + struct _cffi_freeme_s *fp = (struct _cffi_freeme_s *)PyObject_Malloc( + offsetof(struct _cffi_freeme_s, alignment) + (size_t)datasize); + if (fp == NULL) + return -1; + fp->next = *freeme; + *freeme = fp; + p = *output_data = (char *)&fp->alignment; + } + memset((void *)p, 0, (size_t)datasize); + return _cffi_convert_array_from_object(p, ctptr, arg); +} + +#ifdef __GNUC__ + __attribute__((unused)) +#endif +static void _cffi_free_array_arguments(struct _cffi_freeme_s *freeme) +{ + do { + void *p = (void *)freeme; + freeme = freeme->next; + PyObject_Free(p); + } while (freeme != NULL); +} + +static int _cffi_init(void) +{ + PyObject *module, *c_api_object = NULL; + + module = PyImport_ImportModule("_cffi_backend"); + if (module == NULL) + goto failure; + + c_api_object = PyObject_GetAttrString(module, "_C_API"); + if (c_api_object == NULL) + goto failure; + if (!PyCapsule_CheckExact(c_api_object)) { + PyErr_SetNone(PyExc_ImportError); + goto failure; + } + memcpy(_cffi_exports, PyCapsule_GetPointer(c_api_object, "cffi"), + _CFFI_NUM_EXPORTS * sizeof(void *)); + + Py_DECREF(module); + Py_DECREF(c_api_object); + return 0; + + failure: + Py_XDECREF(module); + Py_XDECREF(c_api_object); + return -1; +} + +#define _cffi_type(num) ((CTypeDescrObject *)PyList_GET_ITEM(_cffi_types, num)) + +/**********/ +''' diff --git a/lib/python3.10/site-packages/cffi/vengine_gen.py b/lib/python3.10/site-packages/cffi/vengine_gen.py new file mode 100644 index 0000000000000000000000000000000000000000..26421526f62a07e04419cd57f1f19a64ecd36452 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/vengine_gen.py @@ -0,0 +1,675 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys, os +import types + +from . import model +from .error import VerificationError + + +class VGenericEngine(object): + _class_key = 'g' + _gen_python_module = False + + def __init__(self, verifier): + self.verifier = verifier + self.ffi = verifier.ffi + self.export_symbols = [] + self._struct_pending_verification = {} + + def patch_extension_kwds(self, kwds): + # add 'export_symbols' to the dictionary. Note that we add the + # list before filling it. When we fill it, it will thus also show + # up in kwds['export_symbols']. + kwds.setdefault('export_symbols', self.export_symbols) + + def find_module(self, module_name, path, so_suffixes): + for so_suffix in so_suffixes: + basename = module_name + so_suffix + if path is None: + path = sys.path + for dirname in path: + filename = os.path.join(dirname, basename) + if os.path.isfile(filename): + return filename + + def collect_types(self): + pass # not needed in the generic engine + + def _prnt(self, what=''): + self._f.write(what + '\n') + + def write_source_to_f(self): + prnt = self._prnt + # first paste some standard set of lines that are mostly '#include' + prnt(cffimod_header) + # then paste the C source given by the user, verbatim. + prnt(self.verifier.preamble) + # + # call generate_gen_xxx_decl(), for every xxx found from + # ffi._parser._declarations. This generates all the functions. + self._generate('decl') + # + # on Windows, distutils insists on putting init_cffi_xyz in + # 'export_symbols', so instead of fighting it, just give up and + # give it one + if sys.platform == 'win32': + if sys.version_info >= (3,): + prefix = 'PyInit_' + else: + prefix = 'init' + modname = self.verifier.get_module_name() + prnt("void %s%s(void) { }\n" % (prefix, modname)) + + def load_library(self, flags=0): + # import it with the CFFI backend + backend = self.ffi._backend + # needs to make a path that contains '/', on Posix + filename = os.path.join(os.curdir, self.verifier.modulefilename) + module = backend.load_library(filename, flags) + # + # call loading_gen_struct() to get the struct layout inferred by + # the C compiler + self._load(module, 'loading') + + # build the FFILibrary class and instance, this is a module subclass + # because modules are expected to have usually-constant-attributes and + # in PyPy this means the JIT is able to treat attributes as constant, + # which we want. + class FFILibrary(types.ModuleType): + _cffi_generic_module = module + _cffi_ffi = self.ffi + _cffi_dir = [] + def __dir__(self): + return FFILibrary._cffi_dir + library = FFILibrary("") + # + # finally, call the loaded_gen_xxx() functions. This will set + # up the 'library' object. + self._load(module, 'loaded', library=library) + return library + + def _get_declarations(self): + lst = [(key, tp) for (key, (tp, qual)) in + self.ffi._parser._declarations.items()] + lst.sort() + return lst + + def _generate(self, step_name): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + try: + method = getattr(self, '_generate_gen_%s_%s' % (kind, + step_name)) + except AttributeError: + raise VerificationError( + "not implemented in verify(): %r" % name) + try: + method(tp, realname) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _load(self, module, step_name, **kwds): + for name, tp in self._get_declarations(): + kind, realname = name.split(' ', 1) + method = getattr(self, '_%s_gen_%s' % (step_name, kind)) + try: + method(tp, realname, module, **kwds) + except Exception as e: + model.attach_exception_info(e, name) + raise + + def _generate_nothing(self, tp, name): + pass + + def _loaded_noop(self, tp, name, module, **kwds): + pass + + # ---------- + # typedefs: generates no code so far + + _generate_gen_typedef_decl = _generate_nothing + _loading_gen_typedef = _loaded_noop + _loaded_gen_typedef = _loaded_noop + + # ---------- + # function declarations + + def _generate_gen_function_decl(self, tp, name): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + # cannot support vararg functions better than this: check for its + # exact type (including the fixed arguments), and build it as a + # constant function pointer (no _cffi_f_%s wrapper) + self._generate_gen_const(False, name, tp) + return + prnt = self._prnt + numargs = len(tp.args) + argnames = [] + for i, type in enumerate(tp.args): + indirection = '' + if isinstance(type, model.StructOrUnion): + indirection = '*' + argnames.append('%sx%d' % (indirection, i)) + context = 'argument of %s' % name + arglist = [type.get_c_name(' %s' % arg, context) + for type, arg in zip(tp.args, argnames)] + tpresult = tp.result + if isinstance(tpresult, model.StructOrUnion): + arglist.insert(0, tpresult.get_c_name(' *r', context)) + tpresult = model.void_type + arglist = ', '.join(arglist) or 'void' + wrappername = '_cffi_f_%s' % name + self.export_symbols.append(wrappername) + if tp.abi: + abi = tp.abi + ' ' + else: + abi = '' + funcdecl = ' %s%s(%s)' % (abi, wrappername, arglist) + context = 'result of %s' % name + prnt(tpresult.get_c_name(funcdecl, context)) + prnt('{') + # + if isinstance(tp.result, model.StructOrUnion): + result_code = '*r = ' + elif not isinstance(tp.result, model.VoidType): + result_code = 'return ' + else: + result_code = '' + prnt(' %s%s(%s);' % (result_code, name, ', '.join(argnames))) + prnt('}') + prnt() + + _loading_gen_function = _loaded_noop + + def _loaded_gen_function(self, tp, name, module, library): + assert isinstance(tp, model.FunctionPtrType) + if tp.ellipsis: + newfunction = self._load_constant(False, tp, name, module) + else: + indirections = [] + base_tp = tp + if (any(isinstance(typ, model.StructOrUnion) for typ in tp.args) + or isinstance(tp.result, model.StructOrUnion)): + indirect_args = [] + for i, typ in enumerate(tp.args): + if isinstance(typ, model.StructOrUnion): + typ = model.PointerType(typ) + indirections.append((i, typ)) + indirect_args.append(typ) + indirect_result = tp.result + if isinstance(indirect_result, model.StructOrUnion): + if indirect_result.fldtypes is None: + raise TypeError("'%s' is used as result type, " + "but is opaque" % ( + indirect_result._get_c_name(),)) + indirect_result = model.PointerType(indirect_result) + indirect_args.insert(0, indirect_result) + indirections.insert(0, ("result", indirect_result)) + indirect_result = model.void_type + tp = model.FunctionPtrType(tuple(indirect_args), + indirect_result, tp.ellipsis) + BFunc = self.ffi._get_cached_btype(tp) + wrappername = '_cffi_f_%s' % name + newfunction = module.load_function(BFunc, wrappername) + for i, typ in indirections: + newfunction = self._make_struct_wrapper(newfunction, i, typ, + base_tp) + setattr(library, name, newfunction) + type(library)._cffi_dir.append(name) + + def _make_struct_wrapper(self, oldfunc, i, tp, base_tp): + backend = self.ffi._backend + BType = self.ffi._get_cached_btype(tp) + if i == "result": + ffi = self.ffi + def newfunc(*args): + res = ffi.new(BType) + oldfunc(res, *args) + return res[0] + else: + def newfunc(*args): + args = args[:i] + (backend.newp(BType, args[i]),) + args[i+1:] + return oldfunc(*args) + newfunc._cffi_base_type = base_tp + return newfunc + + # ---------- + # named structs + + def _generate_gen_struct_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'struct', name) + + def _loading_gen_struct(self, tp, name, module): + self._loading_struct_or_union(tp, 'struct', name, module) + + def _loaded_gen_struct(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_gen_union_decl(self, tp, name): + assert name == tp.name + self._generate_struct_or_union_decl(tp, 'union', name) + + def _loading_gen_union(self, tp, name, module): + self._loading_struct_or_union(tp, 'union', name, module) + + def _loaded_gen_union(self, tp, name, module, **kwds): + self._loaded_struct_or_union(tp) + + def _generate_struct_or_union_decl(self, tp, prefix, name): + if tp.fldnames is None: + return # nothing to do with opaque structs + checkfuncname = '_cffi_check_%s_%s' % (prefix, name) + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + cname = ('%s %s' % (prefix, name)).strip() + # + prnt = self._prnt + prnt('static void %s(%s *p)' % (checkfuncname, cname)) + prnt('{') + prnt(' /* only to generate compile-time warnings or errors */') + prnt(' (void)p;') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if (isinstance(ftype, model.PrimitiveType) + and ftype.is_integer_type()) or fbitsize >= 0: + # accept all integers, but complain on float or double + prnt(' (void)((p->%s) << 1);' % fname) + else: + # only accept exactly the type declared. + try: + prnt(' { %s = &p->%s; (void)tmp; }' % ( + ftype.get_c_name('*tmp', 'field %r'%fname, quals=fqual), + fname)) + except VerificationError as e: + prnt(' /* %s */' % str(e)) # cannot verify it, ignore + prnt('}') + self.export_symbols.append(layoutfuncname) + prnt('intptr_t %s(intptr_t i)' % (layoutfuncname,)) + prnt('{') + prnt(' struct _cffi_aligncheck { char x; %s y; };' % cname) + prnt(' static intptr_t nums[] = {') + prnt(' sizeof(%s),' % cname) + prnt(' offsetof(struct _cffi_aligncheck, y),') + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + prnt(' offsetof(%s, %s),' % (cname, fname)) + if isinstance(ftype, model.ArrayType) and ftype.length is None: + prnt(' 0, /* %s */' % ftype._get_c_name()) + else: + prnt(' sizeof(((%s *)0)->%s),' % (cname, fname)) + prnt(' -1') + prnt(' };') + prnt(' return nums[i];') + prnt(' /* the next line is not executed, but compiled */') + prnt(' %s(0);' % (checkfuncname,)) + prnt('}') + prnt() + + def _loading_struct_or_union(self, tp, prefix, name, module): + if tp.fldnames is None: + return # nothing to do with opaque structs + layoutfuncname = '_cffi_layout_%s_%s' % (prefix, name) + # + BFunc = self.ffi._typeof_locked("intptr_t(*)(intptr_t)")[0] + function = module.load_function(BFunc, layoutfuncname) + layout = [] + num = 0 + while True: + x = function(num) + if x < 0: break + layout.append(x) + num += 1 + if isinstance(tp, model.StructOrUnion) and tp.partial: + # use the function()'s sizes and offsets to guide the + # layout of the struct + totalsize = layout[0] + totalalignment = layout[1] + fieldofs = layout[2::2] + fieldsize = layout[3::2] + tp.force_flatten() + assert len(fieldofs) == len(fieldsize) == len(tp.fldnames) + tp.fixedlayout = fieldofs, fieldsize, totalsize, totalalignment + else: + cname = ('%s %s' % (prefix, name)).strip() + self._struct_pending_verification[tp] = layout, cname + + def _loaded_struct_or_union(self, tp): + if tp.fldnames is None: + return # nothing to do with opaque structs + self.ffi._get_cached_btype(tp) # force 'fixedlayout' to be considered + + if tp in self._struct_pending_verification: + # check that the layout sizes and offsets match the real ones + def check(realvalue, expectedvalue, msg): + if realvalue != expectedvalue: + raise VerificationError( + "%s (we have %d, but C compiler says %d)" + % (msg, expectedvalue, realvalue)) + ffi = self.ffi + BStruct = ffi._get_cached_btype(tp) + layout, cname = self._struct_pending_verification.pop(tp) + check(layout[0], ffi.sizeof(BStruct), "wrong total size") + check(layout[1], ffi.alignof(BStruct), "wrong total alignment") + i = 2 + for fname, ftype, fbitsize, fqual in tp.enumfields(): + if fbitsize >= 0: + continue # xxx ignore fbitsize for now + check(layout[i], ffi.offsetof(BStruct, fname), + "wrong offset for field %r" % (fname,)) + if layout[i+1] != 0: + BField = ffi._get_cached_btype(ftype) + check(layout[i+1], ffi.sizeof(BField), + "wrong size for field %r" % (fname,)) + i += 2 + assert i == len(layout) + + # ---------- + # 'anonymous' declarations. These are produced for anonymous structs + # or unions; the 'name' is obtained by a typedef. + + def _generate_gen_anonymous_decl(self, tp, name): + if isinstance(tp, model.EnumType): + self._generate_gen_enum_decl(tp, name, '') + else: + self._generate_struct_or_union_decl(tp, '', name) + + def _loading_gen_anonymous(self, tp, name, module): + if isinstance(tp, model.EnumType): + self._loading_gen_enum(tp, name, module, '') + else: + self._loading_struct_or_union(tp, '', name, module) + + def _loaded_gen_anonymous(self, tp, name, module, **kwds): + if isinstance(tp, model.EnumType): + self._loaded_gen_enum(tp, name, module, **kwds) + else: + self._loaded_struct_or_union(tp) + + # ---------- + # constants, likely declared with '#define' + + def _generate_gen_const(self, is_int, name, tp=None, category='const', + check_value=None): + prnt = self._prnt + funcname = '_cffi_%s_%s' % (category, name) + self.export_symbols.append(funcname) + if check_value is not None: + assert is_int + assert category == 'const' + prnt('int %s(char *out_error)' % funcname) + prnt('{') + self._check_int_constant_value(name, check_value) + prnt(' return 0;') + prnt('}') + elif is_int: + assert category == 'const' + prnt('int %s(long long *out_value)' % funcname) + prnt('{') + prnt(' *out_value = (long long)(%s);' % (name,)) + prnt(' return (%s) <= 0;' % (name,)) + prnt('}') + else: + assert tp is not None + assert check_value is None + if category == 'var': + ampersand = '&' + else: + ampersand = '' + extra = '' + if category == 'const' and isinstance(tp, model.StructOrUnion): + extra = 'const *' + ampersand = '&' + prnt(tp.get_c_name(' %s%s(void)' % (extra, funcname), name)) + prnt('{') + prnt(' return (%s%s);' % (ampersand, name)) + prnt('}') + prnt() + + def _generate_gen_constant_decl(self, tp, name): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + self._generate_gen_const(is_int, name, tp) + + _loading_gen_constant = _loaded_noop + + def _load_constant(self, is_int, tp, name, module, check_value=None): + funcname = '_cffi_const_%s' % name + if check_value is not None: + assert is_int + self._load_known_int_constant(module, funcname) + value = check_value + elif is_int: + BType = self.ffi._typeof_locked("long long*")[0] + BFunc = self.ffi._typeof_locked("int(*)(long long*)")[0] + function = module.load_function(BFunc, funcname) + p = self.ffi.new(BType) + negative = function(p) + value = int(p[0]) + if value < 0 and not negative: + BLongLong = self.ffi._typeof_locked("long long")[0] + value += (1 << (8*self.ffi.sizeof(BLongLong))) + else: + assert check_value is None + fntypeextra = '(*)(void)' + if isinstance(tp, model.StructOrUnion): + fntypeextra = '*' + fntypeextra + BFunc = self.ffi._typeof_locked(tp.get_c_name(fntypeextra, name))[0] + function = module.load_function(BFunc, funcname) + value = function() + if isinstance(tp, model.StructOrUnion): + value = value[0] + return value + + def _loaded_gen_constant(self, tp, name, module, library): + is_int = isinstance(tp, model.PrimitiveType) and tp.is_integer_type() + value = self._load_constant(is_int, tp, name, module) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + + # ---------- + # enums + + def _check_int_constant_value(self, name, value): + prnt = self._prnt + if value <= 0: + prnt(' if ((%s) > 0 || (long)(%s) != %dL) {' % ( + name, name, value)) + else: + prnt(' if ((%s) <= 0 || (unsigned long)(%s) != %dUL) {' % ( + name, name, value)) + prnt(' char buf[64];') + prnt(' if ((%s) <= 0)' % name) + prnt(' sprintf(buf, "%%ld", (long)(%s));' % name) + prnt(' else') + prnt(' sprintf(buf, "%%lu", (unsigned long)(%s));' % + name) + prnt(' sprintf(out_error, "%s has the real value %s, not %s",') + prnt(' "%s", buf, "%d");' % (name[:100], value)) + prnt(' return -1;') + prnt(' }') + + def _load_known_int_constant(self, module, funcname): + BType = self.ffi._typeof_locked("char[]")[0] + BFunc = self.ffi._typeof_locked("int(*)(char*)")[0] + function = module.load_function(BFunc, funcname) + p = self.ffi.new(BType, 256) + if function(p) < 0: + error = self.ffi.string(p) + if sys.version_info >= (3,): + error = str(error, 'utf-8') + raise VerificationError(error) + + def _enum_funcname(self, prefix, name): + # "$enum_$1" => "___D_enum____D_1" + name = name.replace('$', '___D_') + return '_cffi_e_%s_%s' % (prefix, name) + + def _generate_gen_enum_decl(self, tp, name, prefix='enum'): + if tp.partial: + for enumerator in tp.enumerators: + self._generate_gen_const(True, enumerator) + return + # + funcname = self._enum_funcname(prefix, name) + self.export_symbols.append(funcname) + prnt = self._prnt + prnt('int %s(char *out_error)' % funcname) + prnt('{') + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + self._check_int_constant_value(enumerator, enumvalue) + prnt(' return 0;') + prnt('}') + prnt() + + def _loading_gen_enum(self, tp, name, module, prefix='enum'): + if tp.partial: + enumvalues = [self._load_constant(True, tp, enumerator, module) + for enumerator in tp.enumerators] + tp.enumvalues = tuple(enumvalues) + tp.partial_resolved = True + else: + funcname = self._enum_funcname(prefix, name) + self._load_known_int_constant(module, funcname) + + def _loaded_gen_enum(self, tp, name, module, library): + for enumerator, enumvalue in zip(tp.enumerators, tp.enumvalues): + setattr(library, enumerator, enumvalue) + type(library)._cffi_dir.append(enumerator) + + # ---------- + # macros: for now only for integers + + def _generate_gen_macro_decl(self, tp, name): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + self._generate_gen_const(True, name, check_value=check_value) + + _loading_gen_macro = _loaded_noop + + def _loaded_gen_macro(self, tp, name, module, library): + if tp == '...': + check_value = None + else: + check_value = tp # an integer + value = self._load_constant(True, tp, name, module, + check_value=check_value) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + + # ---------- + # global variables + + def _generate_gen_variable_decl(self, tp, name): + if isinstance(tp, model.ArrayType): + if tp.length_is_unknown(): + prnt = self._prnt + funcname = '_cffi_sizeof_%s' % (name,) + self.export_symbols.append(funcname) + prnt("size_t %s(void)" % funcname) + prnt("{") + prnt(" return sizeof(%s);" % (name,)) + prnt("}") + tp_ptr = model.PointerType(tp.item) + self._generate_gen_const(False, name, tp_ptr) + else: + tp_ptr = model.PointerType(tp) + self._generate_gen_const(False, name, tp_ptr, category='var') + + _loading_gen_variable = _loaded_noop + + def _loaded_gen_variable(self, tp, name, module, library): + if isinstance(tp, model.ArrayType): # int a[5] is "constant" in the + # sense that "a=..." is forbidden + if tp.length_is_unknown(): + funcname = '_cffi_sizeof_%s' % (name,) + BFunc = self.ffi._typeof_locked('size_t(*)(void)')[0] + function = module.load_function(BFunc, funcname) + size = function() + BItemType = self.ffi._get_cached_btype(tp.item) + length, rest = divmod(size, self.ffi.sizeof(BItemType)) + if rest != 0: + raise VerificationError( + "bad size: %r does not seem to be an array of %s" % + (name, tp.item)) + tp = tp.resolve_length(length) + tp_ptr = model.PointerType(tp.item) + value = self._load_constant(False, tp_ptr, name, module) + # 'value' is a which we have to replace with + # a if the N is actually known + if tp.length is not None: + BArray = self.ffi._get_cached_btype(tp) + value = self.ffi.cast(BArray, value) + setattr(library, name, value) + type(library)._cffi_dir.append(name) + return + # remove ptr= from the library instance, and replace + # it by a property on the class, which reads/writes into ptr[0]. + funcname = '_cffi_var_%s' % name + BFunc = self.ffi._typeof_locked(tp.get_c_name('*(*)(void)', name))[0] + function = module.load_function(BFunc, funcname) + ptr = function() + def getter(library): + return ptr[0] + def setter(library, value): + ptr[0] = value + setattr(type(library), name, property(getter, setter)) + type(library)._cffi_dir.append(name) + +cffimod_header = r''' +#include +#include +#include +#include +#include /* XXX for ssize_t on some platforms */ + +/* this block of #ifs should be kept exactly identical between + c/_cffi_backend.c, cffi/vengine_cpy.py, cffi/vengine_gen.py + and cffi/_cffi_include.h */ +#if defined(_MSC_VER) +# include /* for alloca() */ +# if _MSC_VER < 1600 /* MSVC < 2010 */ + typedef __int8 int8_t; + typedef __int16 int16_t; + typedef __int32 int32_t; + typedef __int64 int64_t; + typedef unsigned __int8 uint8_t; + typedef unsigned __int16 uint16_t; + typedef unsigned __int32 uint32_t; + typedef unsigned __int64 uint64_t; + typedef __int8 int_least8_t; + typedef __int16 int_least16_t; + typedef __int32 int_least32_t; + typedef __int64 int_least64_t; + typedef unsigned __int8 uint_least8_t; + typedef unsigned __int16 uint_least16_t; + typedef unsigned __int32 uint_least32_t; + typedef unsigned __int64 uint_least64_t; + typedef __int8 int_fast8_t; + typedef __int16 int_fast16_t; + typedef __int32 int_fast32_t; + typedef __int64 int_fast64_t; + typedef unsigned __int8 uint_fast8_t; + typedef unsigned __int16 uint_fast16_t; + typedef unsigned __int32 uint_fast32_t; + typedef unsigned __int64 uint_fast64_t; + typedef __int64 intmax_t; + typedef unsigned __int64 uintmax_t; +# else +# include +# endif +# if _MSC_VER < 1800 /* MSVC < 2013 */ +# ifndef __cplusplus + typedef unsigned char _Bool; +# endif +# endif +#else +# include +# if (defined (__SVR4) && defined (__sun)) || defined(_AIX) || defined(__hpux) +# include +# endif +#endif +''' diff --git a/lib/python3.10/site-packages/cffi/verifier.py b/lib/python3.10/site-packages/cffi/verifier.py new file mode 100644 index 0000000000000000000000000000000000000000..a500c7814adf8ce52e911e0679d0b98335ae6597 --- /dev/null +++ b/lib/python3.10/site-packages/cffi/verifier.py @@ -0,0 +1,307 @@ +# +# DEPRECATED: implementation for ffi.verify() +# +import sys, os, binascii, shutil, io +from . import __version_verifier_modules__ +from . import ffiplatform +from .error import VerificationError + +if sys.version_info >= (3, 3): + import importlib.machinery + def _extension_suffixes(): + return importlib.machinery.EXTENSION_SUFFIXES[:] +else: + import imp + def _extension_suffixes(): + return [suffix for suffix, _, type in imp.get_suffixes() + if type == imp.C_EXTENSION] + + +if sys.version_info >= (3,): + NativeIO = io.StringIO +else: + class NativeIO(io.BytesIO): + def write(self, s): + if isinstance(s, unicode): + s = s.encode('ascii') + super(NativeIO, self).write(s) + + +class Verifier(object): + + def __init__(self, ffi, preamble, tmpdir=None, modulename=None, + ext_package=None, tag='', force_generic_engine=False, + source_extension='.c', flags=None, relative_to=None, **kwds): + if ffi._parser._uses_new_feature: + raise VerificationError( + "feature not supported with ffi.verify(), but only " + "with ffi.set_source(): %s" % (ffi._parser._uses_new_feature,)) + self.ffi = ffi + self.preamble = preamble + if not modulename: + flattened_kwds = ffiplatform.flatten(kwds) + vengine_class = _locate_engine_class(ffi, force_generic_engine) + self._vengine = vengine_class(self) + self._vengine.patch_extension_kwds(kwds) + self.flags = flags + self.kwds = self.make_relative_to(kwds, relative_to) + # + if modulename: + if tag: + raise TypeError("can't specify both 'modulename' and 'tag'") + else: + key = '\x00'.join(['%d.%d' % sys.version_info[:2], + __version_verifier_modules__, + preamble, flattened_kwds] + + ffi._cdefsources) + if sys.version_info >= (3,): + key = key.encode('utf-8') + k1 = hex(binascii.crc32(key[0::2]) & 0xffffffff) + k1 = k1.lstrip('0x').rstrip('L') + k2 = hex(binascii.crc32(key[1::2]) & 0xffffffff) + k2 = k2.lstrip('0').rstrip('L') + modulename = '_cffi_%s_%s%s%s' % (tag, self._vengine._class_key, + k1, k2) + suffix = _get_so_suffixes()[0] + self.tmpdir = tmpdir or _caller_dir_pycache() + self.sourcefilename = os.path.join(self.tmpdir, modulename + source_extension) + self.modulefilename = os.path.join(self.tmpdir, modulename + suffix) + self.ext_package = ext_package + self._has_source = False + self._has_module = False + + def write_source(self, file=None): + """Write the C source code. It is produced in 'self.sourcefilename', + which can be tweaked beforehand.""" + with self.ffi._lock: + if self._has_source and file is None: + raise VerificationError( + "source code already written") + self._write_source(file) + + def compile_module(self): + """Write the C source code (if not done already) and compile it. + This produces a dynamic link library in 'self.modulefilename'.""" + with self.ffi._lock: + if self._has_module: + raise VerificationError("module already compiled") + if not self._has_source: + self._write_source() + self._compile_module() + + def load_library(self): + """Get a C module from this Verifier instance. + Returns an instance of a FFILibrary class that behaves like the + objects returned by ffi.dlopen(), but that delegates all + operations to the C module. If necessary, the C code is written + and compiled first. + """ + with self.ffi._lock: + if not self._has_module: + self._locate_module() + if not self._has_module: + if not self._has_source: + self._write_source() + self._compile_module() + return self._load_library() + + def get_module_name(self): + basename = os.path.basename(self.modulefilename) + # kill both the .so extension and the other .'s, as introduced + # by Python 3: 'basename.cpython-33m.so' + basename = basename.split('.', 1)[0] + # and the _d added in Python 2 debug builds --- but try to be + # conservative and not kill a legitimate _d + if basename.endswith('_d') and hasattr(sys, 'gettotalrefcount'): + basename = basename[:-2] + return basename + + def get_extension(self): + ffiplatform._hack_at_distutils() # backward compatibility hack + if not self._has_source: + with self.ffi._lock: + if not self._has_source: + self._write_source() + sourcename = ffiplatform.maybe_relative_path(self.sourcefilename) + modname = self.get_module_name() + return ffiplatform.get_extension(sourcename, modname, **self.kwds) + + def generates_python_module(self): + return self._vengine._gen_python_module + + def make_relative_to(self, kwds, relative_to): + if relative_to and os.path.dirname(relative_to): + dirname = os.path.dirname(relative_to) + kwds = kwds.copy() + for key in ffiplatform.LIST_OF_FILE_NAMES: + if key in kwds: + lst = kwds[key] + if not isinstance(lst, (list, tuple)): + raise TypeError("keyword '%s' should be a list or tuple" + % (key,)) + lst = [os.path.join(dirname, fn) for fn in lst] + kwds[key] = lst + return kwds + + # ---------- + + def _locate_module(self): + if not os.path.isfile(self.modulefilename): + if self.ext_package: + try: + pkg = __import__(self.ext_package, None, None, ['__doc__']) + except ImportError: + return # cannot import the package itself, give up + # (e.g. it might be called differently before installation) + path = pkg.__path__ + else: + path = None + filename = self._vengine.find_module(self.get_module_name(), path, + _get_so_suffixes()) + if filename is None: + return + self.modulefilename = filename + self._vengine.collect_types() + self._has_module = True + + def _write_source_to(self, file): + self._vengine._f = file + try: + self._vengine.write_source_to_f() + finally: + del self._vengine._f + + def _write_source(self, file=None): + if file is not None: + self._write_source_to(file) + else: + # Write our source file to an in memory file. + f = NativeIO() + self._write_source_to(f) + source_data = f.getvalue() + + # Determine if this matches the current file + if os.path.exists(self.sourcefilename): + with open(self.sourcefilename, "r") as fp: + needs_written = not (fp.read() == source_data) + else: + needs_written = True + + # Actually write the file out if it doesn't match + if needs_written: + _ensure_dir(self.sourcefilename) + with open(self.sourcefilename, "w") as fp: + fp.write(source_data) + + # Set this flag + self._has_source = True + + def _compile_module(self): + # compile this C source + tmpdir = os.path.dirname(self.sourcefilename) + outputfilename = ffiplatform.compile(tmpdir, self.get_extension()) + try: + same = ffiplatform.samefile(outputfilename, self.modulefilename) + except OSError: + same = False + if not same: + _ensure_dir(self.modulefilename) + shutil.move(outputfilename, self.modulefilename) + self._has_module = True + + def _load_library(self): + assert self._has_module + if self.flags is not None: + return self._vengine.load_library(self.flags) + else: + return self._vengine.load_library() + +# ____________________________________________________________ + +_FORCE_GENERIC_ENGINE = False # for tests + +def _locate_engine_class(ffi, force_generic_engine): + if _FORCE_GENERIC_ENGINE: + force_generic_engine = True + if not force_generic_engine: + if '__pypy__' in sys.builtin_module_names: + force_generic_engine = True + else: + try: + import _cffi_backend + except ImportError: + _cffi_backend = '?' + if ffi._backend is not _cffi_backend: + force_generic_engine = True + if force_generic_engine: + from . import vengine_gen + return vengine_gen.VGenericEngine + else: + from . import vengine_cpy + return vengine_cpy.VCPythonEngine + +# ____________________________________________________________ + +_TMPDIR = None + +def _caller_dir_pycache(): + if _TMPDIR: + return _TMPDIR + result = os.environ.get('CFFI_TMPDIR') + if result: + return result + filename = sys._getframe(2).f_code.co_filename + return os.path.abspath(os.path.join(os.path.dirname(filename), + '__pycache__')) + +def set_tmpdir(dirname): + """Set the temporary directory to use instead of __pycache__.""" + global _TMPDIR + _TMPDIR = dirname + +def cleanup_tmpdir(tmpdir=None, keep_so=False): + """Clean up the temporary directory by removing all files in it + called `_cffi_*.{c,so}` as well as the `build` subdirectory.""" + tmpdir = tmpdir or _caller_dir_pycache() + try: + filelist = os.listdir(tmpdir) + except OSError: + return + if keep_so: + suffix = '.c' # only remove .c files + else: + suffix = _get_so_suffixes()[0].lower() + for fn in filelist: + if fn.lower().startswith('_cffi_') and ( + fn.lower().endswith(suffix) or fn.lower().endswith('.c')): + try: + os.unlink(os.path.join(tmpdir, fn)) + except OSError: + pass + clean_dir = [os.path.join(tmpdir, 'build')] + for dir in clean_dir: + try: + for fn in os.listdir(dir): + fn = os.path.join(dir, fn) + if os.path.isdir(fn): + clean_dir.append(fn) + else: + os.unlink(fn) + except OSError: + pass + +def _get_so_suffixes(): + suffixes = _extension_suffixes() + if not suffixes: + # bah, no C_EXTENSION available. Occurs on pypy without cpyext + if sys.platform == 'win32': + suffixes = [".pyd"] + else: + suffixes = [".so"] + + return suffixes + +def _ensure_dir(filename): + dirname = os.path.dirname(filename) + if dirname and not os.path.isdir(dirname): + os.makedirs(dirname) diff --git a/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/INSTALLER b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/LICENSE b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ad82355b802d542e8443dc78b937fa36fdcc0ace --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 TAHRI Ahmed R. + +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. \ No newline at end of file diff --git a/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/METADATA b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..822550e36bfc53472baf4f4d059b878817c87496 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/METADATA @@ -0,0 +1,683 @@ +Metadata-Version: 2.1 +Name: charset-normalizer +Version: 3.3.2 +Summary: The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet. +Home-page: https://github.com/Ousret/charset_normalizer +Author: Ahmed TAHRI +Author-email: ahmed.tahri@cloudnursery.dev +License: MIT +Project-URL: Bug Reports, https://github.com/Ousret/charset_normalizer/issues +Project-URL: Documentation, https://charset-normalizer.readthedocs.io/en/latest +Keywords: encoding,charset,charset-detector,detector,normalization,unicode,chardet,detect +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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 :: Implementation :: PyPy +Classifier: Topic :: Text Processing :: Linguistic +Classifier: Topic :: Utilities +Classifier: Typing :: Typed +Requires-Python: >=3.7.0 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: unicode_backport + +

Charset Detection, for Everyone 👋

+ +

+ The Real First Universal Charset Detector
+ + + + + Download Count Total + + + + +

+

+ Featured Packages
+ + Static Badge + + + Static Badge + +

+

+ In other language (unofficial port - by the community)
+ + Static Badge + +

+ +> A library that helps you read text from an unknown charset encoding.
Motivated by `chardet`, +> I'm trying to resolve the issue by taking a new approach. +> All IANA character set names for which the Python core library provides codecs are supported. + +

+ >>>>> 👉 Try Me Online Now, Then Adopt Me 👈 <<<<< +

+ +This project offers you an alternative to **Universal Charset Encoding Detector**, also known as **Chardet**. + +| Feature | [Chardet](https://github.com/chardet/chardet) | Charset Normalizer | [cChardet](https://github.com/PyYoshi/cChardet) | +|--------------------------------------------------|:---------------------------------------------:|:--------------------------------------------------------------------------------------------------:|:-----------------------------------------------:| +| `Fast` | ❌ | ✅ | ✅ | +| `Universal**` | ❌ | ✅ | ❌ | +| `Reliable` **without** distinguishable standards | ❌ | ✅ | ✅ | +| `Reliable` **with** distinguishable standards | ✅ | ✅ | ✅ | +| `License` | LGPL-2.1
_restrictive_ | MIT | MPL-1.1
_restrictive_ | +| `Native Python` | ✅ | ✅ | ❌ | +| `Detect spoken language` | ❌ | ✅ | N/A | +| `UnicodeDecodeError Safety` | ❌ | ✅ | ❌ | +| `Whl Size (min)` | 193.6 kB | 42 kB | ~200 kB | +| `Supported Encoding` | 33 | 🎉 [99](https://charset-normalizer.readthedocs.io/en/latest/user/support.html#supported-encodings) | 40 | + +

+Reading Normalized TextCat Reading Text +

+ +*\*\* : They are clearly using specific code for a specific encoding even if covering most of used one*
+Did you got there because of the logs? See [https://charset-normalizer.readthedocs.io/en/latest/user/miscellaneous.html](https://charset-normalizer.readthedocs.io/en/latest/user/miscellaneous.html) + +## ⚡ Performance + +This package offer better performance than its counterpart Chardet. Here are some numbers. + +| Package | Accuracy | Mean per file (ms) | File per sec (est) | +|-----------------------------------------------|:--------:|:------------------:|:------------------:| +| [chardet](https://github.com/chardet/chardet) | 86 % | 200 ms | 5 file/sec | +| charset-normalizer | **98 %** | **10 ms** | 100 file/sec | + +| Package | 99th percentile | 95th percentile | 50th percentile | +|-----------------------------------------------|:---------------:|:---------------:|:---------------:| +| [chardet](https://github.com/chardet/chardet) | 1200 ms | 287 ms | 23 ms | +| charset-normalizer | 100 ms | 50 ms | 5 ms | + +Chardet's performance on larger file (1MB+) are very poor. Expect huge difference on large payload. + +> Stats are generated using 400+ files using default parameters. More details on used files, see GHA workflows. +> And yes, these results might change at any time. The dataset can be updated to include more files. +> The actual delays heavily depends on your CPU capabilities. The factors should remain the same. +> Keep in mind that the stats are generous and that Chardet accuracy vs our is measured using Chardet initial capability +> (eg. Supported Encoding) Challenge-them if you want. + +## ✨ Installation + +Using pip: + +```sh +pip install charset-normalizer -U +``` + +## 🚀 Basic Usage + +### CLI +This package comes with a CLI. + +``` +usage: normalizer [-h] [-v] [-a] [-n] [-m] [-r] [-f] [-t THRESHOLD] + file [file ...] + +The Real First Universal Charset Detector. Discover originating encoding used +on text file. Normalize text to unicode. + +positional arguments: + files File(s) to be analysed + +optional arguments: + -h, --help show this help message and exit + -v, --verbose Display complementary information about file if any. + Stdout will contain logs about the detection process. + -a, --with-alternative + Output complementary possibilities if any. Top-level + JSON WILL be a list. + -n, --normalize Permit to normalize input file. If not set, program + does not write anything. + -m, --minimal Only output the charset detected to STDOUT. Disabling + JSON output. + -r, --replace Replace file when trying to normalize it instead of + creating a new one. + -f, --force Replace file without asking if you are sure, use this + flag with caution. + -t THRESHOLD, --threshold THRESHOLD + Define a custom maximum amount of chaos allowed in + decoded content. 0. <= chaos <= 1. + --version Show version information and exit. +``` + +```bash +normalizer ./data/sample.1.fr.srt +``` + +or + +```bash +python -m charset_normalizer ./data/sample.1.fr.srt +``` + +🎉 Since version 1.4.0 the CLI produce easily usable stdout result in JSON format. + +```json +{ + "path": "/home/default/projects/charset_normalizer/data/sample.1.fr.srt", + "encoding": "cp1252", + "encoding_aliases": [ + "1252", + "windows_1252" + ], + "alternative_encodings": [ + "cp1254", + "cp1256", + "cp1258", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + "mbcs" + ], + "language": "French", + "alphabets": [ + "Basic Latin", + "Latin-1 Supplement" + ], + "has_sig_or_bom": false, + "chaos": 0.149, + "coherence": 97.152, + "unicode_path": null, + "is_preferred": true +} +``` + +### Python +*Just print out normalized text* +```python +from charset_normalizer import from_path + +results = from_path('./my_subtitle.srt') + +print(str(results.best())) +``` + +*Upgrade your code without effort* +```python +from charset_normalizer import detect +``` + +The above code will behave the same as **chardet**. We ensure that we offer the best (reasonable) BC result possible. + +See the docs for advanced usage : [readthedocs.io](https://charset-normalizer.readthedocs.io/en/latest/) + +## 😇 Why + +When I started using Chardet, I noticed that it was not suited to my expectations, and I wanted to propose a +reliable alternative using a completely different method. Also! I never back down on a good challenge! + +I **don't care** about the **originating charset** encoding, because **two different tables** can +produce **two identical rendered string.** +What I want is to get readable text, the best I can. + +In a way, **I'm brute forcing text decoding.** How cool is that ? 😎 + +Don't confuse package **ftfy** with charset-normalizer or chardet. ftfy goal is to repair unicode string whereas charset-normalizer to convert raw file in unknown encoding to unicode. + +## 🍰 How + + - Discard all charset encoding table that could not fit the binary content. + - Measure noise, or the mess once opened (by chunks) with a corresponding charset encoding. + - Extract matches with the lowest mess detected. + - Additionally, we measure coherence / probe for a language. + +**Wait a minute**, what is noise/mess and coherence according to **YOU ?** + +*Noise :* I opened hundred of text files, **written by humans**, with the wrong encoding table. **I observed**, then +**I established** some ground rules about **what is obvious** when **it seems like** a mess. + I know that my interpretation of what is noise is probably incomplete, feel free to contribute in order to + improve or rewrite it. + +*Coherence :* For each language there is on earth, we have computed ranked letter appearance occurrences (the best we can). So I thought +that intel is worth something here. So I use those records against decoded text to check if I can detect intelligent design. + +## ⚡ Known limitations + + - Language detection is unreliable when text contains two or more languages sharing identical letters. (eg. HTML (english tags) + Turkish content (Sharing Latin characters)) + - Every charset detector heavily depends on sufficient content. In common cases, do not bother run detection on very tiny content. + +## ⚠️ About Python EOLs + +**If you are running:** + +- Python >=2.7,<3.5: Unsupported +- Python 3.5: charset-normalizer < 2.1 +- Python 3.6: charset-normalizer < 3.1 +- Python 3.7: charset-normalizer < 4.0 + +Upgrade your Python interpreter as soon as possible. + +## 👤 Contributing + +Contributions, issues and feature requests are very much welcome.
+Feel free to check [issues page](https://github.com/ousret/charset_normalizer/issues) if you want to contribute. + +## 📝 License + +Copyright © [Ahmed TAHRI @Ousret](https://github.com/Ousret).
+This project is [MIT](https://github.com/Ousret/charset_normalizer/blob/master/LICENSE) licensed. + +Characters frequencies used in this project © 2012 [Denny Vrandečić](http://simia.net/letters/) + +## 💼 For Enterprise + +Professional support for charset-normalizer is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-charset-normalizer?utm_source=pypi-charset-normalizer&utm_medium=readme + +# Changelog +All notable changes to charset-normalizer will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [3.3.2](https://github.com/Ousret/charset_normalizer/compare/3.3.1...3.3.2) (2023-10-31) + +### Fixed +- Unintentional memory usage regression when using large payload that match several encoding (#376) +- Regression on some detection case showcased in the documentation (#371) + +### Added +- Noise (md) probe that identify malformed arabic representation due to the presence of letters in isolated form (credit to my wife) + +## [3.3.1](https://github.com/Ousret/charset_normalizer/compare/3.3.0...3.3.1) (2023-10-22) + +### Changed +- Optional mypyc compilation upgraded to version 1.6.1 for Python >= 3.8 +- Improved the general detection reliability based on reports from the community + +## [3.3.0](https://github.com/Ousret/charset_normalizer/compare/3.2.0...3.3.0) (2023-09-30) + +### Added +- Allow to execute the CLI (e.g. normalizer) through `python -m charset_normalizer.cli` or `python -m charset_normalizer` +- Support for 9 forgotten encoding that are supported by Python but unlisted in `encoding.aliases` as they have no alias (#323) + +### Removed +- (internal) Redundant utils.is_ascii function and unused function is_private_use_only +- (internal) charset_normalizer.assets is moved inside charset_normalizer.constant + +### Changed +- (internal) Unicode code blocks in constants are updated using the latest v15.0.0 definition to improve detection +- Optional mypyc compilation upgraded to version 1.5.1 for Python >= 3.8 + +### Fixed +- Unable to properly sort CharsetMatch when both chaos/noise and coherence were close due to an unreachable condition in \_\_lt\_\_ (#350) + +## [3.2.0](https://github.com/Ousret/charset_normalizer/compare/3.1.0...3.2.0) (2023-06-07) + +### Changed +- Typehint for function `from_path` no longer enforce `PathLike` as its first argument +- Minor improvement over the global detection reliability + +### Added +- Introduce function `is_binary` that relies on main capabilities, and optimized to detect binaries +- Propagate `enable_fallback` argument throughout `from_bytes`, `from_path`, and `from_fp` that allow a deeper control over the detection (default True) +- Explicit support for Python 3.12 + +### Fixed +- Edge case detection failure where a file would contain 'very-long' camel cased word (Issue #289) + +## [3.1.0](https://github.com/Ousret/charset_normalizer/compare/3.0.1...3.1.0) (2023-03-06) + +### Added +- Argument `should_rename_legacy` for legacy function `detect` and disregard any new arguments without errors (PR #262) + +### Removed +- Support for Python 3.6 (PR #260) + +### Changed +- Optional speedup provided by mypy/c 1.0.1 + +## [3.0.1](https://github.com/Ousret/charset_normalizer/compare/3.0.0...3.0.1) (2022-11-18) + +### Fixed +- Multi-bytes cutter/chunk generator did not always cut correctly (PR #233) + +### Changed +- Speedup provided by mypy/c 0.990 on Python >= 3.7 + +## [3.0.0](https://github.com/Ousret/charset_normalizer/compare/2.1.1...3.0.0) (2022-10-20) + +### Added +- Extend the capability of explain=True when cp_isolation contains at most two entries (min one), will log in details of the Mess-detector results +- Support for alternative language frequency set in charset_normalizer.assets.FREQUENCIES +- Add parameter `language_threshold` in `from_bytes`, `from_path` and `from_fp` to adjust the minimum expected coherence ratio +- `normalizer --version` now specify if current version provide extra speedup (meaning mypyc compilation whl) + +### Changed +- Build with static metadata using 'build' frontend +- Make the language detection stricter +- Optional: Module `md.py` can be compiled using Mypyc to provide an extra speedup up to 4x faster than v2.1 + +### Fixed +- CLI with opt --normalize fail when using full path for files +- TooManyAccentuatedPlugin induce false positive on the mess detection when too few alpha character have been fed to it +- Sphinx warnings when generating the documentation + +### Removed +- Coherence detector no longer return 'Simple English' instead return 'English' +- Coherence detector no longer return 'Classical Chinese' instead return 'Chinese' +- Breaking: Method `first()` and `best()` from CharsetMatch +- UTF-7 will no longer appear as "detected" without a recognized SIG/mark (is unreliable/conflict with ASCII) +- Breaking: Class aliases CharsetDetector, CharsetDoctor, CharsetNormalizerMatch and CharsetNormalizerMatches +- Breaking: Top-level function `normalize` +- Breaking: Properties `chaos_secondary_pass`, `coherence_non_latin` and `w_counter` from CharsetMatch +- Support for the backport `unicodedata2` + +## [3.0.0rc1](https://github.com/Ousret/charset_normalizer/compare/3.0.0b2...3.0.0rc1) (2022-10-18) + +### Added +- Extend the capability of explain=True when cp_isolation contains at most two entries (min one), will log in details of the Mess-detector results +- Support for alternative language frequency set in charset_normalizer.assets.FREQUENCIES +- Add parameter `language_threshold` in `from_bytes`, `from_path` and `from_fp` to adjust the minimum expected coherence ratio + +### Changed +- Build with static metadata using 'build' frontend +- Make the language detection stricter + +### Fixed +- CLI with opt --normalize fail when using full path for files +- TooManyAccentuatedPlugin induce false positive on the mess detection when too few alpha character have been fed to it + +### Removed +- Coherence detector no longer return 'Simple English' instead return 'English' +- Coherence detector no longer return 'Classical Chinese' instead return 'Chinese' + +## [3.0.0b2](https://github.com/Ousret/charset_normalizer/compare/3.0.0b1...3.0.0b2) (2022-08-21) + +### Added +- `normalizer --version` now specify if current version provide extra speedup (meaning mypyc compilation whl) + +### Removed +- Breaking: Method `first()` and `best()` from CharsetMatch +- UTF-7 will no longer appear as "detected" without a recognized SIG/mark (is unreliable/conflict with ASCII) + +### Fixed +- Sphinx warnings when generating the documentation + +## [3.0.0b1](https://github.com/Ousret/charset_normalizer/compare/2.1.0...3.0.0b1) (2022-08-15) + +### Changed +- Optional: Module `md.py` can be compiled using Mypyc to provide an extra speedup up to 4x faster than v2.1 + +### Removed +- Breaking: Class aliases CharsetDetector, CharsetDoctor, CharsetNormalizerMatch and CharsetNormalizerMatches +- Breaking: Top-level function `normalize` +- Breaking: Properties `chaos_secondary_pass`, `coherence_non_latin` and `w_counter` from CharsetMatch +- Support for the backport `unicodedata2` + +## [2.1.1](https://github.com/Ousret/charset_normalizer/compare/2.1.0...2.1.1) (2022-08-19) + +### Deprecated +- Function `normalize` scheduled for removal in 3.0 + +### Changed +- Removed useless call to decode in fn is_unprintable (#206) + +### Fixed +- Third-party library (i18n xgettext) crashing not recognizing utf_8 (PEP 263) with underscore from [@aleksandernovikov](https://github.com/aleksandernovikov) (#204) + +## [2.1.0](https://github.com/Ousret/charset_normalizer/compare/2.0.12...2.1.0) (2022-06-19) + +### Added +- Output the Unicode table version when running the CLI with `--version` (PR #194) + +### Changed +- Re-use decoded buffer for single byte character sets from [@nijel](https://github.com/nijel) (PR #175) +- Fixing some performance bottlenecks from [@deedy5](https://github.com/deedy5) (PR #183) + +### Fixed +- Workaround potential bug in cpython with Zero Width No-Break Space located in Arabic Presentation Forms-B, Unicode 1.1 not acknowledged as space (PR #175) +- CLI default threshold aligned with the API threshold from [@oleksandr-kuzmenko](https://github.com/oleksandr-kuzmenko) (PR #181) + +### Removed +- Support for Python 3.5 (PR #192) + +### Deprecated +- Use of backport unicodedata from `unicodedata2` as Python is quickly catching up, scheduled for removal in 3.0 (PR #194) + +## [2.0.12](https://github.com/Ousret/charset_normalizer/compare/2.0.11...2.0.12) (2022-02-12) + +### Fixed +- ASCII miss-detection on rare cases (PR #170) + +## [2.0.11](https://github.com/Ousret/charset_normalizer/compare/2.0.10...2.0.11) (2022-01-30) + +### Added +- Explicit support for Python 3.11 (PR #164) + +### Changed +- The logging behavior have been completely reviewed, now using only TRACE and DEBUG levels (PR #163 #165) + +## [2.0.10](https://github.com/Ousret/charset_normalizer/compare/2.0.9...2.0.10) (2022-01-04) + +### Fixed +- Fallback match entries might lead to UnicodeDecodeError for large bytes sequence (PR #154) + +### Changed +- Skipping the language-detection (CD) on ASCII (PR #155) + +## [2.0.9](https://github.com/Ousret/charset_normalizer/compare/2.0.8...2.0.9) (2021-12-03) + +### Changed +- Moderating the logging impact (since 2.0.8) for specific environments (PR #147) + +### Fixed +- Wrong logging level applied when setting kwarg `explain` to True (PR #146) + +## [2.0.8](https://github.com/Ousret/charset_normalizer/compare/2.0.7...2.0.8) (2021-11-24) +### Changed +- Improvement over Vietnamese detection (PR #126) +- MD improvement on trailing data and long foreign (non-pure latin) data (PR #124) +- Efficiency improvements in cd/alphabet_languages from [@adbar](https://github.com/adbar) (PR #122) +- call sum() without an intermediary list following PEP 289 recommendations from [@adbar](https://github.com/adbar) (PR #129) +- Code style as refactored by Sourcery-AI (PR #131) +- Minor adjustment on the MD around european words (PR #133) +- Remove and replace SRTs from assets / tests (PR #139) +- Initialize the library logger with a `NullHandler` by default from [@nmaynes](https://github.com/nmaynes) (PR #135) +- Setting kwarg `explain` to True will add provisionally (bounded to function lifespan) a specific stream handler (PR #135) + +### Fixed +- Fix large (misleading) sequence giving UnicodeDecodeError (PR #137) +- Avoid using too insignificant chunk (PR #137) + +### Added +- Add and expose function `set_logging_handler` to configure a specific StreamHandler from [@nmaynes](https://github.com/nmaynes) (PR #135) +- Add `CHANGELOG.md` entries, format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) (PR #141) + +## [2.0.7](https://github.com/Ousret/charset_normalizer/compare/2.0.6...2.0.7) (2021-10-11) +### Added +- Add support for Kazakh (Cyrillic) language detection (PR #109) + +### Changed +- Further, improve inferring the language from a given single-byte code page (PR #112) +- Vainly trying to leverage PEP263 when PEP3120 is not supported (PR #116) +- Refactoring for potential performance improvements in loops from [@adbar](https://github.com/adbar) (PR #113) +- Various detection improvement (MD+CD) (PR #117) + +### Removed +- Remove redundant logging entry about detected language(s) (PR #115) + +### Fixed +- Fix a minor inconsistency between Python 3.5 and other versions regarding language detection (PR #117 #102) + +## [2.0.6](https://github.com/Ousret/charset_normalizer/compare/2.0.5...2.0.6) (2021-09-18) +### Fixed +- Unforeseen regression with the loss of the backward-compatibility with some older minor of Python 3.5.x (PR #100) +- Fix CLI crash when using --minimal output in certain cases (PR #103) + +### Changed +- Minor improvement to the detection efficiency (less than 1%) (PR #106 #101) + +## [2.0.5](https://github.com/Ousret/charset_normalizer/compare/2.0.4...2.0.5) (2021-09-14) +### Changed +- The project now comply with: flake8, mypy, isort and black to ensure a better overall quality (PR #81) +- The BC-support with v1.x was improved, the old staticmethods are restored (PR #82) +- The Unicode detection is slightly improved (PR #93) +- Add syntax sugar \_\_bool\_\_ for results CharsetMatches list-container (PR #91) + +### Removed +- The project no longer raise warning on tiny content given for detection, will be simply logged as warning instead (PR #92) + +### Fixed +- In some rare case, the chunks extractor could cut in the middle of a multi-byte character and could mislead the mess detection (PR #95) +- Some rare 'space' characters could trip up the UnprintablePlugin/Mess detection (PR #96) +- The MANIFEST.in was not exhaustive (PR #78) + +## [2.0.4](https://github.com/Ousret/charset_normalizer/compare/2.0.3...2.0.4) (2021-07-30) +### Fixed +- The CLI no longer raise an unexpected exception when no encoding has been found (PR #70) +- Fix accessing the 'alphabets' property when the payload contains surrogate characters (PR #68) +- The logger could mislead (explain=True) on detected languages and the impact of one MBCS match (PR #72) +- Submatch factoring could be wrong in rare edge cases (PR #72) +- Multiple files given to the CLI were ignored when publishing results to STDOUT. (After the first path) (PR #72) +- Fix line endings from CRLF to LF for certain project files (PR #67) + +### Changed +- Adjust the MD to lower the sensitivity, thus improving the global detection reliability (PR #69 #76) +- Allow fallback on specified encoding if any (PR #71) + +## [2.0.3](https://github.com/Ousret/charset_normalizer/compare/2.0.2...2.0.3) (2021-07-16) +### Changed +- Part of the detection mechanism has been improved to be less sensitive, resulting in more accurate detection results. Especially ASCII. (PR #63) +- According to the community wishes, the detection will fall back on ASCII or UTF-8 in a last-resort case. (PR #64) + +## [2.0.2](https://github.com/Ousret/charset_normalizer/compare/2.0.1...2.0.2) (2021-07-15) +### Fixed +- Empty/Too small JSON payload miss-detection fixed. Report from [@tseaver](https://github.com/tseaver) (PR #59) + +### Changed +- Don't inject unicodedata2 into sys.modules from [@akx](https://github.com/akx) (PR #57) + +## [2.0.1](https://github.com/Ousret/charset_normalizer/compare/2.0.0...2.0.1) (2021-07-13) +### Fixed +- Make it work where there isn't a filesystem available, dropping assets frequencies.json. Report from [@sethmlarson](https://github.com/sethmlarson). (PR #55) +- Using explain=False permanently disable the verbose output in the current runtime (PR #47) +- One log entry (language target preemptive) was not show in logs when using explain=True (PR #47) +- Fix undesired exception (ValueError) on getitem of instance CharsetMatches (PR #52) + +### Changed +- Public function normalize default args values were not aligned with from_bytes (PR #53) + +### Added +- You may now use charset aliases in cp_isolation and cp_exclusion arguments (PR #47) + +## [2.0.0](https://github.com/Ousret/charset_normalizer/compare/1.4.1...2.0.0) (2021-07-02) +### Changed +- 4x to 5 times faster than the previous 1.4.0 release. At least 2x faster than Chardet. +- Accent has been made on UTF-8 detection, should perform rather instantaneous. +- The backward compatibility with Chardet has been greatly improved. The legacy detect function returns an identical charset name whenever possible. +- The detection mechanism has been slightly improved, now Turkish content is detected correctly (most of the time) +- The program has been rewritten to ease the readability and maintainability. (+Using static typing)+ +- utf_7 detection has been reinstated. + +### Removed +- This package no longer require anything when used with Python 3.5 (Dropped cached_property) +- Removed support for these languages: Catalan, Esperanto, Kazakh, Baque, Volapük, Azeri, Galician, Nynorsk, Macedonian, and Serbocroatian. +- The exception hook on UnicodeDecodeError has been removed. + +### Deprecated +- Methods coherence_non_latin, w_counter, chaos_secondary_pass of the class CharsetMatch are now deprecated and scheduled for removal in v3.0 + +### Fixed +- The CLI output used the relative path of the file(s). Should be absolute. + +## [1.4.1](https://github.com/Ousret/charset_normalizer/compare/1.4.0...1.4.1) (2021-05-28) +### Fixed +- Logger configuration/usage no longer conflict with others (PR #44) + +## [1.4.0](https://github.com/Ousret/charset_normalizer/compare/1.3.9...1.4.0) (2021-05-21) +### Removed +- Using standard logging instead of using the package loguru. +- Dropping nose test framework in favor of the maintained pytest. +- Choose to not use dragonmapper package to help with gibberish Chinese/CJK text. +- Require cached_property only for Python 3.5 due to constraint. Dropping for every other interpreter version. +- Stop support for UTF-7 that does not contain a SIG. +- Dropping PrettyTable, replaced with pure JSON output in CLI. + +### Fixed +- BOM marker in a CharsetNormalizerMatch instance could be False in rare cases even if obviously present. Due to the sub-match factoring process. +- Not searching properly for the BOM when trying utf32/16 parent codec. + +### Changed +- Improving the package final size by compressing frequencies.json. +- Huge improvement over the larges payload. + +### Added +- CLI now produces JSON consumable output. +- Return ASCII if given sequences fit. Given reasonable confidence. + +## [1.3.9](https://github.com/Ousret/charset_normalizer/compare/1.3.8...1.3.9) (2021-05-13) + +### Fixed +- In some very rare cases, you may end up getting encode/decode errors due to a bad bytes payload (PR #40) + +## [1.3.8](https://github.com/Ousret/charset_normalizer/compare/1.3.7...1.3.8) (2021-05-12) + +### Fixed +- Empty given payload for detection may cause an exception if trying to access the `alphabets` property. (PR #39) + +## [1.3.7](https://github.com/Ousret/charset_normalizer/compare/1.3.6...1.3.7) (2021-05-12) + +### Fixed +- The legacy detect function should return UTF-8-SIG if sig is present in the payload. (PR #38) + +## [1.3.6](https://github.com/Ousret/charset_normalizer/compare/1.3.5...1.3.6) (2021-02-09) + +### Changed +- Amend the previous release to allow prettytable 2.0 (PR #35) + +## [1.3.5](https://github.com/Ousret/charset_normalizer/compare/1.3.4...1.3.5) (2021-02-08) + +### Fixed +- Fix error while using the package with a python pre-release interpreter (PR #33) + +### Changed +- Dependencies refactoring, constraints revised. + +### Added +- Add python 3.9 and 3.10 to the supported interpreters + +MIT License + +Copyright (c) 2019 TAHRI Ahmed R. + +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/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/direct_url.json b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..8e9e5fd8ad5947b6c63f7cb87a9f4dccc56c4235 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///croot/charset-normalizer_1721748349566/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/entry_points.txt b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..65619e73ec06c20c2a70c9507b872ad624d1a85c --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer-3.3.2.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +normalizer = charset_normalizer.cli:cli_detect diff --git a/lib/python3.10/site-packages/charset_normalizer/__init__.py b/lib/python3.10/site-packages/charset_normalizer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..55991fc38062b9c800805437ee49b0cf42b98103 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/__init__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Charset-Normalizer +~~~~~~~~~~~~~~ +The Real First Universal Charset Detector. +A library that helps you read text from an unknown charset encoding. +Motivated by chardet, This package is trying to resolve the issue by taking a new approach. +All IANA character set names for which the Python core library provides codecs are supported. + +Basic usage: + >>> from charset_normalizer import from_bytes + >>> results = from_bytes('Bсеки човек има право на образование. Oбразованието!'.encode('utf_8')) + >>> best_guess = results.best() + >>> str(best_guess) + 'Bсеки човек има право на образование. Oбразованието!' + +Others methods and usages are available - see the full documentation +at . +:copyright: (c) 2021 by Ahmed TAHRI +:license: MIT, see LICENSE for more details. +""" +import logging + +from .api import from_bytes, from_fp, from_path, is_binary +from .legacy import detect +from .models import CharsetMatch, CharsetMatches +from .utils import set_logging_handler +from .version import VERSION, __version__ + +__all__ = ( + "from_fp", + "from_path", + "from_bytes", + "is_binary", + "detect", + "CharsetMatch", + "CharsetMatches", + "__version__", + "VERSION", + "set_logging_handler", +) + +# Attach a NullHandler to the top level logger by default +# https://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library + +logging.getLogger("charset_normalizer").addHandler(logging.NullHandler()) diff --git a/lib/python3.10/site-packages/charset_normalizer/__main__.py b/lib/python3.10/site-packages/charset_normalizer/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..beae2ef77490c9f9c9255dd68facbb6de132841f --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/__main__.py @@ -0,0 +1,4 @@ +from .cli import cli_detect + +if __name__ == "__main__": + cli_detect() diff --git a/lib/python3.10/site-packages/charset_normalizer/api.py b/lib/python3.10/site-packages/charset_normalizer/api.py new file mode 100644 index 0000000000000000000000000000000000000000..0ba08e3a50ba6d61e75f3f31772eb4dfdd3f8f05 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/api.py @@ -0,0 +1,626 @@ +import logging +from os import PathLike +from typing import BinaryIO, List, Optional, Set, Union + +from .cd import ( + coherence_ratio, + encoding_languages, + mb_encoding_languages, + merge_coherence_ratios, +) +from .constant import IANA_SUPPORTED, TOO_BIG_SEQUENCE, TOO_SMALL_SEQUENCE, TRACE +from .md import mess_ratio +from .models import CharsetMatch, CharsetMatches +from .utils import ( + any_specified_encoding, + cut_sequence_chunks, + iana_name, + identify_sig_or_bom, + is_cp_similar, + is_multi_byte_encoding, + should_strip_sig_or_bom, +) + +# Will most likely be controversial +# logging.addLevelName(TRACE, "TRACE") +logger = logging.getLogger("charset_normalizer") +explain_handler = logging.StreamHandler() +explain_handler.setFormatter( + logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") +) + + +def from_bytes( + sequences: Union[bytes, bytearray], + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.2, + cp_isolation: Optional[List[str]] = None, + cp_exclusion: Optional[List[str]] = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Given a raw bytes sequence, return the best possibles charset usable to render str objects. + If there is no results, it is a strong indicator that the source is binary/not text. + By default, the process will extract 5 blocks of 512o each to assess the mess and coherence of a given sequence. + And will give up a particular code page after 20% of measured mess. Those criteria are customizable at will. + + The preemptive behavior DOES NOT replace the traditional detection workflow, it prioritize a particular code page + but never take it for granted. Can improve the performance. + + You may want to focus your attention to some code page or/and not others, use cp_isolation and cp_exclusion for that + purpose. + + This function will strip the SIG in the payload/sequence every time except on UTF-16, UTF-32. + By default the library does not setup any handler other than the NullHandler, if you choose to set the 'explain' + toggle to True it will alter the logger configuration to add a StreamHandler that is suitable for debugging. + Custom logging format and handler can be set manually. + """ + + if not isinstance(sequences, (bytearray, bytes)): + raise TypeError( + "Expected object of type bytes or bytearray, got: {0}".format( + type(sequences) + ) + ) + + if explain: + previous_logger_level: int = logger.level + logger.addHandler(explain_handler) + logger.setLevel(TRACE) + + length: int = len(sequences) + + if length == 0: + logger.debug("Encoding detection on empty bytes, assuming utf_8 intention.") + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level or logging.WARNING) + return CharsetMatches([CharsetMatch(sequences, "utf_8", 0.0, False, [], "")]) + + if cp_isolation is not None: + logger.log( + TRACE, + "cp_isolation is set. use this flag for debugging purpose. " + "limited list of encoding allowed : %s.", + ", ".join(cp_isolation), + ) + cp_isolation = [iana_name(cp, False) for cp in cp_isolation] + else: + cp_isolation = [] + + if cp_exclusion is not None: + logger.log( + TRACE, + "cp_exclusion is set. use this flag for debugging purpose. " + "limited list of encoding excluded : %s.", + ", ".join(cp_exclusion), + ) + cp_exclusion = [iana_name(cp, False) for cp in cp_exclusion] + else: + cp_exclusion = [] + + if length <= (chunk_size * steps): + logger.log( + TRACE, + "override steps (%i) and chunk_size (%i) as content does not fit (%i byte(s) given) parameters.", + steps, + chunk_size, + length, + ) + steps = 1 + chunk_size = length + + if steps > 1 and length / steps < chunk_size: + chunk_size = int(length / steps) + + is_too_small_sequence: bool = len(sequences) < TOO_SMALL_SEQUENCE + is_too_large_sequence: bool = len(sequences) >= TOO_BIG_SEQUENCE + + if is_too_small_sequence: + logger.log( + TRACE, + "Trying to detect encoding from a tiny portion of ({}) byte(s).".format( + length + ), + ) + elif is_too_large_sequence: + logger.log( + TRACE, + "Using lazy str decoding because the payload is quite large, ({}) byte(s).".format( + length + ), + ) + + prioritized_encodings: List[str] = [] + + specified_encoding: Optional[str] = ( + any_specified_encoding(sequences) if preemptive_behaviour else None + ) + + if specified_encoding is not None: + prioritized_encodings.append(specified_encoding) + logger.log( + TRACE, + "Detected declarative mark in sequence. Priority +1 given for %s.", + specified_encoding, + ) + + tested: Set[str] = set() + tested_but_hard_failure: List[str] = [] + tested_but_soft_failure: List[str] = [] + + fallback_ascii: Optional[CharsetMatch] = None + fallback_u8: Optional[CharsetMatch] = None + fallback_specified: Optional[CharsetMatch] = None + + results: CharsetMatches = CharsetMatches() + + sig_encoding, sig_payload = identify_sig_or_bom(sequences) + + if sig_encoding is not None: + prioritized_encodings.append(sig_encoding) + logger.log( + TRACE, + "Detected a SIG or BOM mark on first %i byte(s). Priority +1 given for %s.", + len(sig_payload), + sig_encoding, + ) + + prioritized_encodings.append("ascii") + + if "utf_8" not in prioritized_encodings: + prioritized_encodings.append("utf_8") + + for encoding_iana in prioritized_encodings + IANA_SUPPORTED: + if cp_isolation and encoding_iana not in cp_isolation: + continue + + if cp_exclusion and encoding_iana in cp_exclusion: + continue + + if encoding_iana in tested: + continue + + tested.add(encoding_iana) + + decoded_payload: Optional[str] = None + bom_or_sig_available: bool = sig_encoding == encoding_iana + strip_sig_or_bom: bool = bom_or_sig_available and should_strip_sig_or_bom( + encoding_iana + ) + + if encoding_iana in {"utf_16", "utf_32"} and not bom_or_sig_available: + logger.log( + TRACE, + "Encoding %s won't be tested as-is because it require a BOM. Will try some sub-encoder LE/BE.", + encoding_iana, + ) + continue + if encoding_iana in {"utf_7"} and not bom_or_sig_available: + logger.log( + TRACE, + "Encoding %s won't be tested as-is because detection is unreliable without BOM/SIG.", + encoding_iana, + ) + continue + + try: + is_multi_byte_decoder: bool = is_multi_byte_encoding(encoding_iana) + except (ModuleNotFoundError, ImportError): + logger.log( + TRACE, + "Encoding %s does not provide an IncrementalDecoder", + encoding_iana, + ) + continue + + try: + if is_too_large_sequence and is_multi_byte_decoder is False: + str( + sequences[: int(50e4)] + if strip_sig_or_bom is False + else sequences[len(sig_payload) : int(50e4)], + encoding=encoding_iana, + ) + else: + decoded_payload = str( + sequences + if strip_sig_or_bom is False + else sequences[len(sig_payload) :], + encoding=encoding_iana, + ) + except (UnicodeDecodeError, LookupError) as e: + if not isinstance(e, LookupError): + logger.log( + TRACE, + "Code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + tested_but_hard_failure.append(encoding_iana) + continue + + similar_soft_failure_test: bool = False + + for encoding_soft_failed in tested_but_soft_failure: + if is_cp_similar(encoding_iana, encoding_soft_failed): + similar_soft_failure_test = True + break + + if similar_soft_failure_test: + logger.log( + TRACE, + "%s is deemed too similar to code page %s and was consider unsuited already. Continuing!", + encoding_iana, + encoding_soft_failed, + ) + continue + + r_ = range( + 0 if not bom_or_sig_available else len(sig_payload), + length, + int(length / steps), + ) + + multi_byte_bonus: bool = ( + is_multi_byte_decoder + and decoded_payload is not None + and len(decoded_payload) < length + ) + + if multi_byte_bonus: + logger.log( + TRACE, + "Code page %s is a multi byte encoding table and it appear that at least one character " + "was encoded using n-bytes.", + encoding_iana, + ) + + max_chunk_gave_up: int = int(len(r_) / 4) + + max_chunk_gave_up = max(max_chunk_gave_up, 2) + early_stop_count: int = 0 + lazy_str_hard_failure = False + + md_chunks: List[str] = [] + md_ratios = [] + + try: + for chunk in cut_sequence_chunks( + sequences, + encoding_iana, + r_, + chunk_size, + bom_or_sig_available, + strip_sig_or_bom, + sig_payload, + is_multi_byte_decoder, + decoded_payload, + ): + md_chunks.append(chunk) + + md_ratios.append( + mess_ratio( + chunk, + threshold, + explain is True and 1 <= len(cp_isolation) <= 2, + ) + ) + + if md_ratios[-1] >= threshold: + early_stop_count += 1 + + if (early_stop_count >= max_chunk_gave_up) or ( + bom_or_sig_available and strip_sig_or_bom is False + ): + break + except ( + UnicodeDecodeError + ) as e: # Lazy str loading may have missed something there + logger.log( + TRACE, + "LazyStr Loading: After MD chunk decode, code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + early_stop_count = max_chunk_gave_up + lazy_str_hard_failure = True + + # We might want to check the sequence again with the whole content + # Only if initial MD tests passes + if ( + not lazy_str_hard_failure + and is_too_large_sequence + and not is_multi_byte_decoder + ): + try: + sequences[int(50e3) :].decode(encoding_iana, errors="strict") + except UnicodeDecodeError as e: + logger.log( + TRACE, + "LazyStr Loading: After final lookup, code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + tested_but_hard_failure.append(encoding_iana) + continue + + mean_mess_ratio: float = sum(md_ratios) / len(md_ratios) if md_ratios else 0.0 + if mean_mess_ratio >= threshold or early_stop_count >= max_chunk_gave_up: + tested_but_soft_failure.append(encoding_iana) + logger.log( + TRACE, + "%s was excluded because of initial chaos probing. Gave up %i time(s). " + "Computed mean chaos is %f %%.", + encoding_iana, + early_stop_count, + round(mean_mess_ratio * 100, ndigits=3), + ) + # Preparing those fallbacks in case we got nothing. + if ( + enable_fallback + and encoding_iana in ["ascii", "utf_8", specified_encoding] + and not lazy_str_hard_failure + ): + fallback_entry = CharsetMatch( + sequences, encoding_iana, threshold, False, [], decoded_payload + ) + if encoding_iana == specified_encoding: + fallback_specified = fallback_entry + elif encoding_iana == "ascii": + fallback_ascii = fallback_entry + else: + fallback_u8 = fallback_entry + continue + + logger.log( + TRACE, + "%s passed initial chaos probing. Mean measured chaos is %f %%", + encoding_iana, + round(mean_mess_ratio * 100, ndigits=3), + ) + + if not is_multi_byte_decoder: + target_languages: List[str] = encoding_languages(encoding_iana) + else: + target_languages = mb_encoding_languages(encoding_iana) + + if target_languages: + logger.log( + TRACE, + "{} should target any language(s) of {}".format( + encoding_iana, str(target_languages) + ), + ) + + cd_ratios = [] + + # We shall skip the CD when its about ASCII + # Most of the time its not relevant to run "language-detection" on it. + if encoding_iana != "ascii": + for chunk in md_chunks: + chunk_languages = coherence_ratio( + chunk, + language_threshold, + ",".join(target_languages) if target_languages else None, + ) + + cd_ratios.append(chunk_languages) + + cd_ratios_merged = merge_coherence_ratios(cd_ratios) + + if cd_ratios_merged: + logger.log( + TRACE, + "We detected language {} using {}".format( + cd_ratios_merged, encoding_iana + ), + ) + + results.append( + CharsetMatch( + sequences, + encoding_iana, + mean_mess_ratio, + bom_or_sig_available, + cd_ratios_merged, + decoded_payload, + ) + ) + + if ( + encoding_iana in [specified_encoding, "ascii", "utf_8"] + and mean_mess_ratio < 0.1 + ): + logger.debug( + "Encoding detection: %s is most likely the one.", encoding_iana + ) + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([results[encoding_iana]]) + + if encoding_iana == sig_encoding: + logger.debug( + "Encoding detection: %s is most likely the one as we detected a BOM or SIG within " + "the beginning of the sequence.", + encoding_iana, + ) + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([results[encoding_iana]]) + + if len(results) == 0: + if fallback_u8 or fallback_ascii or fallback_specified: + logger.log( + TRACE, + "Nothing got out of the detection process. Using ASCII/UTF-8/Specified fallback.", + ) + + if fallback_specified: + logger.debug( + "Encoding detection: %s will be used as a fallback match", + fallback_specified.encoding, + ) + results.append(fallback_specified) + elif ( + (fallback_u8 and fallback_ascii is None) + or ( + fallback_u8 + and fallback_ascii + and fallback_u8.fingerprint != fallback_ascii.fingerprint + ) + or (fallback_u8 is not None) + ): + logger.debug("Encoding detection: utf_8 will be used as a fallback match") + results.append(fallback_u8) + elif fallback_ascii: + logger.debug("Encoding detection: ascii will be used as a fallback match") + results.append(fallback_ascii) + + if results: + logger.debug( + "Encoding detection: Found %s as plausible (best-candidate) for content. With %i alternatives.", + results.best().encoding, # type: ignore + len(results) - 1, + ) + else: + logger.debug("Encoding detection: Unable to determine any suitable charset.") + + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + + return results + + +def from_fp( + fp: BinaryIO, + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: Optional[List[str]] = None, + cp_exclusion: Optional[List[str]] = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Same thing than the function from_bytes but using a file pointer that is already ready. + Will not close the file pointer. + """ + return from_bytes( + fp.read(), + steps, + chunk_size, + threshold, + cp_isolation, + cp_exclusion, + preemptive_behaviour, + explain, + language_threshold, + enable_fallback, + ) + + +def from_path( + path: Union[str, bytes, PathLike], # type: ignore[type-arg] + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: Optional[List[str]] = None, + cp_exclusion: Optional[List[str]] = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Same thing than the function from_bytes but with one extra step. Opening and reading given file path in binary mode. + Can raise IOError. + """ + with open(path, "rb") as fp: + return from_fp( + fp, + steps, + chunk_size, + threshold, + cp_isolation, + cp_exclusion, + preemptive_behaviour, + explain, + language_threshold, + enable_fallback, + ) + + +def is_binary( + fp_or_path_or_payload: Union[PathLike, str, BinaryIO, bytes], # type: ignore[type-arg] + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: Optional[List[str]] = None, + cp_exclusion: Optional[List[str]] = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = False, +) -> bool: + """ + Detect if the given input (file, bytes, or path) points to a binary file. aka. not a string. + Based on the same main heuristic algorithms and default kwargs at the sole exception that fallbacks match + are disabled to be stricter around ASCII-compatible but unlikely to be a string. + """ + if isinstance(fp_or_path_or_payload, (str, PathLike)): + guesses = from_path( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + elif isinstance( + fp_or_path_or_payload, + ( + bytes, + bytearray, + ), + ): + guesses = from_bytes( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + else: + guesses = from_fp( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + + return not guesses diff --git a/lib/python3.10/site-packages/charset_normalizer/cd.py b/lib/python3.10/site-packages/charset_normalizer/cd.py new file mode 100644 index 0000000000000000000000000000000000000000..4ea6760c45bce5773bfe4b46d7b3c07c2c139d49 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/cd.py @@ -0,0 +1,395 @@ +import importlib +from codecs import IncrementalDecoder +from collections import Counter +from functools import lru_cache +from typing import Counter as TypeCounter, Dict, List, Optional, Tuple + +from .constant import ( + FREQUENCIES, + KO_NAMES, + LANGUAGE_SUPPORTED_COUNT, + TOO_SMALL_SEQUENCE, + ZH_NAMES, +) +from .md import is_suspiciously_successive_range +from .models import CoherenceMatches +from .utils import ( + is_accentuated, + is_latin, + is_multi_byte_encoding, + is_unicode_range_secondary, + unicode_range, +) + + +def encoding_unicode_range(iana_name: str) -> List[str]: + """ + Return associated unicode ranges in a single byte code page. + """ + if is_multi_byte_encoding(iana_name): + raise IOError("Function not supported on multi-byte code page") + + decoder = importlib.import_module( + "encodings.{}".format(iana_name) + ).IncrementalDecoder + + p: IncrementalDecoder = decoder(errors="ignore") + seen_ranges: Dict[str, int] = {} + character_count: int = 0 + + for i in range(0x40, 0xFF): + chunk: str = p.decode(bytes([i])) + + if chunk: + character_range: Optional[str] = unicode_range(chunk) + + if character_range is None: + continue + + if is_unicode_range_secondary(character_range) is False: + if character_range not in seen_ranges: + seen_ranges[character_range] = 0 + seen_ranges[character_range] += 1 + character_count += 1 + + return sorted( + [ + character_range + for character_range in seen_ranges + if seen_ranges[character_range] / character_count >= 0.15 + ] + ) + + +def unicode_range_languages(primary_range: str) -> List[str]: + """ + Return inferred languages used with a unicode range. + """ + languages: List[str] = [] + + for language, characters in FREQUENCIES.items(): + for character in characters: + if unicode_range(character) == primary_range: + languages.append(language) + break + + return languages + + +@lru_cache() +def encoding_languages(iana_name: str) -> List[str]: + """ + Single-byte encoding language association. Some code page are heavily linked to particular language(s). + This function does the correspondence. + """ + unicode_ranges: List[str] = encoding_unicode_range(iana_name) + primary_range: Optional[str] = None + + for specified_range in unicode_ranges: + if "Latin" not in specified_range: + primary_range = specified_range + break + + if primary_range is None: + return ["Latin Based"] + + return unicode_range_languages(primary_range) + + +@lru_cache() +def mb_encoding_languages(iana_name: str) -> List[str]: + """ + Multi-byte encoding language association. Some code page are heavily linked to particular language(s). + This function does the correspondence. + """ + if ( + iana_name.startswith("shift_") + or iana_name.startswith("iso2022_jp") + or iana_name.startswith("euc_j") + or iana_name == "cp932" + ): + return ["Japanese"] + if iana_name.startswith("gb") or iana_name in ZH_NAMES: + return ["Chinese"] + if iana_name.startswith("iso2022_kr") or iana_name in KO_NAMES: + return ["Korean"] + + return [] + + +@lru_cache(maxsize=LANGUAGE_SUPPORTED_COUNT) +def get_target_features(language: str) -> Tuple[bool, bool]: + """ + Determine main aspects from a supported language if it contains accents and if is pure Latin. + """ + target_have_accents: bool = False + target_pure_latin: bool = True + + for character in FREQUENCIES[language]: + if not target_have_accents and is_accentuated(character): + target_have_accents = True + if target_pure_latin and is_latin(character) is False: + target_pure_latin = False + + return target_have_accents, target_pure_latin + + +def alphabet_languages( + characters: List[str], ignore_non_latin: bool = False +) -> List[str]: + """ + Return associated languages associated to given characters. + """ + languages: List[Tuple[str, float]] = [] + + source_have_accents = any(is_accentuated(character) for character in characters) + + for language, language_characters in FREQUENCIES.items(): + target_have_accents, target_pure_latin = get_target_features(language) + + if ignore_non_latin and target_pure_latin is False: + continue + + if target_have_accents is False and source_have_accents: + continue + + character_count: int = len(language_characters) + + character_match_count: int = len( + [c for c in language_characters if c in characters] + ) + + ratio: float = character_match_count / character_count + + if ratio >= 0.2: + languages.append((language, ratio)) + + languages = sorted(languages, key=lambda x: x[1], reverse=True) + + return [compatible_language[0] for compatible_language in languages] + + +def characters_popularity_compare( + language: str, ordered_characters: List[str] +) -> float: + """ + Determine if a ordered characters list (by occurrence from most appearance to rarest) match a particular language. + The result is a ratio between 0. (absolutely no correspondence) and 1. (near perfect fit). + Beware that is function is not strict on the match in order to ease the detection. (Meaning close match is 1.) + """ + if language not in FREQUENCIES: + raise ValueError("{} not available".format(language)) + + character_approved_count: int = 0 + FREQUENCIES_language_set = set(FREQUENCIES[language]) + + ordered_characters_count: int = len(ordered_characters) + target_language_characters_count: int = len(FREQUENCIES[language]) + + large_alphabet: bool = target_language_characters_count > 26 + + for character, character_rank in zip( + ordered_characters, range(0, ordered_characters_count) + ): + if character not in FREQUENCIES_language_set: + continue + + character_rank_in_language: int = FREQUENCIES[language].index(character) + expected_projection_ratio: float = ( + target_language_characters_count / ordered_characters_count + ) + character_rank_projection: int = int(character_rank * expected_projection_ratio) + + if ( + large_alphabet is False + and abs(character_rank_projection - character_rank_in_language) > 4 + ): + continue + + if ( + large_alphabet is True + and abs(character_rank_projection - character_rank_in_language) + < target_language_characters_count / 3 + ): + character_approved_count += 1 + continue + + characters_before_source: List[str] = FREQUENCIES[language][ + 0:character_rank_in_language + ] + characters_after_source: List[str] = FREQUENCIES[language][ + character_rank_in_language: + ] + characters_before: List[str] = ordered_characters[0:character_rank] + characters_after: List[str] = ordered_characters[character_rank:] + + before_match_count: int = len( + set(characters_before) & set(characters_before_source) + ) + + after_match_count: int = len( + set(characters_after) & set(characters_after_source) + ) + + if len(characters_before_source) == 0 and before_match_count <= 4: + character_approved_count += 1 + continue + + if len(characters_after_source) == 0 and after_match_count <= 4: + character_approved_count += 1 + continue + + if ( + before_match_count / len(characters_before_source) >= 0.4 + or after_match_count / len(characters_after_source) >= 0.4 + ): + character_approved_count += 1 + continue + + return character_approved_count / len(ordered_characters) + + +def alpha_unicode_split(decoded_sequence: str) -> List[str]: + """ + Given a decoded text sequence, return a list of str. Unicode range / alphabet separation. + Ex. a text containing English/Latin with a bit a Hebrew will return two items in the resulting list; + One containing the latin letters and the other hebrew. + """ + layers: Dict[str, str] = {} + + for character in decoded_sequence: + if character.isalpha() is False: + continue + + character_range: Optional[str] = unicode_range(character) + + if character_range is None: + continue + + layer_target_range: Optional[str] = None + + for discovered_range in layers: + if ( + is_suspiciously_successive_range(discovered_range, character_range) + is False + ): + layer_target_range = discovered_range + break + + if layer_target_range is None: + layer_target_range = character_range + + if layer_target_range not in layers: + layers[layer_target_range] = character.lower() + continue + + layers[layer_target_range] += character.lower() + + return list(layers.values()) + + +def merge_coherence_ratios(results: List[CoherenceMatches]) -> CoherenceMatches: + """ + This function merge results previously given by the function coherence_ratio. + The return type is the same as coherence_ratio. + """ + per_language_ratios: Dict[str, List[float]] = {} + for result in results: + for sub_result in result: + language, ratio = sub_result + if language not in per_language_ratios: + per_language_ratios[language] = [ratio] + continue + per_language_ratios[language].append(ratio) + + merge = [ + ( + language, + round( + sum(per_language_ratios[language]) / len(per_language_ratios[language]), + 4, + ), + ) + for language in per_language_ratios + ] + + return sorted(merge, key=lambda x: x[1], reverse=True) + + +def filter_alt_coherence_matches(results: CoherenceMatches) -> CoherenceMatches: + """ + We shall NOT return "English—" in CoherenceMatches because it is an alternative + of "English". This function only keeps the best match and remove the em-dash in it. + """ + index_results: Dict[str, List[float]] = dict() + + for result in results: + language, ratio = result + no_em_name: str = language.replace("—", "") + + if no_em_name not in index_results: + index_results[no_em_name] = [] + + index_results[no_em_name].append(ratio) + + if any(len(index_results[e]) > 1 for e in index_results): + filtered_results: CoherenceMatches = [] + + for language in index_results: + filtered_results.append((language, max(index_results[language]))) + + return filtered_results + + return results + + +@lru_cache(maxsize=2048) +def coherence_ratio( + decoded_sequence: str, threshold: float = 0.1, lg_inclusion: Optional[str] = None +) -> CoherenceMatches: + """ + Detect ANY language that can be identified in given sequence. The sequence will be analysed by layers. + A layer = Character extraction by alphabets/ranges. + """ + + results: List[Tuple[str, float]] = [] + ignore_non_latin: bool = False + + sufficient_match_count: int = 0 + + lg_inclusion_list = lg_inclusion.split(",") if lg_inclusion is not None else [] + if "Latin Based" in lg_inclusion_list: + ignore_non_latin = True + lg_inclusion_list.remove("Latin Based") + + for layer in alpha_unicode_split(decoded_sequence): + sequence_frequencies: TypeCounter[str] = Counter(layer) + most_common = sequence_frequencies.most_common() + + character_count: int = sum(o for c, o in most_common) + + if character_count <= TOO_SMALL_SEQUENCE: + continue + + popular_character_ordered: List[str] = [c for c, o in most_common] + + for language in lg_inclusion_list or alphabet_languages( + popular_character_ordered, ignore_non_latin + ): + ratio: float = characters_popularity_compare( + language, popular_character_ordered + ) + + if ratio < threshold: + continue + elif ratio >= 0.8: + sufficient_match_count += 1 + + results.append((language, round(ratio, 4))) + + if sufficient_match_count >= 3: + break + + return sorted( + filter_alt_coherence_matches(results), key=lambda x: x[1], reverse=True + ) diff --git a/lib/python3.10/site-packages/charset_normalizer/constant.py b/lib/python3.10/site-packages/charset_normalizer/constant.py new file mode 100644 index 0000000000000000000000000000000000000000..863490461eacf57ca5f62658b713685476987149 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/constant.py @@ -0,0 +1,1995 @@ +# -*- coding: utf-8 -*- +from codecs import BOM_UTF8, BOM_UTF16_BE, BOM_UTF16_LE, BOM_UTF32_BE, BOM_UTF32_LE +from encodings.aliases import aliases +from re import IGNORECASE, compile as re_compile +from typing import Dict, List, Set, Union + +# Contain for each eligible encoding a list of/item bytes SIG/BOM +ENCODING_MARKS: Dict[str, Union[bytes, List[bytes]]] = { + "utf_8": BOM_UTF8, + "utf_7": [ + b"\x2b\x2f\x76\x38", + b"\x2b\x2f\x76\x39", + b"\x2b\x2f\x76\x2b", + b"\x2b\x2f\x76\x2f", + b"\x2b\x2f\x76\x38\x2d", + ], + "gb18030": b"\x84\x31\x95\x33", + "utf_32": [BOM_UTF32_BE, BOM_UTF32_LE], + "utf_16": [BOM_UTF16_BE, BOM_UTF16_LE], +} + +TOO_SMALL_SEQUENCE: int = 32 +TOO_BIG_SEQUENCE: int = int(10e6) + +UTF8_MAXIMAL_ALLOCATION: int = 1_112_064 + +# Up-to-date Unicode ucd/15.0.0 +UNICODE_RANGES_COMBINED: Dict[str, range] = { + "Control character": range(32), + "Basic Latin": range(32, 128), + "Latin-1 Supplement": range(128, 256), + "Latin Extended-A": range(256, 384), + "Latin Extended-B": range(384, 592), + "IPA Extensions": range(592, 688), + "Spacing Modifier Letters": range(688, 768), + "Combining Diacritical Marks": range(768, 880), + "Greek and Coptic": range(880, 1024), + "Cyrillic": range(1024, 1280), + "Cyrillic Supplement": range(1280, 1328), + "Armenian": range(1328, 1424), + "Hebrew": range(1424, 1536), + "Arabic": range(1536, 1792), + "Syriac": range(1792, 1872), + "Arabic Supplement": range(1872, 1920), + "Thaana": range(1920, 1984), + "NKo": range(1984, 2048), + "Samaritan": range(2048, 2112), + "Mandaic": range(2112, 2144), + "Syriac Supplement": range(2144, 2160), + "Arabic Extended-B": range(2160, 2208), + "Arabic Extended-A": range(2208, 2304), + "Devanagari": range(2304, 2432), + "Bengali": range(2432, 2560), + "Gurmukhi": range(2560, 2688), + "Gujarati": range(2688, 2816), + "Oriya": range(2816, 2944), + "Tamil": range(2944, 3072), + "Telugu": range(3072, 3200), + "Kannada": range(3200, 3328), + "Malayalam": range(3328, 3456), + "Sinhala": range(3456, 3584), + "Thai": range(3584, 3712), + "Lao": range(3712, 3840), + "Tibetan": range(3840, 4096), + "Myanmar": range(4096, 4256), + "Georgian": range(4256, 4352), + "Hangul Jamo": range(4352, 4608), + "Ethiopic": range(4608, 4992), + "Ethiopic Supplement": range(4992, 5024), + "Cherokee": range(5024, 5120), + "Unified Canadian Aboriginal Syllabics": range(5120, 5760), + "Ogham": range(5760, 5792), + "Runic": range(5792, 5888), + "Tagalog": range(5888, 5920), + "Hanunoo": range(5920, 5952), + "Buhid": range(5952, 5984), + "Tagbanwa": range(5984, 6016), + "Khmer": range(6016, 6144), + "Mongolian": range(6144, 6320), + "Unified Canadian Aboriginal Syllabics Extended": range(6320, 6400), + "Limbu": range(6400, 6480), + "Tai Le": range(6480, 6528), + "New Tai Lue": range(6528, 6624), + "Khmer Symbols": range(6624, 6656), + "Buginese": range(6656, 6688), + "Tai Tham": range(6688, 6832), + "Combining Diacritical Marks Extended": range(6832, 6912), + "Balinese": range(6912, 7040), + "Sundanese": range(7040, 7104), + "Batak": range(7104, 7168), + "Lepcha": range(7168, 7248), + "Ol Chiki": range(7248, 7296), + "Cyrillic Extended-C": range(7296, 7312), + "Georgian Extended": range(7312, 7360), + "Sundanese Supplement": range(7360, 7376), + "Vedic Extensions": range(7376, 7424), + "Phonetic Extensions": range(7424, 7552), + "Phonetic Extensions Supplement": range(7552, 7616), + "Combining Diacritical Marks Supplement": range(7616, 7680), + "Latin Extended Additional": range(7680, 7936), + "Greek Extended": range(7936, 8192), + "General Punctuation": range(8192, 8304), + "Superscripts and Subscripts": range(8304, 8352), + "Currency Symbols": range(8352, 8400), + "Combining Diacritical Marks for Symbols": range(8400, 8448), + "Letterlike Symbols": range(8448, 8528), + "Number Forms": range(8528, 8592), + "Arrows": range(8592, 8704), + "Mathematical Operators": range(8704, 8960), + "Miscellaneous Technical": range(8960, 9216), + "Control Pictures": range(9216, 9280), + "Optical Character Recognition": range(9280, 9312), + "Enclosed Alphanumerics": range(9312, 9472), + "Box Drawing": range(9472, 9600), + "Block Elements": range(9600, 9632), + "Geometric Shapes": range(9632, 9728), + "Miscellaneous Symbols": range(9728, 9984), + "Dingbats": range(9984, 10176), + "Miscellaneous Mathematical Symbols-A": range(10176, 10224), + "Supplemental Arrows-A": range(10224, 10240), + "Braille Patterns": range(10240, 10496), + "Supplemental Arrows-B": range(10496, 10624), + "Miscellaneous Mathematical Symbols-B": range(10624, 10752), + "Supplemental Mathematical Operators": range(10752, 11008), + "Miscellaneous Symbols and Arrows": range(11008, 11264), + "Glagolitic": range(11264, 11360), + "Latin Extended-C": range(11360, 11392), + "Coptic": range(11392, 11520), + "Georgian Supplement": range(11520, 11568), + "Tifinagh": range(11568, 11648), + "Ethiopic Extended": range(11648, 11744), + "Cyrillic Extended-A": range(11744, 11776), + "Supplemental Punctuation": range(11776, 11904), + "CJK Radicals Supplement": range(11904, 12032), + "Kangxi Radicals": range(12032, 12256), + "Ideographic Description Characters": range(12272, 12288), + "CJK Symbols and Punctuation": range(12288, 12352), + "Hiragana": range(12352, 12448), + "Katakana": range(12448, 12544), + "Bopomofo": range(12544, 12592), + "Hangul Compatibility Jamo": range(12592, 12688), + "Kanbun": range(12688, 12704), + "Bopomofo Extended": range(12704, 12736), + "CJK Strokes": range(12736, 12784), + "Katakana Phonetic Extensions": range(12784, 12800), + "Enclosed CJK Letters and Months": range(12800, 13056), + "CJK Compatibility": range(13056, 13312), + "CJK Unified Ideographs Extension A": range(13312, 19904), + "Yijing Hexagram Symbols": range(19904, 19968), + "CJK Unified Ideographs": range(19968, 40960), + "Yi Syllables": range(40960, 42128), + "Yi Radicals": range(42128, 42192), + "Lisu": range(42192, 42240), + "Vai": range(42240, 42560), + "Cyrillic Extended-B": range(42560, 42656), + "Bamum": range(42656, 42752), + "Modifier Tone Letters": range(42752, 42784), + "Latin Extended-D": range(42784, 43008), + "Syloti Nagri": range(43008, 43056), + "Common Indic Number Forms": range(43056, 43072), + "Phags-pa": range(43072, 43136), + "Saurashtra": range(43136, 43232), + "Devanagari Extended": range(43232, 43264), + "Kayah Li": range(43264, 43312), + "Rejang": range(43312, 43360), + "Hangul Jamo Extended-A": range(43360, 43392), + "Javanese": range(43392, 43488), + "Myanmar Extended-B": range(43488, 43520), + "Cham": range(43520, 43616), + "Myanmar Extended-A": range(43616, 43648), + "Tai Viet": range(43648, 43744), + "Meetei Mayek Extensions": range(43744, 43776), + "Ethiopic Extended-A": range(43776, 43824), + "Latin Extended-E": range(43824, 43888), + "Cherokee Supplement": range(43888, 43968), + "Meetei Mayek": range(43968, 44032), + "Hangul Syllables": range(44032, 55216), + "Hangul Jamo Extended-B": range(55216, 55296), + "High Surrogates": range(55296, 56192), + "High Private Use Surrogates": range(56192, 56320), + "Low Surrogates": range(56320, 57344), + "Private Use Area": range(57344, 63744), + "CJK Compatibility Ideographs": range(63744, 64256), + "Alphabetic Presentation Forms": range(64256, 64336), + "Arabic Presentation Forms-A": range(64336, 65024), + "Variation Selectors": range(65024, 65040), + "Vertical Forms": range(65040, 65056), + "Combining Half Marks": range(65056, 65072), + "CJK Compatibility Forms": range(65072, 65104), + "Small Form Variants": range(65104, 65136), + "Arabic Presentation Forms-B": range(65136, 65280), + "Halfwidth and Fullwidth Forms": range(65280, 65520), + "Specials": range(65520, 65536), + "Linear B Syllabary": range(65536, 65664), + "Linear B Ideograms": range(65664, 65792), + "Aegean Numbers": range(65792, 65856), + "Ancient Greek Numbers": range(65856, 65936), + "Ancient Symbols": range(65936, 66000), + "Phaistos Disc": range(66000, 66048), + "Lycian": range(66176, 66208), + "Carian": range(66208, 66272), + "Coptic Epact Numbers": range(66272, 66304), + "Old Italic": range(66304, 66352), + "Gothic": range(66352, 66384), + "Old Permic": range(66384, 66432), + "Ugaritic": range(66432, 66464), + "Old Persian": range(66464, 66528), + "Deseret": range(66560, 66640), + "Shavian": range(66640, 66688), + "Osmanya": range(66688, 66736), + "Osage": range(66736, 66816), + "Elbasan": range(66816, 66864), + "Caucasian Albanian": range(66864, 66928), + "Vithkuqi": range(66928, 67008), + "Linear A": range(67072, 67456), + "Latin Extended-F": range(67456, 67520), + "Cypriot Syllabary": range(67584, 67648), + "Imperial Aramaic": range(67648, 67680), + "Palmyrene": range(67680, 67712), + "Nabataean": range(67712, 67760), + "Hatran": range(67808, 67840), + "Phoenician": range(67840, 67872), + "Lydian": range(67872, 67904), + "Meroitic Hieroglyphs": range(67968, 68000), + "Meroitic Cursive": range(68000, 68096), + "Kharoshthi": range(68096, 68192), + "Old South Arabian": range(68192, 68224), + "Old North Arabian": range(68224, 68256), + "Manichaean": range(68288, 68352), + "Avestan": range(68352, 68416), + "Inscriptional Parthian": range(68416, 68448), + "Inscriptional Pahlavi": range(68448, 68480), + "Psalter Pahlavi": range(68480, 68528), + "Old Turkic": range(68608, 68688), + "Old Hungarian": range(68736, 68864), + "Hanifi Rohingya": range(68864, 68928), + "Rumi Numeral Symbols": range(69216, 69248), + "Yezidi": range(69248, 69312), + "Arabic Extended-C": range(69312, 69376), + "Old Sogdian": range(69376, 69424), + "Sogdian": range(69424, 69488), + "Old Uyghur": range(69488, 69552), + "Chorasmian": range(69552, 69600), + "Elymaic": range(69600, 69632), + "Brahmi": range(69632, 69760), + "Kaithi": range(69760, 69840), + "Sora Sompeng": range(69840, 69888), + "Chakma": range(69888, 69968), + "Mahajani": range(69968, 70016), + "Sharada": range(70016, 70112), + "Sinhala Archaic Numbers": range(70112, 70144), + "Khojki": range(70144, 70224), + "Multani": range(70272, 70320), + "Khudawadi": range(70320, 70400), + "Grantha": range(70400, 70528), + "Newa": range(70656, 70784), + "Tirhuta": range(70784, 70880), + "Siddham": range(71040, 71168), + "Modi": range(71168, 71264), + "Mongolian Supplement": range(71264, 71296), + "Takri": range(71296, 71376), + "Ahom": range(71424, 71504), + "Dogra": range(71680, 71760), + "Warang Citi": range(71840, 71936), + "Dives Akuru": range(71936, 72032), + "Nandinagari": range(72096, 72192), + "Zanabazar Square": range(72192, 72272), + "Soyombo": range(72272, 72368), + "Unified Canadian Aboriginal Syllabics Extended-A": range(72368, 72384), + "Pau Cin Hau": range(72384, 72448), + "Devanagari Extended-A": range(72448, 72544), + "Bhaiksuki": range(72704, 72816), + "Marchen": range(72816, 72896), + "Masaram Gondi": range(72960, 73056), + "Gunjala Gondi": range(73056, 73136), + "Makasar": range(73440, 73472), + "Kawi": range(73472, 73568), + "Lisu Supplement": range(73648, 73664), + "Tamil Supplement": range(73664, 73728), + "Cuneiform": range(73728, 74752), + "Cuneiform Numbers and Punctuation": range(74752, 74880), + "Early Dynastic Cuneiform": range(74880, 75088), + "Cypro-Minoan": range(77712, 77824), + "Egyptian Hieroglyphs": range(77824, 78896), + "Egyptian Hieroglyph Format Controls": range(78896, 78944), + "Anatolian Hieroglyphs": range(82944, 83584), + "Bamum Supplement": range(92160, 92736), + "Mro": range(92736, 92784), + "Tangsa": range(92784, 92880), + "Bassa Vah": range(92880, 92928), + "Pahawh Hmong": range(92928, 93072), + "Medefaidrin": range(93760, 93856), + "Miao": range(93952, 94112), + "Ideographic Symbols and Punctuation": range(94176, 94208), + "Tangut": range(94208, 100352), + "Tangut Components": range(100352, 101120), + "Khitan Small Script": range(101120, 101632), + "Tangut Supplement": range(101632, 101760), + "Kana Extended-B": range(110576, 110592), + "Kana Supplement": range(110592, 110848), + "Kana Extended-A": range(110848, 110896), + "Small Kana Extension": range(110896, 110960), + "Nushu": range(110960, 111360), + "Duployan": range(113664, 113824), + "Shorthand Format Controls": range(113824, 113840), + "Znamenny Musical Notation": range(118528, 118736), + "Byzantine Musical Symbols": range(118784, 119040), + "Musical Symbols": range(119040, 119296), + "Ancient Greek Musical Notation": range(119296, 119376), + "Kaktovik Numerals": range(119488, 119520), + "Mayan Numerals": range(119520, 119552), + "Tai Xuan Jing Symbols": range(119552, 119648), + "Counting Rod Numerals": range(119648, 119680), + "Mathematical Alphanumeric Symbols": range(119808, 120832), + "Sutton SignWriting": range(120832, 121520), + "Latin Extended-G": range(122624, 122880), + "Glagolitic Supplement": range(122880, 122928), + "Cyrillic Extended-D": range(122928, 123024), + "Nyiakeng Puachue Hmong": range(123136, 123216), + "Toto": range(123536, 123584), + "Wancho": range(123584, 123648), + "Nag Mundari": range(124112, 124160), + "Ethiopic Extended-B": range(124896, 124928), + "Mende Kikakui": range(124928, 125152), + "Adlam": range(125184, 125280), + "Indic Siyaq Numbers": range(126064, 126144), + "Ottoman Siyaq Numbers": range(126208, 126288), + "Arabic Mathematical Alphabetic Symbols": range(126464, 126720), + "Mahjong Tiles": range(126976, 127024), + "Domino Tiles": range(127024, 127136), + "Playing Cards": range(127136, 127232), + "Enclosed Alphanumeric Supplement": range(127232, 127488), + "Enclosed Ideographic Supplement": range(127488, 127744), + "Miscellaneous Symbols and Pictographs": range(127744, 128512), + "Emoticons range(Emoji)": range(128512, 128592), + "Ornamental Dingbats": range(128592, 128640), + "Transport and Map Symbols": range(128640, 128768), + "Alchemical Symbols": range(128768, 128896), + "Geometric Shapes Extended": range(128896, 129024), + "Supplemental Arrows-C": range(129024, 129280), + "Supplemental Symbols and Pictographs": range(129280, 129536), + "Chess Symbols": range(129536, 129648), + "Symbols and Pictographs Extended-A": range(129648, 129792), + "Symbols for Legacy Computing": range(129792, 130048), + "CJK Unified Ideographs Extension B": range(131072, 173792), + "CJK Unified Ideographs Extension C": range(173824, 177984), + "CJK Unified Ideographs Extension D": range(177984, 178208), + "CJK Unified Ideographs Extension E": range(178208, 183984), + "CJK Unified Ideographs Extension F": range(183984, 191472), + "CJK Compatibility Ideographs Supplement": range(194560, 195104), + "CJK Unified Ideographs Extension G": range(196608, 201552), + "CJK Unified Ideographs Extension H": range(201552, 205744), + "Tags": range(917504, 917632), + "Variation Selectors Supplement": range(917760, 918000), + "Supplementary Private Use Area-A": range(983040, 1048576), + "Supplementary Private Use Area-B": range(1048576, 1114112), +} + + +UNICODE_SECONDARY_RANGE_KEYWORD: List[str] = [ + "Supplement", + "Extended", + "Extensions", + "Modifier", + "Marks", + "Punctuation", + "Symbols", + "Forms", + "Operators", + "Miscellaneous", + "Drawing", + "Block", + "Shapes", + "Supplemental", + "Tags", +] + +RE_POSSIBLE_ENCODING_INDICATION = re_compile( + r"(?:(?:encoding)|(?:charset)|(?:coding))(?:[\:= ]{1,10})(?:[\"\']?)([a-zA-Z0-9\-_]+)(?:[\"\']?)", + IGNORECASE, +) + +IANA_NO_ALIASES = [ + "cp720", + "cp737", + "cp856", + "cp874", + "cp875", + "cp1006", + "koi8_r", + "koi8_t", + "koi8_u", +] + +IANA_SUPPORTED: List[str] = sorted( + filter( + lambda x: x.endswith("_codec") is False + and x not in {"rot_13", "tactis", "mbcs"}, + list(set(aliases.values())) + IANA_NO_ALIASES, + ) +) + +IANA_SUPPORTED_COUNT: int = len(IANA_SUPPORTED) + +# pre-computed code page that are similar using the function cp_similarity. +IANA_SUPPORTED_SIMILAR: Dict[str, List[str]] = { + "cp037": ["cp1026", "cp1140", "cp273", "cp500"], + "cp1026": ["cp037", "cp1140", "cp273", "cp500"], + "cp1125": ["cp866"], + "cp1140": ["cp037", "cp1026", "cp273", "cp500"], + "cp1250": ["iso8859_2"], + "cp1251": ["kz1048", "ptcp154"], + "cp1252": ["iso8859_15", "iso8859_9", "latin_1"], + "cp1253": ["iso8859_7"], + "cp1254": ["iso8859_15", "iso8859_9", "latin_1"], + "cp1257": ["iso8859_13"], + "cp273": ["cp037", "cp1026", "cp1140", "cp500"], + "cp437": ["cp850", "cp858", "cp860", "cp861", "cp862", "cp863", "cp865"], + "cp500": ["cp037", "cp1026", "cp1140", "cp273"], + "cp850": ["cp437", "cp857", "cp858", "cp865"], + "cp857": ["cp850", "cp858", "cp865"], + "cp858": ["cp437", "cp850", "cp857", "cp865"], + "cp860": ["cp437", "cp861", "cp862", "cp863", "cp865"], + "cp861": ["cp437", "cp860", "cp862", "cp863", "cp865"], + "cp862": ["cp437", "cp860", "cp861", "cp863", "cp865"], + "cp863": ["cp437", "cp860", "cp861", "cp862", "cp865"], + "cp865": ["cp437", "cp850", "cp857", "cp858", "cp860", "cp861", "cp862", "cp863"], + "cp866": ["cp1125"], + "iso8859_10": ["iso8859_14", "iso8859_15", "iso8859_4", "iso8859_9", "latin_1"], + "iso8859_11": ["tis_620"], + "iso8859_13": ["cp1257"], + "iso8859_14": [ + "iso8859_10", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_15": [ + "cp1252", + "cp1254", + "iso8859_10", + "iso8859_14", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_16": [ + "iso8859_14", + "iso8859_15", + "iso8859_2", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_2": ["cp1250", "iso8859_16", "iso8859_4"], + "iso8859_3": ["iso8859_14", "iso8859_15", "iso8859_16", "iso8859_9", "latin_1"], + "iso8859_4": ["iso8859_10", "iso8859_2", "iso8859_9", "latin_1"], + "iso8859_7": ["cp1253"], + "iso8859_9": [ + "cp1252", + "cp1254", + "cp1258", + "iso8859_10", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_4", + "latin_1", + ], + "kz1048": ["cp1251", "ptcp154"], + "latin_1": [ + "cp1252", + "cp1254", + "cp1258", + "iso8859_10", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_4", + "iso8859_9", + ], + "mac_iceland": ["mac_roman", "mac_turkish"], + "mac_roman": ["mac_iceland", "mac_turkish"], + "mac_turkish": ["mac_iceland", "mac_roman"], + "ptcp154": ["cp1251", "kz1048"], + "tis_620": ["iso8859_11"], +} + + +CHARDET_CORRESPONDENCE: Dict[str, str] = { + "iso2022_kr": "ISO-2022-KR", + "iso2022_jp": "ISO-2022-JP", + "euc_kr": "EUC-KR", + "tis_620": "TIS-620", + "utf_32": "UTF-32", + "euc_jp": "EUC-JP", + "koi8_r": "KOI8-R", + "iso8859_1": "ISO-8859-1", + "iso8859_2": "ISO-8859-2", + "iso8859_5": "ISO-8859-5", + "iso8859_6": "ISO-8859-6", + "iso8859_7": "ISO-8859-7", + "iso8859_8": "ISO-8859-8", + "utf_16": "UTF-16", + "cp855": "IBM855", + "mac_cyrillic": "MacCyrillic", + "gb2312": "GB2312", + "gb18030": "GB18030", + "cp932": "CP932", + "cp866": "IBM866", + "utf_8": "utf-8", + "utf_8_sig": "UTF-8-SIG", + "shift_jis": "SHIFT_JIS", + "big5": "Big5", + "cp1250": "windows-1250", + "cp1251": "windows-1251", + "cp1252": "Windows-1252", + "cp1253": "windows-1253", + "cp1255": "windows-1255", + "cp1256": "windows-1256", + "cp1254": "Windows-1254", + "cp949": "CP949", +} + + +COMMON_SAFE_ASCII_CHARACTERS: Set[str] = { + "<", + ">", + "=", + ":", + "/", + "&", + ";", + "{", + "}", + "[", + "]", + ",", + "|", + '"', + "-", +} + + +KO_NAMES: Set[str] = {"johab", "cp949", "euc_kr"} +ZH_NAMES: Set[str] = {"big5", "cp950", "big5hkscs", "hz"} + +# Logging LEVEL below DEBUG +TRACE: int = 5 + + +# Language label that contain the em dash "—" +# character are to be considered alternative seq to origin +FREQUENCIES: Dict[str, List[str]] = { + "English": [ + "e", + "a", + "t", + "i", + "o", + "n", + "s", + "r", + "h", + "l", + "d", + "c", + "u", + "m", + "f", + "p", + "g", + "w", + "y", + "b", + "v", + "k", + "x", + "j", + "z", + "q", + ], + "English—": [ + "e", + "a", + "t", + "i", + "o", + "n", + "s", + "r", + "h", + "l", + "d", + "c", + "m", + "u", + "f", + "p", + "g", + "w", + "b", + "y", + "v", + "k", + "j", + "x", + "z", + "q", + ], + "German": [ + "e", + "n", + "i", + "r", + "s", + "t", + "a", + "d", + "h", + "u", + "l", + "g", + "o", + "c", + "m", + "b", + "f", + "k", + "w", + "z", + "p", + "v", + "ü", + "ä", + "ö", + "j", + ], + "French": [ + "e", + "a", + "s", + "n", + "i", + "t", + "r", + "l", + "u", + "o", + "d", + "c", + "p", + "m", + "é", + "v", + "g", + "f", + "b", + "h", + "q", + "à", + "x", + "è", + "y", + "j", + ], + "Dutch": [ + "e", + "n", + "a", + "i", + "r", + "t", + "o", + "d", + "s", + "l", + "g", + "h", + "v", + "m", + "u", + "k", + "c", + "p", + "b", + "w", + "j", + "z", + "f", + "y", + "x", + "ë", + ], + "Italian": [ + "e", + "i", + "a", + "o", + "n", + "l", + "t", + "r", + "s", + "c", + "d", + "u", + "p", + "m", + "g", + "v", + "f", + "b", + "z", + "h", + "q", + "è", + "à", + "k", + "y", + "ò", + ], + "Polish": [ + "a", + "i", + "o", + "e", + "n", + "r", + "z", + "w", + "s", + "c", + "t", + "k", + "y", + "d", + "p", + "m", + "u", + "l", + "j", + "ł", + "g", + "b", + "h", + "ą", + "ę", + "ó", + ], + "Spanish": [ + "e", + "a", + "o", + "n", + "s", + "r", + "i", + "l", + "d", + "t", + "c", + "u", + "m", + "p", + "b", + "g", + "v", + "f", + "y", + "ó", + "h", + "q", + "í", + "j", + "z", + "á", + ], + "Russian": [ + "о", + "а", + "е", + "и", + "н", + "с", + "т", + "р", + "в", + "л", + "к", + "м", + "д", + "п", + "у", + "г", + "я", + "ы", + "з", + "б", + "й", + "ь", + "ч", + "х", + "ж", + "ц", + ], + # Jap-Kanji + "Japanese": [ + "人", + "一", + "大", + "亅", + "丁", + "丨", + "竹", + "笑", + "口", + "日", + "今", + "二", + "彳", + "行", + "十", + "土", + "丶", + "寸", + "寺", + "時", + "乙", + "丿", + "乂", + "气", + "気", + "冂", + "巾", + "亠", + "市", + "目", + "儿", + "見", + "八", + "小", + "凵", + "県", + "月", + "彐", + "門", + "間", + "木", + "東", + "山", + "出", + "本", + "中", + "刀", + "分", + "耳", + "又", + "取", + "最", + "言", + "田", + "心", + "思", + "刂", + "前", + "京", + "尹", + "事", + "生", + "厶", + "云", + "会", + "未", + "来", + "白", + "冫", + "楽", + "灬", + "馬", + "尸", + "尺", + "駅", + "明", + "耂", + "者", + "了", + "阝", + "都", + "高", + "卜", + "占", + "厂", + "广", + "店", + "子", + "申", + "奄", + "亻", + "俺", + "上", + "方", + "冖", + "学", + "衣", + "艮", + "食", + "自", + ], + # Jap-Katakana + "Japanese—": [ + "ー", + "ン", + "ス", + "・", + "ル", + "ト", + "リ", + "イ", + "ア", + "ラ", + "ッ", + "ク", + "ド", + "シ", + "レ", + "ジ", + "タ", + "フ", + "ロ", + "カ", + "テ", + "マ", + "ィ", + "グ", + "バ", + "ム", + "プ", + "オ", + "コ", + "デ", + "ニ", + "ウ", + "メ", + "サ", + "ビ", + "ナ", + "ブ", + "ャ", + "エ", + "ュ", + "チ", + "キ", + "ズ", + "ダ", + "パ", + "ミ", + "ェ", + "ョ", + "ハ", + "セ", + "ベ", + "ガ", + "モ", + "ツ", + "ネ", + "ボ", + "ソ", + "ノ", + "ァ", + "ヴ", + "ワ", + "ポ", + "ペ", + "ピ", + "ケ", + "ゴ", + "ギ", + "ザ", + "ホ", + "ゲ", + "ォ", + "ヤ", + "ヒ", + "ユ", + "ヨ", + "ヘ", + "ゼ", + "ヌ", + "ゥ", + "ゾ", + "ヶ", + "ヂ", + "ヲ", + "ヅ", + "ヵ", + "ヱ", + "ヰ", + "ヮ", + "ヽ", + "゠", + "ヾ", + "ヷ", + "ヿ", + "ヸ", + "ヹ", + "ヺ", + ], + # Jap-Hiragana + "Japanese——": [ + "の", + "に", + "る", + "た", + "と", + "は", + "し", + "い", + "を", + "で", + "て", + "が", + "な", + "れ", + "か", + "ら", + "さ", + "っ", + "り", + "す", + "あ", + "も", + "こ", + "ま", + "う", + "く", + "よ", + "き", + "ん", + "め", + "お", + "け", + "そ", + "つ", + "だ", + "や", + "え", + "ど", + "わ", + "ち", + "み", + "せ", + "じ", + "ば", + "へ", + "び", + "ず", + "ろ", + "ほ", + "げ", + "む", + "べ", + "ひ", + "ょ", + "ゆ", + "ぶ", + "ご", + "ゃ", + "ね", + "ふ", + "ぐ", + "ぎ", + "ぼ", + "ゅ", + "づ", + "ざ", + "ぞ", + "ぬ", + "ぜ", + "ぱ", + "ぽ", + "ぷ", + "ぴ", + "ぃ", + "ぁ", + "ぇ", + "ぺ", + "ゞ", + "ぢ", + "ぉ", + "ぅ", + "ゐ", + "ゝ", + "ゑ", + "゛", + "゜", + "ゎ", + "ゔ", + "゚", + "ゟ", + "゙", + "ゕ", + "ゖ", + ], + "Portuguese": [ + "a", + "e", + "o", + "s", + "i", + "r", + "d", + "n", + "t", + "m", + "u", + "c", + "l", + "p", + "g", + "v", + "b", + "f", + "h", + "ã", + "q", + "é", + "ç", + "á", + "z", + "í", + ], + "Swedish": [ + "e", + "a", + "n", + "r", + "t", + "s", + "i", + "l", + "d", + "o", + "m", + "k", + "g", + "v", + "h", + "f", + "u", + "p", + "ä", + "c", + "b", + "ö", + "å", + "y", + "j", + "x", + ], + "Chinese": [ + "的", + "一", + "是", + "不", + "了", + "在", + "人", + "有", + "我", + "他", + "这", + "个", + "们", + "中", + "来", + "上", + "大", + "为", + "和", + "国", + "地", + "到", + "以", + "说", + "时", + "要", + "就", + "出", + "会", + "可", + "也", + "你", + "对", + "生", + "能", + "而", + "子", + "那", + "得", + "于", + "着", + "下", + "自", + "之", + "年", + "过", + "发", + "后", + "作", + "里", + "用", + "道", + "行", + "所", + "然", + "家", + "种", + "事", + "成", + "方", + "多", + "经", + "么", + "去", + "法", + "学", + "如", + "都", + "同", + "现", + "当", + "没", + "动", + "面", + "起", + "看", + "定", + "天", + "分", + "还", + "进", + "好", + "小", + "部", + "其", + "些", + "主", + "样", + "理", + "心", + "她", + "本", + "前", + "开", + "但", + "因", + "只", + "从", + "想", + "实", + ], + "Ukrainian": [ + "о", + "а", + "н", + "і", + "и", + "р", + "в", + "т", + "е", + "с", + "к", + "л", + "у", + "д", + "м", + "п", + "з", + "я", + "ь", + "б", + "г", + "й", + "ч", + "х", + "ц", + "ї", + ], + "Norwegian": [ + "e", + "r", + "n", + "t", + "a", + "s", + "i", + "o", + "l", + "d", + "g", + "k", + "m", + "v", + "f", + "p", + "u", + "b", + "h", + "å", + "y", + "j", + "ø", + "c", + "æ", + "w", + ], + "Finnish": [ + "a", + "i", + "n", + "t", + "e", + "s", + "l", + "o", + "u", + "k", + "ä", + "m", + "r", + "v", + "j", + "h", + "p", + "y", + "d", + "ö", + "g", + "c", + "b", + "f", + "w", + "z", + ], + "Vietnamese": [ + "n", + "h", + "t", + "i", + "c", + "g", + "a", + "o", + "u", + "m", + "l", + "r", + "à", + "đ", + "s", + "e", + "v", + "p", + "b", + "y", + "ư", + "d", + "á", + "k", + "ộ", + "ế", + ], + "Czech": [ + "o", + "e", + "a", + "n", + "t", + "s", + "i", + "l", + "v", + "r", + "k", + "d", + "u", + "m", + "p", + "í", + "c", + "h", + "z", + "á", + "y", + "j", + "b", + "ě", + "é", + "ř", + ], + "Hungarian": [ + "e", + "a", + "t", + "l", + "s", + "n", + "k", + "r", + "i", + "o", + "z", + "á", + "é", + "g", + "m", + "b", + "y", + "v", + "d", + "h", + "u", + "p", + "j", + "ö", + "f", + "c", + ], + "Korean": [ + "이", + "다", + "에", + "의", + "는", + "로", + "하", + "을", + "가", + "고", + "지", + "서", + "한", + "은", + "기", + "으", + "년", + "대", + "사", + "시", + "를", + "리", + "도", + "인", + "스", + "일", + ], + "Indonesian": [ + "a", + "n", + "e", + "i", + "r", + "t", + "u", + "s", + "d", + "k", + "m", + "l", + "g", + "p", + "b", + "o", + "h", + "y", + "j", + "c", + "w", + "f", + "v", + "z", + "x", + "q", + ], + "Turkish": [ + "a", + "e", + "i", + "n", + "r", + "l", + "ı", + "k", + "d", + "t", + "s", + "m", + "y", + "u", + "o", + "b", + "ü", + "ş", + "v", + "g", + "z", + "h", + "c", + "p", + "ç", + "ğ", + ], + "Romanian": [ + "e", + "i", + "a", + "r", + "n", + "t", + "u", + "l", + "o", + "c", + "s", + "d", + "p", + "m", + "ă", + "f", + "v", + "î", + "g", + "b", + "ș", + "ț", + "z", + "h", + "â", + "j", + ], + "Farsi": [ + "ا", + "ی", + "ر", + "د", + "ن", + "ه", + "و", + "م", + "ت", + "ب", + "س", + "ل", + "ک", + "ش", + "ز", + "ف", + "گ", + "ع", + "خ", + "ق", + "ج", + "آ", + "پ", + "ح", + "ط", + "ص", + ], + "Arabic": [ + "ا", + "ل", + "ي", + "م", + "و", + "ن", + "ر", + "ت", + "ب", + "ة", + "ع", + "د", + "س", + "ف", + "ه", + "ك", + "ق", + "أ", + "ح", + "ج", + "ش", + "ط", + "ص", + "ى", + "خ", + "إ", + ], + "Danish": [ + "e", + "r", + "n", + "t", + "a", + "i", + "s", + "d", + "l", + "o", + "g", + "m", + "k", + "f", + "v", + "u", + "b", + "h", + "p", + "å", + "y", + "ø", + "æ", + "c", + "j", + "w", + ], + "Serbian": [ + "а", + "и", + "о", + "е", + "н", + "р", + "с", + "у", + "т", + "к", + "ј", + "в", + "д", + "м", + "п", + "л", + "г", + "з", + "б", + "a", + "i", + "e", + "o", + "n", + "ц", + "ш", + ], + "Lithuanian": [ + "i", + "a", + "s", + "o", + "r", + "e", + "t", + "n", + "u", + "k", + "m", + "l", + "p", + "v", + "d", + "j", + "g", + "ė", + "b", + "y", + "ų", + "š", + "ž", + "c", + "ą", + "į", + ], + "Slovene": [ + "e", + "a", + "i", + "o", + "n", + "r", + "s", + "l", + "t", + "j", + "v", + "k", + "d", + "p", + "m", + "u", + "z", + "b", + "g", + "h", + "č", + "c", + "š", + "ž", + "f", + "y", + ], + "Slovak": [ + "o", + "a", + "e", + "n", + "i", + "r", + "v", + "t", + "s", + "l", + "k", + "d", + "m", + "p", + "u", + "c", + "h", + "j", + "b", + "z", + "á", + "y", + "ý", + "í", + "č", + "é", + ], + "Hebrew": [ + "י", + "ו", + "ה", + "ל", + "ר", + "ב", + "ת", + "מ", + "א", + "ש", + "נ", + "ע", + "ם", + "ד", + "ק", + "ח", + "פ", + "ס", + "כ", + "ג", + "ט", + "צ", + "ן", + "ז", + "ך", + ], + "Bulgarian": [ + "а", + "и", + "о", + "е", + "н", + "т", + "р", + "с", + "в", + "л", + "к", + "д", + "п", + "м", + "з", + "г", + "я", + "ъ", + "у", + "б", + "ч", + "ц", + "й", + "ж", + "щ", + "х", + ], + "Croatian": [ + "a", + "i", + "o", + "e", + "n", + "r", + "j", + "s", + "t", + "u", + "k", + "l", + "v", + "d", + "m", + "p", + "g", + "z", + "b", + "c", + "č", + "h", + "š", + "ž", + "ć", + "f", + ], + "Hindi": [ + "क", + "र", + "स", + "न", + "त", + "म", + "ह", + "प", + "य", + "ल", + "व", + "ज", + "द", + "ग", + "ब", + "श", + "ट", + "अ", + "ए", + "थ", + "भ", + "ड", + "च", + "ध", + "ष", + "इ", + ], + "Estonian": [ + "a", + "i", + "e", + "s", + "t", + "l", + "u", + "n", + "o", + "k", + "r", + "d", + "m", + "v", + "g", + "p", + "j", + "h", + "ä", + "b", + "õ", + "ü", + "f", + "c", + "ö", + "y", + ], + "Thai": [ + "า", + "น", + "ร", + "อ", + "ก", + "เ", + "ง", + "ม", + "ย", + "ล", + "ว", + "ด", + "ท", + "ส", + "ต", + "ะ", + "ป", + "บ", + "ค", + "ห", + "แ", + "จ", + "พ", + "ช", + "ข", + "ใ", + ], + "Greek": [ + "α", + "τ", + "ο", + "ι", + "ε", + "ν", + "ρ", + "σ", + "κ", + "η", + "π", + "ς", + "υ", + "μ", + "λ", + "ί", + "ό", + "ά", + "γ", + "έ", + "δ", + "ή", + "ω", + "χ", + "θ", + "ύ", + ], + "Tamil": [ + "க", + "த", + "ப", + "ட", + "ர", + "ம", + "ல", + "ன", + "வ", + "ற", + "ய", + "ள", + "ச", + "ந", + "இ", + "ண", + "அ", + "ஆ", + "ழ", + "ங", + "எ", + "உ", + "ஒ", + "ஸ", + ], + "Kazakh": [ + "а", + "ы", + "е", + "н", + "т", + "р", + "л", + "і", + "д", + "с", + "м", + "қ", + "к", + "о", + "б", + "и", + "у", + "ғ", + "ж", + "ң", + "з", + "ш", + "й", + "п", + "г", + "ө", + ], +} + +LANGUAGE_SUPPORTED_COUNT: int = len(FREQUENCIES) diff --git a/lib/python3.10/site-packages/charset_normalizer/legacy.py b/lib/python3.10/site-packages/charset_normalizer/legacy.py new file mode 100644 index 0000000000000000000000000000000000000000..43aad21a9dd1c08c8d31e38908485d46b14efbd2 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/legacy.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, Optional, Union +from warnings import warn + +from .api import from_bytes +from .constant import CHARDET_CORRESPONDENCE + + +def detect( + byte_str: bytes, should_rename_legacy: bool = False, **kwargs: Any +) -> Dict[str, Optional[Union[str, float]]]: + """ + chardet legacy method + Detect the encoding of the given byte string. It should be mostly backward-compatible. + Encoding name will match Chardet own writing whenever possible. (Not on encoding name unsupported by it) + This function is deprecated and should be used to migrate your project easily, consult the documentation for + further information. Not planned for removal. + + :param byte_str: The byte sequence to examine. + :param should_rename_legacy: Should we rename legacy encodings + to their more modern equivalents? + """ + if len(kwargs): + warn( + f"charset-normalizer disregard arguments '{','.join(list(kwargs.keys()))}' in legacy function detect()" + ) + + if not isinstance(byte_str, (bytearray, bytes)): + raise TypeError( # pragma: nocover + "Expected object of type bytes or bytearray, got: " + "{0}".format(type(byte_str)) + ) + + if isinstance(byte_str, bytearray): + byte_str = bytes(byte_str) + + r = from_bytes(byte_str).best() + + encoding = r.encoding if r is not None else None + language = r.language if r is not None and r.language != "Unknown" else "" + confidence = 1.0 - r.chaos if r is not None else None + + # Note: CharsetNormalizer does not return 'UTF-8-SIG' as the sig get stripped in the detection/normalization process + # but chardet does return 'utf-8-sig' and it is a valid codec name. + if r is not None and encoding == "utf_8" and r.bom: + encoding += "_sig" + + if should_rename_legacy is False and encoding in CHARDET_CORRESPONDENCE: + encoding = CHARDET_CORRESPONDENCE[encoding] + + return { + "encoding": encoding, + "language": language, + "confidence": confidence, + } diff --git a/lib/python3.10/site-packages/charset_normalizer/md.py b/lib/python3.10/site-packages/charset_normalizer/md.py new file mode 100644 index 0000000000000000000000000000000000000000..77897aae4f44d084d6a59d7f7f1665035ff0047d --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/md.py @@ -0,0 +1,615 @@ +from functools import lru_cache +from logging import getLogger +from typing import List, Optional + +from .constant import ( + COMMON_SAFE_ASCII_CHARACTERS, + TRACE, + UNICODE_SECONDARY_RANGE_KEYWORD, +) +from .utils import ( + is_accentuated, + is_arabic, + is_arabic_isolated_form, + is_case_variable, + is_cjk, + is_emoticon, + is_hangul, + is_hiragana, + is_katakana, + is_latin, + is_punctuation, + is_separator, + is_symbol, + is_thai, + is_unprintable, + remove_accent, + unicode_range, +) + + +class MessDetectorPlugin: + """ + Base abstract class used for mess detection plugins. + All detectors MUST extend and implement given methods. + """ + + def eligible(self, character: str) -> bool: + """ + Determine if given character should be fed in. + """ + raise NotImplementedError # pragma: nocover + + def feed(self, character: str) -> None: + """ + The main routine to be executed upon character. + Insert the logic in witch the text would be considered chaotic. + """ + raise NotImplementedError # pragma: nocover + + def reset(self) -> None: # pragma: no cover + """ + Permit to reset the plugin to the initial state. + """ + raise NotImplementedError + + @property + def ratio(self) -> float: + """ + Compute the chaos ratio based on what your feed() has seen. + Must NOT be lower than 0.; No restriction gt 0. + """ + raise NotImplementedError # pragma: nocover + + +class TooManySymbolOrPunctuationPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._punctuation_count: int = 0 + self._symbol_count: int = 0 + self._character_count: int = 0 + + self._last_printable_char: Optional[str] = None + self._frenzy_symbol_in_word: bool = False + + def eligible(self, character: str) -> bool: + return character.isprintable() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if ( + character != self._last_printable_char + and character not in COMMON_SAFE_ASCII_CHARACTERS + ): + if is_punctuation(character): + self._punctuation_count += 1 + elif ( + character.isdigit() is False + and is_symbol(character) + and is_emoticon(character) is False + ): + self._symbol_count += 2 + + self._last_printable_char = character + + def reset(self) -> None: # pragma: no cover + self._punctuation_count = 0 + self._character_count = 0 + self._symbol_count = 0 + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + ratio_of_punctuation: float = ( + self._punctuation_count + self._symbol_count + ) / self._character_count + + return ratio_of_punctuation if ratio_of_punctuation >= 0.3 else 0.0 + + +class TooManyAccentuatedPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._character_count: int = 0 + self._accentuated_count: int = 0 + + def eligible(self, character: str) -> bool: + return character.isalpha() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if is_accentuated(character): + self._accentuated_count += 1 + + def reset(self) -> None: # pragma: no cover + self._character_count = 0 + self._accentuated_count = 0 + + @property + def ratio(self) -> float: + if self._character_count < 8: + return 0.0 + + ratio_of_accentuation: float = self._accentuated_count / self._character_count + return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0 + + +class UnprintablePlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._unprintable_count: int = 0 + self._character_count: int = 0 + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + if is_unprintable(character): + self._unprintable_count += 1 + self._character_count += 1 + + def reset(self) -> None: # pragma: no cover + self._unprintable_count = 0 + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return (self._unprintable_count * 8) / self._character_count + + +class SuspiciousDuplicateAccentPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._successive_count: int = 0 + self._character_count: int = 0 + + self._last_latin_character: Optional[str] = None + + def eligible(self, character: str) -> bool: + return character.isalpha() and is_latin(character) + + def feed(self, character: str) -> None: + self._character_count += 1 + if ( + self._last_latin_character is not None + and is_accentuated(character) + and is_accentuated(self._last_latin_character) + ): + if character.isupper() and self._last_latin_character.isupper(): + self._successive_count += 1 + # Worse if its the same char duplicated with different accent. + if remove_accent(character) == remove_accent(self._last_latin_character): + self._successive_count += 1 + self._last_latin_character = character + + def reset(self) -> None: # pragma: no cover + self._successive_count = 0 + self._character_count = 0 + self._last_latin_character = None + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return (self._successive_count * 2) / self._character_count + + +class SuspiciousRange(MessDetectorPlugin): + def __init__(self) -> None: + self._suspicious_successive_range_count: int = 0 + self._character_count: int = 0 + self._last_printable_seen: Optional[str] = None + + def eligible(self, character: str) -> bool: + return character.isprintable() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if ( + character.isspace() + or is_punctuation(character) + or character in COMMON_SAFE_ASCII_CHARACTERS + ): + self._last_printable_seen = None + return + + if self._last_printable_seen is None: + self._last_printable_seen = character + return + + unicode_range_a: Optional[str] = unicode_range(self._last_printable_seen) + unicode_range_b: Optional[str] = unicode_range(character) + + if is_suspiciously_successive_range(unicode_range_a, unicode_range_b): + self._suspicious_successive_range_count += 1 + + self._last_printable_seen = character + + def reset(self) -> None: # pragma: no cover + self._character_count = 0 + self._suspicious_successive_range_count = 0 + self._last_printable_seen = None + + @property + def ratio(self) -> float: + if self._character_count <= 24: + return 0.0 + + ratio_of_suspicious_range_usage: float = ( + self._suspicious_successive_range_count * 2 + ) / self._character_count + + return ratio_of_suspicious_range_usage + + +class SuperWeirdWordPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._word_count: int = 0 + self._bad_word_count: int = 0 + self._foreign_long_count: int = 0 + + self._is_current_word_bad: bool = False + self._foreign_long_watch: bool = False + + self._character_count: int = 0 + self._bad_character_count: int = 0 + + self._buffer: str = "" + self._buffer_accent_count: int = 0 + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + if character.isalpha(): + self._buffer += character + if is_accentuated(character): + self._buffer_accent_count += 1 + if ( + self._foreign_long_watch is False + and (is_latin(character) is False or is_accentuated(character)) + and is_cjk(character) is False + and is_hangul(character) is False + and is_katakana(character) is False + and is_hiragana(character) is False + and is_thai(character) is False + ): + self._foreign_long_watch = True + return + if not self._buffer: + return + if ( + character.isspace() or is_punctuation(character) or is_separator(character) + ) and self._buffer: + self._word_count += 1 + buffer_length: int = len(self._buffer) + + self._character_count += buffer_length + + if buffer_length >= 4: + if self._buffer_accent_count / buffer_length > 0.34: + self._is_current_word_bad = True + # Word/Buffer ending with an upper case accentuated letter are so rare, + # that we will consider them all as suspicious. Same weight as foreign_long suspicious. + if ( + is_accentuated(self._buffer[-1]) + and self._buffer[-1].isupper() + and all(_.isupper() for _ in self._buffer) is False + ): + self._foreign_long_count += 1 + self._is_current_word_bad = True + if buffer_length >= 24 and self._foreign_long_watch: + camel_case_dst = [ + i + for c, i in zip(self._buffer, range(0, buffer_length)) + if c.isupper() + ] + probable_camel_cased: bool = False + + if camel_case_dst and (len(camel_case_dst) / buffer_length <= 0.3): + probable_camel_cased = True + + if not probable_camel_cased: + self._foreign_long_count += 1 + self._is_current_word_bad = True + + if self._is_current_word_bad: + self._bad_word_count += 1 + self._bad_character_count += len(self._buffer) + self._is_current_word_bad = False + + self._foreign_long_watch = False + self._buffer = "" + self._buffer_accent_count = 0 + elif ( + character not in {"<", ">", "-", "=", "~", "|", "_"} + and character.isdigit() is False + and is_symbol(character) + ): + self._is_current_word_bad = True + self._buffer += character + + def reset(self) -> None: # pragma: no cover + self._buffer = "" + self._is_current_word_bad = False + self._foreign_long_watch = False + self._bad_word_count = 0 + self._word_count = 0 + self._character_count = 0 + self._bad_character_count = 0 + self._foreign_long_count = 0 + + @property + def ratio(self) -> float: + if self._word_count <= 10 and self._foreign_long_count == 0: + return 0.0 + + return self._bad_character_count / self._character_count + + +class CjkInvalidStopPlugin(MessDetectorPlugin): + """ + GB(Chinese) based encoding often render the stop incorrectly when the content does not fit and + can be easily detected. Searching for the overuse of '丅' and '丄'. + """ + + def __init__(self) -> None: + self._wrong_stop_count: int = 0 + self._cjk_character_count: int = 0 + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + if character in {"丅", "丄"}: + self._wrong_stop_count += 1 + return + if is_cjk(character): + self._cjk_character_count += 1 + + def reset(self) -> None: # pragma: no cover + self._wrong_stop_count = 0 + self._cjk_character_count = 0 + + @property + def ratio(self) -> float: + if self._cjk_character_count < 16: + return 0.0 + return self._wrong_stop_count / self._cjk_character_count + + +class ArchaicUpperLowerPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._buf: bool = False + + self._character_count_since_last_sep: int = 0 + + self._successive_upper_lower_count: int = 0 + self._successive_upper_lower_count_final: int = 0 + + self._character_count: int = 0 + + self._last_alpha_seen: Optional[str] = None + self._current_ascii_only: bool = True + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + is_concerned = character.isalpha() and is_case_variable(character) + chunk_sep = is_concerned is False + + if chunk_sep and self._character_count_since_last_sep > 0: + if ( + self._character_count_since_last_sep <= 64 + and character.isdigit() is False + and self._current_ascii_only is False + ): + self._successive_upper_lower_count_final += ( + self._successive_upper_lower_count + ) + + self._successive_upper_lower_count = 0 + self._character_count_since_last_sep = 0 + self._last_alpha_seen = None + self._buf = False + self._character_count += 1 + self._current_ascii_only = True + + return + + if self._current_ascii_only is True and character.isascii() is False: + self._current_ascii_only = False + + if self._last_alpha_seen is not None: + if (character.isupper() and self._last_alpha_seen.islower()) or ( + character.islower() and self._last_alpha_seen.isupper() + ): + if self._buf is True: + self._successive_upper_lower_count += 2 + self._buf = False + else: + self._buf = True + else: + self._buf = False + + self._character_count += 1 + self._character_count_since_last_sep += 1 + self._last_alpha_seen = character + + def reset(self) -> None: # pragma: no cover + self._character_count = 0 + self._character_count_since_last_sep = 0 + self._successive_upper_lower_count = 0 + self._successive_upper_lower_count_final = 0 + self._last_alpha_seen = None + self._buf = False + self._current_ascii_only = True + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return self._successive_upper_lower_count_final / self._character_count + + +class ArabicIsolatedFormPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._character_count: int = 0 + self._isolated_form_count: int = 0 + + def reset(self) -> None: # pragma: no cover + self._character_count = 0 + self._isolated_form_count = 0 + + def eligible(self, character: str) -> bool: + return is_arabic(character) + + def feed(self, character: str) -> None: + self._character_count += 1 + + if is_arabic_isolated_form(character): + self._isolated_form_count += 1 + + @property + def ratio(self) -> float: + if self._character_count < 8: + return 0.0 + + isolated_form_usage: float = self._isolated_form_count / self._character_count + + return isolated_form_usage + + +@lru_cache(maxsize=1024) +def is_suspiciously_successive_range( + unicode_range_a: Optional[str], unicode_range_b: Optional[str] +) -> bool: + """ + Determine if two Unicode range seen next to each other can be considered as suspicious. + """ + if unicode_range_a is None or unicode_range_b is None: + return True + + if unicode_range_a == unicode_range_b: + return False + + if "Latin" in unicode_range_a and "Latin" in unicode_range_b: + return False + + if "Emoticons" in unicode_range_a or "Emoticons" in unicode_range_b: + return False + + # Latin characters can be accompanied with a combining diacritical mark + # eg. Vietnamese. + if ("Latin" in unicode_range_a or "Latin" in unicode_range_b) and ( + "Combining" in unicode_range_a or "Combining" in unicode_range_b + ): + return False + + keywords_range_a, keywords_range_b = unicode_range_a.split( + " " + ), unicode_range_b.split(" ") + + for el in keywords_range_a: + if el in UNICODE_SECONDARY_RANGE_KEYWORD: + continue + if el in keywords_range_b: + return False + + # Japanese Exception + range_a_jp_chars, range_b_jp_chars = ( + unicode_range_a + in ( + "Hiragana", + "Katakana", + ), + unicode_range_b in ("Hiragana", "Katakana"), + ) + if (range_a_jp_chars or range_b_jp_chars) and ( + "CJK" in unicode_range_a or "CJK" in unicode_range_b + ): + return False + if range_a_jp_chars and range_b_jp_chars: + return False + + if "Hangul" in unicode_range_a or "Hangul" in unicode_range_b: + if "CJK" in unicode_range_a or "CJK" in unicode_range_b: + return False + if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin": + return False + + # Chinese/Japanese use dedicated range for punctuation and/or separators. + if ("CJK" in unicode_range_a or "CJK" in unicode_range_b) or ( + unicode_range_a in ["Katakana", "Hiragana"] + and unicode_range_b in ["Katakana", "Hiragana"] + ): + if "Punctuation" in unicode_range_a or "Punctuation" in unicode_range_b: + return False + if "Forms" in unicode_range_a or "Forms" in unicode_range_b: + return False + if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin": + return False + + return True + + +@lru_cache(maxsize=2048) +def mess_ratio( + decoded_sequence: str, maximum_threshold: float = 0.2, debug: bool = False +) -> float: + """ + Compute a mess ratio given a decoded bytes sequence. The maximum threshold does stop the computation earlier. + """ + + detectors: List[MessDetectorPlugin] = [ + md_class() for md_class in MessDetectorPlugin.__subclasses__() + ] + + length: int = len(decoded_sequence) + 1 + + mean_mess_ratio: float = 0.0 + + if length < 512: + intermediary_mean_mess_ratio_calc: int = 32 + elif length <= 1024: + intermediary_mean_mess_ratio_calc = 64 + else: + intermediary_mean_mess_ratio_calc = 128 + + for character, index in zip(decoded_sequence + "\n", range(length)): + for detector in detectors: + if detector.eligible(character): + detector.feed(character) + + if ( + index > 0 and index % intermediary_mean_mess_ratio_calc == 0 + ) or index == length - 1: + mean_mess_ratio = sum(dt.ratio for dt in detectors) + + if mean_mess_ratio >= maximum_threshold: + break + + if debug: + logger = getLogger("charset_normalizer") + + logger.log( + TRACE, + "Mess-detector extended-analysis start. " + f"intermediary_mean_mess_ratio_calc={intermediary_mean_mess_ratio_calc} mean_mess_ratio={mean_mess_ratio} " + f"maximum_threshold={maximum_threshold}", + ) + + if len(decoded_sequence) > 16: + logger.log(TRACE, f"Starting with: {decoded_sequence[:16]}") + logger.log(TRACE, f"Ending with: {decoded_sequence[-16::]}") + + for dt in detectors: # pragma: nocover + logger.log(TRACE, f"{dt.__class__}: {dt.ratio}") + + return round(mean_mess_ratio, 3) diff --git a/lib/python3.10/site-packages/charset_normalizer/models.py b/lib/python3.10/site-packages/charset_normalizer/models.py new file mode 100644 index 0000000000000000000000000000000000000000..a760b9c558d953f6907d29fa31844d07d06f9ce1 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/models.py @@ -0,0 +1,340 @@ +from encodings.aliases import aliases +from hashlib import sha256 +from json import dumps +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union + +from .constant import TOO_BIG_SEQUENCE +from .utils import iana_name, is_multi_byte_encoding, unicode_range + + +class CharsetMatch: + def __init__( + self, + payload: bytes, + guessed_encoding: str, + mean_mess_ratio: float, + has_sig_or_bom: bool, + languages: "CoherenceMatches", + decoded_payload: Optional[str] = None, + ): + self._payload: bytes = payload + + self._encoding: str = guessed_encoding + self._mean_mess_ratio: float = mean_mess_ratio + self._languages: CoherenceMatches = languages + self._has_sig_or_bom: bool = has_sig_or_bom + self._unicode_ranges: Optional[List[str]] = None + + self._leaves: List[CharsetMatch] = [] + self._mean_coherence_ratio: float = 0.0 + + self._output_payload: Optional[bytes] = None + self._output_encoding: Optional[str] = None + + self._string: Optional[str] = decoded_payload + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CharsetMatch): + raise TypeError( + "__eq__ cannot be invoked on {} and {}.".format( + str(other.__class__), str(self.__class__) + ) + ) + return self.encoding == other.encoding and self.fingerprint == other.fingerprint + + def __lt__(self, other: object) -> bool: + """ + Implemented to make sorted available upon CharsetMatches items. + """ + if not isinstance(other, CharsetMatch): + raise ValueError + + chaos_difference: float = abs(self.chaos - other.chaos) + coherence_difference: float = abs(self.coherence - other.coherence) + + # Below 1% difference --> Use Coherence + if chaos_difference < 0.01 and coherence_difference > 0.02: + return self.coherence > other.coherence + elif chaos_difference < 0.01 and coherence_difference <= 0.02: + # When having a difficult decision, use the result that decoded as many multi-byte as possible. + # preserve RAM usage! + if len(self._payload) >= TOO_BIG_SEQUENCE: + return self.chaos < other.chaos + return self.multi_byte_usage > other.multi_byte_usage + + return self.chaos < other.chaos + + @property + def multi_byte_usage(self) -> float: + return 1.0 - (len(str(self)) / len(self.raw)) + + def __str__(self) -> str: + # Lazy Str Loading + if self._string is None: + self._string = str(self._payload, self._encoding, "strict") + return self._string + + def __repr__(self) -> str: + return "".format(self.encoding, self.fingerprint) + + def add_submatch(self, other: "CharsetMatch") -> None: + if not isinstance(other, CharsetMatch) or other == self: + raise ValueError( + "Unable to add instance <{}> as a submatch of a CharsetMatch".format( + other.__class__ + ) + ) + + other._string = None # Unload RAM usage; dirty trick. + self._leaves.append(other) + + @property + def encoding(self) -> str: + return self._encoding + + @property + def encoding_aliases(self) -> List[str]: + """ + Encoding name are known by many name, using this could help when searching for IBM855 when it's listed as CP855. + """ + also_known_as: List[str] = [] + for u, p in aliases.items(): + if self.encoding == u: + also_known_as.append(p) + elif self.encoding == p: + also_known_as.append(u) + return also_known_as + + @property + def bom(self) -> bool: + return self._has_sig_or_bom + + @property + def byte_order_mark(self) -> bool: + return self._has_sig_or_bom + + @property + def languages(self) -> List[str]: + """ + Return the complete list of possible languages found in decoded sequence. + Usually not really useful. Returned list may be empty even if 'language' property return something != 'Unknown'. + """ + return [e[0] for e in self._languages] + + @property + def language(self) -> str: + """ + Most probable language found in decoded sequence. If none were detected or inferred, the property will return + "Unknown". + """ + if not self._languages: + # Trying to infer the language based on the given encoding + # Its either English or we should not pronounce ourselves in certain cases. + if "ascii" in self.could_be_from_charset: + return "English" + + # doing it there to avoid circular import + from charset_normalizer.cd import encoding_languages, mb_encoding_languages + + languages = ( + mb_encoding_languages(self.encoding) + if is_multi_byte_encoding(self.encoding) + else encoding_languages(self.encoding) + ) + + if len(languages) == 0 or "Latin Based" in languages: + return "Unknown" + + return languages[0] + + return self._languages[0][0] + + @property + def chaos(self) -> float: + return self._mean_mess_ratio + + @property + def coherence(self) -> float: + if not self._languages: + return 0.0 + return self._languages[0][1] + + @property + def percent_chaos(self) -> float: + return round(self.chaos * 100, ndigits=3) + + @property + def percent_coherence(self) -> float: + return round(self.coherence * 100, ndigits=3) + + @property + def raw(self) -> bytes: + """ + Original untouched bytes. + """ + return self._payload + + @property + def submatch(self) -> List["CharsetMatch"]: + return self._leaves + + @property + def has_submatch(self) -> bool: + return len(self._leaves) > 0 + + @property + def alphabets(self) -> List[str]: + if self._unicode_ranges is not None: + return self._unicode_ranges + # list detected ranges + detected_ranges: List[Optional[str]] = [ + unicode_range(char) for char in str(self) + ] + # filter and sort + self._unicode_ranges = sorted(list({r for r in detected_ranges if r})) + return self._unicode_ranges + + @property + def could_be_from_charset(self) -> List[str]: + """ + The complete list of encoding that output the exact SAME str result and therefore could be the originating + encoding. + This list does include the encoding available in property 'encoding'. + """ + return [self._encoding] + [m.encoding for m in self._leaves] + + def output(self, encoding: str = "utf_8") -> bytes: + """ + Method to get re-encoded bytes payload using given target encoding. Default to UTF-8. + Any errors will be simply ignored by the encoder NOT replaced. + """ + if self._output_encoding is None or self._output_encoding != encoding: + self._output_encoding = encoding + self._output_payload = str(self).encode(encoding, "replace") + + return self._output_payload # type: ignore + + @property + def fingerprint(self) -> str: + """ + Retrieve the unique SHA256 computed using the transformed (re-encoded) payload. Not the original one. + """ + return sha256(self.output()).hexdigest() + + +class CharsetMatches: + """ + Container with every CharsetMatch items ordered by default from most probable to the less one. + Act like a list(iterable) but does not implements all related methods. + """ + + def __init__(self, results: Optional[List[CharsetMatch]] = None): + self._results: List[CharsetMatch] = sorted(results) if results else [] + + def __iter__(self) -> Iterator[CharsetMatch]: + yield from self._results + + def __getitem__(self, item: Union[int, str]) -> CharsetMatch: + """ + Retrieve a single item either by its position or encoding name (alias may be used here). + Raise KeyError upon invalid index or encoding not present in results. + """ + if isinstance(item, int): + return self._results[item] + if isinstance(item, str): + item = iana_name(item, False) + for result in self._results: + if item in result.could_be_from_charset: + return result + raise KeyError + + def __len__(self) -> int: + return len(self._results) + + def __bool__(self) -> bool: + return len(self._results) > 0 + + def append(self, item: CharsetMatch) -> None: + """ + Insert a single match. Will be inserted accordingly to preserve sort. + Can be inserted as a submatch. + """ + if not isinstance(item, CharsetMatch): + raise ValueError( + "Cannot append instance '{}' to CharsetMatches".format( + str(item.__class__) + ) + ) + # We should disable the submatch factoring when the input file is too heavy (conserve RAM usage) + if len(item.raw) <= TOO_BIG_SEQUENCE: + for match in self._results: + if match.fingerprint == item.fingerprint and match.chaos == item.chaos: + match.add_submatch(item) + return + self._results.append(item) + self._results = sorted(self._results) + + def best(self) -> Optional["CharsetMatch"]: + """ + Simply return the first match. Strict equivalent to matches[0]. + """ + if not self._results: + return None + return self._results[0] + + def first(self) -> Optional["CharsetMatch"]: + """ + Redundant method, call the method best(). Kept for BC reasons. + """ + return self.best() + + +CoherenceMatch = Tuple[str, float] +CoherenceMatches = List[CoherenceMatch] + + +class CliDetectionResult: + def __init__( + self, + path: str, + encoding: Optional[str], + encoding_aliases: List[str], + alternative_encodings: List[str], + language: str, + alphabets: List[str], + has_sig_or_bom: bool, + chaos: float, + coherence: float, + unicode_path: Optional[str], + is_preferred: bool, + ): + self.path: str = path + self.unicode_path: Optional[str] = unicode_path + self.encoding: Optional[str] = encoding + self.encoding_aliases: List[str] = encoding_aliases + self.alternative_encodings: List[str] = alternative_encodings + self.language: str = language + self.alphabets: List[str] = alphabets + self.has_sig_or_bom: bool = has_sig_or_bom + self.chaos: float = chaos + self.coherence: float = coherence + self.is_preferred: bool = is_preferred + + @property + def __dict__(self) -> Dict[str, Any]: # type: ignore + return { + "path": self.path, + "encoding": self.encoding, + "encoding_aliases": self.encoding_aliases, + "alternative_encodings": self.alternative_encodings, + "language": self.language, + "alphabets": self.alphabets, + "has_sig_or_bom": self.has_sig_or_bom, + "chaos": self.chaos, + "coherence": self.coherence, + "unicode_path": self.unicode_path, + "is_preferred": self.is_preferred, + } + + def to_json(self) -> str: + return dumps(self.__dict__, ensure_ascii=True, indent=4) diff --git a/lib/python3.10/site-packages/charset_normalizer/py.typed b/lib/python3.10/site-packages/charset_normalizer/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/charset_normalizer/utils.py b/lib/python3.10/site-packages/charset_normalizer/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..e5cbbf4c0ddfa5c1b5898d8a4405e27292100d41 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/utils.py @@ -0,0 +1,421 @@ +import importlib +import logging +import unicodedata +from codecs import IncrementalDecoder +from encodings.aliases import aliases +from functools import lru_cache +from re import findall +from typing import Generator, List, Optional, Set, Tuple, Union + +from _multibytecodec import MultibyteIncrementalDecoder + +from .constant import ( + ENCODING_MARKS, + IANA_SUPPORTED_SIMILAR, + RE_POSSIBLE_ENCODING_INDICATION, + UNICODE_RANGES_COMBINED, + UNICODE_SECONDARY_RANGE_KEYWORD, + UTF8_MAXIMAL_ALLOCATION, +) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_accentuated(character: str) -> bool: + try: + description: str = unicodedata.name(character) + except ValueError: + return False + return ( + "WITH GRAVE" in description + or "WITH ACUTE" in description + or "WITH CEDILLA" in description + or "WITH DIAERESIS" in description + or "WITH CIRCUMFLEX" in description + or "WITH TILDE" in description + or "WITH MACRON" in description + or "WITH RING ABOVE" in description + ) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def remove_accent(character: str) -> str: + decomposed: str = unicodedata.decomposition(character) + if not decomposed: + return character + + codes: List[str] = decomposed.split(" ") + + return chr(int(codes[0], 16)) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def unicode_range(character: str) -> Optional[str]: + """ + Retrieve the Unicode range official name from a single character. + """ + character_ord: int = ord(character) + + for range_name, ord_range in UNICODE_RANGES_COMBINED.items(): + if character_ord in ord_range: + return range_name + + return None + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_latin(character: str) -> bool: + try: + description: str = unicodedata.name(character) + except ValueError: + return False + return "LATIN" in description + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_punctuation(character: str) -> bool: + character_category: str = unicodedata.category(character) + + if "P" in character_category: + return True + + character_range: Optional[str] = unicode_range(character) + + if character_range is None: + return False + + return "Punctuation" in character_range + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_symbol(character: str) -> bool: + character_category: str = unicodedata.category(character) + + if "S" in character_category or "N" in character_category: + return True + + character_range: Optional[str] = unicode_range(character) + + if character_range is None: + return False + + return "Forms" in character_range and character_category != "Lo" + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_emoticon(character: str) -> bool: + character_range: Optional[str] = unicode_range(character) + + if character_range is None: + return False + + return "Emoticons" in character_range or "Pictographs" in character_range + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_separator(character: str) -> bool: + if character.isspace() or character in {"|", "+", "<", ">"}: + return True + + character_category: str = unicodedata.category(character) + + return "Z" in character_category or character_category in {"Po", "Pd", "Pc"} + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_case_variable(character: str) -> bool: + return character.islower() != character.isupper() + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_cjk(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "CJK" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hiragana(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "HIRAGANA" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_katakana(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "KATAKANA" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hangul(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "HANGUL" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_thai(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "THAI" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "ARABIC" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic_isolated_form(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: + return False + + return "ARABIC" in character_name and "ISOLATED FORM" in character_name + + +@lru_cache(maxsize=len(UNICODE_RANGES_COMBINED)) +def is_unicode_range_secondary(range_name: str) -> bool: + return any(keyword in range_name for keyword in UNICODE_SECONDARY_RANGE_KEYWORD) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_unprintable(character: str) -> bool: + return ( + character.isspace() is False # includes \n \t \r \v + and character.isprintable() is False + and character != "\x1A" # Why? Its the ASCII substitute character. + and character != "\ufeff" # bug discovered in Python, + # Zero Width No-Break Space located in Arabic Presentation Forms-B, Unicode 1.1 not acknowledged as space. + ) + + +def any_specified_encoding(sequence: bytes, search_zone: int = 8192) -> Optional[str]: + """ + Extract using ASCII-only decoder any specified encoding in the first n-bytes. + """ + if not isinstance(sequence, bytes): + raise TypeError + + seq_len: int = len(sequence) + + results: List[str] = findall( + RE_POSSIBLE_ENCODING_INDICATION, + sequence[: min(seq_len, search_zone)].decode("ascii", errors="ignore"), + ) + + if len(results) == 0: + return None + + for specified_encoding in results: + specified_encoding = specified_encoding.lower().replace("-", "_") + + encoding_alias: str + encoding_iana: str + + for encoding_alias, encoding_iana in aliases.items(): + if encoding_alias == specified_encoding: + return encoding_iana + if encoding_iana == specified_encoding: + return encoding_iana + + return None + + +@lru_cache(maxsize=128) +def is_multi_byte_encoding(name: str) -> bool: + """ + Verify is a specific encoding is a multi byte one based on it IANA name + """ + return name in { + "utf_8", + "utf_8_sig", + "utf_16", + "utf_16_be", + "utf_16_le", + "utf_32", + "utf_32_le", + "utf_32_be", + "utf_7", + } or issubclass( + importlib.import_module("encodings.{}".format(name)).IncrementalDecoder, + MultibyteIncrementalDecoder, + ) + + +def identify_sig_or_bom(sequence: bytes) -> Tuple[Optional[str], bytes]: + """ + Identify and extract SIG/BOM in given sequence. + """ + + for iana_encoding in ENCODING_MARKS: + marks: Union[bytes, List[bytes]] = ENCODING_MARKS[iana_encoding] + + if isinstance(marks, bytes): + marks = [marks] + + for mark in marks: + if sequence.startswith(mark): + return iana_encoding, mark + + return None, b"" + + +def should_strip_sig_or_bom(iana_encoding: str) -> bool: + return iana_encoding not in {"utf_16", "utf_32"} + + +def iana_name(cp_name: str, strict: bool = True) -> str: + cp_name = cp_name.lower().replace("-", "_") + + encoding_alias: str + encoding_iana: str + + for encoding_alias, encoding_iana in aliases.items(): + if cp_name in [encoding_alias, encoding_iana]: + return encoding_iana + + if strict: + raise ValueError("Unable to retrieve IANA for '{}'".format(cp_name)) + + return cp_name + + +def range_scan(decoded_sequence: str) -> List[str]: + ranges: Set[str] = set() + + for character in decoded_sequence: + character_range: Optional[str] = unicode_range(character) + + if character_range is None: + continue + + ranges.add(character_range) + + return list(ranges) + + +def cp_similarity(iana_name_a: str, iana_name_b: str) -> float: + if is_multi_byte_encoding(iana_name_a) or is_multi_byte_encoding(iana_name_b): + return 0.0 + + decoder_a = importlib.import_module( + "encodings.{}".format(iana_name_a) + ).IncrementalDecoder + decoder_b = importlib.import_module( + "encodings.{}".format(iana_name_b) + ).IncrementalDecoder + + id_a: IncrementalDecoder = decoder_a(errors="ignore") + id_b: IncrementalDecoder = decoder_b(errors="ignore") + + character_match_count: int = 0 + + for i in range(255): + to_be_decoded: bytes = bytes([i]) + if id_a.decode(to_be_decoded) == id_b.decode(to_be_decoded): + character_match_count += 1 + + return character_match_count / 254 + + +def is_cp_similar(iana_name_a: str, iana_name_b: str) -> bool: + """ + Determine if two code page are at least 80% similar. IANA_SUPPORTED_SIMILAR dict was generated using + the function cp_similarity. + """ + return ( + iana_name_a in IANA_SUPPORTED_SIMILAR + and iana_name_b in IANA_SUPPORTED_SIMILAR[iana_name_a] + ) + + +def set_logging_handler( + name: str = "charset_normalizer", + level: int = logging.INFO, + format_string: str = "%(asctime)s | %(levelname)s | %(message)s", +) -> None: + logger = logging.getLogger(name) + logger.setLevel(level) + + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter(format_string)) + logger.addHandler(handler) + + +def cut_sequence_chunks( + sequences: bytes, + encoding_iana: str, + offsets: range, + chunk_size: int, + bom_or_sig_available: bool, + strip_sig_or_bom: bool, + sig_payload: bytes, + is_multi_byte_decoder: bool, + decoded_payload: Optional[str] = None, +) -> Generator[str, None, None]: + if decoded_payload and is_multi_byte_decoder is False: + for i in offsets: + chunk = decoded_payload[i : i + chunk_size] + if not chunk: + break + yield chunk + else: + for i in offsets: + chunk_end = i + chunk_size + if chunk_end > len(sequences) + 8: + continue + + cut_sequence = sequences[i : i + chunk_size] + + if bom_or_sig_available and strip_sig_or_bom is False: + cut_sequence = sig_payload + cut_sequence + + chunk = cut_sequence.decode( + encoding_iana, + errors="ignore" if is_multi_byte_decoder else "strict", + ) + + # multi-byte bad cutting detector and adjustment + # not the cleanest way to perform that fix but clever enough for now. + if is_multi_byte_decoder and i > 0: + chunk_partial_size_chk: int = min(chunk_size, 16) + + if ( + decoded_payload + and chunk[:chunk_partial_size_chk] not in decoded_payload + ): + for j in range(i, i - 4, -1): + cut_sequence = sequences[j:chunk_end] + + if bom_or_sig_available and strip_sig_or_bom is False: + cut_sequence = sig_payload + cut_sequence + + chunk = cut_sequence.decode(encoding_iana, errors="ignore") + + if chunk[:chunk_partial_size_chk] in decoded_payload: + break + + yield chunk diff --git a/lib/python3.10/site-packages/charset_normalizer/version.py b/lib/python3.10/site-packages/charset_normalizer/version.py new file mode 100644 index 0000000000000000000000000000000000000000..5a4da4ff49bc80ef49e8aa7e01cc8555518bd1b1 --- /dev/null +++ b/lib/python3.10/site-packages/charset_normalizer/version.py @@ -0,0 +1,6 @@ +""" +Expose version +""" + +__version__ = "3.3.2" +VERSION = __version__.split(".") diff --git a/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/RECORD b/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..180360bdf47b229692ba2b1e8f55d8c228dbb3e9 --- /dev/null +++ b/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/RECORD @@ -0,0 +1,20 @@ +exceptiongroup-1.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +exceptiongroup-1.3.0.dist-info/METADATA,sha256=oiAq-llXqOaV0B3dJN316SjtCpthQvuQjmVuVObbaeQ,6725 +exceptiongroup-1.3.0.dist-info/RECORD,, +exceptiongroup-1.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +exceptiongroup-1.3.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +exceptiongroup-1.3.0.dist-info/direct_url.json,sha256=dnu71jvKtbggw_qhsTkH2IUNanQ25Yy7PQPRXWrT6w8,110 +exceptiongroup-1.3.0.dist-info/licenses/LICENSE,sha256=blBw12UDHgrUA6HL-Qrm0ZoCKPgC4yC3rP9GCqcu1Hw,3704 +exceptiongroup/__init__.py,sha256=7DHS0hDk-RIs3IQc3SbZVB0-1MhiSCJ9XgvEyEloL7M,1049 +exceptiongroup/__pycache__/__init__.cpython-39.pyc,, +exceptiongroup/__pycache__/_catch.cpython-39.pyc,, +exceptiongroup/__pycache__/_exceptions.cpython-39.pyc,, +exceptiongroup/__pycache__/_formatting.cpython-39.pyc,, +exceptiongroup/__pycache__/_suppress.cpython-39.pyc,, +exceptiongroup/__pycache__/_version.cpython-39.pyc,, +exceptiongroup/_catch.py,sha256=CaJez3E-Jkr-7B7RT3fzusdLWnuyeekooSFn7KyWt9s,4680 +exceptiongroup/_exceptions.py,sha256=wPwPsZ64SXEptuwb4XrTIa1Mc78uqF5vmCrXTdllLn4,11463 +exceptiongroup/_formatting.py,sha256=4UC6bUN3K-FxEX96_ozzgeX05fCyprl6qCVR-efBmQ0,21032 +exceptiongroup/_suppress.py,sha256=LX11PRNpchwfNWwEMY92nYN1F_5qFenQcS8EjIONXKE,1772 +exceptiongroup/_version.py,sha256=qDtcPZdKzxLpd8vVl6fpIFIMkWt2HK_cO9gLDwaHEdk,511 +exceptiongroup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/REQUESTED b/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/WHEEL b/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..d8b9936dad9ab2513fa6979f411560d3b6b57e37 --- /dev/null +++ b/lib/python3.10/site-packages/exceptiongroup-1.3.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/INSTALLER b/lib/python3.10/site-packages/executing-2.2.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/LICENSE.txt b/lib/python3.10/site-packages/executing-2.2.0.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..473e36e246edd5800325e9fa1eaa7697c95be1ef --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alex Hall + +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/lib/python3.10/site-packages/executing-2.2.0.dist-info/METADATA b/lib/python3.10/site-packages/executing-2.2.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..e01fdfe8711d948b3b6e3d8588899ad68cf1d8fc --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/METADATA @@ -0,0 +1,171 @@ +Metadata-Version: 2.4 +Name: executing +Version: 2.2.0 +Summary: Get the currently executing AST node of a frame, and other information +Home-page: https://github.com/alexmojaki/executing +Author: Alex Hall +Author-email: alex.mojaki@gmail.com +License: MIT +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +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 +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +License-File: LICENSE.txt +Provides-Extra: tests +Requires-Dist: asttokens>=2.1.0; extra == "tests" +Requires-Dist: ipython; extra == "tests" +Requires-Dist: pytest; extra == "tests" +Requires-Dist: coverage; extra == "tests" +Requires-Dist: coverage-enable-subprocess; extra == "tests" +Requires-Dist: littleutils; extra == "tests" +Requires-Dist: rich; python_version >= "3.11" and extra == "tests" +Dynamic: license-file + +# executing + +[![Build Status](https://github.com/alexmojaki/executing/workflows/Tests/badge.svg?branch=master)](https://github.com/alexmojaki/executing/actions) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/executing/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/executing?branch=master) [![Supports Python versions 3.5+, including PyPy](https://img.shields.io/pypi/pyversions/executing.svg)](https://pypi.python.org/pypi/executing) + +This mini-package lets you get information about what a frame is currently doing, particularly the AST node being executed. + +* [Usage](#usage) + * [Getting the AST node](#getting-the-ast-node) + * [Getting the source code of the node](#getting-the-source-code-of-the-node) + * [Getting the `__qualname__` of the current function](#getting-the-__qualname__-of-the-current-function) + * [The Source class](#the-source-class) +* [Installation](#installation) +* [How does it work?](#how-does-it-work) +* [Is it reliable?](#is-it-reliable) +* [Which nodes can it identify?](#which-nodes-can-it-identify) +* [Projects that use this](#projects-that-use-this) + +## Usage + +### Getting the AST node + +```python +import executing + +node = executing.Source.executing(frame).node +``` + +Then `node` will be an AST node (from the `ast` standard library module) or None if the node couldn't be identified (which may happen often and should always be checked). + +`node` will always be the same instance for multiple calls with frames at the same point of execution. + +If you have a traceback object, pass it directly to `Source.executing()` rather than the `tb_frame` attribute to get the correct node. + +### Getting the source code of the node + +For this you will need to separately install the [`asttokens`](https://github.com/gristlabs/asttokens) library, then obtain an `ASTTokens` object: + +```python +executing.Source.executing(frame).source.asttokens() +``` + +or: + +```python +executing.Source.for_frame(frame).asttokens() +``` + +or use one of the convenience methods: + +```python +executing.Source.executing(frame).text() +executing.Source.executing(frame).text_range() +``` + +### Getting the `__qualname__` of the current function + +```python +executing.Source.executing(frame).code_qualname() +``` + +or: + +```python +executing.Source.for_frame(frame).code_qualname(frame.f_code) +``` + +### The `Source` class + +Everything goes through the `Source` class. Only one instance of the class is created for each filename. Subclassing it to add more attributes on creation or methods is recommended. The classmethods such as `executing` will respect this. See the source code and docstrings for more detail. + +## Installation + + pip install executing + +If you don't like that you can just copy the file `executing.py`, there are no dependencies (but of course you won't get updates). + +## How does it work? + +Suppose the frame is executing this line: + +```python +self.foo(bar.x) +``` + +and in particular it's currently obtaining the attribute `self.foo`. Looking at the bytecode, specifically `frame.f_code.co_code[frame.f_lasti]`, we can tell that it's loading an attribute, but it's not obvious which one. We can narrow down the statement being executed using `frame.f_lineno` and find the two `ast.Attribute` nodes representing `self.foo` and `bar.x`. How do we find out which one it is, without recreating the entire compiler in Python? + +The trick is to modify the AST slightly for each candidate expression and observe the changes in the bytecode instructions. We change the AST to this: + +```python +(self.foo ** 'longuniqueconstant')(bar.x) +``` + +and compile it, and the bytecode will be almost the same but there will be two new instructions: + + LOAD_CONST 'longuniqueconstant' + BINARY_POWER + +and just before that will be a `LOAD_ATTR` instruction corresponding to `self.foo`. Seeing that it's in the same position as the original instruction lets us know we've found our match. + +## Is it reliable? + +Yes - if it identifies a node, you can trust that it's identified the correct one. The tests are very thorough - in addition to unit tests which check various situations directly, there are property tests against a large number of files (see the filenames printed in [this build](https://travis-ci.org/alexmojaki/executing/jobs/557970457)) with real code. Specifically, for each file, the tests: + + 1. Identify as many nodes as possible from all the bytecode instructions in the file, and assert that they are all distinct + 2. Find all the nodes that should be identifiable, and assert that they were indeed identified somewhere + +In other words, it shows that there is a one-to-one mapping between the nodes and the instructions that can be handled. This leaves very little room for a bug to creep in. + +Furthermore, `executing` checks that the instructions compiled from the modified AST exactly match the original code save for a few small known exceptions. This accounts for all the quirks and optimisations in the interpreter. + +## Which nodes can it identify? + +Currently it works in almost all cases for the following `ast` nodes: + + - `Call`, e.g. `self.foo(bar)` + - `Attribute`, e.g. `point.x` + - `Subscript`, e.g. `lst[1]` + - `BinOp`, e.g. `x + y` (doesn't include `and` and `or`) + - `UnaryOp`, e.g. `-n` (includes `not` but only works sometimes) + - `Compare` e.g. `a < b` (not for chains such as `0 < p < 1`) + +The plan is to extend to more operations in the future. + +## Projects that use this + +### My Projects + +- **[`stack_data`](https://github.com/alexmojaki/stack_data)**: Extracts data from stack frames and tracebacks, particularly to display more useful tracebacks than the default. Also uses another related library of mine: **[`pure_eval`](https://github.com/alexmojaki/pure_eval)**. +- **[`futurecoder`](https://futurecoder.io/)**: Highlights the executing node in tracebacks using `executing` via `stack_data`, and provides debugging with `snoop`. +- **[`snoop`](https://github.com/alexmojaki/snoop)**: A feature-rich and convenient debugging library. Uses `executing` to show the operation which caused an exception and to allow the `pp` function to display the source of its arguments. +- **[`heartrate`](https://github.com/alexmojaki/heartrate)**: A simple real time visualisation of the execution of a Python program. Uses `executing` to highlight currently executing operations, particularly in each frame of the stack trace. +- **[`sorcery`](https://github.com/alexmojaki/sorcery)**: Dark magic delights in Python. Uses `executing` to let special callables called spells know where they're being called from. + +### Projects I've contributed to + +- **[`IPython`](https://github.com/ipython/ipython/pull/12150)**: Highlights the executing node in tracebacks using `executing` via [`stack_data`](https://github.com/alexmojaki/stack_data). +- **[`icecream`](https://github.com/gruns/icecream)**: 🍦 Sweet and creamy print debugging. Uses `executing` to identify where `ic` is called and print its arguments. +- **[`friendly_traceback`](https://github.com/friendly-traceback/friendly-traceback)**: Uses `stack_data` and `executing` to pinpoint the cause of errors and provide helpful explanations. +- **[`python-devtools`](https://github.com/samuelcolvin/python-devtools)**: Uses `executing` for print debugging similar to `icecream`. +- **[`sentry_sdk`](https://github.com/getsentry/sentry-python)**: Add the integration `sentry_sdk.integrations.executingExecutingIntegration()` to show the function `__qualname__` in each frame in sentry events. +- **[`varname`](https://github.com/pwwang/python-varname)**: Dark magics about variable names in python. Uses `executing` to find where its various magical functions like `varname` and `nameof` are called from. diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/RECORD b/lib/python3.10/site-packages/executing-2.2.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..53c9a2bf8fa4250d4fbae4abdbc329247404c173 --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/RECORD @@ -0,0 +1,21 @@ +executing-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +executing-2.2.0.dist-info/METADATA,sha256=ThuHp_69cMauxoHhSWxsHXtFx04cYwvY_zNpqNRMszM,8885 +executing-2.2.0.dist-info/RECORD,, +executing-2.2.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +executing-2.2.0.dist-info/WHEEL,sha256=AeO2BvogYWm3eGaHCvhzmUYt8ia7KfURiHzO_1atlys,109 +executing-2.2.0.dist-info/direct_url.json,sha256=cGsAaAVURhtJXIG56bNA0ieOsen_841GhiOEJr8EdjI,105 +executing-2.2.0.dist-info/licenses/LICENSE.txt,sha256=pHaiyw70xBRQNApXeii5GsTH9mkTay7hSAR_q9X8QYE,1066 +executing-2.2.0.dist-info/top_level.txt,sha256=b9Rtf3NtSqc0_Kak6L_lvnbdKPA0GUim2p-XcFQsf5g,10 +executing/__init__.py,sha256=agdZWnui3FaB1FepFzVWX5ydS0mlUsVeA0zBLMxhvjk,831 +executing/__pycache__/__init__.cpython-39.pyc,, +executing/__pycache__/_exceptions.cpython-39.pyc,, +executing/__pycache__/_position_node_finder.cpython-39.pyc,, +executing/__pycache__/_pytest_utils.cpython-39.pyc,, +executing/__pycache__/executing.cpython-39.pyc,, +executing/__pycache__/version.cpython-39.pyc,, +executing/_exceptions.py,sha256=nf5P5jPnSjjo_8YWlh5AOyLZHF_hNyJpDv0OG2XFYgw,568 +executing/_position_node_finder.py,sha256=Itr5vfBTSKMXT36AybFvMoQAqxSoYjHLIveBXv9CS6Q,34701 +executing/_pytest_utils.py,sha256=NRj90nTcExS-8R2P8M1wYm9sodhrTlq74RSd4ZvjQRE,354 +executing/executing.py,sha256=J0mNe-49OGHG4Pe2WbkPm77F1u9qWDgCIkN_qZLPwmE,42226 +executing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +executing/version.py,sha256=m9ym-CJaJF3mnGNkOqNvAqYNkLuKxfEkjCkGbeFeIsQ,21 diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/REQUESTED b/lib/python3.10/site-packages/executing-2.2.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/WHEEL b/lib/python3.10/site-packages/executing-2.2.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..586ed37c553156cb4053308fd11fb4992e46261a --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (79.0.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/direct_url.json b/lib/python3.10/site-packages/executing-2.2.0.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..f6b0f53609813bc53f9dd1bca31424c8ea651e10 --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///home/conda/feedstock_root/build_artifacts/executing_1745502089858/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/executing-2.2.0.dist-info/top_level.txt b/lib/python3.10/site-packages/executing-2.2.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..a920f2c56c3cd68fce639efd047e869fe0736ba3 --- /dev/null +++ b/lib/python3.10/site-packages/executing-2.2.0.dist-info/top_level.txt @@ -0,0 +1 @@ +executing diff --git a/lib/python3.10/site-packages/executing/__init__.py b/lib/python3.10/site-packages/executing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e5181a5c326ce2267ebc5847ed809d0c0ede5865 --- /dev/null +++ b/lib/python3.10/site-packages/executing/__init__.py @@ -0,0 +1,28 @@ +""" +Get information about what a frame is currently doing. Typical usage: + + import executing + + node = executing.Source.executing(frame).node + # node will be an AST node or None +""" + +from collections import namedtuple +_VersionInfo = namedtuple('_VersionInfo', ('major', 'minor', 'micro')) +from .executing import Source, Executing, only, NotOneValueFound, cache, future_flags + +from ._pytest_utils import is_pytest_compatible + +try: + from .version import __version__ # type: ignore[import] + if "dev" in __version__: + raise ValueError +except Exception: + # version.py is auto-generated with the git tag when building + __version__ = "???" + __version_info__ = _VersionInfo(-1, -1, -1) +else: + __version_info__ = _VersionInfo(*map(int, __version__.split('.'))) + + +__all__ = ["Source","is_pytest_compatible"] diff --git a/lib/python3.10/site-packages/executing/_exceptions.py b/lib/python3.10/site-packages/executing/_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..1e88e3f29c9e16bc9776e47f5ff746e680ca5351 --- /dev/null +++ b/lib/python3.10/site-packages/executing/_exceptions.py @@ -0,0 +1,22 @@ + +class KnownIssue(Exception): + """ + Raised in case of an known problem. Mostly because of cpython bugs. + Executing.node gets set to None in this case. + """ + + pass + + +class VerifierFailure(Exception): + """ + Thrown for an unexpected mapping from instruction to ast node + Executing.node gets set to None in this case. + """ + + def __init__(self, title, node, instruction): + # type: (object, object, object) -> None + self.node = node + self.instruction = instruction + + super().__init__(title) # type: ignore[call-arg] diff --git a/lib/python3.10/site-packages/executing/_position_node_finder.py b/lib/python3.10/site-packages/executing/_position_node_finder.py new file mode 100644 index 0000000000000000000000000000000000000000..0f8344106f227711071b8f30cc679c06ee924bf7 --- /dev/null +++ b/lib/python3.10/site-packages/executing/_position_node_finder.py @@ -0,0 +1,992 @@ +import ast +import sys +import dis +from types import CodeType, FrameType +from typing import Any, Callable, Iterator, Optional, Sequence, Set, Tuple, Type, Union, cast +from .executing import EnhancedAST, NotOneValueFound, Source, only, function_node_types, assert_ +from ._exceptions import KnownIssue, VerifierFailure + +from functools import lru_cache + +# the code in this module can use all python>=3.11 features + + +def parents(node: EnhancedAST) -> Iterator[EnhancedAST]: + while True: + if hasattr(node, "parent"): + node = node.parent + yield node + else: + break # pragma: no mutate + + +def node_and_parents(node: EnhancedAST) -> Iterator[EnhancedAST]: + yield node + yield from parents(node) + + +def mangled_name(node: EnhancedAST) -> str: + """ + + Parameters: + node: the node which should be mangled + name: the name of the node + + Returns: + The mangled name of `node` + """ + if isinstance(node, ast.Attribute): + name = node.attr + elif isinstance(node, ast.Name): + name = node.id + elif isinstance(node, (ast.alias)): + name = node.asname or node.name.split(".")[0] + elif isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)): + name = node.name + elif isinstance(node, ast.ExceptHandler): + assert node.name + name = node.name + elif sys.version_info >= (3,12) and isinstance(node,ast.TypeVar): + name=node.name + else: + raise TypeError("no node to mangle for type "+repr(type(node))) + + if name.startswith("__") and not name.endswith("__"): + + parent,child=node.parent,node + + while not (isinstance(parent,ast.ClassDef) and child not in parent.bases): + if not hasattr(parent,"parent"): + break # pragma: no mutate + + parent,child=parent.parent,parent + else: + class_name=parent.name.lstrip("_") + if class_name!="": + return "_" + class_name + name + + + + return name + + +@lru_cache(128) # pragma: no mutate +def get_instructions(code: CodeType) -> list[dis.Instruction]: + return list(dis.get_instructions(code)) + + +types_cmp_issue_fix = ( + ast.IfExp, + ast.If, + ast.Assert, + ast.While, +) + +types_cmp_issue = types_cmp_issue_fix + ( + ast.ListComp, + ast.SetComp, + ast.DictComp, + ast.GeneratorExp, +) + +op_type_map = { + "**": ast.Pow, + "*": ast.Mult, + "@": ast.MatMult, + "//": ast.FloorDiv, + "/": ast.Div, + "%": ast.Mod, + "+": ast.Add, + "-": ast.Sub, + "<<": ast.LShift, + ">>": ast.RShift, + "&": ast.BitAnd, + "^": ast.BitXor, + "|": ast.BitOr, +} + + +class PositionNodeFinder(object): + """ + Mapping bytecode to ast-node based on the source positions, which where introduced in pyhon 3.11. + In general every ast-node can be exactly referenced by its begin/end line/col_offset, which is stored in the bytecode. + There are only some exceptions for methods and attributes. + """ + + def __init__(self, frame: FrameType, stmts: Set[EnhancedAST], tree: ast.Module, lasti: int, source: Source): + self.bc_dict={bc.offset:bc for bc in get_instructions(frame.f_code) } + + self.source = source + self.decorator: Optional[EnhancedAST] = None + + # work around for https://github.com/python/cpython/issues/96970 + while self.opname(lasti) == "CACHE": + lasti -= 2 + + try: + # try to map with all match_positions + self.result = self.find_node(lasti) + except NotOneValueFound: + typ: tuple[Type] + # LOAD_METHOD could load "".join for long "..."%(...) BinOps + # this can only be associated by using all positions + if self.opname(lasti) in ( + "LOAD_METHOD", + "LOAD_ATTR", + "STORE_ATTR", + "DELETE_ATTR", + ): + # lineno and col_offset of LOAD_METHOD and *_ATTR instructions get set to the beginning of + # the attribute by the python compiler to improved error messages (PEP-657) + # we ignore here the start position and try to find the ast-node just by end position and expected node type + # This is save, because there can only be one attribute ending at a specific point in the source code. + typ = (ast.Attribute,) + elif self.opname(lasti) in ("CALL", "CALL_KW"): + # A CALL instruction can be a method call, in which case the lineno and col_offset gets changed by the compiler. + # Therefore we ignoring here this attributes and searchnig for a Call-node only by end_col_offset and end_lineno. + # This is save, because there can only be one method ending at a specific point in the source code. + # One closing ) only belongs to one method. + typ = (ast.Call,) + else: + raise + + self.result = self.find_node( + lasti, + match_positions=("end_col_offset", "end_lineno"), + typ=typ, + ) + + instruction = self.instruction(lasti) + assert instruction is not None + + self.result = self.fix_result(self.result, instruction) + + self.known_issues(self.result, instruction) + + self.test_for_decorator(self.result, lasti) + + # verify + if self.decorator is None: + self.verify(self.result, instruction) + else: + assert_(self.decorator in self.result.decorator_list) + + def test_for_decorator(self, node: EnhancedAST, index: int) -> None: + if ( + isinstance(node.parent, (ast.ClassDef, function_node_types)) + and node in node.parent.decorator_list # type: ignore[attr-defined] + ): + node_func = node.parent + + while True: + # the generated bytecode looks like follow: + + # index opname + # ------------------ + # index-4 PRECALL (only in 3.11) + # index-2 CACHE + # index CALL <- the call instruction + # ... CACHE some CACHE instructions + + # maybe multiple other bytecode blocks for other decorators + # index-4 PRECALL (only in 3.11) + # index-2 CACHE + # index CALL <- index of the next loop + # ... CACHE some CACHE instructions + + # index+x STORE_* the ast-node of this instruction points to the decorated thing + + if not ( + (self.opname(index - 4) == "PRECALL" or sys.version_info >= (3, 12)) + and self.opname(index) == "CALL" + ): # pragma: no mutate + break # pragma: no mutate + + index += 2 + + while self.opname(index) in ("CACHE", "EXTENDED_ARG"): + index += 2 + + if ( + self.opname(index).startswith("STORE_") + and self.find_node(index) == node_func + ): + self.result = node_func + self.decorator = node + return + + if sys.version_info < (3, 12): + index += 4 + + def fix_result( + self, node: EnhancedAST, instruction: dis.Instruction + ) -> EnhancedAST: + if ( + sys.version_info >= (3, 12, 5) + and instruction.opname in ("GET_ITER", "FOR_ITER") + and isinstance(node.parent, ast.For) + and node is node.parent.iter + ): + # node positions have changed in 3.12.5 + # https://github.com/python/cpython/issues/93691 + # `for` calls __iter__ and __next__ during execution, the calling + # expression of these calls was the ast.For node since cpython 3.11 (see test_iter). + # cpython 3.12.5 changed this to the `iter` node of the loop, to make tracebacks easier to read. + # This keeps backward compatibility with older executing versions. + + # there are also cases like: + # + # for a in iter(l): pass + # + # where `iter(l)` would be otherwise the resulting node for the `iter()` call and the __iter__ call of the for implementation. + # keeping the old behaviour makes it possible to distinguish both cases. + + return node.parent + + if ( + sys.version_info >= (3, 12, 6) + and instruction.opname in ("GET_ITER", "FOR_ITER") + and isinstance( + node.parent.parent, + (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp), + ) + and isinstance(node.parent,ast.comprehension) + and node is node.parent.iter + ): + # same as above but only for comprehensions, see: + # https://github.com/python/cpython/issues/123142 + + return node.parent.parent + + if sys.version_info >= (3, 12,6) and instruction.opname == "CALL": + before = self.instruction_before(instruction) + if ( + before is not None + and before.opname == "LOAD_CONST" + and before.positions == instruction.positions + and isinstance(node.parent, ast.withitem) + and node is node.parent.context_expr + ): + # node positions for with-statements have change + # and is now equal to the expression which created the context-manager + # https://github.com/python/cpython/pull/120763 + + # with context_manager: + # ... + + # but there is one problem to distinguish call-expressions from __exit__() + + # with context_manager(): + # ... + + # the call for __exit__ + + # 20 1:5 1:22 LOAD_CONST(None) + # 22 1:5 1:22 LOAD_CONST(None) + # 24 1:5 1:22 LOAD_CONST(None) + # 26 1:5 1:22 CALL() # <-- same source range as context_manager() + + # but we can use the fact that the previous load for None + # has the same source range as the call, wich can not happen for normal calls + + # we return the same ast.With statement at the and to preserve backward compatibility + + return node.parent.parent + + if ( + sys.version_info >= (3, 12,6) + and instruction.opname == "BEFORE_WITH" + and isinstance(node.parent, ast.withitem) + and node is node.parent.context_expr + ): + # handle positions changes for __enter__ + return node.parent.parent + + return node + + def known_issues(self, node: EnhancedAST, instruction: dis.Instruction) -> None: + if instruction.opname in ("COMPARE_OP", "IS_OP", "CONTAINS_OP") and isinstance( + node, types_cmp_issue + ): + if isinstance(node, types_cmp_issue_fix): + # this is a workaround for https://github.com/python/cpython/issues/95921 + # we can fix cases with only on comparison inside the test condition + # + # we can not fix cases like: + # if a 1 + ] + + assert_(comparisons, "expected at least one comparison") + + if len(comparisons) == 1: + node = self.result = cast(EnhancedAST, comparisons[0]) + else: + raise KnownIssue( + "multiple chain comparison inside %s can not be fixed" % (node) + ) + + else: + # Comprehension and generators get not fixed for now. + raise KnownIssue("chain comparison inside %s can not be fixed" % (node)) + + if ( + sys.version_info[:3] == (3, 11, 1) + and isinstance(node, ast.Compare) + and instruction.opname == "CALL" + and any(isinstance(n, ast.Assert) for n in node_and_parents(node)) + ): + raise KnownIssue( + "known bug in 3.11.1 https://github.com/python/cpython/issues/95921" + ) + + if isinstance(node, ast.Assert): + # pytest assigns the position of the assertion to all expressions of the rewritten assertion. + # All the rewritten expressions get mapped to ast.Assert, which is the wrong ast-node. + # We don't report this wrong result. + raise KnownIssue("assert") + + if any(isinstance(n, ast.pattern) for n in node_and_parents(node)): + # TODO: investigate + raise KnownIssue("pattern matching ranges seems to be wrong") + + if ( + sys.version_info >= (3, 12) + and isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "super" + ): + # super is optimized to some instructions which do not map nicely to a Call + + # find the enclosing function + func = node.parent + while hasattr(func, "parent") and not isinstance( + func, (ast.AsyncFunctionDef, ast.FunctionDef) + ): + + func = func.parent + + # get the first function argument (self/cls) + first_arg = None + + if hasattr(func, "args"): + args = [*func.args.posonlyargs, *func.args.args] + if args: + first_arg = args[0].arg + + if (instruction.opname, instruction.argval) in [ + ("LOAD_DEREF", "__class__"), + ("LOAD_FAST", first_arg), + ("LOAD_DEREF", first_arg), + ]: + raise KnownIssue("super optimization") + + if self.is_except_cleanup(instruction, node): + raise KnownIssue("exeption cleanup does not belong to the last node in a except block") + + if instruction.opname == "STORE_NAME" and instruction.argval == "__classcell__": + # handle stores to __classcell__ as KnownIssue, + # because they get complicated if they are used in `if` or `for` loops + # example: + # + # class X: + # # ... something + # if some_condition: + # def method(self): + # super() + # + # The `STORE_NAME` instruction gets mapped to the `ast.If` node, + # because it is the last element in the class. + # This last element could be anything and gets dificult to verify. + + raise KnownIssue("store __classcell__") + + if ( + instruction.opname == "CALL" + and not isinstance(node,ast.Call) + and any(isinstance(p, ast.Assert) for p in parents(node)) + and sys.version_info >= (3, 11, 2) + ): + raise KnownIssue("exception generation maps to condition") + + if sys.version_info >= (3, 13): + if instruction.opname in ( + "STORE_FAST_STORE_FAST", + "STORE_FAST_LOAD_FAST", + "LOAD_FAST_LOAD_FAST", + ): + raise KnownIssue(f"can not map {instruction.opname} to two ast nodes") + + if instruction.opname == "LOAD_FAST" and instruction.argval == "__class__": + # example: + # class T: + # def a(): + # super() + # some_node # <- there is a LOAD_FAST for this node because we use super() + + raise KnownIssue( + f"loading of __class__ is accociated with a random node at the end of a class if you use super()" + ) + + if ( + instruction.opname == "COMPARE_OP" + and isinstance(node, ast.UnaryOp) + and isinstance(node.operand,ast.Compare) + and isinstance(node.op, ast.Not) + ): + # work around for + # https://github.com/python/cpython/issues/114671 + self.result = node.operand + + @staticmethod + def is_except_cleanup(inst: dis.Instruction, node: EnhancedAST) -> bool: + if inst.opname not in ( + "STORE_NAME", + "STORE_FAST", + "STORE_DEREF", + "STORE_GLOBAL", + "DELETE_NAME", + "DELETE_FAST", + "DELETE_DEREF", + "DELETE_GLOBAL", + ): + return False + + # This bytecode does something exception cleanup related. + # The position of the instruciton seems to be something in the last ast-node of the ExceptHandler + # this could be a bug, but it might not be observable in normal python code. + + # example: + # except Exception as exc: + # enum_member._value_ = value + + # other example: + # STORE_FAST of e was mapped to Constant(value=False) + # except OSError as e: + # if not _ignore_error(e): + # raise + # return False + + # STORE_FAST of msg was mapped to print(...) + # except TypeError as msg: + # print("Sorry:", msg, file=file) + + if ( + isinstance(node, ast.Name) + and isinstance(node.ctx,ast.Store) + and inst.opname.startswith("STORE_") + and mangled_name(node) == inst.argval + ): + # Storing the variable is valid and no exception cleanup, if the name is correct + return False + + if ( + isinstance(node, ast.Name) + and isinstance(node.ctx,ast.Del) + and inst.opname.startswith("DELETE_") + and mangled_name(node) == inst.argval + ): + # Deleting the variable is valid and no exception cleanup, if the name is correct + return False + + return any( + isinstance(n, ast.ExceptHandler) and n.name and mangled_name(n) == inst.argval + for n in parents(node) + ) + + def verify(self, node: EnhancedAST, instruction: dis.Instruction) -> None: + """ + checks if this node could gererate this instruction + """ + + op_name = instruction.opname + extra_filter: Callable[[EnhancedAST], bool] = lambda e: True + ctx: Type = type(None) + + def inst_match(opnames: Union[str, Sequence[str]], **kwargs: Any) -> bool: + """ + match instruction + + Parameters: + opnames: (str|Seq[str]): inst.opname has to be equal to or in `opname` + **kwargs: every arg has to match inst.arg + + Returns: + True if all conditions match the instruction + + """ + + if isinstance(opnames, str): + opnames = [opnames] + return instruction.opname in opnames and kwargs == { + k: getattr(instruction, k) for k in kwargs + } + + def node_match(node_type: Union[Type, Tuple[Type, ...]], **kwargs: Any) -> bool: + """ + match the ast-node + + Parameters: + node_type: type of the node + **kwargs: every `arg` has to be equal `node.arg` + or `node.arg` has to be an instance of `arg` if it is a type. + """ + return isinstance(node, node_type) and all( + isinstance(getattr(node, k), v) + if isinstance(v, type) + else getattr(node, k) == v + for k, v in kwargs.items() + ) + + if op_name == "CACHE": + return + + if inst_match("CALL") and node_match((ast.With, ast.AsyncWith)): + # call to context.__exit__ + return + + if inst_match(("CALL", "LOAD_FAST")) and node_match( + (ast.ListComp, ast.GeneratorExp, ast.SetComp, ast.DictComp) + ): + # call to the generator function + return + + if ( + sys.version_info >= (3, 12) + and inst_match(("LOAD_FAST_AND_CLEAR", "STORE_FAST")) + and node_match((ast.ListComp, ast.SetComp, ast.DictComp)) + ): + return + + if inst_match(("CALL", "CALL_FUNCTION_EX")) and node_match( + (ast.ClassDef, ast.Call) + ): + return + + if inst_match(("COMPARE_OP", "IS_OP", "CONTAINS_OP")) and node_match( + ast.Compare + ): + return + + if inst_match("LOAD_NAME", argval="__annotations__") and node_match( + ast.AnnAssign + ): + return + + if ( + ( + inst_match("LOAD_METHOD", argval="join") + or inst_match("LOAD_ATTR", argval="join") # 3.12 + or inst_match(("CALL", "BUILD_STRING")) + ) + and node_match(ast.BinOp, left=ast.Constant, op=ast.Mod) + and isinstance(cast(ast.Constant, cast(ast.BinOp, node).left).value, str) + ): + # "..."%(...) uses "".join + return + + if inst_match("STORE_SUBSCR") and node_match(ast.AnnAssign): + # data: int + return + + + if inst_match(("DELETE_NAME", "DELETE_FAST")) and node_match( + ast.Name, id=instruction.argval, ctx=ast.Del + ): + return + + if inst_match("BUILD_STRING") and ( + node_match(ast.JoinedStr) or node_match(ast.BinOp, op=ast.Mod) + ): + return + + if inst_match(("BEFORE_WITH","WITH_EXCEPT_START")) and node_match(ast.With): + return + + if inst_match(("STORE_NAME", "STORE_GLOBAL"), argval="__doc__") and node_match( + ast.Constant + ): + # store docstrings + return + + if ( + inst_match(("STORE_NAME", "STORE_FAST", "STORE_GLOBAL", "STORE_DEREF")) + and node_match(ast.ExceptHandler) + and instruction.argval == mangled_name(node) + ): + # store exception in variable + return + + if ( + inst_match(("STORE_NAME", "STORE_FAST", "STORE_DEREF", "STORE_GLOBAL")) + and node_match((ast.Import, ast.ImportFrom)) + and any(mangled_name(cast(EnhancedAST, alias)) == instruction.argval for alias in cast(ast.Import, node).names) + ): + # store imported module in variable + return + + if ( + inst_match(("STORE_FAST", "STORE_DEREF", "STORE_NAME", "STORE_GLOBAL")) + and ( + node_match((ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)) + or node_match( + ast.Name, + ctx=ast.Store, + ) + ) + and instruction.argval == mangled_name(node) + ): + return + + if False: + # TODO: match expressions are not supported for now + if inst_match(("STORE_FAST", "STORE_NAME")) and node_match( + ast.MatchAs, name=instruction.argval + ): + return + + if inst_match("COMPARE_OP", argval="==") and node_match(ast.MatchSequence): + return + + if inst_match("COMPARE_OP", argval="==") and node_match(ast.MatchValue): + return + + if inst_match("BINARY_OP") and node_match( + ast.AugAssign, op=op_type_map[instruction.argrepr.removesuffix("=")] + ): + # a+=5 + return + + if node_match(ast.Attribute, ctx=ast.Del) and inst_match( + "DELETE_ATTR", argval=mangled_name(node) + ): + return + + if inst_match( + ( + "JUMP_IF_TRUE_OR_POP", + "JUMP_IF_FALSE_OR_POP", + "POP_JUMP_IF_TRUE", + "POP_JUMP_IF_FALSE", + ) + ) and node_match(ast.BoolOp): + # and/or short circuit + return + + if inst_match("DELETE_SUBSCR") and node_match(ast.Subscript, ctx=ast.Del): + return + + if ( + node_match(ast.Name, ctx=ast.Load) + or ( + node_match(ast.Name, ctx=ast.Store) + and isinstance(node.parent, ast.AugAssign) + ) + ) and inst_match( + ( + "LOAD_NAME", + "LOAD_FAST", + "LOAD_FAST_CHECK", + "LOAD_GLOBAL", + "LOAD_DEREF", + "LOAD_FROM_DICT_OR_DEREF", + ), + argval=mangled_name(node), + ): + return + + if node_match(ast.Name, ctx=ast.Del) and inst_match( + ("DELETE_NAME", "DELETE_GLOBAL", "DELETE_DEREF"), argval=mangled_name(node) + ): + return + + if node_match(ast.Constant) and inst_match( + "LOAD_CONST", argval=cast(ast.Constant, node).value + ): + return + + if node_match( + (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp, ast.For) + ) and inst_match(("GET_ITER", "FOR_ITER")): + return + + if sys.version_info >= (3, 12): + if node_match(ast.UnaryOp, op=ast.UAdd) and inst_match( + "CALL_INTRINSIC_1", argrepr="INTRINSIC_UNARY_POSITIVE" + ): + return + + if node_match(ast.Subscript) and inst_match("BINARY_SLICE"): + return + + if node_match(ast.ImportFrom) and inst_match( + "CALL_INTRINSIC_1", argrepr="INTRINSIC_IMPORT_STAR" + ): + return + + if ( + node_match(ast.Yield) or isinstance(node.parent, ast.GeneratorExp) + ) and inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_ASYNC_GEN_WRAP"): + return + + if node_match(ast.Name) and inst_match("LOAD_DEREF",argval="__classdict__"): + return + + if node_match(ast.TypeVar) and ( + inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEVAR") + or inst_match( + "CALL_INTRINSIC_2", argrepr="INTRINSIC_TYPEVAR_WITH_BOUND" + ) + or inst_match( + "CALL_INTRINSIC_2", argrepr="INTRINSIC_TYPEVAR_WITH_CONSTRAINTS" + ) + or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=mangled_name(node)) + ): + return + + if node_match(ast.TypeVarTuple) and ( + inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEVARTUPLE") + or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=node.name) + ): + return + + if node_match(ast.ParamSpec) and ( + inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_PARAMSPEC") + + or inst_match(("STORE_FAST", "STORE_DEREF"), argrepr=node.name)): + return + + + if node_match(ast.TypeAlias): + if( + inst_match("CALL_INTRINSIC_1", argrepr="INTRINSIC_TYPEALIAS") + or inst_match( + ("STORE_NAME", "STORE_FAST", "STORE_DEREF"), argrepr=node.name.id + ) + or inst_match("CALL") + ): + return + + + if node_match(ast.ClassDef) and node.type_params: + if inst_match( + ("STORE_DEREF", "LOAD_DEREF", "LOAD_FROM_DICT_OR_DEREF"), + argrepr=".type_params", + ): + return + + if inst_match(("STORE_FAST", "LOAD_FAST"), argrepr=".generic_base"): + return + + if inst_match( + "CALL_INTRINSIC_1", argrepr="INTRINSIC_SUBSCRIPT_GENERIC" + ): + return + + if inst_match("LOAD_DEREF",argval="__classdict__"): + return + + if node_match((ast.FunctionDef,ast.AsyncFunctionDef)) and node.type_params: + if inst_match("CALL"): + return + + if inst_match( + "CALL_INTRINSIC_2", argrepr="INTRINSIC_SET_FUNCTION_TYPE_PARAMS" + ): + return + + if inst_match("LOAD_FAST",argval=".defaults"): + return + + if inst_match("LOAD_FAST",argval=".kwdefaults"): + return + + if inst_match("STORE_NAME", argval="__classdictcell__"): + # this is a general thing + return + + + # f-strings + + if node_match(ast.JoinedStr) and ( + inst_match("LOAD_ATTR", argval="join") + or inst_match(("LIST_APPEND", "CALL")) + ): + return + + if node_match(ast.FormattedValue) and inst_match("FORMAT_VALUE"): + return + + if sys.version_info >= (3, 13): + + if inst_match("NOP"): + return + + if inst_match("TO_BOOL") and node_match(ast.BoolOp): + return + + if inst_match("CALL_KW") and node_match((ast.Call, ast.ClassDef)): + return + + if inst_match("LOAD_FAST", argval=".type_params"): + return + + if inst_match("LOAD_FAST", argval="__classdict__"): + return + + if inst_match("LOAD_FAST") and node_match( + ( + ast.FunctionDef, + ast.ClassDef, + ast.TypeAlias, + ast.TypeVar, + ast.Lambda, + ast.AsyncFunctionDef, + ) + ): + # These are loads for closure variables. + # It is difficult to check that this is actually closure variable, see: + # https://github.com/alexmojaki/executing/pull/80#discussion_r1716027317 + return + + if ( + inst_match("LOAD_FAST") + and node_match(ast.TypeAlias) + and node.name.id == instruction.argval + ): + return + + if inst_match("STORE_NAME",argval="__static_attributes__"): + # the node is the first node in the body + return + + if inst_match("LOAD_FAST") and isinstance(node.parent,ast.TypeVar): + return + + + # old verifier + + typ: Type = type(None) + op_type: Type = type(None) + + if op_name.startswith(("BINARY_SUBSCR", "SLICE+")): + typ = ast.Subscript + ctx = ast.Load + elif op_name.startswith("BINARY_"): + typ = ast.BinOp + op_type = op_type_map[instruction.argrepr] + extra_filter = lambda e: isinstance(cast(ast.BinOp, e).op, op_type) + elif op_name.startswith("UNARY_"): + typ = ast.UnaryOp + op_type = dict( + UNARY_POSITIVE=ast.UAdd, + UNARY_NEGATIVE=ast.USub, + UNARY_NOT=ast.Not, + UNARY_INVERT=ast.Invert, + )[op_name] + extra_filter = lambda e: isinstance(cast(ast.UnaryOp, e).op, op_type) + elif op_name in ("LOAD_ATTR", "LOAD_METHOD", "LOOKUP_METHOD","LOAD_SUPER_ATTR"): + typ = ast.Attribute + ctx = ast.Load + extra_filter = lambda e: mangled_name(e) == instruction.argval + elif op_name in ( + "LOAD_NAME", + "LOAD_GLOBAL", + "LOAD_FAST", + "LOAD_DEREF", + "LOAD_CLASSDEREF", + ): + typ = ast.Name + ctx = ast.Load + extra_filter = lambda e: cast(ast.Name, e).id == instruction.argval + elif op_name in ("COMPARE_OP", "IS_OP", "CONTAINS_OP"): + typ = ast.Compare + extra_filter = lambda e: len(cast(ast.Compare, e).ops) == 1 + elif op_name.startswith(("STORE_SLICE", "STORE_SUBSCR")): + ctx = ast.Store + typ = ast.Subscript + elif op_name.startswith("STORE_ATTR"): + ctx = ast.Store + typ = ast.Attribute + extra_filter = lambda e: mangled_name(e) == instruction.argval + + node_ctx = getattr(node, "ctx", None) + + ctx_match = ( + ctx is not type(None) + or not hasattr(node, "ctx") + or isinstance(node_ctx, ctx) + ) + + # check for old verifier + if isinstance(node, typ) and ctx_match and extra_filter(node): + return + + # generate error + + title = "ast.%s is not created from %s" % ( + type(node).__name__, + instruction.opname, + ) + + raise VerifierFailure(title, node, instruction) + + def instruction(self, index: int) -> Optional[dis.Instruction]: + return self.bc_dict.get(index,None) + + def instruction_before( + self, instruction: dis.Instruction + ) -> Optional[dis.Instruction]: + return self.bc_dict.get(instruction.offset - 2, None) + + def opname(self, index: int) -> str: + i=self.instruction(index) + if i is None: + return "CACHE" + return i.opname + + extra_node_types=() + if sys.version_info >= (3,12): + extra_node_types = (ast.type_param,) + + def find_node( + self, + index: int, + match_positions: Sequence[str] = ( + "lineno", + "end_lineno", + "col_offset", + "end_col_offset", + ), + typ: tuple[Type, ...] = ( + ast.expr, + ast.stmt, + ast.excepthandler, + ast.pattern, + *extra_node_types, + ), + ) -> EnhancedAST: + instruction = self.instruction(index) + assert instruction is not None + + position = instruction.positions + assert position is not None and position.lineno is not None + + return only( + cast(EnhancedAST, node) + for node in self.source._nodes_by_line[position.lineno] + if isinstance(node, typ) + if not isinstance(node, ast.Expr) + # matchvalue.value has the same positions as matchvalue themself, so we exclude ast.MatchValue + if not isinstance(node, ast.MatchValue) + if all( + getattr(position, attr) == getattr(node, attr) + for attr in match_positions + ) + ) diff --git a/lib/python3.10/site-packages/executing/_pytest_utils.py b/lib/python3.10/site-packages/executing/_pytest_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fab8693baf20570fd34b8d4bcb9242ba8878b8fb --- /dev/null +++ b/lib/python3.10/site-packages/executing/_pytest_utils.py @@ -0,0 +1,16 @@ +import sys + + + +def is_pytest_compatible() -> bool: + """ returns true if executing can be used for expressions inside assert statements which are rewritten by pytest + """ + if sys.version_info < (3, 11): + return False + + try: + import pytest + except ImportError: + return False + + return pytest.version_tuple >= (8, 3, 4) diff --git a/lib/python3.10/site-packages/executing/executing.py b/lib/python3.10/site-packages/executing/executing.py new file mode 100644 index 0000000000000000000000000000000000000000..5cf117e18c55d24e989e5e7cace3bf09ce23744f --- /dev/null +++ b/lib/python3.10/site-packages/executing/executing.py @@ -0,0 +1,1160 @@ +""" +MIT License + +Copyright (c) 2021 Alex Hall + +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. +""" + +import __future__ +import ast +import dis +import inspect +import io +import linecache +import re +import sys +import types +from collections import defaultdict +from copy import deepcopy +from functools import lru_cache +from itertools import islice +from itertools import zip_longest +from operator import attrgetter +from pathlib import Path +from threading import RLock +from tokenize import detect_encoding +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Sized, Tuple, \ + Type, TypeVar, Union, cast + +if TYPE_CHECKING: # pragma: no cover + from asttokens import ASTTokens, ASTText + from asttokens.asttokens import ASTTextBase + + +function_node_types = (ast.FunctionDef, ast.AsyncFunctionDef) # type: Tuple[Type, ...] + +cache = lru_cache(maxsize=None) + +# Type class used to expand out the definition of AST to include fields added by this library +# It's not actually used for anything other than type checking though! +class EnhancedAST(ast.AST): + parent = None # type: EnhancedAST + + +class Instruction(dis.Instruction): + lineno = None # type: int + + +# Type class used to expand out the definition of AST to include fields added by this library +# It's not actually used for anything other than type checking though! +class EnhancedInstruction(Instruction): + _copied = None # type: bool + + + +def assert_(condition, message=""): + # type: (Any, str) -> None + """ + Like an assert statement, but unaffected by -O + :param condition: value that is expected to be truthy + :type message: Any + """ + if not condition: + raise AssertionError(str(message)) + + +def get_instructions(co): + # type: (types.CodeType) -> Iterator[EnhancedInstruction] + lineno = co.co_firstlineno + for inst in dis.get_instructions(co): + inst = cast(EnhancedInstruction, inst) + lineno = inst.starts_line or lineno + assert_(lineno) + inst.lineno = lineno + yield inst + + +TESTING = 0 + + +class NotOneValueFound(Exception): + def __init__(self,msg,values=[]): + # type: (str, Sequence) -> None + self.values=values + super(NotOneValueFound,self).__init__(msg) + +T = TypeVar('T') + + +def only(it): + # type: (Iterable[T]) -> T + if isinstance(it, Sized): + if len(it) != 1: + raise NotOneValueFound('Expected one value, found %s' % len(it)) + # noinspection PyTypeChecker + return list(it)[0] + + lst = tuple(islice(it, 2)) + if len(lst) == 0: + raise NotOneValueFound('Expected one value, found 0') + if len(lst) > 1: + raise NotOneValueFound('Expected one value, found several',lst) + return lst[0] + + +class Source(object): + """ + The source code of a single file and associated metadata. + + The main method of interest is the classmethod `executing(frame)`. + + If you want an instance of this class, don't construct it. + Ideally use the classmethod `for_frame(frame)`. + If you don't have a frame, use `for_filename(filename [, module_globals])`. + These methods cache instances by filename, so at most one instance exists per filename. + + Attributes: + - filename + - text + - lines + - tree: AST parsed from text, or None if text is not valid Python + All nodes in the tree have an extra `parent` attribute + + Other methods of interest: + - statements_at_line + - asttokens + - code_qualname + """ + + def __init__(self, filename, lines): + # type: (str, Sequence[str]) -> None + """ + Don't call this constructor, see the class docstring. + """ + + self.filename = filename + self.text = ''.join(lines) + self.lines = [line.rstrip('\r\n') for line in lines] + + self._nodes_by_line = defaultdict(list) + self.tree = None + self._qualnames = {} + self._asttokens = None # type: Optional[ASTTokens] + self._asttext = None # type: Optional[ASTText] + + try: + self.tree = ast.parse(self.text, filename=filename) + except (SyntaxError, ValueError): + pass + else: + for node in ast.walk(self.tree): + for child in ast.iter_child_nodes(node): + cast(EnhancedAST, child).parent = cast(EnhancedAST, node) + for lineno in node_linenos(node): + self._nodes_by_line[lineno].append(node) + + visitor = QualnameVisitor() + visitor.visit(self.tree) + self._qualnames = visitor.qualnames + + @classmethod + def for_frame(cls, frame, use_cache=True): + # type: (types.FrameType, bool) -> "Source" + """ + Returns the `Source` object corresponding to the file the frame is executing in. + """ + return cls.for_filename(frame.f_code.co_filename, frame.f_globals or {}, use_cache) + + @classmethod + def for_filename( + cls, + filename, + module_globals=None, + use_cache=True, # noqa no longer used + ): + # type: (Union[str, Path], Optional[Dict[str, Any]], bool) -> "Source" + if isinstance(filename, Path): + filename = str(filename) + + def get_lines(): + # type: () -> List[str] + return linecache.getlines(cast(str, filename), module_globals) + + # Save the current linecache entry, then ensure the cache is up to date. + entry = linecache.cache.get(filename) # type: ignore[attr-defined] + linecache.checkcache(filename) + lines = get_lines() + if entry is not None and not lines: + # There was an entry, checkcache removed it, and nothing replaced it. + # This means the file wasn't simply changed (because the `lines` wouldn't be empty) + # but rather the file was found not to exist, probably because `filename` was fake. + # Restore the original entry so that we still have something. + linecache.cache[filename] = entry # type: ignore[attr-defined] + lines = get_lines() + + return cls._for_filename_and_lines(filename, tuple(lines)) + + @classmethod + def _for_filename_and_lines(cls, filename, lines): + # type: (str, Sequence[str]) -> "Source" + source_cache = cls._class_local('__source_cache_with_lines', {}) # type: Dict[Tuple[str, Sequence[str]], Source] + try: + return source_cache[(filename, lines)] + except KeyError: + pass + + result = source_cache[(filename, lines)] = cls(filename, lines) + return result + + @classmethod + def lazycache(cls, frame): + # type: (types.FrameType) -> None + linecache.lazycache(frame.f_code.co_filename, frame.f_globals) + + @classmethod + def executing(cls, frame_or_tb): + # type: (Union[types.TracebackType, types.FrameType]) -> "Executing" + """ + Returns an `Executing` object representing the operation + currently executing in the given frame or traceback object. + """ + if isinstance(frame_or_tb, types.TracebackType): + # https://docs.python.org/3/reference/datamodel.html#traceback-objects + # "tb_lineno gives the line number where the exception occurred; + # tb_lasti indicates the precise instruction. + # The line number and last instruction in the traceback may differ + # from the line number of its frame object + # if the exception occurred in a try statement with no matching except clause + # or with a finally clause." + tb = frame_or_tb + frame = tb.tb_frame + lineno = tb.tb_lineno + lasti = tb.tb_lasti + else: + frame = frame_or_tb + lineno = frame.f_lineno + lasti = frame.f_lasti + + + + code = frame.f_code + key = (code, id(code), lasti) + executing_cache = cls._class_local('__executing_cache', {}) # type: Dict[Tuple[types.CodeType, int, int], Any] + + args = executing_cache.get(key) + if not args: + node = stmts = decorator = None + source = cls.for_frame(frame) + tree = source.tree + if tree: + try: + stmts = source.statements_at_line(lineno) + if stmts: + if is_ipython_cell_code(code): + decorator, node = find_node_ipython(frame, lasti, stmts, source) + else: + node_finder = NodeFinder(frame, stmts, tree, lasti, source) + node = node_finder.result + decorator = node_finder.decorator + + if node: + new_stmts = {statement_containing_node(node)} + assert_(new_stmts <= stmts) + stmts = new_stmts + except Exception: + if TESTING: + raise + + executing_cache[key] = args = source, node, stmts, decorator + + return Executing(frame, *args) + + @classmethod + def _class_local(cls, name, default): + # type: (str, T) -> T + """ + Returns an attribute directly associated with this class + (as opposed to subclasses), setting default if necessary + """ + # classes have a mappingproxy preventing us from using setdefault + result = cls.__dict__.get(name, default) + setattr(cls, name, result) + return result + + @cache + def statements_at_line(self, lineno): + # type: (int) -> Set[EnhancedAST] + """ + Returns the statement nodes overlapping the given line. + + Returns at most one statement unless semicolons are present. + + If the `text` attribute is not valid python, meaning + `tree` is None, returns an empty set. + + Otherwise, `Source.for_frame(frame).statements_at_line(frame.f_lineno)` + should return at least one statement. + """ + + return { + statement_containing_node(node) + for node in + self._nodes_by_line[lineno] + } + + def asttext(self): + # type: () -> ASTText + """ + Returns an ASTText object for getting the source of specific AST nodes. + + See http://asttokens.readthedocs.io/en/latest/api-index.html + """ + from asttokens import ASTText # must be installed separately + + if self._asttext is None: + self._asttext = ASTText(self.text, tree=self.tree, filename=self.filename) + + return self._asttext + + def asttokens(self): + # type: () -> ASTTokens + """ + Returns an ASTTokens object for getting the source of specific AST nodes. + + See http://asttokens.readthedocs.io/en/latest/api-index.html + """ + import asttokens # must be installed separately + + if self._asttokens is None: + if hasattr(asttokens, 'ASTText'): + self._asttokens = self.asttext().asttokens + else: # pragma: no cover + self._asttokens = asttokens.ASTTokens(self.text, tree=self.tree, filename=self.filename) + return self._asttokens + + def _asttext_base(self): + # type: () -> ASTTextBase + import asttokens # must be installed separately + + if hasattr(asttokens, 'ASTText'): + return self.asttext() + else: # pragma: no cover + return self.asttokens() + + @staticmethod + def decode_source(source): + # type: (Union[str, bytes]) -> str + if isinstance(source, bytes): + encoding = Source.detect_encoding(source) + return source.decode(encoding) + else: + return source + + @staticmethod + def detect_encoding(source): + # type: (bytes) -> str + return detect_encoding(io.BytesIO(source).readline)[0] + + def code_qualname(self, code): + # type: (types.CodeType) -> str + """ + Imitates the __qualname__ attribute of functions for code objects. + Given: + + - A function `func` + - A frame `frame` for an execution of `func`, meaning: + `frame.f_code is func.__code__` + + `Source.for_frame(frame).code_qualname(frame.f_code)` + will be equal to `func.__qualname__`*. Works for Python 2 as well, + where of course no `__qualname__` attribute exists. + + Falls back to `code.co_name` if there is no appropriate qualname. + + Based on https://github.com/wbolster/qualname + + (* unless `func` is a lambda + nested inside another lambda on the same line, in which case + the outer lambda's qualname will be returned for the codes + of both lambdas) + """ + assert_(code.co_filename == self.filename) + return self._qualnames.get((code.co_name, code.co_firstlineno), code.co_name) + + +class Executing(object): + """ + Information about the operation a frame is currently executing. + + Generally you will just want `node`, which is the AST node being executed, + or None if it's unknown. + + If a decorator is currently being called, then: + - `node` is a function or class definition + - `decorator` is the expression in `node.decorator_list` being called + - `statements == {node}` + """ + + def __init__(self, frame, source, node, stmts, decorator): + # type: (types.FrameType, Source, EnhancedAST, Set[ast.stmt], Optional[EnhancedAST]) -> None + self.frame = frame + self.source = source + self.node = node + self.statements = stmts + self.decorator = decorator + + def code_qualname(self): + # type: () -> str + return self.source.code_qualname(self.frame.f_code) + + def text(self): + # type: () -> str + return self.source._asttext_base().get_text(self.node) + + def text_range(self): + # type: () -> Tuple[int, int] + return self.source._asttext_base().get_text_range(self.node) + + +class QualnameVisitor(ast.NodeVisitor): + def __init__(self): + # type: () -> None + super(QualnameVisitor, self).__init__() + self.stack = [] # type: List[str] + self.qualnames = {} # type: Dict[Tuple[str, int], str] + + def add_qualname(self, node, name=None): + # type: (ast.AST, Optional[str]) -> None + name = name or node.name # type: ignore[attr-defined] + self.stack.append(name) + if getattr(node, 'decorator_list', ()): + lineno = node.decorator_list[0].lineno # type: ignore[attr-defined] + else: + lineno = node.lineno # type: ignore[attr-defined] + self.qualnames.setdefault((name, lineno), ".".join(self.stack)) + + def visit_FunctionDef(self, node, name=None): + # type: (ast.AST, Optional[str]) -> None + assert isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)), node + self.add_qualname(node, name) + self.stack.append('') + children = [] # type: Sequence[ast.AST] + if isinstance(node, ast.Lambda): + children = [node.body] + else: + children = node.body + for child in children: + self.visit(child) + self.stack.pop() + self.stack.pop() + + # Find lambdas in the function definition outside the body, + # e.g. decorators or default arguments + # Based on iter_child_nodes + for field, child in ast.iter_fields(node): + if field == 'body': + continue + if isinstance(child, ast.AST): + self.visit(child) + elif isinstance(child, list): + for grandchild in child: + if isinstance(grandchild, ast.AST): + self.visit(grandchild) + + visit_AsyncFunctionDef = visit_FunctionDef + + def visit_Lambda(self, node): + # type: (ast.AST) -> None + assert isinstance(node, ast.Lambda) + self.visit_FunctionDef(node, '') + + def visit_ClassDef(self, node): + # type: (ast.AST) -> None + assert isinstance(node, ast.ClassDef) + self.add_qualname(node) + self.generic_visit(node) + self.stack.pop() + + + + + +future_flags = sum( + getattr(__future__, fname).compiler_flag for fname in __future__.all_feature_names +) + + +def compile_similar_to(source, matching_code): + # type: (ast.Module, types.CodeType) -> Any + return compile( + source, + matching_code.co_filename, + 'exec', + flags=future_flags & matching_code.co_flags, + dont_inherit=True, + ) + + +sentinel = 'io8urthglkjdghvljusketgIYRFYUVGHFRTBGVHKGF78678957647698' + +def is_rewritten_by_pytest(code): + # type: (types.CodeType) -> bool + return any( + bc.opname != "LOAD_CONST" and isinstance(bc.argval,str) and bc.argval.startswith("@py") + for bc in get_instructions(code) + ) + + +class SentinelNodeFinder(object): + result = None # type: EnhancedAST + + def __init__(self, frame, stmts, tree, lasti, source): + # type: (types.FrameType, Set[EnhancedAST], ast.Module, int, Source) -> None + assert_(stmts) + self.frame = frame + self.tree = tree + self.code = code = frame.f_code + self.is_pytest = is_rewritten_by_pytest(code) + + if self.is_pytest: + self.ignore_linenos = frozenset(assert_linenos(tree)) + else: + self.ignore_linenos = frozenset() + + self.decorator = None + + self.instruction = instruction = self.get_actual_current_instruction(lasti) + op_name = instruction.opname + extra_filter = lambda e: True # type: Callable[[Any], bool] + ctx = type(None) # type: Type + + typ = type(None) # type: Type + if op_name.startswith('CALL_'): + typ = ast.Call + elif op_name.startswith(('BINARY_SUBSCR', 'SLICE+')): + typ = ast.Subscript + ctx = ast.Load + elif op_name.startswith('BINARY_'): + typ = ast.BinOp + op_type = dict( + BINARY_POWER=ast.Pow, + BINARY_MULTIPLY=ast.Mult, + BINARY_MATRIX_MULTIPLY=getattr(ast, "MatMult", ()), + BINARY_FLOOR_DIVIDE=ast.FloorDiv, + BINARY_TRUE_DIVIDE=ast.Div, + BINARY_MODULO=ast.Mod, + BINARY_ADD=ast.Add, + BINARY_SUBTRACT=ast.Sub, + BINARY_LSHIFT=ast.LShift, + BINARY_RSHIFT=ast.RShift, + BINARY_AND=ast.BitAnd, + BINARY_XOR=ast.BitXor, + BINARY_OR=ast.BitOr, + )[op_name] + extra_filter = lambda e: isinstance(e.op, op_type) + elif op_name.startswith('UNARY_'): + typ = ast.UnaryOp + op_type = dict( + UNARY_POSITIVE=ast.UAdd, + UNARY_NEGATIVE=ast.USub, + UNARY_NOT=ast.Not, + UNARY_INVERT=ast.Invert, + )[op_name] + extra_filter = lambda e: isinstance(e.op, op_type) + elif op_name in ('LOAD_ATTR', 'LOAD_METHOD', 'LOOKUP_METHOD'): + typ = ast.Attribute + ctx = ast.Load + extra_filter = lambda e: attr_names_match(e.attr, instruction.argval) + elif op_name in ('LOAD_NAME', 'LOAD_GLOBAL', 'LOAD_FAST', 'LOAD_DEREF', 'LOAD_CLASSDEREF'): + typ = ast.Name + ctx = ast.Load + extra_filter = lambda e: e.id == instruction.argval + elif op_name in ('COMPARE_OP', 'IS_OP', 'CONTAINS_OP'): + typ = ast.Compare + extra_filter = lambda e: len(e.ops) == 1 + elif op_name.startswith(('STORE_SLICE', 'STORE_SUBSCR')): + ctx = ast.Store + typ = ast.Subscript + elif op_name.startswith('STORE_ATTR'): + ctx = ast.Store + typ = ast.Attribute + extra_filter = lambda e: attr_names_match(e.attr, instruction.argval) + else: + raise RuntimeError(op_name) + + with lock: + exprs = { + cast(EnhancedAST, node) + for stmt in stmts + for node in ast.walk(stmt) + if isinstance(node, typ) + if isinstance(getattr(node, "ctx", None), ctx) + if extra_filter(node) + if statement_containing_node(node) == stmt + } + + if ctx == ast.Store: + # No special bytecode tricks here. + # We can handle multiple assigned attributes with different names, + # but only one assigned subscript. + self.result = only(exprs) + return + + matching = list(self.matching_nodes(exprs)) + if not matching and typ == ast.Call: + self.find_decorator(stmts) + else: + self.result = only(matching) + + def find_decorator(self, stmts): + # type: (Union[List[EnhancedAST], Set[EnhancedAST]]) -> None + stmt = only(stmts) + assert_(isinstance(stmt, (ast.ClassDef, function_node_types))) + decorators = stmt.decorator_list # type: ignore[attr-defined] + assert_(decorators) + line_instructions = [ + inst + for inst in self.clean_instructions(self.code) + if inst.lineno == self.frame.f_lineno + ] + last_decorator_instruction_index = [ + i + for i, inst in enumerate(line_instructions) + if inst.opname == "CALL_FUNCTION" + ][-1] + assert_( + line_instructions[last_decorator_instruction_index + 1].opname.startswith( + "STORE_" + ) + ) + decorator_instructions = line_instructions[ + last_decorator_instruction_index + - len(decorators) + + 1 : last_decorator_instruction_index + + 1 + ] + assert_({inst.opname for inst in decorator_instructions} == {"CALL_FUNCTION"}) + decorator_index = decorator_instructions.index(self.instruction) + decorator = decorators[::-1][decorator_index] + self.decorator = decorator + self.result = stmt + + def clean_instructions(self, code): + # type: (types.CodeType) -> List[EnhancedInstruction] + return [ + inst + for inst in get_instructions(code) + if inst.opname not in ("EXTENDED_ARG", "NOP") + if inst.lineno not in self.ignore_linenos + ] + + def get_original_clean_instructions(self): + # type: () -> List[EnhancedInstruction] + result = self.clean_instructions(self.code) + + # pypy sometimes (when is not clear) + # inserts JUMP_IF_NOT_DEBUG instructions in bytecode + # If they're not present in our compiled instructions, + # ignore them in the original bytecode + if not any( + inst.opname == "JUMP_IF_NOT_DEBUG" + for inst in self.compile_instructions() + ): + result = [ + inst for inst in result + if inst.opname != "JUMP_IF_NOT_DEBUG" + ] + + return result + + def matching_nodes(self, exprs): + # type: (Set[EnhancedAST]) -> Iterator[EnhancedAST] + original_instructions = self.get_original_clean_instructions() + original_index = only( + i + for i, inst in enumerate(original_instructions) + if inst == self.instruction + ) + for expr_index, expr in enumerate(exprs): + setter = get_setter(expr) + assert setter is not None + # noinspection PyArgumentList + replacement = ast.BinOp( + left=expr, + op=ast.Pow(), + right=ast.Str(s=sentinel), + ) + ast.fix_missing_locations(replacement) + setter(replacement) + try: + instructions = self.compile_instructions() + finally: + setter(expr) + + if sys.version_info >= (3, 10): + try: + handle_jumps(instructions, original_instructions) + except Exception: + # Give other candidates a chance + if TESTING or expr_index < len(exprs) - 1: + continue + raise + + indices = [ + i + for i, instruction in enumerate(instructions) + if instruction.argval == sentinel + ] + + # There can be several indices when the bytecode is duplicated, + # as happens in a finally block in 3.9+ + # First we remove the opcodes caused by our modifications + for index_num, sentinel_index in enumerate(indices): + # Adjustment for removing sentinel instructions below + # in past iterations + sentinel_index -= index_num * 2 + + assert_(instructions.pop(sentinel_index).opname == 'LOAD_CONST') + assert_(instructions.pop(sentinel_index).opname == 'BINARY_POWER') + + # Then we see if any of the instruction indices match + for index_num, sentinel_index in enumerate(indices): + sentinel_index -= index_num * 2 + new_index = sentinel_index - 1 + + if new_index != original_index: + continue + + original_inst = original_instructions[original_index] + new_inst = instructions[new_index] + + # In Python 3.9+, changing 'not x in y' to 'not sentinel_transformation(x in y)' + # changes a CONTAINS_OP(invert=1) to CONTAINS_OP(invert=0),,UNARY_NOT + if ( + original_inst.opname == new_inst.opname in ('CONTAINS_OP', 'IS_OP') + and original_inst.arg != new_inst.arg # type: ignore[attr-defined] + and ( + original_instructions[original_index + 1].opname + != instructions[new_index + 1].opname == 'UNARY_NOT' + )): + # Remove the difference for the upcoming assert + instructions.pop(new_index + 1) + + # Check that the modified instructions don't have anything unexpected + # 3.10 is a bit too weird to assert this in all cases but things still work + if sys.version_info < (3, 10): + for inst1, inst2 in zip_longest( + original_instructions, instructions + ): + assert_(inst1 and inst2 and opnames_match(inst1, inst2)) + + yield expr + + def compile_instructions(self): + # type: () -> List[EnhancedInstruction] + module_code = compile_similar_to(self.tree, self.code) + code = only(self.find_codes(module_code)) + return self.clean_instructions(code) + + def find_codes(self, root_code): + # type: (types.CodeType) -> list + checks = [ + attrgetter('co_firstlineno'), + attrgetter('co_freevars'), + attrgetter('co_cellvars'), + lambda c: is_ipython_cell_code_name(c.co_name) or c.co_name, + ] # type: List[Callable] + if not self.is_pytest: + checks += [ + attrgetter('co_names'), + attrgetter('co_varnames'), + ] + + def matches(c): + # type: (types.CodeType) -> bool + return all( + f(c) == f(self.code) + for f in checks + ) + + code_options = [] + if matches(root_code): + code_options.append(root_code) + + def finder(code): + # type: (types.CodeType) -> None + for const in code.co_consts: + if not inspect.iscode(const): + continue + + if matches(const): + code_options.append(const) + finder(const) + + finder(root_code) + return code_options + + def get_actual_current_instruction(self, lasti): + # type: (int) -> EnhancedInstruction + """ + Get the instruction corresponding to the current + frame offset, skipping EXTENDED_ARG instructions + """ + # Don't use get_original_clean_instructions + # because we need the actual instructions including + # EXTENDED_ARG + instructions = list(get_instructions(self.code)) + index = only( + i + for i, inst in enumerate(instructions) + if inst.offset == lasti + ) + + while True: + instruction = instructions[index] + if instruction.opname != "EXTENDED_ARG": + return instruction + index += 1 + + + +def non_sentinel_instructions(instructions, start): + # type: (List[EnhancedInstruction], int) -> Iterator[Tuple[int, EnhancedInstruction]] + """ + Yields (index, instruction) pairs excluding the basic + instructions introduced by the sentinel transformation + """ + skip_power = False + for i, inst in islice(enumerate(instructions), start, None): + if inst.argval == sentinel: + assert_(inst.opname == "LOAD_CONST") + skip_power = True + continue + elif skip_power: + assert_(inst.opname == "BINARY_POWER") + skip_power = False + continue + yield i, inst + + +def walk_both_instructions(original_instructions, original_start, instructions, start): + # type: (List[EnhancedInstruction], int, List[EnhancedInstruction], int) -> Iterator[Tuple[int, EnhancedInstruction, int, EnhancedInstruction]] + """ + Yields matching indices and instructions from the new and original instructions, + leaving out changes made by the sentinel transformation. + """ + original_iter = islice(enumerate(original_instructions), original_start, None) + new_iter = non_sentinel_instructions(instructions, start) + inverted_comparison = False + while True: + try: + original_i, original_inst = next(original_iter) + new_i, new_inst = next(new_iter) + except StopIteration: + return + if ( + inverted_comparison + and original_inst.opname != new_inst.opname == "UNARY_NOT" + ): + new_i, new_inst = next(new_iter) + inverted_comparison = ( + original_inst.opname == new_inst.opname in ("CONTAINS_OP", "IS_OP") + and original_inst.arg != new_inst.arg # type: ignore[attr-defined] + ) + yield original_i, original_inst, new_i, new_inst + + +def handle_jumps(instructions, original_instructions): + # type: (List[EnhancedInstruction], List[EnhancedInstruction]) -> None + """ + Transforms instructions in place until it looks more like original_instructions. + This is only needed in 3.10+ where optimisations lead to more drastic changes + after the sentinel transformation. + Replaces JUMP instructions that aren't also present in original_instructions + with the sections that they jump to until a raise or return. + In some other cases duplication found in `original_instructions` + is replicated in `instructions`. + """ + while True: + for original_i, original_inst, new_i, new_inst in walk_both_instructions( + original_instructions, 0, instructions, 0 + ): + if opnames_match(original_inst, new_inst): + continue + + if "JUMP" in new_inst.opname and "JUMP" not in original_inst.opname: + # Find where the new instruction is jumping to, ignoring + # instructions which have been copied in previous iterations + start = only( + i + for i, inst in enumerate(instructions) + if inst.offset == new_inst.argval + and not getattr(inst, "_copied", False) + ) + # Replace the jump instruction with the jumped to section of instructions + # That section may also be deleted if it's not similarly duplicated + # in original_instructions + new_instructions = handle_jump( + original_instructions, original_i, instructions, start + ) + assert new_instructions is not None + instructions[new_i : new_i + 1] = new_instructions + else: + # Extract a section of original_instructions from original_i to return/raise + orig_section = [] + for section_inst in original_instructions[original_i:]: + orig_section.append(section_inst) + if section_inst.opname in ("RETURN_VALUE", "RAISE_VARARGS"): + break + else: + # No return/raise - this is just a mismatch we can't handle + raise AssertionError + + instructions[new_i:new_i] = only(find_new_matching(orig_section, instructions)) + + # instructions has been modified, the for loop can't sensibly continue + # Restart it from the beginning, checking for other issues + break + + else: # No mismatched jumps found, we're done + return + + +def find_new_matching(orig_section, instructions): + # type: (List[EnhancedInstruction], List[EnhancedInstruction]) -> Iterator[List[EnhancedInstruction]] + """ + Yields sections of `instructions` which match `orig_section`. + The yielded sections include sentinel instructions, but these + are ignored when checking for matches. + """ + for start in range(len(instructions) - len(orig_section)): + indices, dup_section = zip( + *islice( + non_sentinel_instructions(instructions, start), + len(orig_section), + ) + ) + if len(dup_section) < len(orig_section): + return + if sections_match(orig_section, dup_section): + yield instructions[start:indices[-1] + 1] + + +def handle_jump(original_instructions, original_start, instructions, start): + # type: (List[EnhancedInstruction], int, List[EnhancedInstruction], int) -> Optional[List[EnhancedInstruction]] + """ + Returns the section of instructions starting at `start` and ending + with a RETURN_VALUE or RAISE_VARARGS instruction. + There should be a matching section in original_instructions starting at original_start. + If that section doesn't appear elsewhere in original_instructions, + then also delete the returned section of instructions. + """ + for original_j, original_inst, new_j, new_inst in walk_both_instructions( + original_instructions, original_start, instructions, start + ): + assert_(opnames_match(original_inst, new_inst)) + if original_inst.opname in ("RETURN_VALUE", "RAISE_VARARGS"): + inlined = deepcopy(instructions[start : new_j + 1]) + for inl in inlined: + inl._copied = True + orig_section = original_instructions[original_start : original_j + 1] + if not check_duplicates( + original_start, orig_section, original_instructions + ): + instructions[start : new_j + 1] = [] + return inlined + + return None + + +def check_duplicates(original_i, orig_section, original_instructions): + # type: (int, List[EnhancedInstruction], List[EnhancedInstruction]) -> bool + """ + Returns True if a section of original_instructions starting somewhere other + than original_i and matching orig_section is found, i.e. orig_section is duplicated. + """ + for dup_start in range(len(original_instructions)): + if dup_start == original_i: + continue + dup_section = original_instructions[dup_start : dup_start + len(orig_section)] + if len(dup_section) < len(orig_section): + return False + if sections_match(orig_section, dup_section): + return True + + return False + +def sections_match(orig_section, dup_section): + # type: (List[EnhancedInstruction], List[EnhancedInstruction]) -> bool + """ + Returns True if the given lists of instructions have matching linenos and opnames. + """ + return all( + ( + orig_inst.lineno == dup_inst.lineno + # POP_BLOCKs have been found to have differing linenos in innocent cases + or "POP_BLOCK" == orig_inst.opname == dup_inst.opname + ) + and opnames_match(orig_inst, dup_inst) + for orig_inst, dup_inst in zip(orig_section, dup_section) + ) + + +def opnames_match(inst1, inst2): + # type: (Instruction, Instruction) -> bool + return ( + inst1.opname == inst2.opname + or "JUMP" in inst1.opname + and "JUMP" in inst2.opname + or (inst1.opname == "PRINT_EXPR" and inst2.opname == "POP_TOP") + or ( + inst1.opname in ("LOAD_METHOD", "LOOKUP_METHOD") + and inst2.opname == "LOAD_ATTR" + ) + or (inst1.opname == "CALL_METHOD" and inst2.opname == "CALL_FUNCTION") + ) + + +def get_setter(node): + # type: (EnhancedAST) -> Optional[Callable[[ast.AST], None]] + parent = node.parent + for name, field in ast.iter_fields(parent): + if field is node: + def setter(new_node): + # type: (ast.AST) -> None + return setattr(parent, name, new_node) + return setter + elif isinstance(field, list): + for i, item in enumerate(field): + if item is node: + def setter(new_node): + # type: (ast.AST) -> None + field[i] = new_node + + return setter + return None + +lock = RLock() + + +@cache +def statement_containing_node(node): + # type: (ast.AST) -> EnhancedAST + while not isinstance(node, ast.stmt): + node = cast(EnhancedAST, node).parent + return cast(EnhancedAST, node) + + +def assert_linenos(tree): + # type: (ast.AST) -> Iterator[int] + for node in ast.walk(tree): + if ( + hasattr(node, 'parent') and + isinstance(statement_containing_node(node), ast.Assert) + ): + for lineno in node_linenos(node): + yield lineno + + +def _extract_ipython_statement(stmt): + # type: (EnhancedAST) -> ast.Module + # IPython separates each statement in a cell to be executed separately + # So NodeFinder should only compile one statement at a time or it + # will find a code mismatch. + while not isinstance(stmt.parent, ast.Module): + stmt = stmt.parent + # use `ast.parse` instead of `ast.Module` for better portability + # python3.8 changes the signature of `ast.Module` + # Inspired by https://github.com/pallets/werkzeug/pull/1552/files + tree = ast.parse("") + tree.body = [cast(ast.stmt, stmt)] + ast.copy_location(tree, stmt) + return tree + + +def is_ipython_cell_code_name(code_name): + # type: (str) -> bool + return bool(re.match(r"(|)$", code_name)) + + +def is_ipython_cell_filename(filename): + # type: (str) -> bool + return bool(re.search(r" bool + return ( + is_ipython_cell_filename(code_obj.co_filename) and + is_ipython_cell_code_name(code_obj.co_name) + ) + + +def find_node_ipython(frame, lasti, stmts, source): + # type: (types.FrameType, int, Set[EnhancedAST], Source) -> Tuple[Optional[Any], Optional[Any]] + node = decorator = None + for stmt in stmts: + tree = _extract_ipython_statement(stmt) + try: + node_finder = NodeFinder(frame, stmts, tree, lasti, source) + if (node or decorator) and (node_finder.result or node_finder.decorator): + # Found potential nodes in separate statements, + # cannot resolve ambiguity, give up here + return None, None + + node = node_finder.result + decorator = node_finder.decorator + except Exception: + pass + return decorator, node + + +def attr_names_match(attr, argval): + # type: (str, str) -> bool + """ + Checks that the user-visible attr (from ast) can correspond to + the argval in the bytecode, i.e. the real attribute fetched internally, + which may be mangled for private attributes. + """ + if attr == argval: + return True + if not attr.startswith("__"): + return False + return bool(re.match(r"^_\w+%s$" % attr, argval)) + + +def node_linenos(node): + # type: (ast.AST) -> Iterator[int] + if hasattr(node, "lineno"): + linenos = [] # type: Sequence[int] + if hasattr(node, "end_lineno") and isinstance(node, ast.expr): + assert node.end_lineno is not None # type: ignore[attr-defined] + linenos = range(node.lineno, node.end_lineno + 1) # type: ignore[attr-defined] + else: + linenos = [node.lineno] # type: ignore[attr-defined] + for lineno in linenos: + yield lineno + + +if sys.version_info >= (3, 11): + from ._position_node_finder import PositionNodeFinder as NodeFinder +else: + NodeFinder = SentinelNodeFinder + diff --git a/lib/python3.10/site-packages/executing/py.typed b/lib/python3.10/site-packages/executing/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/executing/version.py b/lib/python3.10/site-packages/executing/version.py new file mode 100644 index 0000000000000000000000000000000000000000..e212710ff7b8062af3cbe0c10c9ad9a777352fa8 --- /dev/null +++ b/lib/python3.10/site-packages/executing/version.py @@ -0,0 +1 @@ +__version__ = '2.2.0' \ No newline at end of file diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/AUTHORS b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/AUTHORS new file mode 100644 index 0000000000000000000000000000000000000000..721bda3e350be4b838aff43e922cb4f83c0a6040 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/AUTHORS @@ -0,0 +1,11 @@ +MAINTAINER +Michal Hořejšek + +CONTRIBUTORS +anentropic +Antti Jokipii +bcaller +Frederik Petersen +Guillaume Desvé +Kris Molendyke +David Majda diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/INSTALLER b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/LICENSE b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1d77bbf99f3a4b9e4b38e7a75c368c84514fcce1 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018, Michal Horejsek +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/METADATA b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..5d0b0d171b9eb63fa5e3548494932a3fec93d543 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/METADATA @@ -0,0 +1,54 @@ +Metadata-Version: 2.1 +Name: fastjsonschema +Version: 2.21.1 +Summary: Fastest Python implementation of JSON schema +Home-page: https://github.com/horejsek/python-fastjsonschema +Author: Michal Horejsek +Author-email: fastjsonschema@horejsek.com +License: BSD +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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 :: Implementation :: CPython +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Libraries :: Python Modules +License-File: LICENSE +License-File: AUTHORS +Provides-Extra: devel +Requires-Dist: colorama; extra == "devel" +Requires-Dist: jsonschema; extra == "devel" +Requires-Dist: json-spec; extra == "devel" +Requires-Dist: pylint; extra == "devel" +Requires-Dist: pytest; extra == "devel" +Requires-Dist: pytest-benchmark; extra == "devel" +Requires-Dist: pytest-cache; extra == "devel" +Requires-Dist: validictory; extra == "devel" + +=========================== +Fast JSON schema for Python +=========================== + +|PyPI| |Pythons| + +.. |PyPI| image:: https://img.shields.io/pypi/v/fastjsonschema.svg + :alt: PyPI version + :target: https://pypi.python.org/pypi/fastjsonschema + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/fastjsonschema.svg + :alt: Supported Python versions + :target: https://pypi.python.org/pypi/fastjsonschema + +See `documentation `_. diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/RECORD b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..f478c281276e8f7929bae498acdc08e3e2bf2e17 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/RECORD @@ -0,0 +1,27 @@ +fastjsonschema-2.21.1.dist-info/AUTHORS,sha256=DLGgN1TEmM2VoBM4cRn-gklc4HA8jLLPDDCeBD1kGhU,350 +fastjsonschema-2.21.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +fastjsonschema-2.21.1.dist-info/LICENSE,sha256=nM3faes5mKYBSN6-hblMWv7VNpG2R0aS54q8wKDlRPE,1518 +fastjsonschema-2.21.1.dist-info/METADATA,sha256=BCBfPGXzH4nCu_0NeyJ8y37GFhQ96iJm_AaZuA8SrAI,2152 +fastjsonschema-2.21.1.dist-info/RECORD,, +fastjsonschema-2.21.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91 +fastjsonschema-2.21.1.dist-info/top_level.txt,sha256=8RQcPDFXXHZKduTjgzugpPNW3zIjxFT0axTh4UsT6gE,15 +fastjsonschema/__init__.py,sha256=GzCywWlandjQQsJLXaZkHYdnydNcITF6r24Av5gQYgU,10347 +fastjsonschema/__main__.py,sha256=4hfd23przxmQc8VjL0fUsbsrvvA73gJ2HDNPgLLFdAI,312 +fastjsonschema/__pycache__/__init__.cpython-310.pyc,, +fastjsonschema/__pycache__/__main__.cpython-310.pyc,, +fastjsonschema/__pycache__/draft04.cpython-310.pyc,, +fastjsonschema/__pycache__/draft06.cpython-310.pyc,, +fastjsonschema/__pycache__/draft07.cpython-310.pyc,, +fastjsonschema/__pycache__/exceptions.cpython-310.pyc,, +fastjsonschema/__pycache__/generator.cpython-310.pyc,, +fastjsonschema/__pycache__/indent.cpython-310.pyc,, +fastjsonschema/__pycache__/ref_resolver.cpython-310.pyc,, +fastjsonschema/__pycache__/version.cpython-310.pyc,, +fastjsonschema/draft04.py,sha256=aFhmYp1Rjx6mDZohnBnCfd3gOqUUylpQXCkClAvWKPc,30808 +fastjsonschema/draft06.py,sha256=cSPnflqydr6EV4p02T_gh4VFX7mVVdoKCxnNwnC_PPA,7892 +fastjsonschema/draft07.py,sha256=D4qNNhWcjg0TrEiHQ0BJNwvlyv1Rp8gyEBYgRBmV2b8,4449 +fastjsonschema/exceptions.py,sha256=w749JgqKi8clBFcObdcbZVqsmF4oJ_QByhZ1SGbUFNw,1612 +fastjsonschema/generator.py,sha256=bYZt_QfrCH_v7rJDBMteeJx4UDygEV7XZjOtFL3ikls,13059 +fastjsonschema/indent.py,sha256=juZFW9LSvmDJbPFIUm3GPqdPqJoUnqvM8neHN5rkvzU,920 +fastjsonschema/ref_resolver.py,sha256=PWnu-2MZzWH5cymDvdcvXfx3iOW_Mr6c-xXMYm9FD7Q,5577 +fastjsonschema/version.py,sha256=dNAwyKYTo58dhA957101JXTUnXy1nRqqewytAwxSmEM,19 diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/WHEEL b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..ae527e7d64811439e61b93aa375defb30e06edfe --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.6.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/top_level.txt b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..f4ffe07938cdca77c89d04365af53e237cb2ddbb --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema-2.21.1.dist-info/top_level.txt @@ -0,0 +1 @@ +fastjsonschema diff --git a/lib/python3.10/site-packages/fastjsonschema/__init__.py b/lib/python3.10/site-packages/fastjsonschema/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0fe1bf72f5668bab76860a256de3e97ae344b15e --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/__init__.py @@ -0,0 +1,277 @@ +# ___ +# \./ DANGER: This project implements some code generation +# .--.O.--. techniques involving string concatenation. +# \/ \/ If you look at it, you might die. +# + +r""" +Installation +************ + +.. code-block:: bash + + pip install fastjsonschema + +Support only for Python 3.3 and higher. + +About +***** + +``fastjsonschema`` implements validation of JSON documents by JSON schema. +The library implements JSON schema drafts 04, 06, and 07. The main purpose is +to have a really fast implementation. See some numbers: + + * Probably the most popular, ``jsonschema``, can take up to 5 seconds for valid + inputs and 1.2 seconds for invalid inputs. + * Second most popular, ``json-spec``, is even worse with up to 7.2 and 1.7 seconds. + * Last ``validictory``, now deprecated, is much better with 370 or 23 milliseconds, + but it does not follow all standards, and it can be still slow for some purposes. + +With this library you can gain big improvements as ``fastjsonschema`` takes +only about 25 milliseconds for valid inputs and 2 milliseconds for invalid ones. +Pretty amazing, right? :-) + +Technically it works by generating the most stupid code on the fly, which is fast but +is hard to write by hand. The best efficiency is achieved when a validator is compiled +once and used many times, of course. It works similarly like regular expressions. But +you can also generate the code to a file, which is even slightly faster. + +You can run the performance benchmarks on your computer or server with the included +script: + +.. code-block:: bash + + $ make performance + fast_compiled valid ==> 0.0993900 + fast_compiled invalid ==> 0.0041089 + fast_compiled_without_exc valid ==> 0.0465258 + fast_compiled_without_exc invalid ==> 0.0023688 + fast_file valid ==> 0.0989483 + fast_file invalid ==> 0.0041104 + fast_not_compiled valid ==> 11.9572681 + fast_not_compiled invalid ==> 2.9512092 + jsonschema valid ==> 5.2233240 + jsonschema invalid ==> 1.3227916 + jsonschema_compiled valid ==> 0.4447982 + jsonschema_compiled invalid ==> 0.0231333 + jsonspec valid ==> 4.1450569 + jsonspec invalid ==> 1.0485777 + validictory valid ==> 0.2730411 + validictory invalid ==> 0.0183669 + +This library follows and implements `JSON schema draft-04, draft-06, and draft-07 +`_. Sometimes it's not perfectly clear, so I recommend also +check out this `understanding JSON schema `_. + +Note that there are some differences compared to JSON schema standard: + + * Regular expressions are full Python ones, not only what JSON schema allows. It's easier + to allow everything, and also it's faster to compile without limits. So keep in mind that when + you will use a more advanced regular expression, it may not work with other libraries or in + other languages. + * Because Python matches new line for a dollar in regular expressions (``a$`` matches ``a`` and ``a\\n``), + instead of ``$`` is used ``\Z`` and all dollars in your regular expression are changed to ``\\Z`` + as well. When you want to use dollar as regular character, you have to escape it (``\$``). + * JSON schema says you can use keyword ``default`` for providing default values. This implementation + uses that and always returns transformed input data. + +Usage +***** + +.. code-block:: python + + import fastjsonschema + + point_schema = { + "type": "object", + "properties": { + "x": { + "type": "number", + }, + "y": { + "type": "number", + }, + }, + "required": ["x", "y"], + "additionalProperties": False, + } + + point_validator = fastjsonschema.compile(point_schema) + try: + point_validator({"x": 1.0, "y": 2.0}) + except fastjsonschema.JsonSchemaException as e: + print(f"Data failed validation: {e}") + +API +*** +""" +from functools import partial, update_wrapper + +from .draft04 import CodeGeneratorDraft04 +from .draft06 import CodeGeneratorDraft06 +from .draft07 import CodeGeneratorDraft07 +from .exceptions import JsonSchemaException, JsonSchemaValueException, JsonSchemaDefinitionException +from .ref_resolver import RefResolver +from .version import VERSION + +__all__ = ( + 'VERSION', + 'JsonSchemaException', + 'JsonSchemaValueException', + 'JsonSchemaDefinitionException', + 'validate', + 'compile', + 'compile_to_code', +) + + +def validate(definition, data, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + """ + Validation function for lazy programmers or for use cases when you need + to call validation only once, so you do not have to compile it first. + Use it only when you do not care about performance (even though it will + be still faster than alternative implementations). + + .. code-block:: python + + import fastjsonschema + + fastjsonschema.validate({'type': 'string'}, 'hello') + # same as: compile({'type': 'string'})('hello') + + Preferred is to use :any:`compile` function. + """ + return compile(definition, handlers, formats, use_default, use_formats, detailed_exceptions)(data) + + +#TODO: Change use_default to False when upgrading to version 3. +# pylint: disable=redefined-builtin,dangerous-default-value,exec-used +def compile(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + """ + Generates validation function for validating JSON schema passed in ``definition``. + Example: + + .. code-block:: python + + import fastjsonschema + + validate = fastjsonschema.compile({'type': 'string'}) + validate('hello') + + This implementation supports keyword ``default`` (can be turned off + by passing `use_default=False`): + + .. code-block:: python + + validate = fastjsonschema.compile({ + 'type': 'object', + 'properties': { + 'a': {'type': 'number', 'default': 42}, + }, + }) + + data = validate({}) + assert data == {'a': 42} + + Supported implementations are draft-04, draft-06 and draft-07. Which version + should be used is determined by `$draft` in your ``definition``. When not + specified, the latest implementation is used (draft-07). + + .. code-block:: python + + validate = fastjsonschema.compile({ + '$schema': 'http://json-schema.org/draft-04/schema', + 'type': 'number', + }) + + You can pass mapping from URI to function that should be used to retrieve + remote schemes used in your ``definition`` in parameter ``handlers``. + + Also, you can pass mapping for custom formats. Key is the name of your + formatter and value can be regular expression, which will be compiled or + callback returning `bool` (or you can raise your own exception). + + .. code-block:: python + + validate = fastjsonschema.compile(definition, formats={ + 'foo': r'foo|bar', + 'bar': lambda value: value in ('foo', 'bar'), + }) + + Note that formats are automatically used as assertions. It can be turned + off by passing `use_formats=False`. When disabled, custom formats are + disabled as well. (Added in 2.19.0.) + + If you don't need detailed exceptions, you can turn the details off and gain + additional performance by passing `detailed_exceptions=False`. + + Exception :any:`JsonSchemaDefinitionException` is raised when generating the + code fails (bad definition). + + Exception :any:`JsonSchemaValueException` is raised from generated function when + validation fails (data do not follow the definition). + """ + resolver, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions) + global_state = code_generator.global_state + # Do not pass local state so it can recursively call itself. + exec(code_generator.func_code, global_state) + func = global_state[resolver.get_scope_name()] + if formats: + return update_wrapper(partial(func, custom_formats=formats), func) + return func + + +# pylint: disable=dangerous-default-value +def compile_to_code(definition, handlers={}, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + """ + Generates validation code for validating JSON schema passed in ``definition``. + Example: + + .. code-block:: python + + import fastjsonschema + + code = fastjsonschema.compile_to_code({'type': 'string'}) + with open('your_file.py', 'w') as f: + f.write(code) + + You can also use it as a script: + + .. code-block:: bash + + echo "{'type': 'string'}" | python3 -m fastjsonschema > your_file.py + python3 -m fastjsonschema "{'type': 'string'}" > your_file.py + + Exception :any:`JsonSchemaDefinitionException` is raised when generating the + code fails (bad definition). + """ + _, code_generator = _factory(definition, handlers, formats, use_default, use_formats, detailed_exceptions) + return ( + 'VERSION = "' + VERSION + '"\n' + + code_generator.global_state_code + '\n' + + code_generator.func_code + ) + + +def _factory(definition, handlers, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + resolver = RefResolver.from_schema(definition, handlers=handlers, store={}) + code_generator = _get_code_generator_class(definition)( + definition, + resolver=resolver, + formats=formats, + use_default=use_default, + use_formats=use_formats, + detailed_exceptions=detailed_exceptions, + ) + return resolver, code_generator + + +def _get_code_generator_class(schema): + # Schema in from draft-06 can be just the boolean value. + if isinstance(schema, dict): + schema_version = schema.get('$schema', '') + if 'draft-04' in schema_version: + return CodeGeneratorDraft04 + if 'draft-06' in schema_version: + return CodeGeneratorDraft06 + return CodeGeneratorDraft07 diff --git a/lib/python3.10/site-packages/fastjsonschema/__main__.py b/lib/python3.10/site-packages/fastjsonschema/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..e5f3aa74c2d28979ec371a9151c77d2d94f2845f --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/__main__.py @@ -0,0 +1,19 @@ +import json +import sys + +from . import compile_to_code + + +def main(): + if len(sys.argv) == 2: + definition = sys.argv[1] + else: + definition = sys.stdin.read() + + definition = json.loads(definition) + code = compile_to_code(definition) + print(code) + + +if __name__ == '__main__': + main() diff --git a/lib/python3.10/site-packages/fastjsonschema/draft04.py b/lib/python3.10/site-packages/fastjsonschema/draft04.py new file mode 100644 index 0000000000000000000000000000000000000000..d8af14b6ceacb33122b292d3807a64243a41f4dd --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/draft04.py @@ -0,0 +1,618 @@ +import decimal +import re + +from .exceptions import JsonSchemaDefinitionException +from .generator import CodeGenerator, enforce_list + + +JSON_TYPE_TO_PYTHON_TYPE = { + 'null': 'NoneType', + 'boolean': 'bool', + 'number': 'int, float, Decimal', + 'integer': 'int', + 'string': 'str', + 'array': 'list, tuple', + 'object': 'dict', +} + +DOLLAR_FINDER = re.compile(r"(? {maxLength}:'): + self.exc('{name} must be shorter than or equal to {maxLength} characters', rule='maxLength') + + def generate_pattern(self): + with self.l('if isinstance({variable}, str):'): + pattern = self._definition['pattern'] + safe_pattern = pattern.replace('\\', '\\\\').replace('"', '\\"') + end_of_string_fixed_pattern = DOLLAR_FINDER.sub(r'\\Z', pattern) + self._compile_regexps[pattern] = re.compile(end_of_string_fixed_pattern) + with self.l('if not REGEX_PATTERNS[{}].search({variable}):', repr(pattern)): + self.exc('{name} must match pattern {}', safe_pattern, rule='pattern') + + def generate_format(self): + """ + Means that value have to be in specified format. For example date, email or other. + + .. code-block:: python + + {'format': 'email'} + + Valid value for this definition is user@example.com but not @username + """ + if not self._use_formats: + return + with self.l('if isinstance({variable}, str):'): + format_ = self._definition['format'] + # Checking custom formats - user is allowed to override default formats. + if format_ in self._custom_formats: + custom_format = self._custom_formats[format_] + if isinstance(custom_format, str): + self._generate_format(format_, format_ + '_re_pattern', custom_format) + else: + with self.l('if not custom_formats["{}"]({variable}):', format_): + self.exc('{name} must be {}', format_, rule='format') + elif format_ in self.FORMAT_REGEXS: + format_regex = self.FORMAT_REGEXS[format_] + self._generate_format(format_, format_ + '_re_pattern', format_regex) + # Format regex is used only in meta schemas. + elif format_ == 'regex': + self._extra_imports_lines = ['import re'] + with self.l('try:', optimize=False): + self.l('re.compile({variable})') + with self.l('except Exception:'): + self.exc('{name} must be a valid regex', rule='format') + else: + raise JsonSchemaDefinitionException('Unknown format: {}'.format(format_)) + + + def _generate_format(self, format_name, regexp_name, regexp): + if self._definition['format'] == format_name: + if not regexp_name in self._compile_regexps: + self._compile_regexps[regexp_name] = re.compile(regexp) + with self.l('if not REGEX_PATTERNS["{}"].match({variable}):', regexp_name): + self.exc('{name} must be {}', format_name, rule='format') + + def generate_minimum(self): + with self.l('if isinstance({variable}, (int, float, Decimal)):'): + if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('minimum must be a number') + if self._definition.get('exclusiveMinimum', False): + with self.l('if {variable} <= {minimum}:'): + self.exc('{name} must be bigger than {minimum}', rule='minimum') + else: + with self.l('if {variable} < {minimum}:'): + self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum') + + def generate_maximum(self): + with self.l('if isinstance({variable}, (int, float, Decimal)):'): + if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('maximum must be a number') + if self._definition.get('exclusiveMaximum', False): + with self.l('if {variable} >= {maximum}:'): + self.exc('{name} must be smaller than {maximum}', rule='maximum') + else: + with self.l('if {variable} > {maximum}:'): + self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum') + + def generate_multiple_of(self): + with self.l('if isinstance({variable}, (int, float, Decimal)):'): + if not isinstance(self._definition['multipleOf'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('multipleOf must be a number') + # For proper multiplication check of floats we need to use decimals, + # because for example 19.01 / 0.01 = 1901.0000000000002. + if isinstance(self._definition['multipleOf'], float): + self.l('quotient = Decimal(repr({variable})) / Decimal(repr({multipleOf}))') + else: + self.l('quotient = {variable} / {multipleOf}') + with self.l('if int(quotient) != quotient:'): + self.exc('{name} must be multiple of {multipleOf}', rule='multipleOf') + # For example, 1e308 / 0.123456789 + with self.l('if {variable} / {multipleOf} == float("inf"):'): + self.exc('inifinity reached', rule='multipleOf') + + def generate_min_items(self): + self.create_variable_is_list() + with self.l('if {variable}_is_list:'): + if not isinstance(self._definition['minItems'], (int, float)): + raise JsonSchemaDefinitionException('minItems must be a number') + self.create_variable_with_length() + with self.l('if {variable}_len < {minItems}:'): + self.exc('{name} must contain at least {minItems} items', rule='minItems') + + def generate_max_items(self): + self.create_variable_is_list() + with self.l('if {variable}_is_list:'): + if not isinstance(self._definition['maxItems'], (int, float)): + raise JsonSchemaDefinitionException('maxItems must be a number') + self.create_variable_with_length() + with self.l('if {variable}_len > {maxItems}:'): + self.exc('{name} must contain less than or equal to {maxItems} items', rule='maxItems') + + def generate_unique_items(self): + """ + With Python 3.4 module ``timeit`` recommended this solutions: + + .. code-block:: python + + >>> timeit.timeit("len(x) > len(set(x))", "x=range(100)+range(100)", number=100000) + 0.5839540958404541 + >>> timeit.timeit("len({}.fromkeys(x)) == len(x)", "x=range(100)+range(100)", number=100000) + 0.7094449996948242 + >>> timeit.timeit("seen = set(); any(i in seen or seen.add(i) for i in x)", "x=range(100)+range(100)", number=100000) + 2.0819358825683594 + >>> timeit.timeit("np.unique(x).size == len(x)", "x=range(100)+range(100); import numpy as np", number=100000) + 2.1439831256866455 + """ + unique_definition = self._definition['uniqueItems'] + if not unique_definition: + return + + self.create_variable_is_list() + with self.l('if {variable}_is_list:'): + self.l( + 'def fn(var): ' + 'return frozenset(dict((k, fn(v)) ' + 'for k, v in var.items()).items()) ' + 'if hasattr(var, "items") else tuple(fn(v) ' + 'for v in var) ' + 'if isinstance(var, (dict, list)) else str(var) ' + 'if isinstance(var, bool) else var') + self.create_variable_with_length() + with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'): + self.exc('{name} must contain unique items', rule='uniqueItems') + + def generate_items(self): + """ + Means array is valid only when all items are valid by this definition. + + .. code-block:: python + + { + 'items': [ + {'type': 'integer'}, + {'type': 'string'}, + ], + } + + Valid arrays are those with integers or strings, nothing else. + + Since draft 06 definition can be also boolean. True means nothing, False + means everything is invalid. + """ + items_definition = self._definition['items'] + if items_definition is True: + return + + self.create_variable_is_list() + with self.l('if {variable}_is_list:'): + self.create_variable_with_length() + if items_definition is False: + with self.l('if {variable}:'): + self.exc('{name} must not be there', rule='items') + elif isinstance(items_definition, list): + for idx, item_definition in enumerate(items_definition): + with self.l('if {variable}_len > {}:', idx): + self.l('{variable}__{0} = {variable}[{0}]', idx) + self.generate_func_code_block( + item_definition, + '{}__{}'.format(self._variable, idx), + '{}[{}]'.format(self._variable_name, idx), + ) + if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition: + self.l('else: {variable}.append({})', repr(item_definition['default'])) + + if 'additionalItems' in self._definition: + if self._definition['additionalItems'] is False: + with self.l('if {variable}_len > {}:', len(items_definition)): + self.exc('{name} must contain only specified items', rule='items') + else: + with self.l('for {variable}_x, {variable}_item in enumerate({variable}[{0}:], {0}):', len(items_definition)): + count = self.generate_func_code_block( + self._definition['additionalItems'], + '{}_item'.format(self._variable), + '{}[{{{}_x}}]'.format(self._variable_name, self._variable), + ) + if count == 0: + self.l('pass') + else: + if items_definition: + with self.l('for {variable}_x, {variable}_item in enumerate({variable}):'): + count = self.generate_func_code_block( + items_definition, + '{}_item'.format(self._variable), + '{}[{{{}_x}}]'.format(self._variable_name, self._variable), + ) + if count == 0: + self.l('pass') + + def generate_min_properties(self): + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + if not isinstance(self._definition['minProperties'], (int, float)): + raise JsonSchemaDefinitionException('minProperties must be a number') + self.create_variable_with_length() + with self.l('if {variable}_len < {minProperties}:'): + self.exc('{name} must contain at least {minProperties} properties', rule='minProperties') + + def generate_max_properties(self): + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + if not isinstance(self._definition['maxProperties'], (int, float)): + raise JsonSchemaDefinitionException('maxProperties must be a number') + self.create_variable_with_length() + with self.l('if {variable}_len > {maxProperties}:'): + self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties') + + def generate_required(self): + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + if not isinstance(self._definition['required'], (list, tuple)): + raise JsonSchemaDefinitionException('required must be an array') + if len(self._definition['required']) != len(set(self._definition['required'])): + raise JsonSchemaDefinitionException('required must contain unique elements') + if not self._definition.get('additionalProperties', True): + not_possible = [ + prop + for prop in self._definition['required'] + if + prop not in self._definition.get('properties', {}) + and not any(re.search(regex, prop) for regex in self._definition.get('patternProperties', {})) + ] + if not_possible: + raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible)) + self.l('{variable}__missing_keys = set({required}) - {variable}.keys()') + with self.l('if {variable}__missing_keys:'): + dynamic = 'str(sorted({variable}__missing_keys)) + " properties"' + self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic) + + def generate_properties(self): + """ + Means object with defined keys. + + .. code-block:: python + + { + 'properties': { + 'key': {'type': 'number'}, + }, + } + + Valid object is containing key called 'key' and value any number. + """ + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + self.create_variable_keys() + for key, prop_definition in self._definition['properties'].items(): + key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key) + if not isinstance(prop_definition, (dict, bool)): + raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name)) + with self.l('if "{}" in {variable}_keys:', self.e(key)): + self.l('{variable}_keys.remove("{}")', self.e(key)) + self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key)) + self.generate_func_code_block( + prop_definition, + '{}__{}'.format(self._variable, key_name), + '{}.{}'.format(self._variable_name, self.e(key)), + clear_variables=True, + ) + if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition: + self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default'])) + + def generate_pattern_properties(self): + """ + Means object with defined keys as patterns. + + .. code-block:: python + + { + 'patternProperties': { + '^x': {'type': 'number'}, + }, + } + + Valid object is containing key starting with a 'x' and value any number. + """ + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + self.create_variable_keys() + for pattern, definition in self._definition['patternProperties'].items(): + self._compile_regexps[pattern] = re.compile(pattern) + with self.l('for {variable}_key, {variable}_val in {variable}.items():'): + for pattern, definition in self._definition['patternProperties'].items(): + with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)): + with self.l('if {variable}_key in {variable}_keys:'): + self.l('{variable}_keys.remove({variable}_key)') + self.generate_func_code_block( + definition, + '{}_val'.format(self._variable), + '{}.{{{}_key}}'.format(self._variable_name, self._variable), + clear_variables=True, + ) + + def generate_additional_properties(self): + """ + Means object with keys with values defined by definition. + + .. code-block:: python + + { + 'properties': { + 'key': {'type': 'number'}, + } + 'additionalProperties': {'type': 'string'}, + } + + Valid object is containing key called 'key' and it's value any number and + any other key with any string. + """ + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + self.create_variable_keys() + add_prop_definition = self._definition["additionalProperties"] + if add_prop_definition is True or add_prop_definition == {}: + return + if add_prop_definition: + properties_keys = list(self._definition.get("properties", {}).keys()) + with self.l('for {variable}_key in {variable}_keys:'): + with self.l('if {variable}_key not in {}:', properties_keys): + self.l('{variable}_value = {variable}.get({variable}_key)') + self.generate_func_code_block( + add_prop_definition, + '{}_value'.format(self._variable), + '{}.{{{}_key}}'.format(self._variable_name, self._variable), + ) + else: + with self.l('if {variable}_keys:'): + self.exc('{name} must not contain "+str({variable}_keys)+" properties', rule='additionalProperties') + + def generate_dependencies(self): + """ + Means when object has property, it needs to have also other property. + + .. code-block:: python + + { + 'dependencies': { + 'bar': ['foo'], + }, + } + + Valid object is containing only foo, both bar and foo or none of them, but not + object with only bar. + + Since draft 06 definition can be boolean or empty array. True and empty array + means nothing, False means that key cannot be there at all. + """ + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + is_empty = True + for key, values in self._definition["dependencies"].items(): + if values == [] or values is True: + continue + is_empty = False + with self.l('if "{}" in {variable}:', self.e(key)): + if values is False: + self.exc('{} in {name} must not be there', key, rule='dependencies') + elif isinstance(values, list): + for value in values: + with self.l('if "{}" not in {variable}:', self.e(value)): + self.exc('{name} missing dependency {} for {}', self.e(value), self.e(key), rule='dependencies') + else: + self.generate_func_code_block(values, self._variable, self._variable_name, clear_variables=True) + if is_empty: + self.l('pass') diff --git a/lib/python3.10/site-packages/fastjsonschema/draft06.py b/lib/python3.10/site-packages/fastjsonschema/draft06.py new file mode 100644 index 0000000000000000000000000000000000000000..07f1e04e3d8699d4cca16c45f2f456ca33019412 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/draft06.py @@ -0,0 +1,188 @@ +import decimal +from .draft04 import CodeGeneratorDraft04, JSON_TYPE_TO_PYTHON_TYPE +from .exceptions import JsonSchemaDefinitionException +from .generator import enforce_list + + +class CodeGeneratorDraft06(CodeGeneratorDraft04): + FORMAT_REGEXS = dict(CodeGeneratorDraft04.FORMAT_REGEXS, **{ + 'json-pointer': r'^(/(([^/~])|(~[01]))*)*\Z', + 'uri-reference': r'^(\w+:(\/?\/?))?[^#\\\s]*(#[^\\\s]*)?\Z', + 'uri-template': ( + r'^(?:(?:[^\x00-\x20\"\'<>%\\^`{|}]|%[0-9a-f]{2})|' + r'\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+' + r'(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+' + r'(?::[1-9][0-9]{0,3}|\*)?)*\})*\Z' + ), + }) + + def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions) + self._json_keywords_to_function.update(( + ('exclusiveMinimum', self.generate_exclusive_minimum), + ('exclusiveMaximum', self.generate_exclusive_maximum), + ('propertyNames', self.generate_property_names), + ('contains', self.generate_contains), + ('const', self.generate_const), + )) + + def _generate_func_code_block(self, definition): + if isinstance(definition, bool): + self.generate_boolean_schema() + elif '$ref' in definition: + # needed because ref overrides any sibling keywords + self.generate_ref() + else: + self.run_generate_functions(definition) + + def generate_boolean_schema(self): + """ + Means that schema can be specified by boolean. + True means everything is valid, False everything is invalid. + """ + if self._definition is True: + self.l('pass') + if self._definition is False: + self.exc('{name} must not be there') + + def generate_type(self): + """ + Validation of type. Can be one type or list of types. + + Since draft 06 a float without fractional part is an integer. + + .. code-block:: python + + {'type': 'string'} + {'type': ['string', 'number']} + """ + types = enforce_list(self._definition['type']) + try: + python_types = ', '.join(JSON_TYPE_TO_PYTHON_TYPE[t] for t in types) + except KeyError as exc: + raise JsonSchemaDefinitionException('Unknown type: {}'.format(exc)) + + extra = '' + + if 'integer' in types: + extra += ' and not (isinstance({variable}, float) and {variable}.is_integer())'.format( + variable=self._variable, + ) + + if ('number' in types or 'integer' in types) and 'boolean' not in types: + extra += ' or isinstance({variable}, bool)'.format(variable=self._variable) + + with self.l('if not isinstance({variable}, ({})){}:', python_types, extra): + self.exc('{name} must be {}', ' or '.join(types), rule='type') + + def generate_exclusive_minimum(self): + with self.l('if isinstance({variable}, (int, float, Decimal)):'): + if not isinstance(self._definition['exclusiveMinimum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('exclusiveMinimum must be an integer, a float or a decimal') + with self.l('if {variable} <= {exclusiveMinimum}:'): + self.exc('{name} must be bigger than {exclusiveMinimum}', rule='exclusiveMinimum') + + def generate_exclusive_maximum(self): + with self.l('if isinstance({variable}, (int, float, Decimal)):'): + if not isinstance(self._definition['exclusiveMaximum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('exclusiveMaximum must be an integer, a float or a decimal') + with self.l('if {variable} >= {exclusiveMaximum}:'): + self.exc('{name} must be smaller than {exclusiveMaximum}', rule='exclusiveMaximum') + + def generate_property_names(self): + """ + Means that keys of object must to follow this definition. + + .. code-block:: python + + { + 'propertyNames': { + 'maxLength': 3, + }, + } + + Valid keys of object for this definition are foo, bar, ... but not foobar for example. + """ + property_names_definition = self._definition.get('propertyNames', {}) + if property_names_definition is True: + pass + elif property_names_definition is False: + self.create_variable_keys() + with self.l('if {variable}_keys:'): + self.exc('{name} must not be there', rule='propertyNames') + else: + self.create_variable_is_dict() + with self.l('if {variable}_is_dict:'): + self.create_variable_with_length() + with self.l('if {variable}_len != 0:'): + self.l('{variable}_property_names = True') + with self.l('for {variable}_key in {variable}:'): + with self.l('try:'): + self.generate_func_code_block( + property_names_definition, + '{}_key'.format(self._variable), + self._variable_name, + clear_variables=True, + ) + with self.l('except JsonSchemaValueException:'): + self.l('{variable}_property_names = False') + with self.l('if not {variable}_property_names:'): + self.exc('{name} must be named by propertyName definition', rule='propertyNames') + + def generate_contains(self): + """ + Means that array must contain at least one defined item. + + .. code-block:: python + + { + 'contains': { + 'type': 'number', + }, + } + + Valid array is any with at least one number. + """ + self.create_variable_is_list() + with self.l('if {variable}_is_list:'): + contains_definition = self._definition['contains'] + + if contains_definition is False: + self.exc('{name} is always invalid', rule='contains') + elif contains_definition is True: + with self.l('if not {variable}:'): + self.exc('{name} must not be empty', rule='contains') + else: + self.l('{variable}_contains = False') + with self.l('for {variable}_key in {variable}:'): + with self.l('try:'): + self.generate_func_code_block( + contains_definition, + '{}_key'.format(self._variable), + self._variable_name, + clear_variables=True, + ) + self.l('{variable}_contains = True') + self.l('break') + self.l('except JsonSchemaValueException: pass') + + with self.l('if not {variable}_contains:'): + self.exc('{name} must contain one of contains definition', rule='contains') + + def generate_const(self): + """ + Means that value is valid when is equeal to const definition. + + .. code-block:: python + + { + 'const': 42, + } + + Only valid value is 42 in this example. + """ + const = self._definition['const'] + if isinstance(const, str): + const = '"{}"'.format(self.e(const)) + with self.l('if {variable} != {}:', const): + self.exc('{name} must be same as const definition: {definition_rule}', rule='const') diff --git a/lib/python3.10/site-packages/fastjsonschema/draft07.py b/lib/python3.10/site-packages/fastjsonschema/draft07.py new file mode 100644 index 0000000000000000000000000000000000000000..227525ef159ebcd7232c87078d6bb5c76110c29f --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/draft07.py @@ -0,0 +1,116 @@ +from .draft06 import CodeGeneratorDraft06 + + +class CodeGeneratorDraft07(CodeGeneratorDraft06): + FORMAT_REGEXS = dict(CodeGeneratorDraft06.FORMAT_REGEXS, **{ + 'date': r'^(?P\d{4})-(?P(0[1-9]|1[0-2]))-(?P(0[1-9]|[12]\d|3[01]))\Z', + 'iri': r'^\w+:(\/?\/?)[^\s]+\Z', + 'iri-reference': r'^(\w+:(\/?\/?))?[^#\\\s]*(#[^\\\s]*)?\Z', + 'idn-email': r'^[^@]+@[^@]+\.[^@]+\Z', + 'idn-hostname': r'^(?!-)(xn--)?[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.(?!-)(xn--)?([a-zA-Z0-9\-]{1,50}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,})$', + 'relative-json-pointer': r'^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)\Z', + #'regex': r'', + 'time': ( + r'^(?P\d{1,2}):(?P\d{1,2})' + r'(?::(?P\d{1,2})(?:\.(?P\d{1,6}))?' + r'([zZ]|[+-]\d\d:\d\d)?)?\Z' + ), + }) + + def __init__(self, definition, resolver=None, formats={}, use_default=True, use_formats=True, detailed_exceptions=True): + super().__init__(definition, resolver, formats, use_default, use_formats, detailed_exceptions) + # pylint: disable=duplicate-code + self._json_keywords_to_function.update(( + ('if', self.generate_if_then_else), + ('contentEncoding', self.generate_content_encoding), + ('contentMediaType', self.generate_content_media_type), + )) + + def generate_if_then_else(self): + """ + Implementation of if-then-else. + + .. code-block:: python + + { + 'if': { + 'exclusiveMaximum': 0, + }, + 'then': { + 'minimum': -10, + }, + 'else': { + 'multipleOf': 2, + }, + } + + Valid values are any between -10 and 0 or any multiplication of two. + """ + with self.l('try:', optimize=False): + self.generate_func_code_block( + self._definition['if'], + self._variable, + self._variable_name, + clear_variables=True + ) + with self.l('except JsonSchemaValueException:'): + if 'else' in self._definition: + self.generate_func_code_block( + self._definition['else'], + self._variable, + self._variable_name, + clear_variables=True + ) + else: + self.l('pass') + if 'then' in self._definition: + with self.l('else:'): + self.generate_func_code_block( + self._definition['then'], + self._variable, + self._variable_name, + clear_variables=True + ) + + def generate_content_encoding(self): + """ + Means decoding value when it's encoded by base64. + + .. code-block:: python + + { + 'contentEncoding': 'base64', + } + """ + if self._definition['contentEncoding'] == 'base64': + with self.l('if isinstance({variable}, str):'): + with self.l('try:'): + self.l('import base64') + self.l('{variable} = base64.b64decode({variable})') + with self.l('except Exception:'): + self.exc('{name} must be encoded by base64') + with self.l('if {variable} == "":'): + self.exc('contentEncoding must be base64') + + def generate_content_media_type(self): + """ + Means loading value when it's specified as JSON. + + .. code-block:: python + + { + 'contentMediaType': 'application/json', + } + """ + if self._definition['contentMediaType'] == 'application/json': + with self.l('if isinstance({variable}, bytes):'): + with self.l('try:'): + self.l('{variable} = {variable}.decode("utf-8")') + with self.l('except Exception:'): + self.exc('{name} must encoded by utf8') + with self.l('if isinstance({variable}, str):'): + with self.l('try:'): + self.l('import json') + self.l('{variable} = json.loads({variable})') + with self.l('except Exception:'): + self.exc('{name} must be valid JSON') diff --git a/lib/python3.10/site-packages/fastjsonschema/exceptions.py b/lib/python3.10/site-packages/fastjsonschema/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..d2dddd6a106f021a4723c1e8f5953ccc09e55e1f --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/exceptions.py @@ -0,0 +1,51 @@ +import re + + +SPLIT_RE = re.compile(r'[\.\[\]]+') + + +class JsonSchemaException(ValueError): + """ + Base exception of ``fastjsonschema`` library. + """ + + +class JsonSchemaValueException(JsonSchemaException): + """ + Exception raised by validation function. Available properties: + + * ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``), + * invalid ``value`` (e.g. ``60``), + * ``name`` of a path in the data structure (e.g. ``data.property[index]``), + * ``path`` as an array in the data structure (e.g. ``['data', 'property', 'index']``), + * the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``), + * ``rule`` which the ``value`` is breaking (e.g. ``maximum``) + * and ``rule_definition`` (e.g. ``42``). + + .. versionchanged:: 2.14.0 + Added all extra properties. + """ + + def __init__(self, message, value=None, name=None, definition=None, rule=None): + super().__init__(message) + self.message = message + self.value = value + self.name = name + self.definition = definition + self.rule = rule + + @property + def path(self): + return [item for item in SPLIT_RE.split(self.name) if item != ''] + + @property + def rule_definition(self): + if not self.rule or not self.definition: + return None + return self.definition.get(self.rule) + + +class JsonSchemaDefinitionException(JsonSchemaException): + """ + Exception raised by generator of validation function. + """ diff --git a/lib/python3.10/site-packages/fastjsonschema/generator.py b/lib/python3.10/site-packages/fastjsonschema/generator.py new file mode 100644 index 0000000000000000000000000000000000000000..64092f753de0c2882dc3d11a89f85b7bfc63193c --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/generator.py @@ -0,0 +1,353 @@ +from collections import OrderedDict +from decimal import Decimal +import re + +from .exceptions import JsonSchemaValueException, JsonSchemaDefinitionException +from .indent import indent +from .ref_resolver import RefResolver + + +def enforce_list(variable): + if isinstance(variable, list): + return variable + return [variable] + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class CodeGenerator: + """ + This class is not supposed to be used directly. Anything + inside of this class can be changed without noticing. + + This class generates code of validation function from JSON + schema object as string. Example: + + .. code-block:: python + + CodeGenerator(json_schema_definition).func_code + """ + + INDENT = 4 # spaces + + def __init__(self, definition, resolver=None, detailed_exceptions=True): + self._code = [] + self._compile_regexps = {} + self._custom_formats = {} + self._detailed_exceptions = detailed_exceptions + + # Any extra library should be here to be imported only once. + # Lines are imports to be printed in the file and objects + # key-value pair to pass to compile function directly. + self._extra_imports_lines = [ + "from decimal import Decimal", + ] + self._extra_imports_objects = { + "Decimal": Decimal, + } + + self._variables = set() + self._indent = 0 + self._indent_last_line = None + self._variable = None + self._variable_name = None + self._root_definition = definition + self._definition = None + + # map schema URIs to validation function names for functions + # that are not yet generated, but need to be generated + self._needed_validation_functions = {} + # validation function names that are already done + self._validation_functions_done = set() + + if resolver is None: + resolver = RefResolver.from_schema(definition, store={}) + self._resolver = resolver + + # add main function to `self._needed_validation_functions` + self._needed_validation_functions[self._resolver.get_uri()] = self._resolver.get_scope_name() + + self._json_keywords_to_function = OrderedDict() + + @property + def func_code(self): + """ + Returns generated code of whole validation function as string. + """ + self._generate_func_code() + + return '\n'.join(self._code) + + @property + def global_state(self): + """ + Returns global variables for generating function from ``func_code``. Includes + compiled regular expressions and imports, so it does not have to do it every + time when validation function is called. + """ + self._generate_func_code() + + return dict( + **self._extra_imports_objects, + REGEX_PATTERNS=self._compile_regexps, + re=re, + JsonSchemaValueException=JsonSchemaValueException, + ) + + @property + def global_state_code(self): + """ + Returns global variables for generating function from ``func_code`` as code. + Includes compiled regular expressions and imports. + """ + self._generate_func_code() + + if not self._compile_regexps: + return '\n'.join(self._extra_imports_lines + [ + 'from fastjsonschema import JsonSchemaValueException', + '', + '', + ]) + return '\n'.join(self._extra_imports_lines + [ + 'import re', + 'from fastjsonschema import JsonSchemaValueException', + '', + '', + 'REGEX_PATTERNS = ' + serialize_regexes(self._compile_regexps), + '', + ]) + + + def _generate_func_code(self): + if not self._code: + self.generate_func_code() + + def generate_func_code(self): + """ + Creates base code of validation function and calls helper + for creating code by definition. + """ + self.l('NoneType = type(None)') + # Generate parts that are referenced and not yet generated + while self._needed_validation_functions: + # During generation of validation function, could be needed to generate + # new one that is added again to `_needed_validation_functions`. + # Therefore usage of while instead of for loop. + uri, name = self._needed_validation_functions.popitem() + self.generate_validation_function(uri, name) + + def generate_validation_function(self, uri, name): + """ + Generate validation function for given uri with given name + """ + self._validation_functions_done.add(uri) + self.l('') + with self._resolver.resolving(uri) as definition: + with self.l('def {}(data, custom_formats={{}}, name_prefix=None):', name): + self.generate_func_code_block(definition, 'data', 'data', clear_variables=True) + self.l('return data') + + def generate_func_code_block(self, definition, variable, variable_name, clear_variables=False): + """ + Creates validation rules for current definition. + + Returns the number of validation rules generated as code. + """ + backup = self._definition, self._variable, self._variable_name + self._definition, self._variable, self._variable_name = definition, variable, variable_name + if clear_variables: + backup_variables = self._variables + self._variables = set() + + count = self._generate_func_code_block(definition) + + self._definition, self._variable, self._variable_name = backup + if clear_variables: + self._variables = backup_variables + + return count + + def _generate_func_code_block(self, definition): + if not isinstance(definition, dict): + raise JsonSchemaDefinitionException("definition must be an object") + if '$ref' in definition: + # needed because ref overrides any sibling keywords + return self.generate_ref() + else: + return self.run_generate_functions(definition) + + def run_generate_functions(self, definition): + """Returns the number of generate functions that were executed.""" + count = 0 + for key, func in self._json_keywords_to_function.items(): + if key in definition: + func() + count += 1 + return count + + def generate_ref(self): + """ + Ref can be link to remote or local definition. + + .. code-block:: python + + {'$ref': 'http://json-schema.org/draft-04/schema#'} + { + 'properties': { + 'foo': {'type': 'integer'}, + 'bar': {'$ref': '#/properties/foo'} + } + } + """ + with self._resolver.in_scope(self._definition['$ref']): + name = self._resolver.get_scope_name() + uri = self._resolver.get_uri() + if uri not in self._validation_functions_done: + self._needed_validation_functions[uri] = name + # call validation function + assert self._variable_name.startswith("data") + path = self._variable_name[4:] + name_arg = '(name_prefix or "data") + "{}"'.format(path) + if '{' in name_arg: + name_arg = name_arg + '.format(**locals())' + self.l('{}({variable}, custom_formats, {name_arg})', name, name_arg=name_arg) + + + # pylint: disable=invalid-name + @indent + def l(self, line, *args, **kwds): + """ + Short-cut of line. Used for inserting line. It's formated with parameters + ``variable``, ``variable_name`` (as ``name`` for short-cut), all keys from + current JSON schema ``definition`` and also passed arguments in ``args`` + and named ``kwds``. + + .. code-block:: python + + self.l('if {variable} not in {enum}: raise JsonSchemaValueException("Wrong!")') + + When you want to indent block, use it as context manager. For example: + + .. code-block:: python + + with self.l('if {variable} not in {enum}:'): + self.l('raise JsonSchemaValueException("Wrong!")') + """ + spaces = ' ' * self.INDENT * self._indent + + name = self._variable_name + if name: + # Add name_prefix to the name when it is being outputted. + assert name.startswith('data') + name = '" + (name_prefix or "data") + "' + name[4:] + if '{' in name: + name = name + '".format(**locals()) + "' + + context = dict( + self._definition if self._definition and self._definition is not True else {}, + variable=self._variable, + name=name, + **kwds + ) + line = line.format(*args, **context) + line = line.replace('\n', '\\n').replace('\r', '\\r') + self._code.append(spaces + line) + return line + + def e(self, string): + """ + Short-cut of escape. Used for inserting user values into a string message. + + .. code-block:: python + + self.l('raise JsonSchemaValueException("Variable: {}")', self.e(variable)) + """ + return str(string).replace('"', '\\"') + + def exc(self, msg, *args, append_to_msg=None, rule=None): + """ + Short-cut for creating raising exception in the code. + """ + if not self._detailed_exceptions: + self.l('raise JsonSchemaValueException("'+msg+'")', *args) + return + + arg = '"'+msg+'"' + if append_to_msg: + arg += ' + (' + append_to_msg + ')' + msg = 'raise JsonSchemaValueException('+arg+', value={variable}, name="{name}", definition={definition}, rule={rule})' + definition = self._expand_refs(self._definition) + definition_rule = self.e(definition.get(rule) if isinstance(definition, dict) else None) + self.l(msg, *args, definition=repr(definition), rule=repr(rule), definition_rule=definition_rule) + + def _expand_refs(self, definition): + if isinstance(definition, list): + return [self._expand_refs(v) for v in definition] + if not isinstance(definition, dict): + return definition + if "$ref" in definition and isinstance(definition["$ref"], str): + with self._resolver.resolving(definition["$ref"]) as schema: + return schema + return {k: self._expand_refs(v) for k, v in definition.items()} + + def create_variable_with_length(self): + """ + Append code for creating variable with length of that variable + (for example length of list or dictionary) with name ``{variable}_len``. + It can be called several times and always it's done only when that variable + still does not exists. + """ + variable_name = '{}_len'.format(self._variable) + if variable_name in self._variables: + return + self._variables.add(variable_name) + self.l('{variable}_len = len({variable})') + + def create_variable_keys(self): + """ + Append code for creating variable with keys of that variable (dictionary) + with a name ``{variable}_keys``. Similar to `create_variable_with_length`. + """ + variable_name = '{}_keys'.format(self._variable) + if variable_name in self._variables: + return + self._variables.add(variable_name) + self.l('{variable}_keys = set({variable}.keys())') + + def create_variable_is_list(self): + """ + Append code for creating variable with bool if it's instance of list + with a name ``{variable}_is_list``. Similar to `create_variable_with_length`. + """ + variable_name = '{}_is_list'.format(self._variable) + if variable_name in self._variables: + return + self._variables.add(variable_name) + self.l('{variable}_is_list = isinstance({variable}, (list, tuple))') + + def create_variable_is_dict(self): + """ + Append code for creating variable with bool if it's instance of list + with a name ``{variable}_is_dict``. Similar to `create_variable_with_length`. + """ + variable_name = '{}_is_dict'.format(self._variable) + if variable_name in self._variables: + return + self._variables.add(variable_name) + self.l('{variable}_is_dict = isinstance({variable}, dict)') + + +def serialize_regexes(patterns_dict): + # Unfortunately using `pprint.pformat` is causing errors + # specially with big regexes + regex_patterns = ( + repr(k) + ": " + repr_regex(v) + for k, v in patterns_dict.items() + ) + return '{\n ' + ",\n ".join(regex_patterns) + "\n}" + + +def repr_regex(regex): + all_flags = ("A", "I", "DEBUG", "L", "M", "S", "X") + flags = " | ".join(f"re.{f}" for f in all_flags if regex.flags & getattr(re, f)) + flags = ", " + flags if flags else "" + return "re.compile({!r}{})".format(regex.pattern, flags) diff --git a/lib/python3.10/site-packages/fastjsonschema/indent.py b/lib/python3.10/site-packages/fastjsonschema/indent.py new file mode 100644 index 0000000000000000000000000000000000000000..411c69f6fa56712317877a3af033bccae9c09dd6 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/indent.py @@ -0,0 +1,28 @@ +def indent(func): + """ + Decorator for allowing to use method as normal method or with + context manager for auto-indenting code blocks. + """ + def wrapper(self, line, *args, optimize=True, **kwds): + last_line = self._indent_last_line + line = func(self, line, *args, **kwds) + # When two blocks have the same condition (such as value has to be dict), + # do the check only once and keep it under one block. + if optimize and last_line == line: + self._code.pop() + self._indent_last_line = line + return Indent(self, line) + return wrapper + + +class Indent: + def __init__(self, instance, line): + self.instance = instance + self.line = line + + def __enter__(self): + self.instance._indent += 1 + + def __exit__(self, type_, value, traceback): + self.instance._indent -= 1 + self.instance._indent_last_line = self.line diff --git a/lib/python3.10/site-packages/fastjsonschema/ref_resolver.py b/lib/python3.10/site-packages/fastjsonschema/ref_resolver.py new file mode 100644 index 0000000000000000000000000000000000000000..431930226f867597c763e4a21d6a1ceaef08ab1a --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/ref_resolver.py @@ -0,0 +1,178 @@ +""" +JSON Schema URI resolution scopes and dereferencing + +https://tools.ietf.org/id/draft-zyp-json-schema-04.html#rfc.section.7 + +Code adapted from https://github.com/Julian/jsonschema +""" + +import contextlib +import json +import re +from urllib import parse as urlparse +from urllib.parse import unquote + +from .exceptions import JsonSchemaDefinitionException + + +def get_id(schema): + """ + Originally ID was `id` and since v7 it's `$id`. + """ + return schema.get('$id', schema.get('id', '')) + + +def resolve_path(schema, fragment): + """ + Return definition from path. + + Path is unescaped according https://tools.ietf.org/html/rfc6901 + """ + fragment = fragment.lstrip('/') + parts = unquote(fragment).split('/') if fragment else [] + for part in parts: + part = part.replace('~1', '/').replace('~0', '~') + if isinstance(schema, list): + schema = schema[int(part)] + elif part in schema: + schema = schema[part] + else: + raise JsonSchemaDefinitionException('Unresolvable ref: {}'.format(part)) + return schema + + +def normalize(uri): + return urlparse.urlsplit(uri).geturl() + + +def resolve_remote(uri, handlers): + """ + Resolve a remote ``uri``. + + .. note:: + + urllib library is used to fetch requests from the remote ``uri`` + if handlers does notdefine otherwise. + """ + scheme = urlparse.urlsplit(uri).scheme + if scheme in handlers: + result = handlers[scheme](uri) + else: + from urllib.request import urlopen + + req = urlopen(uri) + encoding = req.info().get_content_charset() or 'utf-8' + try: + result = json.loads(req.read().decode(encoding),) + except ValueError as exc: + raise JsonSchemaDefinitionException('{} failed to decode: {}'.format(uri, exc)) + finally: + req.close() + return result + + +class RefResolver: + """ + Resolve JSON References. + """ + + # pylint: disable=dangerous-default-value,too-many-arguments + def __init__(self, base_uri, schema, store={}, cache=True, handlers={}): + """ + `base_uri` is URI of the referring document from the `schema`. + `store` is an dictionary that will be used to cache the fetched schemas + (if `cache=True`). + + Please notice that you can have caching problems when compiling schemas + with colliding `$ref`. To force overwriting use `cache=False` or + explicitly pass the `store` argument (with a brand new dictionary) + """ + self.base_uri = base_uri + self.resolution_scope = base_uri + self.schema = schema + self.store = store + self.cache = cache + self.handlers = handlers + self.walk(schema) + + @classmethod + def from_schema(cls, schema, handlers={}, **kwargs): + """ + Construct a resolver from a JSON schema object. + """ + return cls( + get_id(schema) if isinstance(schema, dict) else '', + schema, + handlers=handlers, + **kwargs + ) + + @contextlib.contextmanager + def in_scope(self, scope: str): + """ + Context manager to handle current scope. + """ + old_scope = self.resolution_scope + self.resolution_scope = urlparse.urljoin(old_scope, scope) + try: + yield + finally: + self.resolution_scope = old_scope + + @contextlib.contextmanager + def resolving(self, ref: str): + """ + Context manager which resolves a JSON ``ref`` and enters the + resolution scope of this ref. + """ + new_uri = urlparse.urljoin(self.resolution_scope, ref) + uri, fragment = urlparse.urldefrag(new_uri) + + if uri and normalize(uri) in self.store: + schema = self.store[normalize(uri)] + elif not uri or uri == self.base_uri: + schema = self.schema + else: + schema = resolve_remote(uri, self.handlers) + if self.cache: + self.store[normalize(uri)] = schema + + old_base_uri, old_schema = self.base_uri, self.schema + self.base_uri, self.schema = uri, schema + try: + with self.in_scope(uri): + yield resolve_path(schema, fragment) + finally: + self.base_uri, self.schema = old_base_uri, old_schema + + def get_uri(self): + return normalize(self.resolution_scope) + + def get_scope_name(self): + """ + Get current scope and return it as a valid function name. + """ + name = 'validate_' + unquote(self.resolution_scope).replace('~1', '_').replace('~0', '_').replace('"', '') + name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '_', name) + name = name.lower().rstrip('_') + return name + + def walk(self, node: dict): + """ + Walk thru schema and dereferencing ``id`` and ``$ref`` instances + """ + if isinstance(node, bool): + pass + elif '$ref' in node and isinstance(node['$ref'], str): + ref = node['$ref'] + node['$ref'] = urlparse.urljoin(self.resolution_scope, ref) + elif ('$id' in node or 'id' in node) and isinstance(get_id(node), str): + with self.in_scope(get_id(node)): + self.store[normalize(self.resolution_scope)] = node + for _, item in node.items(): + if isinstance(item, dict): + self.walk(item) + else: + for _, item in node.items(): + if isinstance(item, dict): + self.walk(item) diff --git a/lib/python3.10/site-packages/fastjsonschema/version.py b/lib/python3.10/site-packages/fastjsonschema/version.py new file mode 100644 index 0000000000000000000000000000000000000000..70f17b25c3ced8a9db51bed74a846004b4450899 --- /dev/null +++ b/lib/python3.10/site-packages/fastjsonschema/version.py @@ -0,0 +1 @@ +VERSION = '2.21.1' diff --git a/lib/python3.10/site-packages/fontTools/__init__.py b/lib/python3.10/site-packages/fontTools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d2ff98f6a6858469595ab525c777aff354adeb77 --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/__init__.py @@ -0,0 +1,8 @@ +import logging +from fontTools.misc.loggingTools import configLogger + +log = logging.getLogger(__name__) + +version = __version__ = "4.59.0" + +__all__ = ["version", "log", "configLogger"] diff --git a/lib/python3.10/site-packages/fontTools/__main__.py b/lib/python3.10/site-packages/fontTools/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c74ad3c86e54cb7e9939ed2bf96aa59cc6dcd06 --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/__main__.py @@ -0,0 +1,35 @@ +import sys + + +def main(args=None): + if args is None: + args = sys.argv[1:] + + # TODO Handle library-wide options. Eg.: + # --unicodedata + # --verbose / other logging stuff + + # TODO Allow a way to run arbitrary modules? Useful for setting + # library-wide options and calling another library. Eg.: + # + # $ fonttools --unicodedata=... fontmake ... + # + # This allows for a git-like command where thirdparty commands + # can be added. Should we just try importing the fonttools + # module first and try without if it fails? + + if len(sys.argv) < 2: + sys.argv.append("help") + if sys.argv[1] == "-h" or sys.argv[1] == "--help": + sys.argv[1] = "help" + mod = "fontTools." + sys.argv[1] + sys.argv[1] = sys.argv[0] + " " + sys.argv[1] + del sys.argv[0] + + import runpy + + runpy.run_module(mod, run_name="__main__") + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/lib/python3.10/site-packages/fontTools/fontBuilder.py b/lib/python3.10/site-packages/fontTools/fontBuilder.py new file mode 100644 index 0000000000000000000000000000000000000000..f8da717babb810f91d5adae99435cc355105815d --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/fontBuilder.py @@ -0,0 +1,1014 @@ +__all__ = ["FontBuilder"] + +""" +This module is *experimental*, meaning it still may evolve and change. + +The `FontBuilder` class is a convenient helper to construct working TTF or +OTF fonts from scratch. + +Note that the various setup methods cannot be called in arbitrary order, +due to various interdependencies between OpenType tables. Here is an order +that works: + + fb = FontBuilder(...) + fb.setupGlyphOrder(...) + fb.setupCharacterMap(...) + fb.setupGlyf(...) --or-- fb.setupCFF(...) + fb.setupHorizontalMetrics(...) + fb.setupHorizontalHeader() + fb.setupNameTable(...) + fb.setupOS2() + fb.addOpenTypeFeatures(...) + fb.setupPost() + fb.save(...) + +Here is how to build a minimal TTF: + +```python +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.ttGlyphPen import TTGlyphPen + + +def drawTestGlyph(pen): + pen.moveTo((100, 100)) + pen.lineTo((100, 1000)) + pen.qCurveTo((200, 900), (400, 900), (500, 1000)) + pen.lineTo((500, 100)) + pen.closePath() + + +fb = FontBuilder(1024, isTTF=True) +fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"]) +fb.setupCharacterMap({32: "space", 65: "A", 97: "a"}) +advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0} + +familyName = "HelloTestFont" +styleName = "TotallyNormal" +version = "0.1" + +nameStrings = dict( + familyName=dict(en=familyName, nl="HalloTestFont"), + styleName=dict(en=styleName, nl="TotaalNormaal"), + uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName, + fullName=familyName + "-" + styleName, + psName=familyName + "-" + styleName, + version="Version " + version, +) + +pen = TTGlyphPen(None) +drawTestGlyph(pen) +glyph = pen.glyph() +glyphs = {".notdef": glyph, "space": glyph, "A": glyph, "a": glyph, ".null": glyph} +fb.setupGlyf(glyphs) +metrics = {} +glyphTable = fb.font["glyf"] +for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, glyphTable[gn].xMin) +fb.setupHorizontalMetrics(metrics) +fb.setupHorizontalHeader(ascent=824, descent=-200) +fb.setupNameTable(nameStrings) +fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200) +fb.setupPost() +fb.save("test.ttf") +``` + +And here's how to build a minimal OTF: + +```python +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.t2CharStringPen import T2CharStringPen + + +def drawTestGlyph(pen): + pen.moveTo((100, 100)) + pen.lineTo((100, 1000)) + pen.curveTo((200, 900), (400, 900), (500, 1000)) + pen.lineTo((500, 100)) + pen.closePath() + + +fb = FontBuilder(1024, isTTF=False) +fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"]) +fb.setupCharacterMap({32: "space", 65: "A", 97: "a"}) +advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0} + +familyName = "HelloTestFont" +styleName = "TotallyNormal" +version = "0.1" + +nameStrings = dict( + familyName=dict(en=familyName, nl="HalloTestFont"), + styleName=dict(en=styleName, nl="TotaalNormaal"), + uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName, + fullName=familyName + "-" + styleName, + psName=familyName + "-" + styleName, + version="Version " + version, +) + +pen = T2CharStringPen(600, None) +drawTestGlyph(pen) +charString = pen.getCharString() +charStrings = { + ".notdef": charString, + "space": charString, + "A": charString, + "a": charString, + ".null": charString, +} +fb.setupCFF(nameStrings["psName"], {"FullName": nameStrings["psName"]}, charStrings, {}) +lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()} +metrics = {} +for gn, advanceWidth in advanceWidths.items(): + metrics[gn] = (advanceWidth, lsb[gn]) +fb.setupHorizontalMetrics(metrics) +fb.setupHorizontalHeader(ascent=824, descent=200) +fb.setupNameTable(nameStrings) +fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200) +fb.setupPost() +fb.save("test.otf") +``` +""" + +from .ttLib import TTFont, newTable +from .ttLib.tables._c_m_a_p import cmap_classes +from .ttLib.tables._g_l_y_f import flagCubic +from .ttLib.tables.O_S_2f_2 import Panose +from .misc.timeTools import timestampNow +import struct +from collections import OrderedDict + + +_headDefaults = dict( + tableVersion=1.0, + fontRevision=1.0, + checkSumAdjustment=0, + magicNumber=0x5F0F3CF5, + flags=0x0003, + unitsPerEm=1000, + created=0, + modified=0, + xMin=0, + yMin=0, + xMax=0, + yMax=0, + macStyle=0, + lowestRecPPEM=3, + fontDirectionHint=2, + indexToLocFormat=0, + glyphDataFormat=0, +) + +_maxpDefaultsTTF = dict( + tableVersion=0x00010000, + numGlyphs=0, + maxPoints=0, + maxContours=0, + maxCompositePoints=0, + maxCompositeContours=0, + maxZones=2, + maxTwilightPoints=0, + maxStorage=0, + maxFunctionDefs=0, + maxInstructionDefs=0, + maxStackElements=0, + maxSizeOfInstructions=0, + maxComponentElements=0, + maxComponentDepth=0, +) +_maxpDefaultsOTF = dict( + tableVersion=0x00005000, + numGlyphs=0, +) + +_postDefaults = dict( + formatType=3.0, + italicAngle=0, + underlinePosition=0, + underlineThickness=0, + isFixedPitch=0, + minMemType42=0, + maxMemType42=0, + minMemType1=0, + maxMemType1=0, +) + +_hheaDefaults = dict( + tableVersion=0x00010000, + ascent=0, + descent=0, + lineGap=0, + advanceWidthMax=0, + minLeftSideBearing=0, + minRightSideBearing=0, + xMaxExtent=0, + caretSlopeRise=1, + caretSlopeRun=0, + caretOffset=0, + reserved0=0, + reserved1=0, + reserved2=0, + reserved3=0, + metricDataFormat=0, + numberOfHMetrics=0, +) + +_vheaDefaults = dict( + tableVersion=0x00010000, + ascent=0, + descent=0, + lineGap=0, + advanceHeightMax=0, + minTopSideBearing=0, + minBottomSideBearing=0, + yMaxExtent=0, + caretSlopeRise=0, + caretSlopeRun=0, + reserved0=0, + reserved1=0, + reserved2=0, + reserved3=0, + reserved4=0, + metricDataFormat=0, + numberOfVMetrics=0, +) + +_nameIDs = dict( + copyright=0, + familyName=1, + styleName=2, + uniqueFontIdentifier=3, + fullName=4, + version=5, + psName=6, + trademark=7, + manufacturer=8, + designer=9, + description=10, + vendorURL=11, + designerURL=12, + licenseDescription=13, + licenseInfoURL=14, + # reserved = 15, + typographicFamily=16, + typographicSubfamily=17, + compatibleFullName=18, + sampleText=19, + postScriptCIDFindfontName=20, + wwsFamilyName=21, + wwsSubfamilyName=22, + lightBackgroundPalette=23, + darkBackgroundPalette=24, + variationsPostScriptNamePrefix=25, +) + +# to insert in setupNameTable doc string: +# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1]))) + +_panoseDefaults = Panose() + +_OS2Defaults = dict( + version=3, + xAvgCharWidth=0, + usWeightClass=400, + usWidthClass=5, + fsType=0x0004, # default: Preview & Print embedding + ySubscriptXSize=0, + ySubscriptYSize=0, + ySubscriptXOffset=0, + ySubscriptYOffset=0, + ySuperscriptXSize=0, + ySuperscriptYSize=0, + ySuperscriptXOffset=0, + ySuperscriptYOffset=0, + yStrikeoutSize=0, + yStrikeoutPosition=0, + sFamilyClass=0, + panose=_panoseDefaults, + ulUnicodeRange1=0, + ulUnicodeRange2=0, + ulUnicodeRange3=0, + ulUnicodeRange4=0, + achVendID="????", + fsSelection=0, + usFirstCharIndex=0, + usLastCharIndex=0, + sTypoAscender=0, + sTypoDescender=0, + sTypoLineGap=0, + usWinAscent=0, + usWinDescent=0, + ulCodePageRange1=0, + ulCodePageRange2=0, + sxHeight=0, + sCapHeight=0, + usDefaultChar=0, # .notdef + usBreakChar=32, # space + usMaxContext=0, + usLowerOpticalPointSize=0, + usUpperOpticalPointSize=0, +) + + +class FontBuilder(object): + def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0): + """Initialize a FontBuilder instance. + + If the `font` argument is not given, a new `TTFont` will be + constructed, and `unitsPerEm` must be given. If `isTTF` is True, + the font will be a glyf-based TTF; if `isTTF` is False it will be + a CFF-based OTF. + + The `glyphDataFormat` argument corresponds to the `head` table field + that defines the format of the TrueType `glyf` table (default=0). + TrueType glyphs historically can only contain quadratic splines and static + components, but there's a proposal to add support for cubic Bezier curves as well + as variable composites/components at + https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md + You can experiment with the new features by setting `glyphDataFormat` to 1. + A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added + that contain cubic splines or varcomposites. This is to prevent accidentally + creating fonts that are incompatible with existing TrueType implementations. + + If `font` is given, it must be a `TTFont` instance and `unitsPerEm` + must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored. + """ + if font is None: + self.font = TTFont(recalcTimestamp=False) + self.isTTF = isTTF + now = timestampNow() + assert unitsPerEm is not None + self.setupHead( + unitsPerEm=unitsPerEm, + created=now, + modified=now, + glyphDataFormat=glyphDataFormat, + ) + self.setupMaxp() + else: + assert unitsPerEm is None + self.font = font + self.isTTF = "glyf" in font + + def save(self, file): + """Save the font. The 'file' argument can be either a pathname or a + writable file object. + """ + self.font.save(file) + + def _initTableWithValues(self, tableTag, defaults, values): + table = self.font[tableTag] = newTable(tableTag) + for k, v in defaults.items(): + setattr(table, k, v) + for k, v in values.items(): + setattr(table, k, v) + return table + + def _updateTableWithValues(self, tableTag, values): + table = self.font[tableTag] + for k, v in values.items(): + setattr(table, k, v) + + def setupHead(self, **values): + """Create a new `head` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("head", _headDefaults, values) + + def updateHead(self, **values): + """Update the head table with the fields and values passed as + keyword arguments. + """ + self._updateTableWithValues("head", values) + + def setupGlyphOrder(self, glyphOrder): + """Set the glyph order for the font.""" + self.font.setGlyphOrder(glyphOrder) + + def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False): + """Build the `cmap` table for the font. The `cmapping` argument should + be a dict mapping unicode code points as integers to glyph names. + + The `uvs` argument, when passed, must be a list of tuples, describing + Unicode Variation Sequences. These tuples have three elements: + (unicodeValue, variationSelector, glyphName) + `unicodeValue` and `variationSelector` are integer code points. + `glyphName` may be None, to indicate this is the default variation. + Text processors will then use the cmap to find the glyph name. + Each Unicode Variation Sequence should be an officially supported + sequence, but this is not policed. + """ + subTables = [] + highestUnicode = max(cmapping) if cmapping else 0 + if highestUnicode > 0xFFFF: + cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000) + subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10) + subTables.append(subTable_3_10) + else: + cmapping_3_1 = cmapping + format = 4 + subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1) + try: + subTable_3_1.compile(self.font) + except struct.error: + # format 4 overflowed, fall back to format 12 + if not allowFallback: + raise ValueError( + "cmap format 4 subtable overflowed; sort glyph order by unicode to fix." + ) + format = 12 + subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1) + subTables.append(subTable_3_1) + subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3) + subTables.append(subTable_0_3) + + if uvs is not None: + uvsDict = {} + for unicodeValue, variationSelector, glyphName in uvs: + if cmapping.get(unicodeValue) == glyphName: + # this is a default variation + glyphName = None + if variationSelector not in uvsDict: + uvsDict[variationSelector] = [] + uvsDict[variationSelector].append((unicodeValue, glyphName)) + uvsSubTable = buildCmapSubTable({}, 14, 0, 5) + uvsSubTable.uvsDict = uvsDict + subTables.append(uvsSubTable) + + self.font["cmap"] = newTable("cmap") + self.font["cmap"].tableVersion = 0 + self.font["cmap"].tables = subTables + + def setupNameTable(self, nameStrings, windows=True, mac=True): + """Create the `name` table for the font. The `nameStrings` argument must + be a dict, mapping nameIDs or descriptive names for the nameIDs to name + record values. A value is either a string, or a dict, mapping language codes + to strings, to allow localized name table entries. + + By default, both Windows (platformID=3) and Macintosh (platformID=1) name + records are added, unless any of `windows` or `mac` arguments is False. + + The following descriptive names are available for nameIDs: + + copyright (nameID 0) + familyName (nameID 1) + styleName (nameID 2) + uniqueFontIdentifier (nameID 3) + fullName (nameID 4) + version (nameID 5) + psName (nameID 6) + trademark (nameID 7) + manufacturer (nameID 8) + designer (nameID 9) + description (nameID 10) + vendorURL (nameID 11) + designerURL (nameID 12) + licenseDescription (nameID 13) + licenseInfoURL (nameID 14) + typographicFamily (nameID 16) + typographicSubfamily (nameID 17) + compatibleFullName (nameID 18) + sampleText (nameID 19) + postScriptCIDFindfontName (nameID 20) + wwsFamilyName (nameID 21) + wwsSubfamilyName (nameID 22) + lightBackgroundPalette (nameID 23) + darkBackgroundPalette (nameID 24) + variationsPostScriptNamePrefix (nameID 25) + """ + nameTable = self.font["name"] = newTable("name") + nameTable.names = [] + + for nameName, nameValue in nameStrings.items(): + if isinstance(nameName, int): + nameID = nameName + else: + nameID = _nameIDs[nameName] + if isinstance(nameValue, str): + nameValue = dict(en=nameValue) + nameTable.addMultilingualName( + nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac + ) + + def setupOS2(self, **values): + """Create a new `OS/2` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("OS/2", _OS2Defaults, values) + if "xAvgCharWidth" not in values: + assert ( + "hmtx" in self.font + ), "the 'hmtx' table must be setup before the 'OS/2' table" + self.font["OS/2"].recalcAvgCharWidth(self.font) + if not ( + "ulUnicodeRange1" in values + or "ulUnicodeRange2" in values + or "ulUnicodeRange3" in values + or "ulUnicodeRange3" in values + ): + assert ( + "cmap" in self.font + ), "the 'cmap' table must be setup before the 'OS/2' table" + self.font["OS/2"].recalcUnicodeRanges(self.font) + + def setupCFF(self, psName, fontInfo, charStringsDict, privateDict): + from .cffLib import ( + CFFFontSet, + TopDictIndex, + TopDict, + CharStrings, + GlobalSubrsIndex, + PrivateDict, + ) + + assert not self.isTTF + self.font.sfntVersion = "OTTO" + fontSet = CFFFontSet() + fontSet.major = 1 + fontSet.minor = 0 + fontSet.otFont = self.font + fontSet.fontNames = [psName] + fontSet.topDictIndex = TopDictIndex() + + globalSubrs = GlobalSubrsIndex() + fontSet.GlobalSubrs = globalSubrs + private = PrivateDict() + for key, value in privateDict.items(): + setattr(private, key, value) + fdSelect = None + fdArray = None + + topDict = TopDict() + topDict.charset = self.font.getGlyphOrder() + topDict.Private = private + topDict.GlobalSubrs = fontSet.GlobalSubrs + for key, value in fontInfo.items(): + setattr(topDict, key, value) + if "FontMatrix" not in fontInfo: + scale = 1 / self.font["head"].unitsPerEm + topDict.FontMatrix = [scale, 0, 0, scale, 0, 0] + + charStrings = CharStrings( + None, topDict.charset, globalSubrs, private, fdSelect, fdArray + ) + for glyphName, charString in charStringsDict.items(): + charString.private = private + charString.globalSubrs = globalSubrs + charStrings[glyphName] = charString + topDict.CharStrings = charStrings + + fontSet.topDictIndex.append(topDict) + + self.font["CFF "] = newTable("CFF ") + self.font["CFF "].cff = fontSet + + def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None): + from .cffLib import ( + CFFFontSet, + TopDictIndex, + TopDict, + CharStrings, + GlobalSubrsIndex, + PrivateDict, + FDArrayIndex, + FontDict, + ) + + assert not self.isTTF + self.font.sfntVersion = "OTTO" + fontSet = CFFFontSet() + fontSet.major = 2 + fontSet.minor = 0 + + cff2GetGlyphOrder = self.font.getGlyphOrder + fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None) + + globalSubrs = GlobalSubrsIndex() + fontSet.GlobalSubrs = globalSubrs + + if fdArrayList is None: + fdArrayList = [{}] + fdSelect = None + fdArray = FDArrayIndex() + fdArray.strings = None + fdArray.GlobalSubrs = globalSubrs + for privateDict in fdArrayList: + fontDict = FontDict() + fontDict.setCFF2(True) + private = PrivateDict() + for key, value in privateDict.items(): + setattr(private, key, value) + fontDict.Private = private + fdArray.append(fontDict) + + topDict = TopDict() + topDict.cff2GetGlyphOrder = cff2GetGlyphOrder + topDict.FDArray = fdArray + scale = 1 / self.font["head"].unitsPerEm + topDict.FontMatrix = [scale, 0, 0, scale, 0, 0] + + private = fdArray[0].Private + charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray) + for glyphName, charString in charStringsDict.items(): + charString.private = private + charString.globalSubrs = globalSubrs + charStrings[glyphName] = charString + topDict.CharStrings = charStrings + + fontSet.topDictIndex.append(topDict) + + self.font["CFF2"] = newTable("CFF2") + self.font["CFF2"].cff = fontSet + + if regions: + self.setupCFF2Regions(regions) + + def setupCFF2Regions(self, regions): + from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore + from .cffLib import VarStoreData + + assert "fvar" in self.font, "fvar must to be set up first" + assert "CFF2" in self.font, "CFF2 must to be set up first" + axisTags = [a.axisTag for a in self.font["fvar"].axes] + varRegionList = buildVarRegionList(regions, axisTags) + varData = buildVarData(list(range(len(regions))), None, optimize=False) + varStore = buildVarStore(varRegionList, [varData]) + vstore = VarStoreData(otVarStore=varStore) + topDict = self.font["CFF2"].cff.topDictIndex[0] + topDict.VarStore = vstore + for fontDict in topDict.FDArray: + fontDict.Private.vstore = vstore + + def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True): + """Create the `glyf` table from a dict, that maps glyph names + to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example + as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`. + + If `calcGlyphBounds` is True, the bounds of all glyphs will be + calculated. Only pass False if your glyph objects already have + their bounding box values set. + + If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains + cubic curves or is a variable composite but head.glyphDataFormat=0. + Set it to False to skip the check if you know in advance all the glyphs are + compatible with the specified glyphDataFormat. + """ + assert self.isTTF + + if validateGlyphFormat and self.font["head"].glyphDataFormat == 0: + for name, g in glyphs.items(): + if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags): + raise ValueError( + f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; " + "either convert to quadratics with cu2qu or set glyphDataFormat=1." + ) + + self.font["loca"] = newTable("loca") + self.font["glyf"] = newTable("glyf") + self.font["glyf"].glyphs = glyphs + if hasattr(self.font, "glyphOrder"): + self.font["glyf"].glyphOrder = self.font.glyphOrder + if calcGlyphBounds: + self.calcGlyphBounds() + + def setupFvar(self, axes, instances): + """Adds an font variations table to the font. + + Args: + axes (list): See below. + instances (list): See below. + + ``axes`` should be a list of axes, with each axis either supplied as + a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the + format ```tupletag, minValue, defaultValue, maxValue, name``. + The ``name`` is either a string, or a dict, mapping language codes + to strings, to allow localized name table entries. + + ```instances`` should be a list of instances, with each instance either + supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a + dict with keys ``location`` (mapping of axis tags to float values), + ``stylename`` and (optionally) ``postscriptfontname``. + The ``stylename`` is either a string, or a dict, mapping language codes + to strings, to allow localized name table entries. + """ + + addFvar(self.font, axes, instances) + + def setupAvar(self, axes, mappings=None): + """Adds an axis variations table to the font. + + Args: + axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects. + """ + from .varLib import _add_avar + + if "fvar" not in self.font: + raise KeyError("'fvar' table is missing; can't add 'avar'.") + + axisTags = [axis.axisTag for axis in self.font["fvar"].axes] + axes = OrderedDict(enumerate(axes)) # Only values are used + _add_avar(self.font, axes, mappings, axisTags) + + def setupGvar(self, variations): + gvar = self.font["gvar"] = newTable("gvar") + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = variations + + def setupGVAR(self, variations): + gvar = self.font["GVAR"] = newTable("GVAR") + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = variations + + def calcGlyphBounds(self): + """Calculate the bounding boxes of all glyphs in the `glyf` table. + This is usually not called explicitly by client code. + """ + glyphTable = self.font["glyf"] + for glyph in glyphTable.glyphs.values(): + glyph.recalcBounds(glyphTable) + + def setupHorizontalMetrics(self, metrics): + """Create a new `hmtx` table, for horizontal metrics. + + The `metrics` argument must be a dict, mapping glyph names to + `(width, leftSidebearing)` tuples. + """ + self.setupMetrics("hmtx", metrics) + + def setupVerticalMetrics(self, metrics): + """Create a new `vmtx` table, for horizontal metrics. + + The `metrics` argument must be a dict, mapping glyph names to + `(height, topSidebearing)` tuples. + """ + self.setupMetrics("vmtx", metrics) + + def setupMetrics(self, tableTag, metrics): + """See `setupHorizontalMetrics()` and `setupVerticalMetrics()`.""" + assert tableTag in ("hmtx", "vmtx") + mtxTable = self.font[tableTag] = newTable(tableTag) + roundedMetrics = {} + for gn in metrics: + w, lsb = metrics[gn] + roundedMetrics[gn] = int(round(w)), int(round(lsb)) + mtxTable.metrics = roundedMetrics + + def setupHorizontalHeader(self, **values): + """Create a new `hhea` table initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("hhea", _hheaDefaults, values) + + def setupVerticalHeader(self, **values): + """Create a new `vhea` table initialize it with default values, + which can be overridden by keyword arguments. + """ + self._initTableWithValues("vhea", _vheaDefaults, values) + + def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None): + """Create a new `VORG` table. The `verticalOrigins` argument must be + a dict, mapping glyph names to vertical origin values. + + The `defaultVerticalOrigin` argument should be the most common vertical + origin value. If omitted, this value will be derived from the actual + values in the `verticalOrigins` argument. + """ + if defaultVerticalOrigin is None: + # find the most frequent vorg value + bag = {} + for gn in verticalOrigins: + vorg = verticalOrigins[gn] + if vorg not in bag: + bag[vorg] = 1 + else: + bag[vorg] += 1 + defaultVerticalOrigin = sorted( + bag, key=lambda vorg: bag[vorg], reverse=True + )[0] + self._initTableWithValues( + "VORG", + {}, + dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin), + ) + vorgTable = self.font["VORG"] + vorgTable.majorVersion = 1 + vorgTable.minorVersion = 0 + for gn in verticalOrigins: + vorgTable[gn] = verticalOrigins[gn] + + def setupPost(self, keepGlyphNames=True, **values): + """Create a new `post` table and initialize it with default values, + which can be overridden by keyword arguments. + """ + isCFF2 = "CFF2" in self.font + postTable = self._initTableWithValues("post", _postDefaults, values) + if (self.isTTF or isCFF2) and keepGlyphNames: + postTable.formatType = 2.0 + postTable.extraNames = [] + postTable.mapping = {} + else: + postTable.formatType = 3.0 + + def setupMaxp(self): + """Create a new `maxp` table. This is called implicitly by FontBuilder + itself and is usually not called by client code. + """ + if self.isTTF: + defaults = _maxpDefaultsTTF + else: + defaults = _maxpDefaultsOTF + self._initTableWithValues("maxp", defaults, {}) + + def setupDummyDSIG(self): + """This adds an empty DSIG table to the font to make some MS applications + happy. This does not properly sign the font. + """ + values = dict( + ulVersion=1, + usFlag=0, + usNumSigs=0, + signatureRecords=[], + ) + self._initTableWithValues("DSIG", {}, values) + + def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False): + """Add OpenType features to the font from a string containing + Feature File syntax. + + The `filename` argument is used in error messages and to determine + where to look for "include" files. + + The optional `tables` argument can be a list of OTL tables tags to + build, allowing the caller to only build selected OTL tables. See + `fontTools.feaLib` for details. + + The optional `debug` argument controls whether to add source debugging + information to the font in the `Debg` table. + """ + from .feaLib.builder import addOpenTypeFeaturesFromString + + addOpenTypeFeaturesFromString( + self.font, features, filename=filename, tables=tables, debug=debug + ) + + def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"): + """Add conditional substitutions to a Variable Font. + + See `fontTools.varLib.featureVars.addFeatureVariations`. + """ + from .varLib import featureVars + + if "fvar" not in self.font: + raise KeyError("'fvar' table is missing; can't add FeatureVariations.") + + featureVars.addFeatureVariations( + self.font, conditionalSubstitutions, featureTag=featureTag + ) + + def setupCOLR( + self, + colorLayers, + version=None, + varStore=None, + varIndexMap=None, + clipBoxes=None, + allowLayerReuse=True, + ): + """Build new COLR table using color layers dictionary. + + Cf. `fontTools.colorLib.builder.buildCOLR`. + """ + from fontTools.colorLib.builder import buildCOLR + + glyphMap = self.font.getReverseGlyphMap() + self.font["COLR"] = buildCOLR( + colorLayers, + version=version, + glyphMap=glyphMap, + varStore=varStore, + varIndexMap=varIndexMap, + clipBoxes=clipBoxes, + allowLayerReuse=allowLayerReuse, + ) + + def setupCPAL( + self, + palettes, + paletteTypes=None, + paletteLabels=None, + paletteEntryLabels=None, + ): + """Build new CPAL table using list of palettes. + + Optionally build CPAL v1 table using paletteTypes, paletteLabels and + paletteEntryLabels. + + Cf. `fontTools.colorLib.builder.buildCPAL`. + """ + from fontTools.colorLib.builder import buildCPAL + + self.font["CPAL"] = buildCPAL( + palettes, + paletteTypes=paletteTypes, + paletteLabels=paletteLabels, + paletteEntryLabels=paletteEntryLabels, + nameTable=self.font.get("name"), + ) + + def setupStat(self, axes, locations=None, elidedFallbackName=2): + """Build a new 'STAT' table. + + See `fontTools.otlLib.builder.buildStatTable` for details about + the arguments. + """ + from .otlLib.builder import buildStatTable + + assert "name" in self.font, "name must to be set up first" + + buildStatTable( + self.font, + axes, + locations, + elidedFallbackName, + macNames=any(nr.platformID == 1 for nr in self.font["name"].names), + ) + + +def buildCmapSubTable(cmapping, format, platformID, platEncID): + subTable = cmap_classes[format](format) + subTable.cmap = cmapping + subTable.platformID = platformID + subTable.platEncID = platEncID + subTable.language = 0 + return subTable + + +def addFvar(font, axes, instances): + from .ttLib.tables._f_v_a_r import Axis, NamedInstance + + assert axes + + fvar = newTable("fvar") + nameTable = font["name"] + + # if there are not currently any mac names don't add them here, that's inconsistent + # https://github.com/fonttools/fonttools/issues/683 + macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ())) + + # we have all the best ways to express mac names + platforms = ((3, 1, 0x409),) + if macNames: + platforms = ((1, 0, 0),) + platforms + + for axis_def in axes: + axis = Axis() + + if isinstance(axis_def, tuple): + ( + axis.axisTag, + axis.minValue, + axis.defaultValue, + axis.maxValue, + name, + ) = axis_def + else: + (axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = ( + axis_def.tag, + axis_def.minimum, + axis_def.default, + axis_def.maximum, + axis_def.name, + ) + if axis_def.hidden: + axis.flags = 0x0001 # HIDDEN_AXIS + + if isinstance(name, str): + name = dict(en=name) + + axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font, mac=macNames) + fvar.axes.append(axis) + + for instance in instances: + if isinstance(instance, dict): + coordinates = instance["location"] + name = instance["stylename"] + psname = instance.get("postscriptfontname") + else: + coordinates = instance.location + name = instance.localisedStyleName or instance.styleName + psname = instance.postScriptFontName + + if isinstance(name, str): + name = dict(en=name) + + inst = NamedInstance() + inst.subfamilyNameID = nameTable.addMultilingualName( + name, ttFont=font, mac=macNames + ) + if psname is not None: + inst.postscriptNameID = nameTable.addName(psname, platforms=platforms) + inst.coordinates = coordinates + fvar.instances.append(inst) + + font["fvar"] = fvar diff --git a/lib/python3.10/site-packages/fontTools/help.py b/lib/python3.10/site-packages/fontTools/help.py new file mode 100644 index 0000000000000000000000000000000000000000..4331a26ddefa2e34db97cfe5a78bac0f989ea72d --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/help.py @@ -0,0 +1,36 @@ +import pkgutil +import sys +import fontTools +import importlib +import os +from pathlib import Path + + +def main(): + """Show this help""" + path = fontTools.__path__ + descriptions = {} + for pkg in sorted( + mod.name + for mod in pkgutil.walk_packages([fontTools.__path__[0]], prefix="fontTools.") + ): + try: + imports = __import__(pkg, globals(), locals(), ["main"]) + except ImportError as e: + continue + try: + description = imports.main.__doc__ + # Cython modules seem to return "main()" as the docstring + if description and description != "main()": + pkg = pkg.replace("fontTools.", "").replace(".__main__", "") + # show the docstring's first line only + descriptions[pkg] = description.splitlines()[0] + except AttributeError as e: + pass + for pkg, description in descriptions.items(): + print("fonttools %-25s %s" % (pkg, description), file=sys.stderr) + + +if __name__ == "__main__": + print("fonttools v%s\n" % fontTools.__version__, file=sys.stderr) + main() diff --git a/lib/python3.10/site-packages/fontTools/tfmLib.py b/lib/python3.10/site-packages/fontTools/tfmLib.py new file mode 100644 index 0000000000000000000000000000000000000000..673373ffdf4825d4caac4ce5959eb0ee9e11046c --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/tfmLib.py @@ -0,0 +1,460 @@ +"""Module for reading TFM (TeX Font Metrics) files. + +The TFM format is described in the TFtoPL WEB source code, whose typeset form +can be found on `CTAN `_. + + >>> from fontTools.tfmLib import TFM + >>> tfm = TFM("Tests/tfmLib/data/cmr10.tfm") + >>> + >>> # Accessing an attribute gets you metadata. + >>> tfm.checksum + 1274110073 + >>> tfm.designsize + 10.0 + >>> tfm.codingscheme + 'TeX text' + >>> tfm.family + 'CMR' + >>> tfm.seven_bit_safe_flag + False + >>> tfm.face + 234 + >>> tfm.extraheader + {} + >>> tfm.fontdimens + {'SLANT': 0.0, 'SPACE': 0.33333396911621094, 'STRETCH': 0.16666698455810547, 'SHRINK': 0.11111164093017578, 'XHEIGHT': 0.4305553436279297, 'QUAD': 1.0000028610229492, 'EXTRASPACE': 0.11111164093017578} + >>> # Accessing a character gets you its metrics. + >>> # “width” is always available, other metrics are available only when + >>> # applicable. All values are relative to “designsize”. + >>> tfm.chars[ord("g")] + {'width': 0.5000019073486328, 'height': 0.4305553436279297, 'depth': 0.1944446563720703, 'italic': 0.013888359069824219} + >>> # Kerning and ligature can be accessed as well. + >>> tfm.kerning[ord("c")] + {104: -0.02777862548828125, 107: -0.02777862548828125} + >>> tfm.ligatures[ord("f")] + {105: ('LIG', 12), 102: ('LIG', 11), 108: ('LIG', 13)} +""" + +from types import SimpleNamespace + +from fontTools.misc.sstruct import calcsize, unpack, unpack2 + +SIZES_FORMAT = """ + > + lf: h # length of the entire file, in words + lh: h # length of the header data, in words + bc: h # smallest character code in the font + ec: h # largest character code in the font + nw: h # number of words in the width table + nh: h # number of words in the height table + nd: h # number of words in the depth table + ni: h # number of words in the italic correction table + nl: h # number of words in the ligature/kern table + nk: h # number of words in the kern table + ne: h # number of words in the extensible character table + np: h # number of font parameter words +""" + +SIZES_SIZE = calcsize(SIZES_FORMAT) + +FIXED_FORMAT = "12.20F" + +HEADER_FORMAT1 = f""" + > + checksum: L + designsize: {FIXED_FORMAT} +""" + +HEADER_FORMAT2 = f""" + {HEADER_FORMAT1} + codingscheme: 40p +""" + +HEADER_FORMAT3 = f""" + {HEADER_FORMAT2} + family: 20p +""" + +HEADER_FORMAT4 = f""" + {HEADER_FORMAT3} + seven_bit_safe_flag: ? + ignored: x + ignored: x + face: B +""" + +HEADER_SIZE1 = calcsize(HEADER_FORMAT1) +HEADER_SIZE2 = calcsize(HEADER_FORMAT2) +HEADER_SIZE3 = calcsize(HEADER_FORMAT3) +HEADER_SIZE4 = calcsize(HEADER_FORMAT4) + +LIG_KERN_COMMAND = """ + > + skip_byte: B + next_char: B + op_byte: B + remainder: B +""" + +BASE_PARAMS = [ + "SLANT", + "SPACE", + "STRETCH", + "SHRINK", + "XHEIGHT", + "QUAD", + "EXTRASPACE", +] + +MATHSY_PARAMS = [ + "NUM1", + "NUM2", + "NUM3", + "DENOM1", + "DENOM2", + "SUP1", + "SUP2", + "SUP3", + "SUB1", + "SUB2", + "SUPDROP", + "SUBDROP", + "DELIM1", + "DELIM2", + "AXISHEIGHT", +] + +MATHEX_PARAMS = [ + "DEFAULTRULETHICKNESS", + "BIGOPSPACING1", + "BIGOPSPACING2", + "BIGOPSPACING3", + "BIGOPSPACING4", + "BIGOPSPACING5", +] + +VANILLA = 0 +MATHSY = 1 +MATHEX = 2 + +UNREACHABLE = 0 +PASSTHROUGH = 1 +ACCESSABLE = 2 + +NO_TAG = 0 +LIG_TAG = 1 +LIST_TAG = 2 +EXT_TAG = 3 + +STOP_FLAG = 128 +KERN_FLAG = 128 + + +class TFMException(Exception): + def __init__(self, message): + super().__init__(message) + + +class TFM: + def __init__(self, file): + self._read(file) + + def __repr__(self): + return ( + f"" + ) + + def _read(self, file): + if hasattr(file, "read"): + data = file.read() + else: + with open(file, "rb") as fp: + data = fp.read() + + self._data = data + + if len(data) < SIZES_SIZE: + raise TFMException("Too short input file") + + sizes = SimpleNamespace() + unpack2(SIZES_FORMAT, data, sizes) + + # Do some file structure sanity checks. + # TeX and TFtoPL do additional functional checks and might even correct + # “errors” in the input file, but we instead try to output the file as + # it is as long as it is parsable, even if the data make no sense. + + if sizes.lf < 0: + raise TFMException("The file claims to have negative or zero length!") + + if len(data) < sizes.lf * 4: + raise TFMException("The file has fewer bytes than it claims!") + + for name, length in vars(sizes).items(): + if length < 0: + raise TFMException("The subfile size: '{name}' is negative!") + + if sizes.lh < 2: + raise TFMException(f"The header length is only {sizes.lh}!") + + if sizes.bc > sizes.ec + 1 or sizes.ec > 255: + raise TFMException( + f"The character code range {sizes.bc}..{sizes.ec} is illegal!" + ) + + if sizes.nw == 0 or sizes.nh == 0 or sizes.nd == 0 or sizes.ni == 0: + raise TFMException("Incomplete subfiles for character dimensions!") + + if sizes.ne > 256: + raise TFMException(f"There are {ne} extensible recipes!") + + if sizes.lf != ( + 6 + + sizes.lh + + (sizes.ec - sizes.bc + 1) + + sizes.nw + + sizes.nh + + sizes.nd + + sizes.ni + + sizes.nl + + sizes.nk + + sizes.ne + + sizes.np + ): + raise TFMException("Subfile sizes don’t add up to the stated total") + + # Subfile offsets, used in the helper function below. These all are + # 32-bit word offsets not 8-bit byte offsets. + char_base = 6 + sizes.lh - sizes.bc + width_base = char_base + sizes.ec + 1 + height_base = width_base + sizes.nw + depth_base = height_base + sizes.nh + italic_base = depth_base + sizes.nd + lig_kern_base = italic_base + sizes.ni + kern_base = lig_kern_base + sizes.nl + exten_base = kern_base + sizes.nk + param_base = exten_base + sizes.ne + + # Helper functions for accessing individual data. If this looks + # nonidiomatic Python, I blame the effect of reading the literate WEB + # documentation of TFtoPL. + def char_info(c): + return 4 * (char_base + c) + + def width_index(c): + return data[char_info(c)] + + def noneexistent(c): + return c < sizes.bc or c > sizes.ec or width_index(c) == 0 + + def height_index(c): + return data[char_info(c) + 1] // 16 + + def depth_index(c): + return data[char_info(c) + 1] % 16 + + def italic_index(c): + return data[char_info(c) + 2] // 4 + + def tag(c): + return data[char_info(c) + 2] % 4 + + def remainder(c): + return data[char_info(c) + 3] + + def width(c): + r = 4 * (width_base + width_index(c)) + return read_fixed(r, "v")["v"] + + def height(c): + r = 4 * (height_base + height_index(c)) + return read_fixed(r, "v")["v"] + + def depth(c): + r = 4 * (depth_base + depth_index(c)) + return read_fixed(r, "v")["v"] + + def italic(c): + r = 4 * (italic_base + italic_index(c)) + return read_fixed(r, "v")["v"] + + def exten(c): + return 4 * (exten_base + remainder(c)) + + def lig_step(i): + return 4 * (lig_kern_base + i) + + def lig_kern_command(i): + command = SimpleNamespace() + unpack2(LIG_KERN_COMMAND, data[i:], command) + return command + + def kern(i): + r = 4 * (kern_base + i) + return read_fixed(r, "v")["v"] + + def param(i): + return 4 * (param_base + i) + + def read_fixed(index, key, obj=None): + ret = unpack2(f">;{key}:{FIXED_FORMAT}", data[index:], obj) + return ret[0] + + # Set all attributes to empty values regardless of the header size. + unpack(HEADER_FORMAT4, [0] * HEADER_SIZE4, self) + + offset = 24 + length = sizes.lh * 4 + self.extraheader = {} + if length >= HEADER_SIZE4: + rest = unpack2(HEADER_FORMAT4, data[offset:], self)[1] + if self.face < 18: + s = self.face % 2 + b = self.face // 2 + self.face = "MBL"[b % 3] + "RI"[s] + "RCE"[b // 3] + for i in range(sizes.lh - HEADER_SIZE4 // 4): + rest = unpack2(f">;HEADER{i + 18}:l", rest, self.extraheader)[1] + elif length >= HEADER_SIZE3: + unpack2(HEADER_FORMAT3, data[offset:], self) + elif length >= HEADER_SIZE2: + unpack2(HEADER_FORMAT2, data[offset:], self) + elif length >= HEADER_SIZE1: + unpack2(HEADER_FORMAT1, data[offset:], self) + + self.fonttype = VANILLA + scheme = self.codingscheme.upper() + if scheme.startswith("TEX MATH SY"): + self.fonttype = MATHSY + elif scheme.startswith("TEX MATH EX"): + self.fonttype = MATHEX + + self.fontdimens = {} + for i in range(sizes.np): + name = f"PARAMETER{i+1}" + if i <= 6: + name = BASE_PARAMS[i] + elif self.fonttype == MATHSY and i <= 21: + name = MATHSY_PARAMS[i - 7] + elif self.fonttype == MATHEX and i <= 12: + name = MATHEX_PARAMS[i - 7] + read_fixed(param(i), name, self.fontdimens) + + lig_kern_map = {} + self.right_boundary_char = None + self.left_boundary_char = None + if sizes.nl > 0: + cmd = lig_kern_command(lig_step(0)) + if cmd.skip_byte == 255: + self.right_boundary_char = cmd.next_char + + cmd = lig_kern_command(lig_step((sizes.nl - 1))) + if cmd.skip_byte == 255: + self.left_boundary_char = 256 + r = 256 * cmd.op_byte + cmd.remainder + lig_kern_map[self.left_boundary_char] = r + + self.chars = {} + for c in range(sizes.bc, sizes.ec + 1): + if width_index(c) > 0: + self.chars[c] = info = {} + info["width"] = width(c) + if height_index(c) > 0: + info["height"] = height(c) + if depth_index(c) > 0: + info["depth"] = depth(c) + if italic_index(c) > 0: + info["italic"] = italic(c) + char_tag = tag(c) + if char_tag == NO_TAG: + pass + elif char_tag == LIG_TAG: + lig_kern_map[c] = remainder(c) + elif char_tag == LIST_TAG: + info["nextlarger"] = remainder(c) + elif char_tag == EXT_TAG: + info["varchar"] = varchar = {} + for i in range(4): + part = data[exten(c) + i] + if i == 3 or part > 0: + name = "rep" + if i == 0: + name = "top" + elif i == 1: + name = "mid" + elif i == 2: + name = "bot" + if noneexistent(part): + varchar[name] = c + else: + varchar[name] = part + + self.ligatures = {} + self.kerning = {} + for c, i in sorted(lig_kern_map.items()): + cmd = lig_kern_command(lig_step(i)) + if cmd.skip_byte > STOP_FLAG: + i = 256 * cmd.op_byte + cmd.remainder + + while i < sizes.nl: + cmd = lig_kern_command(lig_step(i)) + if cmd.skip_byte > STOP_FLAG: + pass + else: + if cmd.op_byte >= KERN_FLAG: + r = 256 * (cmd.op_byte - KERN_FLAG) + cmd.remainder + self.kerning.setdefault(c, {})[cmd.next_char] = kern(r) + else: + r = cmd.op_byte + if r == 4 or (r > 7 and r != 11): + # Ligature step with nonstandard code, we output + # the code verbatim. + lig = r + else: + lig = "" + if r % 4 > 1: + lig += "/" + lig += "LIG" + if r % 2 != 0: + lig += "/" + while r > 3: + lig += ">" + r -= 4 + self.ligatures.setdefault(c, {})[cmd.next_char] = ( + lig, + cmd.remainder, + ) + + if cmd.skip_byte >= STOP_FLAG: + break + i += cmd.skip_byte + 1 + + +if __name__ == "__main__": + import sys + + tfm = TFM(sys.argv[1]) + print( + "\n".join( + x + for x in [ + f"tfm.checksum={tfm.checksum}", + f"tfm.designsize={tfm.designsize}", + f"tfm.codingscheme={tfm.codingscheme}", + f"tfm.fonttype={tfm.fonttype}", + f"tfm.family={tfm.family}", + f"tfm.seven_bit_safe_flag={tfm.seven_bit_safe_flag}", + f"tfm.face={tfm.face}", + f"tfm.extraheader={tfm.extraheader}", + f"tfm.fontdimens={tfm.fontdimens}", + f"tfm.right_boundary_char={tfm.right_boundary_char}", + f"tfm.left_boundary_char={tfm.left_boundary_char}", + f"tfm.kerning={tfm.kerning}", + f"tfm.ligatures={tfm.ligatures}", + f"tfm.chars={tfm.chars}", + ] + ) + ) + print(tfm) diff --git a/lib/python3.10/site-packages/fontTools/ttx.py b/lib/python3.10/site-packages/fontTools/ttx.py new file mode 100644 index 0000000000000000000000000000000000000000..1a6330cd4658ab78c3948a44543a41ed38a990f0 --- /dev/null +++ b/lib/python3.10/site-packages/fontTools/ttx.py @@ -0,0 +1,479 @@ +"""\ +usage: ttx [options] inputfile1 [... inputfileN] + +TTX -- From OpenType To XML And Back + +If an input file is a TrueType or OpenType font file, it will be +decompiled to a TTX file (an XML-based text format). +If an input file is a TTX file, it will be compiled to whatever +format the data is in, a TrueType or OpenType/CFF font file. +A special input value of - means read from the standard input. + +Output files are created so they are unique: an existing file is +never overwritten. + +General options +=============== + +-h Help print this message. +--version show version and exit. +-d Specify a directory where the output files are + to be created. +-o Specify a file to write the output to. A special + value of - would use the standard output. +-f Overwrite existing output file(s), ie. don't append + numbers. +-v Verbose: more messages will be written to stdout + about what is being done. +-q Quiet: No messages will be written to stdout about + what is being done. +-a allow virtual glyphs ID's on compile or decompile. + +Dump options +============ + +-l List table info: instead of dumping to a TTX file, list + some minimal info about each table. +-t Specify a table to dump. Multiple -t options + are allowed. When no -t option is specified, all tables + will be dumped. +-x
Specify a table to exclude from the dump. Multiple + -x options are allowed. -t and -x are mutually exclusive. +-s Split tables: save the TTX data into separate TTX files per + table and write one small TTX file that contains references + to the individual table dumps. This file can be used as + input to ttx, as long as the table files are in the + same directory. +-g Split glyf table: Save the glyf data into separate TTX files + per glyph and write a small TTX for the glyf table which + contains references to the individual TTGlyph elements. + NOTE: specifying -g implies -s (no need for -s together + with -g) +-i Do NOT disassemble TT instructions: when this option is + given, all TrueType programs (glyph programs, the font + program and the pre-program) will be written to the TTX + file as hex data instead of assembly. This saves some time + and makes the TTX file smaller. +-z Specify a bitmap data export option for EBDT: + {'raw', 'row', 'bitwise', 'extfile'} or for the CBDT: + {'raw', 'extfile'} Each option does one of the following: + + -z raw + export the bitmap data as a hex dump + -z row + export each row as hex data + -z bitwise + export each row as binary in an ASCII art style + -z extfile + export the data as external files with XML references + + If no export format is specified 'raw' format is used. +-e Don't ignore decompilation errors, but show a full traceback + and abort. +-y Select font number for TrueType Collection (.ttc/.otc), + starting from 0. +--unicodedata + Use custom database file to write character names in the + comments of the cmap TTX output. +--newline + Control how line endings are written in the XML file. It + can be 'LF', 'CR', or 'CRLF'. If not specified, the + default platform-specific line endings are used. + +Compile options +=============== + +-m Merge with TrueType-input-file: specify a TrueType or + OpenType font file to be merged with the TTX file. This + option is only valid when at most one TTX file is specified. +-b Don't recalc glyph bounding boxes: use the values in the + TTX file as-is. +--recalc-timestamp + Set font 'modified' timestamp to current time. + By default, the modification time of the TTX file will be + used. +--no-recalc-timestamp + Keep the original font 'modified' timestamp. +--flavor + Specify flavor of output font file. May be 'woff' or 'woff2'. + Note that WOFF2 requires the Brotli Python extension, + available at https://github.com/google/brotli +--with-zopfli + Use Zopfli instead of Zlib to compress WOFF. The Python + extension is available at https://pypi.python.org/pypi/zopfli +--optimize-font-speed + Enable optimizations that prioritize speed over file size. + This mainly affects how glyf t able and gvar / VARC tables are + compiled. The produced fonts will be larger, but rendering + performance will be improved with HarfBuzz and other text + layout engines. +""" + +from fontTools.ttLib import OPTIMIZE_FONT_SPEED, TTFont, TTLibError +from fontTools.misc.macCreatorType import getMacCreatorAndType +from fontTools.unicode import setUnicodeData +from fontTools.misc.textTools import Tag, tostr +from fontTools.misc.timeTools import timestampSinceEpoch +from fontTools.misc.loggingTools import Timer +from fontTools.misc.cliTools import makeOutputFileName +import os +import sys +import getopt +import re +import logging + + +log = logging.getLogger("fontTools.ttx") + +opentypeheaderRE = re.compile("""sfntVersion=['"]OTTO["']""") + + +class Options(object): + listTables = False + outputDir = None + outputFile = None + overWrite = False + verbose = False + quiet = False + splitTables = False + splitGlyphs = False + disassembleInstructions = True + mergeFile = None + recalcBBoxes = True + ignoreDecompileErrors = True + bitmapGlyphDataFormat = "raw" + unicodedata = None + newlinestr = "\n" + recalcTimestamp = None + flavor = None + useZopfli = False + optimizeFontSpeed = False + + def __init__(self, rawOptions, numFiles): + self.onlyTables = [] + self.skipTables = [] + self.fontNumber = -1 + for option, value in rawOptions: + # general options + if option == "-h": + print(__doc__) + sys.exit(0) + elif option == "--version": + from fontTools import version + + print(version) + sys.exit(0) + elif option == "-d": + if not os.path.isdir(value): + raise getopt.GetoptError( + "The -d option value must be an existing directory" + ) + self.outputDir = value + elif option == "-o": + self.outputFile = value + elif option == "-f": + self.overWrite = True + elif option == "-v": + self.verbose = True + elif option == "-q": + self.quiet = True + # dump options + elif option == "-l": + self.listTables = True + elif option == "-t": + # pad with space if table tag length is less than 4 + value = value.ljust(4) + self.onlyTables.append(value) + elif option == "-x": + # pad with space if table tag length is less than 4 + value = value.ljust(4) + self.skipTables.append(value) + elif option == "-s": + self.splitTables = True + elif option == "-g": + # -g implies (and forces) splitTables + self.splitGlyphs = True + self.splitTables = True + elif option == "-i": + self.disassembleInstructions = False + elif option == "-z": + validOptions = ("raw", "row", "bitwise", "extfile") + if value not in validOptions: + raise getopt.GetoptError( + "-z does not allow %s as a format. Use %s" + % (option, validOptions) + ) + self.bitmapGlyphDataFormat = value + elif option == "-y": + self.fontNumber = int(value) + # compile options + elif option == "-m": + self.mergeFile = value + elif option == "-b": + self.recalcBBoxes = False + elif option == "-e": + self.ignoreDecompileErrors = False + elif option == "--unicodedata": + self.unicodedata = value + elif option == "--newline": + validOptions = ("LF", "CR", "CRLF") + if value == "LF": + self.newlinestr = "\n" + elif value == "CR": + self.newlinestr = "\r" + elif value == "CRLF": + self.newlinestr = "\r\n" + else: + raise getopt.GetoptError( + "Invalid choice for --newline: %r (choose from %s)" + % (value, ", ".join(map(repr, validOptions))) + ) + elif option == "--recalc-timestamp": + self.recalcTimestamp = True + elif option == "--no-recalc-timestamp": + self.recalcTimestamp = False + elif option == "--flavor": + self.flavor = value + elif option == "--with-zopfli": + self.useZopfli = True + elif option == "--optimize-font-speed": + self.optimizeFontSpeed = True + if self.verbose and self.quiet: + raise getopt.GetoptError("-q and -v options are mutually exclusive") + if self.verbose: + self.logLevel = logging.DEBUG + elif self.quiet: + self.logLevel = logging.WARNING + else: + self.logLevel = logging.INFO + if self.mergeFile and self.flavor: + raise getopt.GetoptError("-m and --flavor options are mutually exclusive") + if self.onlyTables and self.skipTables: + raise getopt.GetoptError("-t and -x options are mutually exclusive") + if self.mergeFile and numFiles > 1: + raise getopt.GetoptError( + "Must specify exactly one TTX source file when using -m" + ) + if self.flavor != "woff" and self.useZopfli: + raise getopt.GetoptError("--with-zopfli option requires --flavor 'woff'") + + +def ttList(input, output, options): + ttf = TTFont(input, fontNumber=options.fontNumber, lazy=True) + reader = ttf.reader + tags = sorted(reader.keys()) + print('Listing table info for "%s":' % input) + format = " %4s %10s %8s %8s" + print(format % ("tag ", " checksum", " length", " offset")) + print(format % ("----", "----------", "--------", "--------")) + for tag in tags: + entry = reader.tables[tag] + if ttf.flavor == "woff2": + # WOFF2 doesn't store table checksums, so they must be calculated + from fontTools.ttLib.sfnt import calcChecksum + + data = entry.loadData(reader.transformBuffer) + checkSum = calcChecksum(data) + else: + checkSum = int(entry.checkSum) + if checkSum < 0: + checkSum = checkSum + 0x100000000 + checksum = "0x%08X" % checkSum + print(format % (tag, checksum, entry.length, entry.offset)) + print() + ttf.close() + + +@Timer(log, "Done dumping TTX in %(time).3f seconds") +def ttDump(input, output, options): + input_name = input + if input == "-": + input, input_name = sys.stdin.buffer, sys.stdin.name + output_name = output + if output == "-": + output, output_name = sys.stdout, sys.stdout.name + log.info('Dumping "%s" to "%s"...', input_name, output_name) + if options.unicodedata: + setUnicodeData(options.unicodedata) + ttf = TTFont( + input, + 0, + ignoreDecompileErrors=options.ignoreDecompileErrors, + fontNumber=options.fontNumber, + ) + ttf.saveXML( + output, + tables=options.onlyTables, + skipTables=options.skipTables, + splitTables=options.splitTables, + splitGlyphs=options.splitGlyphs, + disassembleInstructions=options.disassembleInstructions, + bitmapGlyphDataFormat=options.bitmapGlyphDataFormat, + newlinestr=options.newlinestr, + ) + ttf.close() + + +@Timer(log, "Done compiling TTX in %(time).3f seconds") +def ttCompile(input, output, options): + input_name = input + if input == "-": + input, input_name = sys.stdin, sys.stdin.name + output_name = output + if output == "-": + output, output_name = sys.stdout.buffer, sys.stdout.name + log.info('Compiling "%s" to "%s"...' % (input_name, output)) + if options.useZopfli: + from fontTools.ttLib import sfnt + + sfnt.USE_ZOPFLI = True + ttf = TTFont( + options.mergeFile, + flavor=options.flavor, + recalcBBoxes=options.recalcBBoxes, + recalcTimestamp=options.recalcTimestamp, + ) + if options.optimizeFontSpeed: + ttf.cfg[OPTIMIZE_FONT_SPEED] = options.optimizeFontSpeed + ttf.importXML(input) + + if options.recalcTimestamp is None and "head" in ttf and input is not sys.stdin: + # use TTX file modification time for head "modified" timestamp + mtime = os.path.getmtime(input) + ttf["head"].modified = timestampSinceEpoch(mtime) + + ttf.save(output) + + +def guessFileType(fileName): + if fileName == "-": + header = sys.stdin.buffer.peek(256) + ext = "" + else: + base, ext = os.path.splitext(fileName) + try: + with open(fileName, "rb") as f: + header = f.read(256) + except IOError: + return None + + if header.startswith(b"\xef\xbb\xbf", etc. + num = int(num, 16) + unicodes[num] = name + return unicodes + + +class _UnicodeCustom(object): + def __init__(self, f): + if isinstance(f, str): + with open(f) as fd: + codes = _makeunicodes(fd) + else: + codes = _makeunicodes(f) + self.codes = codes + + def __getitem__(self, charCode): + try: + return self.codes[charCode] + except KeyError: + return "????" + + +class _UnicodeBuiltin(object): + def __getitem__(self, charCode): + try: + # use unicodedata backport to python2, if available: + # https://github.com/mikekap/unicodedata2 + import unicodedata2 as unicodedata + except ImportError: + import unicodedata + try: + return unicodedata.name(chr(charCode)) + except ValueError: + return "????" + + +Unicode = _UnicodeBuiltin() + + +def setUnicodeData(f): + global Unicode + Unicode = _UnicodeCustom(f) diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/INSTALLER b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/METADATA b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..19786640848c278dd2320bdd36766d38542be1d6 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/METADATA @@ -0,0 +1,2161 @@ +Metadata-Version: 2.4 +Name: fonttools +Version: 4.59.0 +Summary: Tools to manipulate font files +Home-page: http://github.com/fonttools/fonttools +Author: Just van Rossum +Author-email: just@letterror.com +Maintainer: Behdad Esfahbod +Maintainer-email: behdad@behdad.org +License: MIT +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Other Environment +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: End Users/Desktop +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +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 +Classifier: Topic :: Text Processing :: Fonts +Classifier: Topic :: Multimedia :: Graphics +Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.external +Provides-Extra: ufo +Provides-Extra: lxml +Requires-Dist: lxml>=4.0; extra == "lxml" +Provides-Extra: woff +Requires-Dist: brotli>=1.0.1; platform_python_implementation == "CPython" and extra == "woff" +Requires-Dist: brotlicffi>=0.8.0; platform_python_implementation != "CPython" and extra == "woff" +Requires-Dist: zopfli>=0.1.4; extra == "woff" +Provides-Extra: unicode +Requires-Dist: unicodedata2>=15.1.0; python_version <= "3.12" and extra == "unicode" +Provides-Extra: graphite +Requires-Dist: lz4>=1.7.4.2; extra == "graphite" +Provides-Extra: interpolatable +Requires-Dist: scipy; platform_python_implementation != "PyPy" and extra == "interpolatable" +Requires-Dist: munkres; platform_python_implementation == "PyPy" and extra == "interpolatable" +Requires-Dist: pycairo; extra == "interpolatable" +Provides-Extra: plot +Requires-Dist: matplotlib; extra == "plot" +Provides-Extra: symfont +Requires-Dist: sympy; extra == "symfont" +Provides-Extra: type1 +Requires-Dist: xattr; sys_platform == "darwin" and extra == "type1" +Provides-Extra: pathops +Requires-Dist: skia-pathops>=0.5.0; extra == "pathops" +Provides-Extra: repacker +Requires-Dist: uharfbuzz>=0.23.0; extra == "repacker" +Provides-Extra: all +Requires-Dist: lxml>=4.0; extra == "all" +Requires-Dist: brotli>=1.0.1; platform_python_implementation == "CPython" and extra == "all" +Requires-Dist: brotlicffi>=0.8.0; platform_python_implementation != "CPython" and extra == "all" +Requires-Dist: zopfli>=0.1.4; extra == "all" +Requires-Dist: unicodedata2>=15.1.0; python_version <= "3.12" and extra == "all" +Requires-Dist: lz4>=1.7.4.2; extra == "all" +Requires-Dist: scipy; platform_python_implementation != "PyPy" and extra == "all" +Requires-Dist: munkres; platform_python_implementation == "PyPy" and extra == "all" +Requires-Dist: pycairo; extra == "all" +Requires-Dist: matplotlib; extra == "all" +Requires-Dist: sympy; extra == "all" +Requires-Dist: xattr; sys_platform == "darwin" and extra == "all" +Requires-Dist: skia-pathops>=0.5.0; extra == "all" +Requires-Dist: uharfbuzz>=0.23.0; extra == "all" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: maintainer +Dynamic: maintainer-email +Dynamic: platform +Dynamic: provides-extra +Dynamic: requires-python +Dynamic: summary + +|CI Build Status| |Coverage Status| |PyPI| |Gitter Chat| + +What is this? +~~~~~~~~~~~~~ + +| fontTools is a library for manipulating fonts, written in Python. The + project includes the TTX tool, that can convert TrueType and OpenType + fonts to and from an XML text format, which is also called TTX. It + supports TrueType, OpenType, AFM and to an extent Type 1 and some + Mac-specific formats. The project has an `MIT open-source + license `__. +| Among other things this means you can use it free of charge. + +`User documentation `_ and +`developer documentation `_ +are available at `Read the Docs `_. + +Installation +~~~~~~~~~~~~ + +FontTools requires `Python `__ 3.9 +or later. We try to follow the same schedule of minimum Python version support as +NumPy (see `NEP 29 `__). + +The package is listed in the Python Package Index (PyPI), so you can +install it with `pip `__: + +.. code:: sh + + pip install fonttools + +If you would like to contribute to its development, you can clone the +repository from GitHub, install the package in 'editable' mode and +modify the source code in place. We recommend creating a virtual +environment, using `virtualenv `__ or +Python 3 `venv `__ module. + +.. code:: sh + + # download the source code to 'fonttools' folder + git clone https://github.com/fonttools/fonttools.git + cd fonttools + + # create new virtual environment called e.g. 'fonttools-venv', or anything you like + python -m virtualenv fonttools-venv + + # source the `activate` shell script to enter the environment (Unix-like); to exit, just type `deactivate` + . fonttools-venv/bin/activate + + # to activate the virtual environment in Windows `cmd.exe`, do + fonttools-venv\Scripts\activate.bat + + # install in 'editable' mode + pip install -e . + +Optional Requirements +--------------------- + +The ``fontTools`` package currently has no (required) external dependencies +besides the modules included in the Python Standard Library. +However, a few extra dependencies are required by some of its modules, which +are needed to unlock optional features. +The ``fonttools`` PyPI distribution also supports so-called "extras", i.e. a +set of keywords that describe a group of additional dependencies, which can be +used when installing via pip, or when specifying a requirement. +For example: + +.. code:: sh + + pip install fonttools[ufo,lxml,woff,unicode] + +This command will install fonttools, as well as the optional dependencies that +are required to unlock the extra features named "ufo", etc. + +- ``Lib/fontTools/misc/etree.py`` + + The module exports a ElementTree-like API for reading/writing XML files, and + allows to use as the backend either the built-in ``xml.etree`` module or + `lxml `__. The latter is preferred whenever present, + as it is generally faster and more secure. + + *Extra:* ``lxml`` + +- ``Lib/fontTools/ufoLib`` + + Package for reading and writing UFO source files; it requires: + + * `fs `__: (aka ``pyfilesystem2``) filesystem + abstraction layer. + + *Extra:* ``ufo`` + +- ``Lib/fontTools/ttLib/woff2.py`` + + Module to compress/decompress WOFF 2.0 web fonts; it requires: + + * `brotli `__: Python bindings of + the Brotli compression library. + + *Extra:* ``woff`` + +- ``Lib/fontTools/ttLib/sfnt.py`` + + To better compress WOFF 1.0 web fonts, the following module can be used + instead of the built-in ``zlib`` library: + + * `zopfli `__: Python bindings of + the Zopfli compression library. + + *Extra:* ``woff`` + +- ``Lib/fontTools/unicode.py`` + + To display the Unicode character names when dumping the ``cmap`` table + with ``ttx`` we use the ``unicodedata`` module in the Standard Library. + The version included in there varies between different Python versions. + To use the latest available data, you can install: + + * `unicodedata2 `__: + ``unicodedata`` backport for Python 3.x updated to the latest Unicode + version 15.0. + + *Extra:* ``unicode`` + +- ``Lib/fontTools/varLib/interpolatable.py`` + + Module for finding wrong contour/component order between different masters. + It requires one of the following packages in order to solve the so-called + "minimum weight perfect matching problem in bipartite graphs", or + the Assignment problem: + + * `scipy `__: the Scientific Library + for Python, which internally uses `NumPy `__ + arrays and hence is very fast; + * `munkres `__: a pure-Python + module that implements the Hungarian or Kuhn-Munkres algorithm. + + To plot the results to a PDF or HTML format, you also need to install: + + * `pycairo `__: Python bindings for the + Cairo graphics library. Note that wheels are currently only available for + Windows, for other platforms see pycairo's `installation instructions + `__. + + *Extra:* ``interpolatable`` + +- ``Lib/fontTools/varLib/plot.py`` + + Module for visualizing DesignSpaceDocument and resulting VariationModel. + + * `matplotlib `__: 2D plotting library. + + *Extra:* ``plot`` + +- ``Lib/fontTools/misc/symfont.py`` + + Advanced module for symbolic font statistics analysis; it requires: + + * `sympy `__: the Python library for + symbolic mathematics. + + *Extra:* ``symfont`` + +- ``Lib/fontTools/t1Lib.py`` + + To get the file creator and type of Macintosh PostScript Type 1 fonts + on Python 3 you need to install the following module, as the old ``MacOS`` + module is no longer included in Mac Python: + + * `xattr `__: Python wrapper for + extended filesystem attributes (macOS platform only). + + *Extra:* ``type1`` + +- ``Lib/fontTools/ttLib/removeOverlaps.py`` + + Simplify TrueType glyphs by merging overlapping contours and components. + + * `skia-pathops `__: Python + bindings for the Skia library's PathOps module, performing boolean + operations on paths (union, intersection, etc.). + + *Extra:* ``pathops`` + +- ``Lib/fontTools/pens/cocoaPen.py`` and ``Lib/fontTools/pens/quartzPen.py`` + + Pens for drawing glyphs with Cocoa ``NSBezierPath`` or ``CGPath`` require: + + * `PyObjC `__: the bridge between + Python and the Objective-C runtime (macOS platform only). + +- ``Lib/fontTools/pens/qtPen.py`` + + Pen for drawing glyphs with Qt's ``QPainterPath``, requires: + + * `PyQt5 `__: Python bindings for + the Qt cross platform UI and application toolkit. + +- ``Lib/fontTools/pens/reportLabPen.py`` + + Pen to drawing glyphs as PNG images, requires: + + * `reportlab `__: Python toolkit + for generating PDFs and graphics. + +- ``Lib/fontTools/pens/freetypePen.py`` + + Pen to drawing glyphs with FreeType as raster images, requires: + + * `freetype-py `__: Python binding + for the FreeType library. + +- ``Lib/fontTools/ttLib/tables/otBase.py`` + + Use the Harfbuzz library to serialize GPOS/GSUB using ``hb_repack`` method, requires: + + * `uharfbuzz `__: Streamlined Cython + bindings for the harfbuzz shaping engine + + *Extra:* ``repacker`` + +How to make a new release +~~~~~~~~~~~~~~~~~~~~~~~~~ + +1) Update ``NEWS.rst`` with all the changes since the last release. Write a + changelog entry for each PR, with one or two short sentences summarizing it, + as well as links to the PR and relevant issues addressed by the PR. Do not + put a new title, the next command will do it for you. +2) Use semantic versioning to decide whether the new release will be a 'major', + 'minor' or 'patch' release. It's usually one of the latter two, depending on + whether new backward compatible APIs were added, or simply some bugs were fixed. +3) From inside a venv, first do ``pip install -r dev-requirements.txt``, then run + the ``python setup.py release`` command from the tip of the ``main`` branch. + By default this bumps the third or 'patch' digit only, unless you pass ``--major`` + or ``--minor`` to bump respectively the first or second digit. + This bumps the package version string, extracts the changes since the latest + version from ``NEWS.rst``, and uses that text to create an annotated git tag + (or a signed git tag if you pass the ``--sign`` option and your git and Github + account are configured for `signing commits `__ + using a GPG key). + It also commits an additional version bump which opens the main branch for + the subsequent developmental cycle +4) Push both the tag and commit to the upstream repository, by running the command + ``git push --follow-tags``. Note: it may push other local tags as well, be + careful. +5) Let the CI build the wheel and source distribution packages and verify both + get uploaded to the Python Package Index (PyPI). +6) [Optional] Go to fonttools `Github Releases `__ + page and create a new release, copy-pasting the content of the git tag + message. This way, the release notes are nicely formatted as markdown, and + users watching the repo will get an email notification. One day we shall + automate that too. + + +Acknowledgments +~~~~~~~~~~~~~~~~ + +In alphabetical order: + +aschmitz, Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland, +Jelle Bosma, Sascha Brawer, Tom Byrer, Antonio Cavedoni, Frédéric Coiffier, +Vincent Connare, David Corbett, Simon Cozens, Dave Crossland, Simon Daniels, +Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes Famira, Sam Fishman, +Matt Fontaine, Takaaki Fuji, Rob Hagemans, Yannis Haralambous, Greg Hitchcock, +Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, Jack Jansen, +Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Liang Hai, Peter +Lofting, Cosimo Lupo, Olli Meier, Masaya Nakamura, Dave Opstad, Laurence Penney, +Roozbeh Pournader, Garret Rieger, Read Roberts, Colin Rofls, Guido van Rossum, +Just van Rossum, Andreas Seidel, Georg Seifert, Chris Simpkins, Miguel Sousa, +Adam Twardoch, Adrien Tétar, Vitaly Volkov, Paul Wise. + +Copyrights +~~~~~~~~~~ + +| Copyright (c) 1999-2004 Just van Rossum, LettError + (just@letterror.com) +| See `LICENSE `__ for the full license. + +Copyright (c) 2000 BeOpen.com. All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. All +Rights Reserved. + +Have fun! + +.. |CI Build Status| image:: https://github.com/fonttools/fonttools/workflows/Test/badge.svg + :target: https://github.com/fonttools/fonttools/actions?query=workflow%3ATest +.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/main/graph/badge.svg + :target: https://codecov.io/gh/fonttools/fonttools +.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg + :target: https://pypi.org/project/FontTools +.. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg + :alt: Join the chat at https://gitter.im/fonttools-dev/Lobby + :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + +Changelog +~~~~~~~~~ + +4.59.0 (released 2025-07-16) +---------------------------- + +- Removed hard-dependency on pyfilesystem2 (``fs`` package) from ``fonttools[ufo]`` extra. + This is replaced by the `fontTools.misc.filesystem` package, a stdlib-only, drop-in + replacement for the subset of the pyfilesystem2's API used by ``fontTools.ufoLib``. + The latter should continue to work with the upstream ``fs`` (we even test with/without). + Clients who wish to continue using ``fs`` can do so by depending on it directly instead + of via the ``fonttools[ufo]`` extra (#3885, #3620). +- [xmlWriter] Replace illegal XML characters (e.g. control or non-characters) with "?" + when dumping to ttx (#3868, #71). +- [varLib.hvar] Fixed vertical metrics fields copy/pasta error (#3884). +- Micro optimizations in ttLib and sstruct modules (#3878, #3879). +- [unicodedata] Add Garay script to RTL_SCRIPTS (#3882). +- [roundingPen] Remove unreliable kwarg usage. Argument names aren’t consistent among + point pens’ ``.addComponent()`` implementations, in particular ``baseGlyphName`` + vs ``glyphName`` (#3880). + +4.58.5 (released 2025-07-03) +---------------------------- + +- [feaLib] Don't try to combine ligature & multisub rules (#3874). +- [feaLib/ast] Use weakref proxies to avoid cycles in visitor (#3873). +- [varLib.instancer] Fixed instancing CFF2 fonts where VarData contains more than 64k items (#3858). + +4.58.4 (released 2025-06-13) +---------------------------- + +- [feaLib] Allow for empty MarkFilter & MarkAttach sets (#3856). + +4.58.3 (released 2025-06-13) +---------------------------- + +- [feaLib] Fixed iterable check for Python 3.13.4 and newer (#3854, #3855). + +4.58.2 (released 2025-06-06) +---------------------------- + +- [ttLib.reorderGlyphs] Handle CFF2 when reordering glyphs (#3852) +- [subset] Copy name IDs in use before scrapping or scrambling them for webfonts (#3853) + +4.58.1 (released 2025-05-28) +---------------------------- + +- [varLib] Make sure that fvar named instances only reuse name ID 2 or 17 if they are at the default location across all axes, to match OT spec requirement (#3831). +- [feaLib] Improve single substitution promotion to multiple/ligature substitutions, fixing a few bugs as well (#3849). +- [loggingTools] Make ``Timer._time`` a static method that doesn't take self, makes it easier to override (#3836). +- [featureVars] Use ``None`` for empty ConditionSet, which translates to a null offset in the compiled table (#3850). +- [feaLib] Raise an error on conflicting ligature substitution rules instead of silently taking the last one (#3835). +- Add typing annotations to T2CharStringPen (#3837). +- [feaLib] Add single substitutions that were promoted to multiple or ligature substitutions to ``aalt`` feature (#3847). +- [featureVars] Create a default ``LangSys`` in a ``ScriptRecord`` if missing when adding feature variations to existing GSUB later in the build (#3838). +- [symfont] Added a ``main()``. +- [cffLib.specializer] Fix rmoveto merging when blends used (#3839, #3840). +- [pyftmerge] Add support for cmap format 14 in the merge tool (#3830). +- [varLib.instancer/cff2] Fix vsindex of Private dicts when instantiating (#3828, #3232). +- Update text file read to use UTF-8 with optional BOM so it works with e.g. Windows Notepad.exe (#3824). +- [varLib] Ensure that instances only reuse name ID 2 or 17 if they are at the default location across all axes (#3831). +- [varLib] Create a dflt LangSys in a ScriptRecord when adding variations later, to fix an avoidable crash in an edge case (#3838). + +4.58.0 (released 2025-05-10) +---------------------------- + +- Drop Python 3.8, require 3.9+ (#3819) +- [HVAR, VVAR] Prune unused regions when using a direct mapping (#3797) +- [Docs] Improvements to ufoLib documentation (#3721) +- [Docs] Improvements to varLib documentation (#3727) +- [Docs] Improvements to Pens and pen-module documentation (#3724) +- [Docs] Miscellany updates to docs (misc modules and smaller modules) (#3730) +- [subset] Close codepoints over BiDi mirror variants. (#3801) +- [feaLib] Fix serializing ChainContextPosStatement and + ChainContextSubstStatement in some rare cases (#3788) +- [designspaceLib] Clarify user expectations for getStatNames (#2892) +- [GVAR] Add support for new `GVAR` table (#3728) +- [TSI0, TSI5] Derive number of entries to decompile from data length (#2477) +- [ttLib] Fix `AttributeError` when reporting table overflow (#3808) +- [ttLib] Apply rounding more often in getCoordinates (#3798) +- [ttLib] Ignore component bounds if empty (#3799) +- [ttLib] Change the separator for duplicate glyph names from "#" to "." (#3809) +- [feaLib] Support subtable breaks in CursivePos, MarkBasePos, MarkToLigPos and + MarkToMarkPos lookups (#3800, #3807) +- [feaLib] If the same lookup has single substitutions and ligature + substitutions, upgrade single substitutions to ligature substitutions with + one input glyph (#3805) +- [feaLib] Correctly handle in single pos lookups (#3803) +- [feaLib] Remove duplicates from class pair pos classes instead of raising an + error (#3804) +- [feaLib] Support creating extension lookups using useExtenion lookup flag + instead of silently ignoring it (#3811) +- [STAT] Add typing for the simpler STAT arguments (#3812) +- [otlLib.builder] Add future import for annotations (#3814) +- [cffLib] Fix reading supplement encoding (#3813) +- [voltLib] Add some missing functionality and fixes to voltLib and VoltToFea, + making the conversion to feature files more robust. Add also `fonttools + voltLib` command line tool to compile VOLT sources directly (doing an + intermediate fea conversion internally) (#3818) +- [pens] Add some PointPen annotations (#3820) + +4.57.0 (released 2025-04-03) +---------------------------- + +- [ttLib.__main__] Add `--no-recalc-timestamp` flag (#3771) +- [ttLib.__main__] Add `-b` (recalcBBoxes=False) flag (#3772) +- [cmap] Speed up glyphOrder loading from cmap (#3774) +- [ttLib.__main__] Improvements around the `-t` flag (#3776) +- [Debg] Fix parsing from XML; add roundtrip tests (#3781) +- [fealib] Support \*Base.MinMax tables (#3783, #3786) +- [config] Add OPTIMIZE_FONT_SPEED (#3784) +- [varLib.hvar] New module to add HVAR table to the font (#3780) +- [otlLib.optimize] Fix crash when the provided TTF does not contain a `GPOS` (#3794) + +4.56.0 (released 2025-02-07) +---------------------------- + +- [varStore] Sort the input todo list with the same sorting key used for the opimizer's output (#3767). +- [otData] Fix DeviceTable's ``DeltaValue`` repeat value which caused a crash after importing from XML and then compiling a GPOS containing Device tables (#3758). +- [feaLib] Make ``FeatureLibError`` pickleable, so client can e.g. use feaLib to can compile features in parallel with multiprocessing (#3762). +- [varLib/gvar] Removed workaround for old, long-fixed macOS bug about composite glyphs with all zero deltas (#1381, #1788). +- [Docs] Updated ttLib documentation, beefed up TTFont and TTGlyphSet explanations (#3720). + +4.55.8 (released 2025-01-29) +---------------------------- + +- [MetaTools] Fixed bug in buildUCD.py script whereby the first non-header line of some UCD text file was being skipped. This affected in particular the U+00B7 (MIDDLE DOT) entry of ScriptExtensions.txt (#3756). + +4.55.7 (released 2025-01-28) +---------------------------- + +- Shorten the changelog included in PyPI package description to accommodate maximum length limit imposed by Azure DevOps. No actual code changes since v4.55.6 (#3754). + +4.55.6 (released 2025-01-24) +---------------------------- + +- [glyf] Fixed regression introduced in 4.55.5 when computing bounds of nested composite glyphs with transformed components (#3752). + +4.55.5 (released 2025-01-23) +---------------------------- + +- [glyf] Fixed recalcBounds of transformed components with unrounded coordinates (#3750). +- [feaLib] Allow duplicate script/language statements (#3749). + +4.55.4 (released 2025-01-21) +---------------------------- + +- [bezierTools] Fixed ``splitCubicAtT`` sometimes not returning identical start/end points as result of numerical precision (#3742, #3743). +- [feaLib/ast] Fixed docstring of ``AlternateSubstStatement`` (#3735). +- [transform] Typing fixes (#3734). + +4.55.3 (released 2024-12-10) +---------------------------- + +- [Docs] fill out ttLib table section [#3716] +- [feaLib] More efficient inline format 4 lookups [#3726] + +4.55.2 (released 2024-12-05) +---------------------------- + +- [Docs] update Sphinx config (#3712) +- [designspaceLib] Allow axisOrdering to be set to zero (#3715) +- [feaLib] Don’t modify variable anchors in place (#3717) + +4.55.1 (released 2024-12-02) +---------------------------- + +- [ttGlyphSet] Support VARC CFF2 fonts (#3683) +- [DecomposedTransform] Document and implement always skewY == 0 (#3697) +- [varLib] "Fix" cython iup issue? (#3704) +- Cython minor refactor (#3705) + + +4.55.0 (released 2024-11-14) +---------------------------- + +- [cffLib.specializer] Adjust stack use calculation (#3689) +- [varLib] Lets not add mac names if the rest of name doesn't have them (#3688) +- [ttLib.reorderGlyphs] Update CFF table charstrings and charset (#3682) +- [cffLib.specializer] Add cmdline to specialize a CFF2 font (#3675, #3679) +- [CFF2] Lift uint16 VariationStore.length limitation (#3674) +- [subset] consider variation selectors subsetting cmap14 (#3672) +- [varLib.interpolatable] Support CFF2 fonts (#3670) +- Set isfinal to true in XML parser for proper resource cleanup (#3669) +- [removeOverlaps] Fix CFF CharString width (#3659) +- [glyf] Add optimizeSize option (#3657) +- Python 3.13 support (#3656) +- [TupleVariation] Optimize for loading speed, not size (#3650, #3653) + + +4.54.1 (released 2024-09-24) +---------------------------- + +- [unicodedata] Update to Unicode 16 +- [subset] Escape ``\\`` in doc string + +4.54.0 (released 2024-09-23) +---------------------------- + +- [Docs] Small docs cleanups by @n8willis (#3611) +- [Docs] cleanup code blocks by @n8willis (#3627) +- [Docs] fix Sphinx builds by @n8willis (#3625) +- [merge] Minor fixes to documentation for merge by @drj11 (#3588) +- [subset] Small tweaks to pyftsubset documentation by @RoelN (#3633) +- [Tests] Do not require fonttools command to be available by @behdad (#3612) +- [Tests] subset_test: add failing test to reproduce issue #3616 by @anthrotype (#3622) +- [ttLib] NameRecordVisitor: include whole sequence of character variants' UI labels, not just the first by @anthrotype (#3617) +- [varLib.avar] Reconstruct mappings from binary by @behdad (#3598) +- [varLib.instancer] Fix visual artefacts with partial L2 instancing by @Hoolean (#3635) +- [varLib.interpolatable] Support discrete axes in .designspace by @behdad (#3599) +- [varLib.models] By default, assume OpenType-like normalized space by @behdad (#3601) + +4.53.1 (released 2024-07-05) +---------------------------- + +- [feaLib] Improve the sharing of inline chained lookups (#3559) +- [otlLib] Correct the calculation of OS/2.usMaxContext with reversed chaining contextual single substitutions (#3569) +- [misc.visitor] Visitors search the inheritance chain of objects they are visiting (#3581) + +4.53.0 (released 2024-05-31) +---------------------------- + +- [ttLib.removeOverlaps] Support CFF table to aid in downconverting CFF2 fonts (#3528) +- [avar] Fix crash when accessing not-yet-existing attribute (#3550) +- [docs] Add buildMathTable to otlLib.builder documentation (#3540) +- [feaLib] Allow UTF-8 with BOM when reading features (#3495) +- [SVGPathPen] Revert rounding coordinates to two decimal places by default (#3543) +- [varLib.instancer] Refix output filename decision-making (#3545, #3544, #3548) + +4.52.4 (released 2024-05-27) +---------------------------- + +- [varLib.cff] Restore and deprecate convertCFFtoCFF2 that was removed in 4.52.0 + release as it is used by downstream projects (#3535). + +4.52.3 (released 2024-05-27) +---------------------------- + +- Fixed a small syntax error in the reStructuredText-formatted NEWS.rst file + which caused the upload to PyPI to fail for 4.52.2. No other code changes. + +4.52.2 (released 2024-05-27) +---------------------------- + +- [varLib.interpolatable] Ensure that scipy/numpy output is JSON-serializable + (#3522, #3526). +- [housekeeping] Regenerate table lists, to fix pyinstaller packaging of the new + ``VARC`` table (#3531, #3529). +- [cffLib] Make CFFToCFF2 and CFF2ToCFF more robust (#3521, #3525). + +4.52.1 (released 2024-05-24) +---------------------------- + +- Fixed a small syntax error in the reStructuredText-formatted NEWS.rst file + which caused the upload to PyPI to fail for 4.52.0. No other code changes. + +4.52.0 (released 2024-05-24) +---------------------------- + +- Added support for the new ``VARC`` (Variable Composite) table that is being + proposed to OpenType spec (#3395). For more info: + https://github.com/harfbuzz/boring-expansion-spec/blob/main/VARC.md +- [ttLib.__main__] Fixed decompiling all tables (90fed08). +- [feaLib] Don't reference the same lookup index multiple times within the same + feature record, it is only applied once anyway (#3520). +- [cffLib] Moved methods to desubroutinize, remove hints and unused subroutines + from subset module to cffLib (#3517). +- [varLib.instancer] Added support for partial-instancing CFF2 tables! Also, added + method to down-convert from CFF2 to CFF 1.0, and CLI entry points to convert + CFF<->CFF2 (#3506). +- [subset] Prune unused user name IDs even with --name-IDs='*' (#3410). +- [ttx] use GNU-style getopt to intermix options and positional arguments (#3509). +- [feaLib.variableScalar] Fixed ``value_at_location()`` method (#3491) +- [psCharStrings] Shorten output of ``encodeFloat`` (#3492). +- [bezierTools] Fix infinite-recursion in ``calcCubicArcLength`` (#3502). +- [avar2] Implement ``avar2`` support in ``TTFont.getGlyphSet()`` (#3473). + +4.51.0 (released 2024-04-05) +---------------------------- + +- [ttLib] Optimization on loading aux fields (#3464). +- [ttFont] Add reorderGlyphs (#3468). + +4.50.0 (released 2024-03-15) +---------------------------- + +- [pens] Added decomposing filter pens that draw components as regular contours (#3460). +- [instancer] Drop explicit no-op axes from TupleVariations (#3457). +- [cu2qu/ufo] Return set of modified glyph names from fonts_to_quadratic (#3456). + +4.49.0 (released 2024-02-15) +---------------------------- + +- [otlLib] Add API for building ``MATH`` table (#3446) + +4.48.1 (released 2024-02-06) +---------------------------- + +- Fixed uploading wheels to PyPI, no code changes since v4.48.0. + +4.48.0 (released 2024-02-06) +---------------------------- + +- [varLib] Do not log when there are no OTL tables to be merged. +- [setup.py] Do not restrict lxml<5 any more, tests pass just fine with lxml>=5. +- [feaLib] Remove glyph and class names length restrictions in FEA (#3424). +- [roundingPens] Added ``transformRoundFunc`` parameter to the rounding pens to allow + for custom rounding of the components' transforms (#3426). +- [feaLib] Keep declaration order of ligature components within a ligature set, instead + of sorting by glyph name (#3429). +- [feaLib] Fixed ordering of alternates in ``aalt`` lookups, following the declaration + order of feature references within the ``aalt`` feature block (#3430). +- [varLib.instancer] Fixed a bug in the instancer's IUP optimization (#3432). +- [sbix] Support sbix glyphs with new graphicType "flip" (#3433). +- [svgPathPen] Added ``--glyphs`` option to dump the SVG paths for the named glyphs + in the font (0572f78). +- [designspaceLib] Added "description" attribute to ```` and ```` + elements, and allow multiple ```` elements to group ```` elements + that are logically related (#3435, #3437). +- [otlLib] Correctly choose the most compact GSUB contextual lookup format (#3439). + +4.47.2 (released 2024-01-11) +---------------------------- + +Minor release to fix uploading wheels to PyPI. + +4.47.1 (released 2024-01-11) +---------------------------- + +- [merge] Improve help message and add standard command line options (#3408) +- [otlLib] Pass ``ttFont`` to ``name.addName`` in ``buildStatTable`` (#3406) +- [featureVars] Re-use ``FeatureVariationRecord``'s when possible (#3413) + +4.47.0 (released 2023-12-18) +---------------------------- + +- [varLib.models] New API for VariationModel: ``getMasterScalars`` and + ``interpolateFromValuesAndScalars``. +- [varLib.interpolatable] Various bugfixes and rendering improvements. In particular, + add a Summary page in the front, and an Index and Table-of-Contents in the back. + Change the page size to Letter. +- [Docs/designspaceLib] Defined a new ``public.fontInfo`` lib key, not used anywhere yet (#3358). + +4.46.0 (released 2023-12-02) +---------------------------- + +- [featureVars] Allow to register the same set of substitution rules to multiple features. + The ``addFeatureVariations`` function can now take a list of featureTags; similarly, the + lib key 'com.github.fonttools.varLib.featureVarsFeatureTag' can now take a + comma-separateed string of feature tags (e.g. "salt,ss01") instead of a single tag (#3360). +- [featureVars] Don't overwrite GSUB FeatureVariations, but append new records to it + for features which are not already there. But raise ``VarLibError`` if the feature tag + already has feature variations associated with it (#3363). +- [varLib] Added ``addGSUBFeatureVariations`` function to add GSUB Feature Variations + to an existing variable font from rules defined in a DesignSpace document (#3362). +- [varLib.interpolatable] Various bugfixes and rendering improvements. In particular, + a new test for "underweight" glyphs. The new test reports quite a few false-positives + though. Please send feedback. + +4.45.1 (released 2023-11-23) +---------------------------- + +- [varLib.interpolatable] Various bugfixes and improvements, better reporting, reduced + false positives. +- [ttGlyphSet] Added option to not recalculate glyf bounds (#3348). + +4.45.0 (released 2023-11-20) +---------------------------- + +- [varLib.interpolatable] Vastly improved algorithms. Also available now is ``--pdf`` + and ``--html`` options to generate a PDF or HTML report of the interpolation issues. + The PDF/HTML report showcases the problematic masters, the interpolated broken + glyph, as well as the proposed fixed version. + +4.44.3 (released 2023-11-15) +---------------------------- + +- [subset] Only prune codepage ranges for OS/2.version >= 1, ignore otherwise (#3334). +- [instancer] Ensure hhea vertical metrics stay in sync with OS/2 ones after instancing + MVAR table containing 'hasc', 'hdsc' or 'hlgp' tags (#3297). + +4.44.2 (released 2023-11-14) +---------------------------- + +- [glyf] Have ``Glyph.recalcBounds`` skip empty components (base glyph with no contours) + when computing the bounding box of composite glyphs. This simply restores the existing + behavior before some changes were introduced in fonttools 4.44.0 (#3333). + +4.44.1 (released 2023-11-14) +---------------------------- + +- [feaLib] Ensure variable mark anchors are deep-copied while building since they + get modified in-place and later reused (#3330). +- [OS/2|subset] Added method to ``recalcCodePageRanges`` to OS/2 table class; added + ``--prune-codepage-ranges`` to `fonttools subset` command (#3328, #2607). + +4.44.0 (released 2023-11-03) +---------------------------- + +- [instancer] Recalc OS/2 AvgCharWidth after instancing if default changes (#3317). +- [otlLib] Make ClassDefBuilder class order match varLib.merger's, i.e. large + classes first, then glyph lexicographic order (#3321, #3324). +- [instancer] Allow not specifying any of min:default:max values and let be filled + up with fvar's values (#3322, #3323). +- [instancer] When running --update-name-table ignore axes that have no STAT axis + values (#3318, #3319). +- [Debg] When dumping to ttx, write the embedded JSON as multi-line string with + indentation (92cbfee0d). +- [varStore] Handle > 65535 items per encoding by splitting VarData subtable (#3310). +- [subset] Handle null-offsets in MarkLigPos subtables. +- [subset] Keep East Asian spacing fatures vhal, halt, chws, vchw by default (#3305). +- [instancer.solver] Fixed case where axisDef < lower and upper < axisMax (#3304). +- [glyf] Speed up compilation, mostly around ``recalcBounds`` (#3301). +- [varLib.interpolatable] Speed it up when working on variable fonts, plus various + micro-optimizations (#3300). +- Require unicodedata2 >= 15.1.0 when installed with 'unicode' extra, contains UCD 15.1. + +4.43.1 (released 2023-10-06) +---------------------------- + +- [EBDT] Fixed TypeError exception in `_reverseBytes` method triggered when dumping + some bitmap fonts with `ttx -z bitwise` option (#3162). +- [v/hhea] Fixed UnboundLocalError exception in ``recalc`` method when no vmtx or hmtx + tables are present (#3290). +- [bezierTools] Fixed incorrectly typed cython local variable leading to TypeError when + calling ``calcQuadraticArcLength`` (#3288). +- [feaLib/otlLib] Better error message when building Coverage table with missing glyph (#3286). + +4.43.0 (released 2023-09-29) +---------------------------- + +- [subset] Set up lxml ``XMLParser(resolve_entities=False)`` when parsing OT-SVG documents + to prevent XML External Entity (XXE) attacks (9f61271dc): + https://codeql.github.com/codeql-query-help/python/py-xxe/ +- [varLib.iup] Added workaround for a Cython bug in ``iup_delta_optimize`` that was + leading to IUP tolerance being incorrectly initialised, resulting in sub-optimal deltas + (60126435d, cython/cython#5732). +- [varLib] Added new command-line entry point ``fonttools varLib.avar`` to add an + ``avar`` table to an existing VF from axes mappings in a .designspace file (0a3360e52). +- [instancer] Fixed bug whereby no longer used variation regions were not correctly pruned + after VarData optimization (#3268). +- Added support for Python 3.12 (#3283). + +4.42.1 (released 2023-08-20) +---------------------------- + +- [t1Lib] Fixed several Type 1 issues (#3238, #3240). +- [otBase/packer] Allow sharing tables reached by different offset sizes (#3241, #3236). +- [varLib/merger] Fix Cursive attachment merging error when all anchors are NULL (#3248, #3247). +- [ttLib] Fixed warning when calling ``addMultilingualName`` and ``ttFont`` parameter was not + passed on to ``findMultilingualName`` (#3253). + +4.42.0 (released 2023-08-02) +---------------------------- + +- [varLib] Use sentinel value 0xFFFF to mark a glyph advance in hmtx/vmtx as non + participating, allowing sparse masters to contain glyphs for variation purposes other + than {H,V}VAR (#3235). +- [varLib/cff] Treat empty glyphs in non-default masters as missing, thus not participating + in CFF2 delta computation, similarly to how varLib already treats them for gvar (#3234). +- Added varLib.avarPlanner script to deduce 'correct' avar v1 axis mappings based on + glyph average weights (#3223). + +4.41.1 (released 2023-07-21) +---------------------------- + +- [subset] Fixed perf regression in v4.41.0 by making ``NameRecordVisitor`` only visit + tables that do contain nameID references (#3213, #3214). +- [varLib.instancer] Support instancing fonts containing null ConditionSet offsets in + FeatureVariationRecords (#3211, #3212). +- [statisticsPen] Report font glyph-average weight/width and font-wide slant. +- [fontBuilder] Fixed head.created date incorrectly set to 0 instead of the current + timestamp, regression introduced in v4.40.0 (#3210). +- [varLib.merger] Support sparse ``CursivePos`` masters (#3209). + +4.41.0 (released 2023-07-12) +---------------------------- + +- [fontBuilder] Fixed bug in setupOS2 with default panose attribute incorrectly being + set to a dict instead of a Panose object (#3201). +- [name] Added method to ``removeUnusedNameRecords`` in the user range (#3185). +- [varLib.instancer] Fixed issue with L4 instancing (moving default) (#3179). +- [cffLib] Use latin1 so we can roundtrip non-ASCII in {Full,Font,Family}Name (#3202). +- [designspaceLib] Mark as optional in docs (as it is in the code). +- [glyf-1] Fixed drawPoints() bug whereby last cubic segment becomes quadratic (#3189, #3190). +- [fontBuilder] Propagate the 'hidden' flag to the fvar Axis instance (#3184). +- [fontBuilder] Update setupAvar() to also support avar 2, fixing ``_add_avar()`` call + site (#3183). +- Added new ``voltLib.voltToFea`` submodule (originally Tiro Typeworks' "Volto") for + converting VOLT OpenType Layout sources to FEA format (#3164). + +4.40.0 (released 2023-06-12) +---------------------------- + +- Published native binary wheels to PyPI for all the python minor versions and platform + and architectures currently supported that would benefit from this. They will include + precompiled Cython-accelerated modules (e.g. cu2qu) without requiring to compile them + from source. The pure-python wheel and source distribution will continue to be + published as always (pip will automatically chose them when no binary wheel is + available for the given platform, e.g. pypy). Use ``pip install --no-binary=fonttools fonttools`` + to expliclity request pip to install from the pure-python source. +- [designspaceLib|varLib] Add initial support for specifying axis mappings and build + ``avar2`` table from those (#3123). +- [feaLib] Support variable ligature caret position (#3130). +- [varLib|glyf] Added option to --drop-implied-oncurves; test for impliable oncurve + points either before or after rounding (#3146, #3147, #3155, #3156). +- [TTGlyphPointPen] Don't error with empty contours, simply ignore them (#3145). +- [sfnt] Fixed str vs bytes remnant of py3 transition in code dealing with de/compiling + WOFF metadata (#3129). +- [instancer-solver] Fixed bug when moving default instance with sparse masters (#3139, #3140). +- [feaLib] Simplify variable scalars that don’t vary (#3132). +- [pens] Added filter pen that explicitly emits closing line when lastPt != movePt (#3100). +- [varStore] Improve optimize algorithm and better document the algorithm (#3124, #3127). + Added ``quantization`` option (#3126). +- Added CI workflow config file for building native binary wheels (#3121). +- [fontBuilder] Added glyphDataFormat=0 option; raise error when glyphs contain cubic + outlines but glyphDataFormat was not explicitly set to 1 (#3113, #3119). +- [subset] Prune emptied GDEF.MarkGlyphSetsDef and remap indices; ensure GDEF is + subsetted before GSUB and GPOS (#3114, #3118). +- [xmlReader] Fixed issue whereby DSIG table data was incorrectly parsed (#3115, #2614). +- [varLib/merger] Fixed merging of SinglePos with pos=0 (#3111, #3112). +- [feaLib] Demote "Feature has not been defined" error to a warning when building aalt + and referenced feature is empty (#3110). +- [feaLib] Dedupe multiple substitutions with classes (#3105). + +4.39.4 (released 2023-05-10) +---------------------------- + +- [varLib.interpolatable] Allow for sparse masters (#3075) +- [merge] Handle differing default/nominalWidthX in CFF (#3070) +- [ttLib] Add missing main.py file to ttLib package (#3088) +- [ttx] Fix missing composite instructions in XML (#3092) +- [ttx] Fix split tables option to work on filenames containing '%' (#3096) +- [featureVars] Process lookups for features other than rvrn last (#3099) +- [feaLib] support multiple substitution with classes (#3103) + +4.39.3 (released 2023-03-28) +---------------------------- + +- [sbix] Fixed TypeError when compiling empty glyphs whose imageData is None, regression + was introduced in v4.39 (#3059). +- [ttFont] Fixed AttributeError on python <= 3.10 when opening a TTFont from a tempfile + SpooledTemporaryFile, seekable method only added on python 3.11 (#3052). + +4.39.2 (released 2023-03-16) +---------------------------- + +- [varLib] Fixed regression introduced in 4.39.1 whereby an incomplete 'STAT' table + would be built even though a DesignSpace v5 did contain 'STAT' definitions (#3045, #3046). + +4.39.1 (released 2023-03-16) +---------------------------- + +- [avar2] Added experimental support for reading/writing avar version 2 as specified in + this draft proposal: https://github.com/harfbuzz/boring-expansion-spec/blob/main/avar2.md +- [glifLib] Wrap underlying XML library exceptions with GlifLibError when parsing GLIFs, + and also print the name and path of the glyph that fails to be parsed (#3042). +- [feaLib] Consult avar for normalizing user-space values in ConditionSets and in + VariableScalars (#3042, #3043). +- [ttProgram] Handle string input to Program.fromAssembly() (#3038). +- [otlLib] Added a config option to emit GPOS 7 lookups, currently disabled by default + because of a macOS bug (#3034). +- [COLRv1] Added method to automatically compute ClipBoxes (#3027). +- [ttFont] Fixed getGlyphID to raise KeyError on missing glyphs instead of returning + None. The regression was introduced in v4.27.0 (#3032). +- [sbix] Fixed UnboundLocalError: cannot access local variable 'rawdata' (#3031). +- [varLib] When building VF, do not overwrite a pre-existing ``STAT`` table that was built + with feaLib from FEA feature file. Also, added support for building multiple VFs + defined in Designspace v5 from ``fonttools varLib`` script (#3024). +- [mtiLib] Only add ``Debg`` table with lookup names when ``FONTTOOLS_LOOKUP_DEBUGGING`` + env variable is set (#3023). + +4.39.0 (released 2023-03-06) +---------------------------- + +- [mtiLib] Optionally add `Debg` debug info for MTI feature builds (#3018). +- [ttx] Support reading input file from standard input using special `-` character, + similar to existing `-o -` option to write output to standard output (#3020). +- [cython] Prevent ``cython.compiled`` raise AttributeError if cython not installed + properly (#3017). +- [OS/2] Guard against ZeroDivisionError when calculating xAvgCharWidth in the unlikely + scenario no glyph has non-zero advance (#3015). +- [subset] Recompute xAvgCharWidth independently of --no-prune-unicode-ranges, + previously the two options were involuntarily bundled together (#3012). +- [fontBuilder] Add ``debug`` parameter to addOpenTypeFeatures method to add source + debugging information to the font in the ``Debg`` private table (#3008). +- [name] Make NameRecord `__lt__` comparison not fail on Unicode encoding errors (#3006). +- [featureVars] Fixed bug in ``overlayBox`` (#3003, #3005). +- [glyf] Added experimental support for cubic bezier curves in TrueType glyf table, as + outlined in glyf v1 proposal (#2988): + https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1-cubicOutlines.md +- Added new qu2cu module and related qu2cuPen, the reverse of cu2qu for converting + TrueType quadratic splines to cubic bezier curves (#2993). +- [glyf] Added experimental support for reading and writing Variable Composites/Components + as defined in glyf v1 spec proposal (#2958): + https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1-varComposites.md. +- [pens]: Added `addVarComponent` method to pen protocols' base classes, which pens can implement + to handle varcomponents (by default they get decomposed) (#2958). +- [misc.transform] Added DecomposedTransform class which implements an affine transformation + with separate translate, rotation, scale, skew, and transformation-center components (#2598) +- [sbix] Ensure Glyph.referenceGlyphName is set; fixes error after dumping and + re-compiling sbix table with 'dupe' glyphs (#2984). +- [feaLib] Be cleverer when merging chained single substitutions into same lookup + when they are specified using the inline notation (#2150, #2974). +- [instancer] Clamp user-inputted axis ranges to those of fvar (#2959). +- [otBase/subset] Define ``__getstate__`` for BaseTable so that a copied/pickled 'lazy' + object gets its own OTTableReader to read from; incidentally fixes a bug while + subsetting COLRv1 table containing ClipBoxes on python 3.11 (#2965, #2968). +- [sbix] Handle glyphs with "dupe" graphic type on compile correctly (#2963). +- [glyf] ``endPointsOfContours`` field should be unsigned! Kudos to behdad for + spotting one of the oldest bugs in FT. Probably nobody has ever dared to make + glyphs with more than 32767 points... (#2957). +- [feaLib] Fixed handling of ``ignore`` statements with unmarked glyphs to match + makeotf behavior, which assumes the first glyph is marked (#2950). +- Reformatted code with ``black`` and enforce new code style via CI check (#2925). +- [feaLib] Sort name table entries following OT spec prescribed order in the builder (#2927). +- [cu2quPen] Add Cu2QuMultiPen that converts multiple outlines at a time in + interpolation compatible way; its methods take a list of tuples arguments + that would normally be passed to individual segment pens, and at the end it + dispatches the converted outlines to each pen (#2912). +- [reverseContourPen/ttGlyphPen] Add outputImpliedClosingLine option (#2913, #2914, + #2921, #2922, #2995). +- [gvar] Avoid expanding all glyphs unnecessarily upon compile (#2918). +- [scaleUpem] Fixed bug whereby CFF2 vsindex was scaled; it should not (#2893, #2894). +- [designspaceLib] Add DS.getAxisByTag and refactor getAxis (#2891). +- [unicodedata] map Zmth<->math in ot_tag_{to,from}_script (#1737, #2889). +- [woff2] Support encoding/decoding OVERLAP_SIMPLE glyf flags (#2576, #2884). +- [instancer] Update OS/2 class and post.italicAngle when default moved (L4) +- Dropped support for Python 3.7 which reached EOL, fontTools requires 3.8+. +- [instancer] Fixed instantiateFeatureVariations logic when a rule range becomes + default-applicable (#2737, #2880). +- [ttLib] Add main to ttFont and ttCollection that just decompile and re-compile the + input font (#2869). +- [featureVars] Insert 'rvrn' lookup at the beginning of LookupList, to work around bug + in Apple implementation of 'rvrn' feature which the spec says it should be processed + early whereas on macOS 10.15 it follows lookup order (#2140, #2867). +- [instancer/mutator] Remove 'DSIG' table if present. +- [svgPathPen] Don't close path in endPath(), assume open unless closePath() (#2089, #2865). + +4.38.0 (released 2022-10-21) +---------------------------- + +- [varLib.instancer] Added support for L4 instancing, i.e. moving the default value of + an axis while keeping it variable. Thanks Behdad! (#2728, #2861). + It's now also possible to restrict an axis min/max values beyond the current default + value, e.g. a font wght has min=100, def=400, max=900 and you want a partial VF that + only varies between 500 and 700, you can now do that. + You can either specify two min/max values (wght=500:700), and the new default will be + set to either the minimum or maximum, depending on which one is closer to the current + default (e.g. 500 in this case). Or you can specify three values (e.g. wght=500:600:700) + to specify the new default value explicitly. +- [otlLib/featureVars] Set a few Count values so one doesn't need to compile the font + to update them (#2860). +- [varLib.models] Make extrapolation work for 2-master models as well where one master + is at the default location (#2843, #2846). + Add optional extrapolate=False to normalizeLocation() (#2847, #2849). +- [varLib.cff] Fixed sub-optimal packing of CFF2 deltas by no longer rounding them to + integer (#2838). +- [scaleUpem] Calculate numShorts in VarData after scale; handle CFF hintmasks (#2840). + +4.37.4 (released 2022-09-30) +---------------------------- + +- [subset] Keep nameIDs used by CPAL palette entry labels (#2837). +- [varLib] Avoid negative hmtx values when creating font from variable CFF2 font (#2827). +- [instancer] Don't prune stat.ElidedFallbackNameID (#2828). +- [unicodedata] Update Scripts/Blocks to Unicode 15.0 (#2833). + +4.37.3 (released 2022-09-20) +---------------------------- + +- Fix arguments in calls to (glyf) glyph.draw() and drawPoints(), whereby offset wasn't + correctly passed down; this fix also exposed a second bug, where lsb and tsb were not + set (#2824, #2825, adobe-type-tools/afdko#1560). + +4.37.2 (released 2022-09-15) +---------------------------- + +- [subset] Keep CPAL table and don't attempt to prune unused color indices if OT-SVG + table is present even if COLR table was subsetted away; OT-SVG may be referencing the + CPAL table; for now we assume that's the case (#2814, #2815). +- [varLib.instancer] Downgrade GPOS/GSUB version if there are no more FeatureVariations + after instancing (#2812). +- [subset] Added ``--no-lazy`` to optionally load fonts eagerly (mostly to ease + debugging of table lazy loading, no practical effects) (#2807). +- [varLib] Avoid building empty COLR.DeltaSetIndexMap with only identity mappings (#2803). +- [feaLib] Allow multiple value record types (by promoting to the most general format) + within the same PairPos subtable; e.g. this allows variable and non variable kerning + rules to share the same subtable. This also fixes a bug whereby some kerning pairs + would become unreachable while shapiong because of premature subtable splitting (#2772, #2776). +- [feaLib] Speed up ``VarScalar`` by caching models for recurring master locations (#2798). +- [feaLib] Optionally cythonize ``feaLib.lexer``, speeds up parsing FEA a bit (#2799). +- [designspaceLib] Avoid crash when handling unbounded rule conditions (#2797). +- [post] Don't crash if ``post`` legacy format 1 is malformed/improperly used (#2786) +- [gvar] Don't be "lazy" (load all glyph variations up front) when TTFont.lazy=False (#2771). +- [TTFont] Added ``normalizeLocation`` method to normalize a location dict from the + font's defined axes space (also known as "user space") into the normalized (-1..+1) + space. It applies ``avar`` mapping if the font contains an ``avar`` table (#2789). +- [TTVarGlyphSet] Support drawing glyph instances from CFF2 variable glyph set (#2784). +- [fontBuilder] Do not error when building cmap if there are zero code points (#2785). +- [varLib.plot] Added ability to plot a variation model and set of accompaning master + values corresponding to the model's master locations into a pyplot figure (#2767). +- [Snippets] Added ``statShape.py`` script to draw statistical shape of a glyph as an + ellips (requires pycairo) (baecd88). +- [TTVarGlyphSet] implement drawPoints natively, avoiding going through + SegmentToPointPen (#2778). +- [TTVarGlyphSet] Fixed bug whereby drawing a composite glyph multiple times, its + components would shif; needed an extra copy (#2774). + +4.37.1 (released 2022-08-24) +---------------------------- + +- [subset] Fixed regression introduced with v4.37.0 while subsetting the VarStore of + ``HVAR`` and ``VVAR`` tables, whereby an ``AttributeError: subset_varidxes`` was + thrown because an apparently unused import statement (with the side-effect of + dynamically binding that ``subset_varidxes`` method to the VarStore class) had been + accidentally deleted in an unrelated PR (#2679, #2773). +- [pens] Added ``cairoPen`` (#2678). +- [gvar] Read ``gvar`` more lazily by not parsing all of the ``glyf`` table (#2771). +- [ttGlyphSet] Make ``drawPoints(pointPen)`` method work for CFF fonts as well via + adapter pen (#2770). + +4.37.0 (released 2022-08-23) +---------------------------- + +- [varLib.models] Reverted PR #2717 which added support for "narrow tents" in v4.36.0, + as it introduced a regression (#2764, #2765). It will be restored in upcoming release + once we found a solution to the bug. +- [cff.specializer] Fixed issue in charstring generalizer with the ``blend`` operator + (#2750, #1975). +- [varLib.models] Added support for extrapolation (#2757). +- [ttGlyphSet] Ensure the newly added ``_TTVarGlyphSet`` inherits from ``_TTGlyphSet`` + to keep backward compatibility with existing API (#2762). +- [kern] Allow compiling legacy kern tables with more than 64k entries (d21cfdede). +- [visitor] Added new visitor API to traverse tree of objects and dispatch based + on the attribute type: cf. ``fontTools.misc.visitor`` and ``fontTools.ttLib.ttVisitor``. Added ``fontTools.ttLib.scaleUpem`` module that uses the latter to + change a font's units-per-em and scale all the related fields accordingly (#2718, + #2755). + +4.36.0 (released 2022-08-17) +---------------------------- + +- [varLib.models] Use a simpler model that generates narrower "tents" (regions, master + supports) whenever possible: specifically when any two axes that actively "cooperate" + (have masters at non-zero positions for both axes) have a complete set of intermediates. + The simpler algorithm produces fewer overlapping regions and behaves better with + respect to rounding at the peak positions than the generic solver, always matching + intermediate masters exactly, instead of maximally 0.5 units off. This may be useful + when 100% metrics compatibility is desired (#2218, #2717). +- [feaLib] Remove warning when about ``GDEF`` not being built when explicitly not + requested; don't build one unconditonally even when not requested (#2744, also works + around #2747). +- [ttFont] ``TTFont.getGlyphSet`` method now supports selecting a location that + represents an instance of a variable font (supports both user-scale and normalized + axes coordinates via the ``normalized=False`` parameter). Currently this only works + for TrueType-flavored variable fonts (#2738). + +4.35.0 (released 2022-08-15) +---------------------------- + +- [otData/otConverters] Added support for 'biased' PaintSweepGradient start/end angles + to match latest COLRv1 spec (#2743). +- [varLib.instancer] Fixed bug in ``_instantiateFeatureVariations`` when at the same + time pinning one axis and restricting the range of a subsequent axis; the wrong axis + tag was being used in the latter step (as the records' axisIdx was updated in the + preceding step but looked up using the old axes order in the following step) (#2733, + #2734). +- [mtiLib] Pad script tags with space when less than 4 char long (#1727). +- [merge] Use ``'.'`` instead of ``'#'`` in duplicate glyph names (#2742). +- [gvar] Added support for lazily loading glyph variations (#2741). +- [varLib] In ``build_many``, we forgot to pass on ``colr_layer_reuse`` parameter to + the ``build`` method (#2730). +- [svgPathPen] Add a main that prints SVG for input text (6df779fd). +- [cffLib.width] Fixed off-by-one in optimized values; previous code didn't match the + code block above it (2963fa50). +- [varLib.interpolatable] Support reading .designspace and .glyphs files (via optional + ``glyphsLib``). +- Compile some modules with Cython when available and building/installing fonttools + from source: ``varLib.iup`` (35% faster), ``pens.momentsPen`` (makes + ``varLib.interpolatable`` 3x faster). +- [feaLib] Allow features to be built for VF without also building a GDEF table (e.g. + only build GSUB); warn when GDEF would be needed but isn't requested (#2705, 2694). +- [otBase] Fixed ``AttributeError`` when uharfbuzz < 0.23.0 and 'repack' method is + missing (32aa8eaf). Use new ``uharfbuzz.repack_with_tag`` when available (since + uharfbuzz>=0.30.0), enables table-specific optimizations to be performed during + repacking (#2724). +- [statisticsPen] By default report all glyphs (4139d891). Avoid division-by-zero + (52b28f90). +- [feaLib] Added missing required argument to FeatureLibError exception (#2693) +- [varLib.merge] Fixed error during error reporting (#2689). Fixed undefined + ``NotANone`` variable (#2714). + +4.34.4 (released 2022-07-07) +---------------------------- + +- Fixed typo in varLib/merger.py that causes NameError merging COLR glyphs + containing more than 255 layers (#2685). + +4.34.3 (released 2022-07-07) +---------------------------- + +- [designspaceLib] Don't make up bad PS names when no STAT data (#2684) + +4.34.2 (released 2022-07-06) +---------------------------- + +- [varStore/subset] fixed KeyError exception to do with NO_VARIATION_INDEX while + subsetting varidxes in GPOS/GDEF (a08140d). + +4.34.1 (released 2022-07-06) +---------------------------- + +- [instancer] When optimizing HVAR/VVAR VarStore, use_NO_VARIATION_INDEX=False to avoid + including NO_VARIATION_INDEX in AdvWidthMap, RsbMap, LsbMap mappings, which would + push the VarIdx width to maximum (4bytes), which is not desirable. This also fixes + a hard crash when attempting to subset a varfont after it had been partially instanced + with use_NO_VARIATION_INDEX=True. + +4.34.0 (released 2022-07-06) +---------------------------- + +- [instancer] Set RIBBI bits in head and OS/2 table when cutting instances and the + subfamily nameID=2 contains strings like 'Italic' or 'Bold' (#2673). +- [otTraverse] Addded module containing methods for traversing trees of otData tables + (#2660). +- [otTables] Made DeltaSetIndexMap TTX dump less verbose by omitting no-op entries + (#2660). +- [colorLib.builder] Added option to disable PaintColrLayers's reuse of layers from + LayerList (#2660). +- [varLib] Added support for merging multiple master COLRv1 tables into a variable + COLR table (#2660, #2328). Base color glyphs of same name in different masters must have + identical paint graph structure (incl. number of layers, palette indices, number + of color line stops, corresponding paint formats at each level of the graph), + but can differ in the variable fields (e.g. PaintSolid.Alpha). PaintVar* tables + are produced when this happens and a VarStore/DeltaSetIndexMap is added to the + variable COLR table. It is possible for non-default masters to be 'sparse', i.e. + omit some of the color glyphs present in the default master. +- [feaLib] Let the Parser set nameIDs 1 through 6 that were previously reserved (#2675). +- [varLib.varStore] Support NO_VARIATION_INDEX in optimizer and instancer. +- [feaLib] Show all missing glyphs at once at end of parsing (#2665). +- [varLib.iup] Rewrite force-set conditions and limit DP loopback length (#2651). + For Noto Sans, IUP time drops from 23s down to 9s, with only a slight size increase + in the final font. This basically turns the algorithm from O(n^3) into O(n). +- [featureVars] Report about missing glyphs in substitution rules (#2654). +- [mutator/instancer] Added CLI flag to --no-recalc-timestamp (#2649). +- [SVG] Allow individual SVG documents in SVG OT table to be compressed on uncompressed, + and remember that when roundtripping to/from ttx. The SVG.docList is now a list + of SVGDocument namedtuple-like dataclass containing an extra ``compressed`` field, + and no longer a bare 3-tuple (#2645). +- [designspaceLib] Check for descriptor types with hasattr() to allow custom classes + that don't inherit the default descriptors (#2634). +- [subset] Enable sharing across subtables of extension lookups for harfbuzz packing + (#2626). Updated how table packing falls back to fontTools from harfbuzz (#2668). +- [subset] Updated default feature tags following current Harfbuzz (#2637). +- [svgLib] Fixed regex for real number to support e.g. 1e-4 in addition to 1.0e-4. + Support parsing negative rx, ry on arc commands (#2596, #2611). +- [subset] Fixed subsetting SinglePosFormat2 when ValueFormat=0 (#2603). + +4.33.3 (released 2022-04-26) +---------------------------- + +- [designspaceLib] Fixed typo in ``deepcopyExceptFonts`` method, preventing font + references to be transferred (#2600). Fixed another typo in the name of ``Range`` + dataclass's ``__post_init__`` magic method (#2597). + +4.33.2 (released 2022-04-22) +---------------------------- + +- [otBase] Make logging less verbose when harfbuzz fails to serialize. Do not exit + at the first failure but continue attempting to fix offset overflow error using + the pure-python serializer even when the ``USE_HARFBUZZ_REPACKER`` option was + explicitly set to ``True``. This is normal with fonts with relatively large + tables, at least until hb.repack implements proper table splitting. + +4.33.1 (released 2022-04-22) +---------------------------- + +- [otlLib] Put back the ``FONTTOOLS_GPOS_COMPACT_MODE`` environment variable to fix + regression in ufo2ft (and thus fontmake) introduced with v4.33.0 (#2592, #2593). + This is deprecated and will be removed one ufo2ft gets updated to use the new + config setup. + +4.33.0 (released 2022-04-21) +---------------------------- + +- [OS/2 / merge] Automatically recalculate ``OS/2.xAvgCharWidth`` after merging + fonts with ``fontTools.merge`` (#2591, #2538). +- [misc/config] Added ``fontTools.misc.configTools`` module, a generic configuration + system (#2416, #2439). + Added ``fontTools.config`` module, a fontTools-specific configuration + system using ``configTools`` above. + Attached a ``Config`` object to ``TTFont``. +- [otlLib] Replaced environment variable for GPOS compression level with an + equivalent option using the new config system. +- [designspaceLib] Incremented format version to 5.0 (#2436). + Added discrete axes, variable fonts, STAT information, either design- or + user-space location on instances. + Added ``fontTools.designspaceLib.split`` module to split a designspace + into sub-spaces that interpolate and that represent the variable fonts + listed in the document. + Made instance names optional and allow computing them from STAT data instead. + Added ``fontTools.designspaceLib.statNames`` module. + Allow instances to have the same location as a previously defined STAT label. + Deprecated some attributes: + ``SourceDescriptor``: ``copyLib``, ``copyInfo``, ``copyGroups``, ``copyFeatures``. + ``InstanceDescriptor``: ``kerning``, ``info``; ``glyphs``: use rules or sparse + sources. + For both, ``location``: use the more explicit designLocation. + Note: all are soft deprecations and existing code should keep working. + Updated documentation for Python methods and the XML format. +- [varLib] Added ``build_many`` to build several variable fonts from a single + designspace document (#2436). + Added ``fontTools.varLib.stat`` module to build STAT tables from a designspace + document. +- [otBase] Try to use the Harfbuzz Repacker for packing GSUB/GPOS tables when + ``uharfbuzz`` python bindings are available (#2552). Disable it by setting the + "fontTools.ttLib.tables.otBase:USE_HARFBUZZ_REPACKER" config option to ``False``. + If the option is set explicitly to ``True`` but ``uharfbuzz`` can't be imported + or fails to serialize for any reasons, an error will be raised (ImportError or + uharfbuzz errors). +- [CFF/T2] Ensure that ``pen.closePath()`` gets called for CFF2 charstrings (#2577). + Handle implicit CFF2 closePath within ``T2OutlineExtractor`` (#2580). + +4.32.0 (released 2022-04-08) +---------------------------- + +- [otlLib] Disable GPOS7 optimization to work around bug in Apple CoreText. + Always force Chaining GPOS8 for now (#2540). +- [glifLib] Added ``outputImpliedClosingLine=False`` parameter to ``Glyph.draw()``, + to control behaviour of ``PointToSegmentPen`` (6b4e2e7). +- [varLib.interpolatable] Check for wrong contour starting point (#2571). +- [cffLib] Remove leftover ``GlobalState`` class and fix calls to ``TopDictIndex()`` + (#2569, #2570). +- [instancer] Clear ``AxisValueArray`` if it is empty after instantiating (#2563). + +4.31.2 (released 2022-03-22) +---------------------------- + +- [varLib] fix instantiation of GPOS SinglePos values (#2555). + +4.31.1 (released 2022-03-18) +---------------------------- + +- [subset] fix subsetting OT-SVG when glyph id attribute is on the root ```` + element (#2553). + +4.31.0 (released 2022-03-18) +---------------------------- + +- [ttCollection] Fixed 'ResourceWarning: unclosed file' warning (#2549). +- [varLib.merger] Handle merging SinglePos with valueformat=0 (#2550). +- [ttFont] Update glyf's glyphOrder when calling TTFont.setGlyphOrder() (#2544). +- [ttFont] Added ``ensureDecompiled`` method to load all tables irrespective + of the ``lazy`` attribute (#2551). +- [otBase] Added ``iterSubTable`` method to iterate over BaseTable's children of + type BaseTable; useful for traversing a tree of otTables (#2551). + +4.30.0 (released 2022-03-10) +---------------------------- + +- [varLib] Added debug logger showing the glyph name for which ``gvar`` is built (#2542). +- [varLib.errors] Fixed undefined names in ``FoundANone`` and ``UnsupportedFormat`` + exceptions (ac4d5611). +- [otlLib.builder] Added ``windowsNames`` and ``macNames`` (bool) parameters to the + ``buildStatTabe`` function, so that one can select whether to only add one or both + of the two sets (#2528). +- [t1Lib] Added the ability to recreate PostScript stream (#2504). +- [name] Added ``getFirstDebugName``, ``getBest{Family,SubFamily,Full}Name`` methods (#2526). + +4.29.1 (released 2022-02-01) +---------------------------- + +- [colorLib] Fixed rounding issue with radial gradient's start/end circles inside + one another (#2521). +- [freetypePen] Handle rotate/skew transform when auto-computing width/height of the + buffer; raise PenError wen missing moveTo (#2517) + +4.29.0 (released 2022-01-24) +---------------------------- + +- [ufoLib] Fixed illegal characters and expanded reserved filenames (#2506). +- [COLRv1] Don't emit useless PaintColrLayers of lenght=1 in LayerListBuilder (#2513). +- [ttx] Removed legacy ``waitForKeyPress`` method on Windows (#2509). +- [pens] Added FreeTypePen that uses ``freetype-py`` and the pen protocol for + rasterizating outline paths (#2494). +- [unicodedata] Updated the script direction list to Unicode 14.0 (#2484). + Bumped unicodedata2 dependency to 14.0 (#2499). +- [psLib] Fixed type of ``fontName`` in ``suckfont`` (#2496). + +4.28.5 (released 2021-12-19) +---------------------------- + +- [svgPathPen] Continuation of #2471: make sure all occurrences of ``str()`` are now + replaced with user-defined ``ntos`` callable. +- [merge] Refactored code into submodules, plus several bugfixes and improvements: + fixed duplicate-glyph-resolution GSUB-lookup generation code; use tolerance in glyph + comparison for empty glyph's width; ignore space of default ignorable glyphs; + downgrade duplicates-resolution missing-GSUB from assert to warn; added --drop-tables + option (#2473, #2475, #2476). + +4.28.4 (released 2021-12-15) +---------------------------- + +- [merge] Merge GDEF marksets in Lookups properly (#2474). +- [feaLib] Have ``fontTools feaLib`` script exit with error code when build fails (#2459) +- [svgPathPen] Added ``ntos`` option to customize number formatting (e.g. rounding) (#2471). +- [subset] Speed up subsetting of large CFF fonts (#2467). +- [otTables] Speculatively promote lookups to extension to speed up compilation. If the + offset to lookup N is too big to fit in a ushort, the offset to lookup N+1 is going to + be too big as well, so we promote to extension all lookups from lookup N onwards (#2465). + +4.28.3 (released 2021-12-03) +---------------------------- + +- [subset] Fixed bug while subsetting ``COLR`` table, whereby incomplete layer records + pointing to missing glyphs were being retained leading to ``struct.error`` upon + compiling. Make it so that ``glyf`` glyph closure, which follows the ``COLR`` glyph + closure, does not influence the ``COLR`` table subsetting (#2461, #2462). +- [docs] Fully document the ``cmap`` and ``glyf`` tables (#2454, #2457). +- [colorLib.unbuilder] Fixed CLI by deleting no longer existing parameter (180bb1867). + +4.28.2 (released 2021-11-22) +---------------------------- + +- [otlLib] Remove duplicates when building coverage (#2433). +- [docs] Add interrogate configuration (#2443). +- [docs] Remove comment about missing “start” optional argument to ``calcChecksum`` (#2448). +- [cu2qu/cli] Adapt to the latest ufoLib2. +- [subset] Support subsetting SVG table and remove it from the list of drop by default tables (#534). +- [subset] add ``--pretty-svg`` option to pretty print SVG table contents (#2452). +- [merge] Support merging ``CFF`` tables (CID-keyed ``CFF`` is still not supported) (#2447). +- [merge] Support ``--output-file`` (#2447). +- [docs] Split table docs into individual pages (#2444). +- [feaLib] Forbid empty classes (#2446). +- [docs] Improve documentation for ``fontTools.ttLib.ttFont`` (#2442). + +4.28.1 (released 2021-11-08) +---------------------------- + +- [subset] Fixed AttributeError while traversing a color glyph's Paint graph when there is no + LayerList, which is optional (#2441). + +4.28.0 (released 2021-11-05) +---------------------------- + +- Dropped support for EOL Python 3.6, require Python 3.7 (#2417). +- [ufoLib/glifLib] Make filename-clash checks faster by using a set instead of a list (#2422). +- [subset] Don't crash if optional ClipList and LayerList are ``None`` (empty) (#2424, 2439). +- [OT-SVG] Removed support for old deprecated version 1 and embedded color palettes, + which were never officially part of the OpenType SVG spec. Upon compile, reuse offsets + to SVG documents that are identical (#2430). +- [feaLib] Added support for Variable Feature File syntax. This is experimental and subject + to change until it is finalized in the Adobe FEA spec (#2432). +- [unicodedata] Update Scripts/ScriptExtensions/Blocks to UnicodeData 14.0 (#2437). + +4.27.1 (released 2021-09-23) +---------------------------- + +- [otlLib] Fixed error when chained contextual lookup builder overflows (#2404, #2411). +- [bezierTools] Fixed two floating-point bugs: one when computing `t` for a point + lying on an almost horizontal/vertical line; another when computing the intersection + point between a curve and a line (#2413). + +4.27.0 (released 2021-09-14) +---------------------------- + +- [ttLib/otTables] Cleaned up virtual GID handling: allow virtual GIDs in ``Coverage`` + and ``ClassDef`` readers; removed unused ``allowVID`` argument from ``TTFont`` + constructor, and ``requireReal`` argument in ``TTFont.getGlyphID`` method. + Make ``TTFont.setGlyphOrder`` clear reverse glyphOrder map, and assume ``glyphOrder`` + internal attribute is never modified outside setGlyphOrder; added ``TTFont.getGlyphNameMany`` + and ``getGlyphIDMany`` (#1536, #1654, #2334, #2398). +- [py23] Dropped internal use of ``fontTools.py23`` module to fix deprecation warnings + in client code that imports from fontTools (#2234, #2399, #2400). +- [subset] Fix subsetting COLRv1 clip boxes when font is loaded lazily (#2408). + +4.26.2 (released 2021-08-09) +---------------------------- + +- [otTables] Added missing ``CompositeMode.PLUS`` operator (#2390). + +4.26.1 (released 2021-08-03) +---------------------------- + +- [transform] Added ``transformVector`` and ``transformVectors`` methods to the + ``Transform`` class. Similar to ``transformPoint`` but ignore the translation + part (#2386). + +4.26.0 (released 2021-08-03) +---------------------------- + +- [xmlWriter] Default to ``"\n"`` for ``newlinestr`` instead of platform-specific + ``os.linesep`` (#2384). +- [otData] Define COLRv1 ClipList and ClipBox (#2379). +- [removeOverlaps/instancer] Added --ignore-overlap-errors option to work around + Skia PathOps.Simplify bug (#2382, #2363, google/fonts#3365). +- NOTE: This will be the last version to support Python 3.6. FontTools will require + Python 3.7 or above from the next release (#2350) + +4.25.2 (released 2021-07-26) +---------------------------- + +- [COLRv1] Various changes to sync with the latest CORLv1 draft spec. In particular: + define COLR.VarIndexMap, remove/inline ColorIndex struct, add VarIndexBase to ``PaintVar*`` tables (#2372); + add reduced-precicion specialized transform Paints; + define Angle as fraction of half circle encoded as F2Dot14; + use FWORD (int16) for all Paint center coordinates; + change PaintTransform to have an offset to Affine2x3; +- [ttLib] when importing XML, only set sfntVersion if the font has no reader and is empty (#2376) + +4.25.1 (released 2021-07-16) +---------------------------- + +- [ttGlyphPen] Fixed bug in ``TTGlyphPointPen``, whereby open contours (i.e. starting + with segmentType "move") would throw ``NotImplementedError``. They are now treated + as if they are closed, like with the ``TTGlyphPen`` (#2364, #2366). + +4.25.0 (released 2021-07-05) +---------------------------- + +- [tfmLib] Added new library for parsing TeX Font Metric (TFM) files (#2354). +- [TupleVariation] Make shared tuples order deterministic on python < 3.7 where + Counter (subclass of dict) doesn't remember insertion order (#2351, #2353). +- [otData] Renamed COLRv1 structs to remove 'v1' suffix and match the updated draft + spec: 'LayerV1List' -> 'LayerList', 'BaseGlyphV1List' -> 'BaseGlyphList', + 'BaseGlyphV1Record' -> 'BaseGlyphPaintRecord' (#2346). + Added 8 new ``PaintScale*`` tables: with/without centers, uniform vs non-uniform. + Added ``*AroundCenter`` variants to ``PaintRotate`` and ``PaintSkew``: the default + versions no longer have centerX/Y, but default to origin. + ``PaintRotate``, ``PaintSkew`` and ``PaintComposite`` formats were re-numbered. + NOTE: these are breaking changes; clients using the experimental COLRv1 API will + have to be updated (#2348). +- [pointPens] Allow ``GuessSmoothPointPen`` to accept a tolerance. Fixed call to + ``math.atan2`` with x/y parameters inverted. Sync the code with fontPens (#2344). +- [post] Fixed parsing ``post`` table format 2.0 when it contains extra garbage + at the end of the stringData array (#2314). +- [subset] drop empty features unless 'size' with FeatureParams table (#2324). +- [otlLib] Added ``otlLib.optimize`` module; added GPOS compaction algorithm. + The compaction can be run on existing fonts with ``fonttools otlLib.optimize`` + or using the snippet ``compact_gpos.py``. There's experimental support for + compacting fonts at compilation time using an environment variable, but that + might be removed later (#2326). + +4.24.4 (released 2021-05-25) +---------------------------- + +- [subset/instancer] Fixed ``AttributeError`` when instantiating a VF that + contains GPOS ValueRecords with ``Device`` tables but without the respective + non-Device values (e.g. ``XAdvDevice`` without ``XAdvance``). When not + explicitly set, the latter are assumed to be 0 (#2323). + +4.24.3 (released 2021-05-20) +---------------------------- + +- [otTables] Fixed ``AttributeError`` in methods that split LigatureSubst, + MultipleSubst and AlternateSubst subtables when an offset overflow occurs. + The ``Format`` attribute was removed in v4.22.0 (#2319). + +4.24.2 (released 2021-05-20) +---------------------------- + +- [ttGlyphPen] Fixed typing annotation of TTGlyphPen glyphSet parameter (#2315). +- Fixed two instances of DeprecationWarning: invalid escape sequence (#2311). + +4.24.1 (released 2021-05-20) +---------------------------- + +- [subset] Fixed AttributeError when SinglePos subtable has None Value (ValueFormat 0) + (#2312, #2313). + +4.24.0 (released 2021-05-17) +---------------------------- + +- [pens] Add ``ttGlyphPen.TTGlyphPointPen`` similar to ``TTGlyphPen`` (#2205). + +4.23.1 (released 2021-05-14) +---------------------------- + +- [subset] Fix ``KeyError`` after subsetting ``COLR`` table that initially contains + both v0 and v1 color glyphs when the subset only requested v1 glyphs; we were + not pruning the v0 portion of the table (#2308). +- [colorLib] Set ``LayerV1List`` attribute to ``None`` when empty, it's optional + in CORLv1 (#2308). + +4.23.0 (released 2021-05-13) +---------------------------- + +- [designspaceLib] Allow to use ``\\UNC`` absolute paths on Windows (#2299, #2306). +- [varLib.merger] Fixed bug where ``VarLibMergeError`` was raised with incorrect + parameters (#2300). +- [feaLib] Allow substituting a glyph class with ``NULL`` to delete multiple glyphs + (#2303). +- [glyf] Fixed ``NameError`` exception in ``getPhantomPoints`` (#2295, #2305). +- [removeOverlaps] Retry pathops.simplify after rounding path coordinates to integers + if it fails the first time using floats, to work around a rare and hard to debug + Skia bug (#2288). +- [varLib] Added support for building, reading, writing and optimizing 32-bit + ``ItemVariationStore`` as used in COLRv1 table (#2285). +- [otBase/otConverters] Add array readers/writers for int types (#2285). +- [feaLib] Allow more than one lookahead glyph/class in contextual positioning with + "value at end" (#2293, #2294). +- [COLRv1] Default varIdx should be 0xFFFFFFFF (#2297, #2298). +- [pens] Make RecordingPointPen actually pass on identifiers; replace asserts with + explicit ``PenError`` exception (#2284). +- [mutator] Round lsb for CF2 fonts as well (#2286). + +4.22.1 (released 2021-04-26) +---------------------------- + +- [feaLib] Skip references to named lookups if the lookup block definition + is empty, similarly to makeotf. This also fixes an ``AttributeError`` while + generating ``aalt`` feature (#2276, #2277). +- [subset] Fixed bug with ``--no-hinting`` implementation for Device tables (#2272, + #2275). The previous code was alwyas dropping Device tables if no-hinting was + requested, but some Device tables (DeltaFormat=0x8000) are also used to encode + variation indices and need to be retained. +- [otBase] Fixed bug in getting the ValueRecordSize when decompiling ``MVAR`` + table with ``lazy=True`` (#2273, #2274). +- [varLib/glyf/gvar] Optimized and simplified ``GlyphCoordinates`` and + ``TupleVariation`` classes, use ``bytearray`` where possible, refactored + phantom-points calculations. We measured about 30% speedup in total time + of loading master ttfs, building gvar, and saving (#2261, #2266). +- [subset] Fixed ``AssertionError`` while pruning unused CPAL palettes when + ``0xFFFF`` is present (#2257, #2259). + +4.22.0 (released 2021-04-01) +---------------------------- + +- [ttLib] Remove .Format from Coverage, ClassDef, SingleSubst, LigatureSubst, + AlternateSubst, MultipleSubst (#2238). + ATTENTION: This will change your TTX dumps! +- [misc.arrayTools] move Vector to its own submodule, and rewrite as a tuple + subclass (#2201). +- [docs] Added a terminology section for varLib (#2209). +- [varLib] Move rounding to VariationModel, to avoid error accumulation from + multiple deltas (#2214) +- [varLib] Explain merge errors in more human-friendly terms (#2223, #2226) +- [otlLib] Correct some documentation (#2225) +- [varLib/otlLib] Allow merging into VariationFont without first saving GPOS + PairPos2 (#2229) +- [subset] Improve PairPosFormat2 subsetting (#2221) +- [ttLib] TTFont.save: create file on disk as late as possible (#2253) +- [cffLib] Add missing CFF2 dict operators LanguageGroup and ExpansionFactor + (#2249) + ATTENTION: This will change your TTX dumps! + +4.21.1 (released 2021-02-26) +---------------------------- + +- [pens] Reverted breaking change that turned ``AbstractPen`` and ``AbstractPointPen`` + into abstract base classes (#2164, #2198). + +4.21.0 (released 2021-02-26) +---------------------------- + +- [feaLib] Indent anchor statements in ``asFea()`` to make them more legible and + diff-able (#2193). +- [pens] Turn ``AbstractPen`` and ``AbstractPointPen`` into abstract base classes + (#2164). +- [feaLib] Added support for parsing and building ``STAT`` table from AFDKO feature + files (#2039). +- [instancer] Added option to update name table of generated instance using ``STAT`` + table's axis values (#2189). +- [bezierTools] Added functions to compute bezier point-at-time, as well as line-line, + curve-line and curve-curve intersections (#2192). + +4.20.0 (released 2021-02-15) +---------------------------- + +- [COLRv1] Added ``unbuildColrV1`` to deconstruct COLRv1 otTables to raw json-able + data structure; it does the reverse of ``buildColrV1`` (#2171). +- [feaLib] Allow ``sub X by NULL`` sequence to delete a glyph (#2170). +- [arrayTools] Fixed ``Vector`` division (#2173). +- [COLRv1] Define new ``PaintSweepGradient`` (#2172). +- [otTables] Moved ``Paint.Format`` enum class outside of ``Paint`` class definition, + now named ``PaintFormat``. It was clashing with paint instance ``Format`` attribute + and thus was breaking lazy load of COLR table which relies on magic ``__getattr__`` + (#2175). +- [COLRv1] Replace hand-coded builder functions with otData-driven dynamic + implementation (#2181). +- [COLRv1] Define additional static (non-variable) Paint formats (#2181). +- [subset] Added support for subsetting COLR v1 and CPAL tables (#2174, #2177). +- [fontBuilder] Allow ``setupFvar`` to optionally take ``designspaceLib.AxisDescriptor`` + objects. Added new ``setupAvar`` method. Support localised names for axes and + named instances (#2185). + +4.19.1 (released 2021-01-28) +---------------------------- + +- [woff2] An initial off-curve point with an overlap flag now stays an off-curve + point after compression. + +4.19.0 (released 2021-01-25) +---------------------------- + +- [codecs] Handle ``errors`` parameter different from 'strict' for the custom + extended mac encodings (#2137, #2132). +- [featureVars] Raise better error message when a script is missing the required + default language system (#2154). +- [COLRv1] Avoid abrupt change caused by rounding ``PaintRadialGradient.c0`` when + the start circle almost touches the end circle's perimeter (#2148). +- [COLRv1] Support building unlimited lists of paints as 255-ary trees of + ``PaintColrLayers`` tables (#2153). +- [subset] Prune redundant format-12 cmap subtables when all non-BMP characters + are dropped (#2146). +- [basePen] Raise ``MissingComponentError`` instead of bare ``KeyError`` when a + referenced component is missing (#2145). + +4.18.2 (released 2020-12-16) +---------------------------- + +- [COLRv1] Implemented ``PaintTranslate`` paint format (#2129). +- [varLib.cff] Fixed unbound local variable error (#1787). +- [otlLib] Don't crash when creating OpenType class definitions if some glyphs + occur more than once (#2125). + +4.18.1 (released 2020-12-09) +---------------------------- + +- [colorLib] Speed optimization for ``LayerV1ListBuilder`` (#2119). +- [mutator] Fixed missing tab in ``interpolate_cff2_metrics`` (0957dc7a). + +4.18.0 (released 2020-12-04) +---------------------------- + +- [COLRv1] Update to latest draft: added ``PaintRotate`` and ``PaintSkew`` (#2118). +- [woff2] Support new ``brotlicffi`` bindings for PyPy (#2117). +- [glifLib] Added ``expectContentsFile`` parameter to ``GlyphSet``, for use when + reading existing UFOs, to comply with the specification stating that a + ``contents.plist`` file must exist in a glyph set (#2114). +- [subset] Allow ``LangSys`` tags in ``--layout-scripts`` option (#2112). For example: + ``--layout-scripts=arab.dflt,arab.URD,latn``; this will keep ``DefaultLangSys`` + and ``URD`` language for ``arab`` script, and all languages for ``latn`` script. +- [varLib.interpolatable] Allow UFOs to be checked; report open paths, non existant + glyphs; add a ``--json`` option to produce a machine-readable list of + incompatibilities +- [pens] Added ``QuartzPen`` to create ``CGPath`` from glyph outlines on macOS. + Requires pyobjc (#2107). +- [feaLib] You can export ``FONTTOOLS_LOOKUP_DEBUGGING=1`` to enable feature file + debugging info stored in ``Debg`` table (#2106). +- [otlLib] Build more efficient format 1 and format 2 contextual lookups whenever + possible (#2101). + +4.17.1 (released 2020-11-16) +---------------------------- + +- [colorLib] Fixed regression in 4.17.0 when building COLR v0 table; when color + layers are stored in UFO lib plist, we can't distinguish tuples from lists so + we need to accept either types (e5439eb9, googlefonts/ufo2ft/issues#426). + +4.17.0 (released 2020-11-12) +---------------------------- + +- [colorLib/otData] Updated to latest draft ``COLR`` v1 spec (#2092). +- [svgLib] Fixed parsing error when arc commands' boolean flags are not separated + by space or comma (#2094). +- [varLib] Interpret empty non-default glyphs as 'missing', if the default glyph is + not empty (#2082). +- [feaLib.builder] Only stash lookup location for ``Debg`` if ``Builder.buildLookups_`` + has cooperated (#2065, #2067). +- [varLib] Fixed bug in VarStore optimizer (#2073, #2083). +- [varLib] Add designspace lib key for custom feavar feature tag (#2080). +- Add HashPointPen adapted from psautohint. With this pen, a hash value of a glyph + can be computed, which can later be used to detect glyph changes (#2005). + +4.16.1 (released 2020-10-05) +---------------------------- + +- [varLib.instancer] Fixed ``TypeError`` exception when instantiating a VF with + a GSUB table 1.1 in which ``FeatureVariations`` attribute is present but set to + ``None`` -- indicating that optional ``FeatureVariations`` is missing (#2077). +- [glifLib] Make ``x`` and ``y`` attributes of the ``point`` element required + even when validation is turned off, and raise a meaningful ``GlifLibError`` + message when that happens (#2075). + +4.16.0 (released 2020-09-30) +---------------------------- + +- [removeOverlaps] Added new module and ``removeOverlaps`` function that merges + overlapping contours and components in TrueType glyphs. It requires the + `skia-pathops `__ module. + Note that removing overlaps invalidates the TrueType hinting (#2068). +- [varLib.instancer] Added ``--remove-overlaps`` command-line option. + The ``overlap`` option in ``instantiateVariableFont`` now takes an ``OverlapMode`` + enum: 0: KEEP_AND_DONT_SET_FLAGS, 1: KEEP_AND_SET_FLAGS (default), and 2: REMOVE. + The latter is equivalent to calling ``removeOverlaps`` on the generated static + instance. The option continues to accept ``bool`` value for backward compatibility. + + +4.15.0 (released 2020-09-21) +---------------------------- + +- [plistlib] Added typing annotations to plistlib module. Set up mypy static + typechecker to run automatically on CI (#2061). +- [ttLib] Implement private ``Debg`` table, a reverse-DNS namespaced JSON dict. +- [feaLib] Optionally add an entry into the ``Debg`` table with the original + lookup name (if any), feature name / script / language combination (if any), + and original source filename and line location. Annotate the ttx output for + a lookup with the information from the Debg table (#2052). +- [sfnt] Disabled checksum checking by default in ``SFNTReader`` (#2058). +- [Docs] Document ``mtiLib`` module (#2027). +- [varLib.interpolatable] Added checks for contour node count and operation type + of each node (#2054). +- [ttLib] Added API to register custom table packer/unpacker classes (#2055). + +4.14.0 (released 2020-08-19) +---------------------------- + +- [feaLib] Allow anonymous classes in LookupFlags definitions (#2037). +- [Docs] Better document DesignSpace rules processing order (#2041). +- [ttLib] Fixed 21-year old bug in ``maxp.maxComponentDepth`` calculation (#2044, + #2045). +- [varLib.models] Fixed misspelled argument name in CLI entry point (81d0042a). +- [subset] When subsetting GSUB v1.1, fixed TypeError by checking whether the + optional FeatureVariations table is present (e63ecc5b). +- [Snippets] Added snippet to show how to decompose glyphs in a TTF (#2030). +- [otlLib] Generate GSUB type 5 and GPOS type 7 contextual lookups where appropriate + (#2016). + +4.13.0 (released 2020-07-10) +---------------------------- + +- [feaLib/otlLib] Moved lookup subtable builders from feaLib to otlLib; refactored + some common code (#2004, #2007). +- [docs] Document otlLib module (#2009). +- [glifLib] Fixed bug with some UFO .glif filenames clashing on case-insensitive + filesystems (#2001, #2002). +- [colorLib] Updated COLRv1 implementation following changes in the draft spec: + (#2008, googlefonts/colr-gradients-spec#24). + +4.12.1 (released 2020-06-16) +---------------------------- + +- [_n_a_m_e] Fixed error in ``addMultilingualName`` with one-character names. + Only attempt to recovered malformed UTF-16 data from a ``bytes`` string, + not from unicode ``str`` (#1997, #1998). + +4.12.0 (released 2020-06-09) +---------------------------- + +- [otlLib/varLib] Ensure that the ``AxisNameID`` in the ``STAT`` and ``fvar`` + tables is grater than 255 as per OpenType spec (#1985, #1986). +- [docs] Document more modules in ``fontTools.misc`` package: ``filenames``, + ``fixedTools``, ``intTools``, ``loggingTools``, ``macCreatorType``, ``macRes``, + ``plistlib`` (#1981). +- [OS/2] Don't calculate whole sets of unicode codepoints, use faster and more memory + efficient ranges and bisect lookups (#1984). +- [voltLib] Support writing back abstract syntax tree as VOLT data (#1983). +- [voltLib] Accept DO_NOT_TOUCH_CMAP keyword (#1987). +- [subset/merge] Fixed a namespace clash involving a private helper class (#1955). + +4.11.0 (released 2020-05-28) +---------------------------- + +- [feaLib] Introduced ``includeDir`` parameter on Parser and IncludingLexer to + explicitly specify the directory to search when ``include()`` statements are + encountered (#1973). +- [ufoLib] Silently delete duplicate glyphs within the same kerning group when reading + groups (#1970). +- [ttLib] Set version of COLR table when decompiling COLRv1 (commit 9d8a7e2). + +4.10.2 (released 2020-05-20) +---------------------------- + +- [sfnt] Fixed ``NameError: SimpleNamespace`` while reading TTC header. The regression + was introduced with 4.10.1 after removing ``py23`` star import. + +4.10.1 (released 2020-05-19) +---------------------------- + +- [sfnt] Make ``SFNTReader`` pickleable even when TTFont is loaded with lazy=True + option and thus keeps a reference to an external file (#1962, #1967). +- [feaLib.ast] Restore backward compatibility (broken in 4.10 with #1905) for + ``ChainContextPosStatement`` and ``ChainContextSubstStatement`` classes. + Make them accept either list of lookups or list of lists of lookups (#1961). +- [docs] Document some modules in ``fontTools.misc`` package: ``arrayTools``, + ``bezierTools`` ``cliTools`` and ``eexec`` (#1956). +- [ttLib._n_a_m_e] Fixed ``findMultilingualName()`` when name record's ``string`` is + encoded as bytes sequence (#1963). + +4.10.0 (released 2020-05-15) +---------------------------- + +- [varLib] Allow feature variations to be active across the entire space (#1957). +- [ufoLib] Added support for ``formatVersionMinor`` in UFO's ``fontinfo.plist`` and for + ``formatMinor`` attribute in GLIF file as discussed in unified-font-object/ufo-spec#78. + No changes in reading or writing UFOs until an upcoming (non-0) minor update of the + UFO specification is published (#1786). +- [merge] Fixed merging fonts with different versions of ``OS/2`` table (#1865, #1952). +- [subset] Fixed ``AttributeError`` while subsetting ``ContextSubst`` and ``ContextPos`` + Format 3 subtable (#1879, #1944). +- [ttLib.table._m_e_t_a] if data happens to be ascii, emit comment in TTX (#1938). +- [feaLib] Support multiple lookups per glyph position (#1905). +- [psCharStrings] Use inheritance to avoid repeated code in initializer (#1932). +- [Doc] Improved documentation for the following modules: ``afmLib`` (#1933), ``agl`` + (#1934), ``cffLib`` (#1935), ``cu2qu`` (#1937), ``encodings`` (#1940), ``feaLib`` + (#1941), ``merge`` (#1949). +- [Doc] Split off developer-centric info to new page, making front page of docs more + user-focused. List all utilities and sub-modules with brief descriptions. + Make README more concise and focused (#1914). +- [otlLib] Add function to build STAT table from high-level description (#1926). +- [ttLib._n_a_m_e] Add ``findMultilingualName()`` method (#1921). +- [unicodedata] Update ``RTL_SCRIPTS`` for Unicode 13.0 (#1925). +- [gvar] Sort ``gvar`` XML output by glyph name, not glyph order (#1907, #1908). +- [Doc] Added help options to ``fonttools`` command line tool (#1913, #1920). + Ensure all fonttools CLI tools have help documentation (#1948). +- [ufoLib] Only write fontinfo.plist when there actually is content (#1911). + +4.9.0 (released 2020-04-29) +--------------------------- + +- [subset] Fixed subsetting of FeatureVariations table. The subsetter no longer drops + FeatureVariationRecords that have empty substitutions as that will keep the search + going and thus change the logic. It will only drop empty records that occur at the + end of the FeatureVariationRecords array (#1881). +- [subset] Remove FeatureVariations table and downgrade GSUB/GPOS to version 0x10000 + when FeatureVariations contain no FeatureVariationRecords after subsetting (#1903). +- [agl] Add support for legacy Adobe Glyph List of glyph names in ``fontTools.agl`` + (#1895). +- [feaLib] Ignore superfluous script statements (#1883). +- [feaLib] Hide traceback by default on ``fonttools feaLib`` command line. + Use ``--traceback`` option to show (#1898). +- [feaLib] Check lookup index in chaining sub/pos lookups and print better error + message (#1896, #1897). +- [feaLib] Fix building chained alt substitutions (#1902). +- [Doc] Included all fontTools modules in the sphinx-generated documentation, and + published it to ReadTheDocs for continuous documentation of the fontTools project + (#1333). Check it out at https://fonttools.readthedocs.io/. Thanks to Chris Simpkins! +- [transform] The ``Transform`` class is now subclass of ``typing.NamedTuple``. No + change in functionality (#1904). + + +4.8.1 (released 2020-04-17) +--------------------------- + +- [feaLib] Fixed ``AttributeError: 'NoneType' has no attribute 'getAlternateGlyphs'`` + when ``aalt`` feature references a chain contextual substitution lookup + (googlefonts/fontmake#648, #1878). + +4.8.0 (released 2020-04-16) +--------------------------- + +- [feaLib] If Parser is initialized without a ``glyphNames`` parameter, it cannot + distinguish between a glyph name containing an hyphen, or a range of glyph names; + instead of raising an error, it now interprets them as literal glyph names, while + also outputting a logging warning to alert user about the ambiguity (#1768, #1870). +- [feaLib] When serializing AST to string, emit spaces around hyphens that denote + ranges. Also, fixed an issue with CID ranges when round-tripping AST->string->AST + (#1872). +- [Snippets/otf2ttf] In otf2ttf.py script update LSB in hmtx to match xMin (#1873). +- [colorLib] Added experimental support for building ``COLR`` v1 tables as per + the `colr-gradients-spec `__ + draft proposal. **NOTE**: both the API and the XML dump of ``COLR`` v1 are + susceptible to change while the proposal is being discussed and formalized (#1822). + +4.7.0 (released 2020-04-03) +--------------------------- + +- [cu2qu] Added ``fontTools.cu2qu`` package, imported from the original + `cu2qu `__ project. The ``cu2qu.pens`` module + was moved to ``fontTools.pens.cu2quPen``. The optional cu2qu extension module + can be compiled by installing `Cython `__ before installing + fonttools from source (i.e. git repo or sdist tarball). The wheel package that + is published on PyPI (i.e. the one ``pip`` downloads, unless ``--no-binary`` + option is used), will continue to be pure-Python for now (#1868). + +4.6.0 (released 2020-03-24) +--------------------------- + +- [varLib] Added support for building variable ``BASE`` table version 1.1 (#1858). +- [CPAL] Added ``fromRGBA`` method to ``Color`` class (#1861). + + +4.5.0 (released 2020-03-20) +--------------------------- + +- [designspaceLib] Added ``add{Axis,Source,Instance,Rule}Descriptor`` methods to + ``DesignSpaceDocument`` class, to initialize new descriptor objects using keyword + arguments, and at the same time append them to the current document (#1860). +- [unicodedata] Update to Unicode 13.0 (#1859). + +4.4.3 (released 2020-03-13) +--------------------------- + +- [varLib] Always build ``gvar`` table for TrueType-flavored Variable Fonts, + even if it contains no variation data. The table is required according to + the OpenType spec (#1855, #1857). + +4.4.2 (released 2020-03-12) +--------------------------- + +- [ttx] Annotate ``LookupFlag`` in XML dump with comment explaining what bits + are set and what they mean (#1850). +- [feaLib] Added more descriptive message to ``IncludedFeaNotFound`` error (#1842). + +4.4.1 (released 2020-02-26) +--------------------------- + +- [woff2] Skip normalizing ``glyf`` and ``loca`` tables if these are missing from + a font (e.g. in NotoColorEmoji using ``CBDT/CBLC`` tables). +- [timeTools] Use non-localized date parsing in ``timestampFromString``, to fix + error when non-English ``LC_TIME`` locale is set (#1838, #1839). +- [fontBuilder] Make sure the CFF table generated by fontBuilder can be used by varLib + without having to compile and decompile the table first. This was breaking in + converting the CFF table to CFF2 due to some unset attributes (#1836). + +4.4.0 (released 2020-02-18) +--------------------------- + +- [colorLib] Added ``fontTools.colorLib.builder`` module, initially with ``buildCOLR`` + and ``buildCPAL`` public functions. More color font formats will follow (#1827). +- [fontBuilder] Added ``setupCOLR`` and ``setupCPAL`` methods (#1826). +- [ttGlyphPen] Quantize ``GlyphComponent.transform`` floats to ``F2Dot14`` to fix + round-trip issue when computing bounding boxes of transformed components (#1830). +- [glyf] If a component uses reference points (``firstPt`` and ``secondPt``) for + alignment (instead of X and Y offsets), compute the effective translation offset + *after* having applied any transform (#1831). +- [glyf] When all glyphs have zero contours, compile ``glyf`` table data as a single + null byte in order to pass validation by OTS and Windows (#1829). +- [feaLib] Parsing feature code now ensures that referenced glyph names are part of + the known glyph set, unless a glyph set was not provided. +- [varLib] When filling in the default axis value for a missing location of a source or + instance, correctly map the value forward. +- [varLib] The avar table can now contain mapping output values that are greater than + OR EQUAL to the preceeding value, as the avar specification allows this. +- [varLib] The errors of the module are now ordered hierarchically below VarLibError. + See #1821. + +4.3.0 (released 2020-02-03) +--------------------------- + +- [EBLC/CBLC] Fixed incorrect padding length calculation for Format 3 IndexSubTable + (#1817, #1818). +- [varLib] Fixed error when merging OTL tables and TTFonts were loaded as ``lazy=True`` + (#1808, #1809). +- [varLib] Allow to use master fonts containing ``CFF2`` table when building VF (#1816). +- [ttLib] Make ``recalcBBoxes`` option work also with ``CFF2`` table (#1816). +- [feaLib] Don't reset ``lookupflag`` in lookups defined inside feature blocks. + They will now inherit the current ``lookupflag`` of the feature. This is what + Adobe ``makeotf`` also does in this case (#1815). +- [feaLib] Fixed bug with mixed single/multiple substitutions. If a single substitution + involved a glyph class, we were incorrectly using only the first glyph in the class + (#1814). + +4.2.5 (released 2020-01-29) +--------------------------- + +- [feaLib] Do not fail on duplicate multiple substitutions, only warn (#1811). +- [subset] Optimize SinglePos subtables to Format 1 if all ValueRecords are the same + (#1802). + +4.2.4 (released 2020-01-09) +--------------------------- + +- [unicodedata] Update RTL_SCRIPTS for Unicode 11 and 12. + +4.2.3 (released 2020-01-07) +--------------------------- + +- [otTables] Fixed bug when splitting `MarkBasePos` subtables as offsets overflow. + The mark class values in the split subtable were not being updated, leading to + invalid mark-base attachments (#1797, googlefonts/noto-source#145). +- [feaLib] Only log a warning instead of error when features contain duplicate + substitutions (#1767). +- [glifLib] Strip XML comments when parsing with lxml (#1784, #1785). + +4.2.2 (released 2019-12-12) +--------------------------- + +- [subset] Fixed issue with subsetting FeatureVariations table when the index + of features changes as features get dropped. The feature index need to be + remapped to point to index of the remaining features (#1777, #1782). +- [fontBuilder] Added `addFeatureVariations` method to `FontBuilder` class. This + is a shorthand for calling `featureVars.addFeatureVariations` on the builder's + TTFont object (#1781). +- [glyf] Fixed the flags bug in glyph.drawPoints() like we did for glyph.draw() + (#1771, #1774). + +4.2.1 (released 2019-12-06) +--------------------------- + +- [glyf] Use the ``flagOnCurve`` bit mask in ``glyph.draw()``, so that we ignore + the ``overlap`` flag that may be set when instantiating variable fonts (#1771). + +4.2.0 (released 2019-11-28) +--------------------------- + +- [pens] Added the following pens: + + * ``roundingPen.RoundingPen``: filter pen that rounds coordinates and components' + offsets to integer; + * ``roundingPen.RoundingPointPen``: like the above, but using PointPen protocol. + * ``filterPen.FilterPointPen``: base class for filter point pens; + * ``transformPen.TransformPointPen``: filter point pen to apply affine transform; + * ``recordingPen.RecordingPointPen``: records and replays point-pen commands. + +- [ttGlyphPen] Always round float coordinates and component offsets to integers + (#1763). +- [ufoLib] When converting kerning groups from UFO2 to UFO3, avoid confusing + groups with the same name as one of the glyphs (#1761, #1762, + unified-font-object/ufo-spec#98). + +4.1.0 (released 2019-11-18) +--------------------------- + +- [instancer] Implemented restricting axis ranges (level 3 partial instancing). + You can now pass ``{axis_tag: (min, max)}`` tuples as input to the + ``instantiateVariableFont`` function. Note that changing the default axis + position is not supported yet. The command-line script also accepts axis ranges + in the form of colon-separated float values, e.g. ``wght=400:700`` (#1753, #1537). +- [instancer] Never drop STAT ``DesignAxis`` records, but only prune out-of-range + ``AxisValue`` records. +- [otBase/otTables] Enforce that VarStore.RegionAxisCount == fvar.axisCount, even + when regions list is empty to appease OTS < v8.0 (#1752). +- [designspaceLib] Defined new ``processing`` attribute for ```` element, + with values "first" or "last", plus other editorial changes to DesignSpace + specification. Bumped format version to 4.1 (#1750). +- [varLib] Improved error message when masters' glyph orders do not match (#1758, + #1759). +- [featureVars] Allow to specify custom feature tag in ``addFeatureVariations``; + allow said feature to already exist, in which case we append new lookup indices + to existing features. Implemented ```` attribute ``processing`` according to + DesignSpace specification update in #1750. Depending on this flag, we generate + either an 'rvrn' (always processed first) or a 'rclt' feature (follows lookup order, + therefore last) (#1747, #1625, #1371). +- [ttCollection] Added support for context manager auto-closing via ``with`` statement + like with ``TTFont`` (#1751). +- [unicodedata] Require unicodedata2 >= 12.1.0. +- [py2.py3] Removed yet more PY2 vestiges (#1743). +- [_n_a_m_e] Fixed issue when comparing NameRecords with different string types (#1742). +- [fixedTools] Changed ``fixedToFloat`` to not do any rounding but simply return + ``value / (1 << precisionBits)``. Added ``floatToFixedToStr`` and + ``strToFixedToFloat`` functions to be used when loading from or dumping to XML. + Fixed values (e.g. fvar axes and instance coordinates, avar mappings, etc.) are + are now stored as un-rounded decimal floats upon decompiling (#1740, #737). +- [feaLib] Fixed handling of multiple ``LigatureCaret`` statements for the same glyph. + Only the first rule per glyph is used, additional ones are ignored (#1733). + +4.0.2 (released 2019-09-26) +--------------------------- + +- [voltLib] Added support for ``ALL`` and ``NONE`` in ``PROCESS_MARKS`` (#1732). +- [Silf] Fixed issue in ``Silf`` table compilation and decompilation regarding str vs + bytes in python3 (#1728). +- [merge] Handle duplicate glyph names better: instead of appending font index to + all glyph names, use similar code like we use in ``post`` and ``CFF`` tables (#1729). + +4.0.1 (released 2019-09-11) +--------------------------- + +- [otTables] Support fixing offset overflows in ``MultipleSubst`` lookup subtables + (#1706). +- [subset] Prune empty strikes in ``EBDT`` and ``CBDT`` table data (#1698, #1633). +- [pens] Fixed issue in ``PointToSegmentPen`` when last point of closed contour has + same coordinates as the starting point and was incorrectly dropped (#1720). +- [Graphite] Fixed ``Sill`` table output to pass OTS (#1705). +- [name] Added ``removeNames`` method to ``table__n_a_m_e`` class (#1719). +- [ttLib] Added aliases for renamed entries ``ascender`` and ``descender`` in + ``hhea`` table (#1715). + +4.0.0 (released 2019-08-22) +--------------------------- + +- NOTE: The v4.x version series only supports Python 3.6 or greater. You can keep + using fonttools 3.x if you need support for Python 2. +- [py23] Removed all the python2-only code since it is no longer reachable, thus + unused; only the Python3 symbols were kept, but these are no-op. The module is now + DEPRECATED and will removed in the future. +- [ttLib] Fixed UnboundLocalError for empty loca/glyph tables (#1680). Also, allow + the glyf table to be incomplete when dumping to XML (#1681). +- [varLib.models] Fixed KeyError while sorting masters and there are no on-axis for + a given axis (38a8eb0e). +- [cffLib] Make sure glyph names are unique (#1699). +- [feaLib] Fix feature parser to correctly handle octal numbers (#1700). + +\... see `here `__ for earlier changes diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/RECORD b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..9abd96f6038d78ea28270aec363852c0aa65a712 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/RECORD @@ -0,0 +1,675 @@ +../../../bin/fonttools,sha256=wwclS9dk9vogE-EznmUtFu6Tn_2I7q7TGFjx1dq9HAE,272 +../../../bin/pyftmerge,sha256=GP9Qq9ylfqhabmFPO_NLTeEq9yt3o4C_Rn4oNeagZLM,269 +../../../bin/pyftsubset,sha256=KwdsF4i2hdB1aFIzQuC-ZVHDR2hOuRCJD81ysq28L1M,270 +../../../bin/ttx,sha256=25wUh8fgq7Q1QECDkl18juLPllcTgfOHPEvv-I1T_vg,267 +../../../share/man/man1/ttx.1,sha256=cLbm_pOOj1C76T2QXvDxzwDj9gk-GTd5RztvTMsouFw,5377 +fontTools/__init__.py,sha256=fgEzyisKw1rSSjT7FZcZnqK0rK2jqxT-G6ctz4mAi9U,183 +fontTools/__main__.py,sha256=VjkGh1UD-i1zTDA1dXo1uecSs6PxHdGQ5vlCk_mCCYs,925 +fontTools/__pycache__/__init__.cpython-310.pyc,, +fontTools/__pycache__/__main__.cpython-310.pyc,, +fontTools/__pycache__/afmLib.cpython-310.pyc,, +fontTools/__pycache__/agl.cpython-310.pyc,, +fontTools/__pycache__/fontBuilder.cpython-310.pyc,, +fontTools/__pycache__/help.cpython-310.pyc,, +fontTools/__pycache__/tfmLib.cpython-310.pyc,, +fontTools/__pycache__/ttx.cpython-310.pyc,, +fontTools/__pycache__/unicode.cpython-310.pyc,, +fontTools/afmLib.py,sha256=1MagIItOzRV4vV5kKPxeDZbPJsfxLB3wdHLFkQvl0uk,13164 +fontTools/agl.py,sha256=05bm8Uq45uVWW8nPbP6xbNgmFyxQr8sWhYAiP0VSjnI,112975 +fontTools/cffLib/CFF2ToCFF.py,sha256=pvwh6qxJ0D7c4xgXBcyAdmZGzpTiywMy45-jjp7dKck,6088 +fontTools/cffLib/CFFToCFF2.py,sha256=Qnk7lYlsTRHnlZQ6NXNdr_f4MJwZQ21kcS08KFbsyY8,10119 +fontTools/cffLib/__init__.py,sha256=62vpcR7u8cE407kXduAwnFttHnsoCpDQ7IBK-qOYFQ8,107886 +fontTools/cffLib/__pycache__/CFF2ToCFF.cpython-310.pyc,, +fontTools/cffLib/__pycache__/CFFToCFF2.cpython-310.pyc,, +fontTools/cffLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/cffLib/__pycache__/specializer.cpython-310.pyc,, +fontTools/cffLib/__pycache__/transforms.cpython-310.pyc,, +fontTools/cffLib/__pycache__/width.cpython-310.pyc,, +fontTools/cffLib/specializer.py,sha256=vsOPkR_jHNe6tESQEjmm0i76y7sWI5MKo3bsTmI3sNM,32609 +fontTools/cffLib/transforms.py,sha256=kHBnYQmcJBLIMUC6Uws4eor2mJiNNHiR_eRePXHDPC8,17371 +fontTools/cffLib/width.py,sha256=IqGL0CLyCZqi_hvsHySG08qpYxS3kaqW-tsAT-bjHV4,6074 +fontTools/colorLib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +fontTools/colorLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/colorLib/__pycache__/builder.cpython-310.pyc,, +fontTools/colorLib/__pycache__/errors.cpython-310.pyc,, +fontTools/colorLib/__pycache__/geometry.cpython-310.pyc,, +fontTools/colorLib/__pycache__/table_builder.cpython-310.pyc,, +fontTools/colorLib/__pycache__/unbuilder.cpython-310.pyc,, +fontTools/colorLib/builder.py,sha256=kmO7OuudQQb3fEOS7aLzgTDVjqS9i2xIQmk9p1uBe8A,23008 +fontTools/colorLib/errors.py,sha256=CsaviiRxxrpgVX4blm7KCyK8553ljwL44xkJOeC5U7U,41 +fontTools/colorLib/geometry.py,sha256=3ScySrR2YDJa7d5K5_xM5Yt1-3NCV-ry8ikYA5VwVbI,5518 +fontTools/colorLib/table_builder.py,sha256=ZeltWY6n-YPiJv_hQ1iBXoEFAG70EKxZyScgsMKUFGU,7469 +fontTools/colorLib/unbuilder.py,sha256=iW-E5I39WsV82K3NgCO4Cjzwm1WqzGrtypHt8epwbHM,2142 +fontTools/config/__init__.py,sha256=JICOHIz06KuHCiBmrxj-ga19P6ZTuLXh0lHmPh-Ra1w,3154 +fontTools/config/__pycache__/__init__.cpython-310.pyc,, +fontTools/cu2qu/__init__.py,sha256=Cuc7Uglb0nSgaraTxXY5J8bReznH5wApW0uakN7MycY,618 +fontTools/cu2qu/__main__.py,sha256=kTUI-jczsHeelULLlory74QEeFjZWp9zigCc7PrdVQY,92 +fontTools/cu2qu/__pycache__/__init__.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/__main__.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/benchmark.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/cli.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/cu2qu.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/errors.cpython-310.pyc,, +fontTools/cu2qu/__pycache__/ufo.cpython-310.pyc,, +fontTools/cu2qu/benchmark.py,sha256=wasPJmf8q9k9UHjpHChC3WQAGbBAyHN9PvJzXvWC0Fw,1296 +fontTools/cu2qu/cli.py,sha256=MbAQnOpZwrUFe_tjAP3Tgf6uLdOgHlONUcPNeTXwH0Y,6076 +fontTools/cu2qu/cu2qu.c,sha256=5MqBt8fFiiaXVyfUVZbAkI1P1CP9g6ZSyk7LbKzmx78,629339 +fontTools/cu2qu/cu2qu.cpython-310-x86_64-linux-gnu.so,sha256=h_-2nNpp4TjpN50hFZnvIb0PFC_PQA54v7GHylEND7k,1024424 +fontTools/cu2qu/cu2qu.py,sha256=GGNdNWT4xgrvxeZTczIj9Nxi6vN-5lb90KyVmB95W0Y,16439 +fontTools/cu2qu/errors.py,sha256=PyJNMy8lHDtKpfFkc0nkM8F4jNLZAC4lPQCN1Km4bpg,2441 +fontTools/cu2qu/ufo.py,sha256=qZR70uWdCia19Ff8GLn5NeItscvvn69DegjDZVF4eNI,11794 +fontTools/designspaceLib/__init__.py,sha256=NGIC5zaq0NDdSkOyl6-i327cAzgCS3jeayEDvMEXRwY,129263 +fontTools/designspaceLib/__main__.py,sha256=xhtYXo1T1tsykhQDD0tcconSNYgWL5hoTBORpVDUYrc,103 +fontTools/designspaceLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/designspaceLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/designspaceLib/__pycache__/split.cpython-310.pyc,, +fontTools/designspaceLib/__pycache__/statNames.cpython-310.pyc,, +fontTools/designspaceLib/__pycache__/types.cpython-310.pyc,, +fontTools/designspaceLib/split.py,sha256=FB1NuvhUO453UXveQZi9oyrW_caoCPM3RADp1rYWkDs,19239 +fontTools/designspaceLib/statNames.py,sha256=gXGKWVr1ju2_oL-R_DkyoZ3GlI7mfLORovpk1Ebgmvc,9237 +fontTools/designspaceLib/types.py,sha256=ofK65qXNADqcpl7zI72Pa5s07-cm7G41iEmLVV44-Es,5320 +fontTools/encodings/MacRoman.py,sha256=4vEooUDm2gLCG8KIIDhRxm5-A64w7XrhP9cjDRr2Eo0,3576 +fontTools/encodings/StandardEncoding.py,sha256=Eo3AGE8FE_p-IVYYuV097KouSsF3UrXoRRN0XyvYbrs,3581 +fontTools/encodings/__init__.py,sha256=DJBWmoX_Haau7qlgmvWyfbhSzrX2qL636Rns7CG01pk,75 +fontTools/encodings/__pycache__/MacRoman.cpython-310.pyc,, +fontTools/encodings/__pycache__/StandardEncoding.cpython-310.pyc,, +fontTools/encodings/__pycache__/__init__.cpython-310.pyc,, +fontTools/encodings/__pycache__/codecs.cpython-310.pyc,, +fontTools/encodings/codecs.py,sha256=u50ruwz9fcRsrUrRGpR17Cr55Ovn1fvCHCKrElVumDE,4721 +fontTools/feaLib/__init__.py,sha256=jlIru2ghxvb1HhC5Je2BCXjFJmFQlYKpruorPoz3BvQ,213 +fontTools/feaLib/__main__.py,sha256=Df2PA6LXwna98lSXiL7R4as_ZEdWCIk3egSM5w7GpvM,2240 +fontTools/feaLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/feaLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/feaLib/__pycache__/ast.cpython-310.pyc,, +fontTools/feaLib/__pycache__/builder.cpython-310.pyc,, +fontTools/feaLib/__pycache__/error.cpython-310.pyc,, +fontTools/feaLib/__pycache__/lexer.cpython-310.pyc,, +fontTools/feaLib/__pycache__/location.cpython-310.pyc,, +fontTools/feaLib/__pycache__/lookupDebugInfo.cpython-310.pyc,, +fontTools/feaLib/__pycache__/parser.cpython-310.pyc,, +fontTools/feaLib/__pycache__/variableScalar.cpython-310.pyc,, +fontTools/feaLib/ast.py,sha256=48Y6vpSD_wYfucWyh_bQtzf2AQFX-pOwBvsxdcpVDz0,74158 +fontTools/feaLib/builder.py,sha256=f8bet-VuwM6aIBIUPgKUQ01chyTSaWOzn152zq4Y1rY,73165 +fontTools/feaLib/error.py,sha256=Bz_5tNcNVcY7_nrAmFlQNhQldtqZWd8WUGQ2E3PWhZo,648 +fontTools/feaLib/lexer.c,sha256=yVnAVeP6s8MEmqVhbu3aZHdgJZxF-AQyzNssVS-09rY,753326 +fontTools/feaLib/lexer.cpython-310-x86_64-linux-gnu.so,sha256=GVewKKuK-Fasd0UH8NxHVa4uWc-SQ_UTrZBnhYPOVgw,1414432 +fontTools/feaLib/lexer.py,sha256=emyMPmRoqNZkzxnJyI6JRCCtXrbCOFofwa9O6ABGLiw,11121 +fontTools/feaLib/location.py,sha256=JXzHqGV56EHdcq823AwA5oaK05hf_1ySWpScbo3zGC0,234 +fontTools/feaLib/lookupDebugInfo.py,sha256=gVRr5-APWfT_a5-25hRuawSVX8fEvXVsOSLWkH91T2w,304 +fontTools/feaLib/parser.py,sha256=YDy5ySnQaNXB-q-fSpcIAbBRzQO9jJH8t2Y7jt3vpuI,99331 +fontTools/feaLib/variableScalar.py,sha256=Xu8tpDlQbfIfjnKnYDEf43EqVdyIJUy8_1ROVPg9_mg,4069 +fontTools/fontBuilder.py,sha256=yF2-IYl_hao-Zy_FWSI4R-HnlFpzFrz0YBGQO8zfaOs,34130 +fontTools/help.py,sha256=bAjatvIhV7TJyXI7WhsxdYO4YVlhScZXu_kRtHANEPo,1125 +fontTools/merge/__init__.py,sha256=8i6ownyQTAOBKWnTEHvvCYFw64Mv7Z1HPBgJI-ZiuKo,8256 +fontTools/merge/__main__.py,sha256=hDx3gfbUBO83AJKumSEhiV-xqNTJNNgK2uFjazOGTmw,94 +fontTools/merge/__pycache__/__init__.cpython-310.pyc,, +fontTools/merge/__pycache__/__main__.cpython-310.pyc,, +fontTools/merge/__pycache__/base.cpython-310.pyc,, +fontTools/merge/__pycache__/cmap.cpython-310.pyc,, +fontTools/merge/__pycache__/layout.cpython-310.pyc,, +fontTools/merge/__pycache__/options.cpython-310.pyc,, +fontTools/merge/__pycache__/tables.cpython-310.pyc,, +fontTools/merge/__pycache__/unicode.cpython-310.pyc,, +fontTools/merge/__pycache__/util.cpython-310.pyc,, +fontTools/merge/base.py,sha256=l0G1Px98E9ZdVuFLMUBKWdtr7Jb8JX8vxcjeaDUUnzY,2389 +fontTools/merge/cmap.py,sha256=HpthxVH5lA7VegJ8yHoBjd9vrFBV7UB5OknKGYpxWY8,6728 +fontTools/merge/layout.py,sha256=fkMPGPLxEdxohS3scVM4W7LmNthSz-UPyocsffe2KqE,16075 +fontTools/merge/options.py,sha256=xko_1-WErcNQkirECzIOOYxSJR_bRtdQYQYOtmgccYI,2501 +fontTools/merge/tables.py,sha256=7SzXYL04awDEDhvU2-9T_8A2gAjvgGyYAHUICUJOpZg,10958 +fontTools/merge/unicode.py,sha256=kb1Jrfuoq1KUcVhhSKnflAED_wMZxXDjVwB-CI9k05Y,4273 +fontTools/merge/util.py,sha256=BH3bZWNFy-Tsj1cth7aSpGVJ18YXKXqDakPn6Wzku6U,3378 +fontTools/misc/__init__.py,sha256=DJBWmoX_Haau7qlgmvWyfbhSzrX2qL636Rns7CG01pk,75 +fontTools/misc/__pycache__/__init__.cpython-310.pyc,, +fontTools/misc/__pycache__/arrayTools.cpython-310.pyc,, +fontTools/misc/__pycache__/bezierTools.cpython-310.pyc,, +fontTools/misc/__pycache__/classifyTools.cpython-310.pyc,, +fontTools/misc/__pycache__/cliTools.cpython-310.pyc,, +fontTools/misc/__pycache__/configTools.cpython-310.pyc,, +fontTools/misc/__pycache__/cython.cpython-310.pyc,, +fontTools/misc/__pycache__/dictTools.cpython-310.pyc,, +fontTools/misc/__pycache__/eexec.cpython-310.pyc,, +fontTools/misc/__pycache__/encodingTools.cpython-310.pyc,, +fontTools/misc/__pycache__/etree.cpython-310.pyc,, +fontTools/misc/__pycache__/filenames.cpython-310.pyc,, +fontTools/misc/__pycache__/fixedTools.cpython-310.pyc,, +fontTools/misc/__pycache__/intTools.cpython-310.pyc,, +fontTools/misc/__pycache__/iterTools.cpython-310.pyc,, +fontTools/misc/__pycache__/lazyTools.cpython-310.pyc,, +fontTools/misc/__pycache__/loggingTools.cpython-310.pyc,, +fontTools/misc/__pycache__/macCreatorType.cpython-310.pyc,, +fontTools/misc/__pycache__/macRes.cpython-310.pyc,, +fontTools/misc/__pycache__/psCharStrings.cpython-310.pyc,, +fontTools/misc/__pycache__/psLib.cpython-310.pyc,, +fontTools/misc/__pycache__/psOperators.cpython-310.pyc,, +fontTools/misc/__pycache__/py23.cpython-310.pyc,, +fontTools/misc/__pycache__/roundTools.cpython-310.pyc,, +fontTools/misc/__pycache__/sstruct.cpython-310.pyc,, +fontTools/misc/__pycache__/symfont.cpython-310.pyc,, +fontTools/misc/__pycache__/testTools.cpython-310.pyc,, +fontTools/misc/__pycache__/textTools.cpython-310.pyc,, +fontTools/misc/__pycache__/timeTools.cpython-310.pyc,, +fontTools/misc/__pycache__/transform.cpython-310.pyc,, +fontTools/misc/__pycache__/treeTools.cpython-310.pyc,, +fontTools/misc/__pycache__/vector.cpython-310.pyc,, +fontTools/misc/__pycache__/visitor.cpython-310.pyc,, +fontTools/misc/__pycache__/xmlReader.cpython-310.pyc,, +fontTools/misc/__pycache__/xmlWriter.cpython-310.pyc,, +fontTools/misc/arrayTools.py,sha256=jZk__GE-K9VViZE_H-LPPj0smWbKng-yfPE8BfGp8HI,11483 +fontTools/misc/bezierTools.c,sha256=bvcuD_CKMVNW8nJ7nv5AQQrGrhv72MCeSDR_GQBrF1Y,1820774 +fontTools/misc/bezierTools.cpython-310-x86_64-linux-gnu.so,sha256=C7oZ_dcR4C-8Kddy4xPvURk5pWpc7p-I0ZLgI8bXliQ,4714576 +fontTools/misc/bezierTools.py,sha256=OmR3pzCGExNvZyTPrByH7gQHpAJsYOl1cmvfYQIVfQA,45038 +fontTools/misc/classifyTools.py,sha256=zcg3EM4GOerBW9c063ljaLllgeeZ772EpFZjp9CdgLI,5613 +fontTools/misc/cliTools.py,sha256=qCznJMLCQu3ZHQD_4ctUnr3TkfAUdkGl-UuxZUrppy0,1862 +fontTools/misc/configTools.py,sha256=YXBE_vL2dMWCnK4oY3vtU15B79q82DtKp7h7XRqJc1Q,11188 +fontTools/misc/cython.py,sha256=eyLcL2Bw-SSToYro8f44dkkYRlQfiFbhcza0afS-qHE,682 +fontTools/misc/dictTools.py,sha256=VxjarsGJuk_wa3z29FSCtKZNCFfXtMBiNEu0RPAlpDk,2417 +fontTools/misc/eexec.py,sha256=GNn2OCRvO1HbbIeDPxk9i0glO7cux_AQaoVMXhBR8y8,3331 +fontTools/misc/encodingTools.py,sha256=hCv5PFfnXQJVCZA8Wyn1vr3vzLBbUuEPtGk5CzWM9RY,2073 +fontTools/misc/etree.py,sha256=ZzJc6TvAS579deAgZLVDvTY_HeTm-ZsKJ5s3LYhZSSY,16304 +fontTools/misc/filenames.py,sha256=MMCO3xjk1pcDc-baobcKd8IdoFPt-bcGqu8t8HUGAkI,8223 +fontTools/misc/filesystem/__init__.py,sha256=iwoOj6DpXKk8q-NRRHqOfRxFF6lcXIhsIA46j-cZswU,2011 +fontTools/misc/filesystem/__pycache__/__init__.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_base.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_copy.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_errors.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_info.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_osfs.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_path.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_subfs.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_tempfs.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_tools.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_walk.cpython-310.pyc,, +fontTools/misc/filesystem/__pycache__/_zipfs.cpython-310.pyc,, +fontTools/misc/filesystem/_base.py,sha256=p74O7xREadfPQgzPJ9mP3ehu0ZHDgsmXlpsL9CnTRso,4010 +fontTools/misc/filesystem/_copy.py,sha256=ifMSs-A_bz1Aa4tIQrlUd9HtdJQ5fp5M3B6mbYuDtXI,1361 +fontTools/misc/filesystem/_errors.py,sha256=-YziRB1BT1I80ypmufvCR-M_4XoerCHyBVqX-cRnIzU,641 +fontTools/misc/filesystem/_info.py,sha256=pbV7bDTJ5F8ms6alK34J0FZYWzmRO7FT0NM3yRA3czo,2013 +fontTools/misc/filesystem/_osfs.py,sha256=RkKCE2IxcRaj7gyqFW10LhEm_-VJYtXxsS5s0DCXihM,5737 +fontTools/misc/filesystem/_path.py,sha256=frP6ZLmMeP9E3NiwoCbbgBPWpQbLBRh7T-0vOE-EuPo,1745 +fontTools/misc/filesystem/_subfs.py,sha256=vRotQwyLVfINbR88xIBQUbq9j4Kmg1_mJvEhnpvK_t4,3028 +fontTools/misc/filesystem/_tempfs.py,sha256=9FUXdCBTwFtZMFx8ghYuZVYoQdDb0tDB-jXNu3D-Qy0,924 +fontTools/misc/filesystem/_tools.py,sha256=r75dpadp7C9EdQ6r7pJQKZlCZDUJzVq2ikb_LXN-wCI,972 +fontTools/misc/filesystem/_walk.py,sha256=KMQ-GavWYr4SsA5V8ohLPmz3boilvY2P0JKrLoxW6NU,1655 +fontTools/misc/filesystem/_zipfs.py,sha256=i3qolbkDRntB_oL3v79KuEgfVlVojecPBnBA0X04PWc,6301 +fontTools/misc/fixedTools.py,sha256=gsotTCOJLyMis13M4_jQJ8-QPob2Gl2TtNJhW6FER1I,7647 +fontTools/misc/intTools.py,sha256=l6pjk4UYlXcyLtfC0DdOC5RL6UJ8ihRR0zRiYow5xA8,586 +fontTools/misc/iterTools.py,sha256=17H6LPZszp32bTKoNorp6uZF1PKj47BAbe5QG8irUjo,390 +fontTools/misc/lazyTools.py,sha256=BC6MmF-OzJ3GrBD8TYDZ-VCSN4UOx0pN0r3oF4GSoiw,1020 +fontTools/misc/loggingTools.py,sha256=NOYROsLK5TzONK5967OGdVonNyXC6kP_CmPr7M2PW_c,19933 +fontTools/misc/macCreatorType.py,sha256=Je9jtqUr7EPbpH3QxlVl3pizoQ-1AOPMBIctHIMTM3k,1593 +fontTools/misc/macRes.py,sha256=GT_pnfPw2NCvvOF86nHLAnOtZ6SMHqEuLntaplXzvHM,8579 +fontTools/misc/plistlib/__init__.py,sha256=1HfhHPt3As6u2eRSlFfl6XdnXv_ypQImeQdWIw6wK7Y,21113 +fontTools/misc/plistlib/__pycache__/__init__.cpython-310.pyc,, +fontTools/misc/plistlib/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +fontTools/misc/psCharStrings.py,sha256=Tb5-k_5krP0eu7qD054iGxE4Zybk9oB4jdiKzcsV0rw,43036 +fontTools/misc/psLib.py,sha256=ioIPm5x3MHkBXF2vzNkC4iVZYobrkWcyvFhmYsjOrPY,12099 +fontTools/misc/psOperators.py,sha256=9SLl5PPBulLo0Xxg_dqlJMitNIBdiGKdkXhOWsNSYZE,15700 +fontTools/misc/py23.py,sha256=aPVCEUz_deggwLBCeTSsccX6QgJavZqvdVtuhpzrPvA,2238 +fontTools/misc/roundTools.py,sha256=1RSXZ0gyi1qW42tz6WSBMJD1FlPdtgqKfWixVN9bd78,3173 +fontTools/misc/sstruct.py,sha256=vUODd2CKvHLtjr7yn1K94Hui_yxOPWKmlgAmBMm3KDQ,7009 +fontTools/misc/symfont.py,sha256=x5ZwqK9Ik9orG6qSftgVGygBFE1wTSngrMK2We1Z5AM,6977 +fontTools/misc/testTools.py,sha256=3vj_KllUQVEiVFbS0SzTmeuKv44-L-disI1dZ4XhOfw,7052 +fontTools/misc/textTools.py,sha256=pbhr6LVhm3J-0Z4saYnJfxBDzyoiw4BR9pAgwypiOw8,3377 +fontTools/misc/timeTools.py,sha256=e9h5pgzL04tBDXmCv_8eRGB4boFV8GKXlS6dq3ggEpw,2234 +fontTools/misc/transform.py,sha256=OR8dPsAw87z77gkZQMq00iUkDWLIxYv-12XiKH1-erk,15798 +fontTools/misc/treeTools.py,sha256=tLWkwyDHeZUPVOGNnJeD4Pn7x2bQeZetwJKaEAW2J2M,1269 +fontTools/misc/vector.py,sha256=6lqZcDjAgHJFQgjzD-ULQ_PrigAMfeZKaBZmAfcC0ig,4062 +fontTools/misc/visitor.py,sha256=c9YQs14bXfL1SmqwoAhEGCvXlmyZIKR5_Vr4n7iWaRs,5610 +fontTools/misc/xmlReader.py,sha256=igut4_d13RT4WarliqVvuuPybO1uSXVeoBOeW4j0_e4,6580 +fontTools/misc/xmlWriter.py,sha256=CrNXQfNJRdt5CHKAZ4-qy3h1yJeL63ot17kXkNbeI_E,6829 +fontTools/mtiLib/__init__.py,sha256=EzYwNaENLf906h1THBeq6nSRHUKpOAYxuzO9x9PHzh8,46602 +fontTools/mtiLib/__main__.py,sha256=gd8X89jnZOe-752k7uaR1lWoiju-2zIT5Yx35Kl0Xek,94 +fontTools/mtiLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/mtiLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/otlLib/__init__.py,sha256=D2leUW-3gsUTOFcJYGC18edBYjIJ804ut4qitJYWsaQ,45 +fontTools/otlLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/otlLib/__pycache__/builder.cpython-310.pyc,, +fontTools/otlLib/__pycache__/error.cpython-310.pyc,, +fontTools/otlLib/__pycache__/maxContextCalc.cpython-310.pyc,, +fontTools/otlLib/builder.py,sha256=MHuQKul3Nl3vzXmY7Wb6x54ZNbJGUzRUD7-VKpOG7lE,128727 +fontTools/otlLib/error.py,sha256=cthuhBuOwZYpkTLi5gFPupUxkXkCHe-L_YgkE7N1wCI,335 +fontTools/otlLib/maxContextCalc.py,sha256=3es4Kt84TaZ49sA2ev1zrlwPJikJCAECx5KavwhyB-I,3175 +fontTools/otlLib/optimize/__init__.py,sha256=UUQRpNkHU2RczCRt-Gz7sEiYE9AQq9BHLXZEOyvsnX4,1530 +fontTools/otlLib/optimize/__main__.py,sha256=BvP472kA9KxBb9RMyyehPNevAfpmgW9MfdazkUiAO3M,104 +fontTools/otlLib/optimize/__pycache__/__init__.cpython-310.pyc,, +fontTools/otlLib/optimize/__pycache__/__main__.cpython-310.pyc,, +fontTools/otlLib/optimize/__pycache__/gpos.cpython-310.pyc,, +fontTools/otlLib/optimize/gpos.py,sha256=htOgSP743DZDUKF3eWAeJ-kdqNYOnpGXdlV-rbEXJ1A,17668 +fontTools/pens/__init__.py,sha256=DJBWmoX_Haau7qlgmvWyfbhSzrX2qL636Rns7CG01pk,75 +fontTools/pens/__pycache__/__init__.cpython-310.pyc,, +fontTools/pens/__pycache__/areaPen.cpython-310.pyc,, +fontTools/pens/__pycache__/basePen.cpython-310.pyc,, +fontTools/pens/__pycache__/boundsPen.cpython-310.pyc,, +fontTools/pens/__pycache__/cairoPen.cpython-310.pyc,, +fontTools/pens/__pycache__/cocoaPen.cpython-310.pyc,, +fontTools/pens/__pycache__/cu2quPen.cpython-310.pyc,, +fontTools/pens/__pycache__/explicitClosingLinePen.cpython-310.pyc,, +fontTools/pens/__pycache__/filterPen.cpython-310.pyc,, +fontTools/pens/__pycache__/freetypePen.cpython-310.pyc,, +fontTools/pens/__pycache__/hashPointPen.cpython-310.pyc,, +fontTools/pens/__pycache__/momentsPen.cpython-310.pyc,, +fontTools/pens/__pycache__/perimeterPen.cpython-310.pyc,, +fontTools/pens/__pycache__/pointInsidePen.cpython-310.pyc,, +fontTools/pens/__pycache__/pointPen.cpython-310.pyc,, +fontTools/pens/__pycache__/qtPen.cpython-310.pyc,, +fontTools/pens/__pycache__/qu2cuPen.cpython-310.pyc,, +fontTools/pens/__pycache__/quartzPen.cpython-310.pyc,, +fontTools/pens/__pycache__/recordingPen.cpython-310.pyc,, +fontTools/pens/__pycache__/reportLabPen.cpython-310.pyc,, +fontTools/pens/__pycache__/reverseContourPen.cpython-310.pyc,, +fontTools/pens/__pycache__/roundingPen.cpython-310.pyc,, +fontTools/pens/__pycache__/statisticsPen.cpython-310.pyc,, +fontTools/pens/__pycache__/svgPathPen.cpython-310.pyc,, +fontTools/pens/__pycache__/t2CharStringPen.cpython-310.pyc,, +fontTools/pens/__pycache__/teePen.cpython-310.pyc,, +fontTools/pens/__pycache__/transformPen.cpython-310.pyc,, +fontTools/pens/__pycache__/ttGlyphPen.cpython-310.pyc,, +fontTools/pens/__pycache__/wxPen.cpython-310.pyc,, +fontTools/pens/areaPen.py,sha256=Y1WkmqzcC4z_bpGAR0IZUKrtHFtxKUQBmr5-64_zCOk,1472 +fontTools/pens/basePen.py,sha256=eIGSKrKm6w4LLHuG6XJoQZ3eObtoKV5P6aF4gT4sk7U,17073 +fontTools/pens/boundsPen.py,sha256=wE3owOQA8DfhH-zBGC3lJvnVwp-oyIt0KZrEqXbmS9I,3129 +fontTools/pens/cairoPen.py,sha256=wuuOJ1qQDSt_K3zscM2nukRyHZTZMwMzzCXCirfq_qQ,592 +fontTools/pens/cocoaPen.py,sha256=IJRQcAxRuVOTQ90bB_Bgjnmz7px_ST5uLF9CW-Y0KPY,612 +fontTools/pens/cu2quPen.py,sha256=gMUwFUsm_-WzBlDjTMQiNnEuI2heomGeOJBX81zYXPo,13007 +fontTools/pens/explicitClosingLinePen.py,sha256=kKKtdZiwaf8Cj4_ytrIDdGB2GMpPPDXm5Nwbw5WDgwU,3219 +fontTools/pens/filterPen.py,sha256=kKSvLmWCW4MkCF0ciJhjTj-LdUGOQL593PFkpm5PhP8,7790 +fontTools/pens/freetypePen.py,sha256=HD-gXJSbgImJdBc8sIBk0HWBdjv3WKFofs6PgCCsGOY,19908 +fontTools/pens/hashPointPen.py,sha256=gElrFyQoOQp3ZbpKHRWPwC61A9OgT2Js8crVUD8BQAY,3573 +fontTools/pens/momentsPen.c,sha256=LkH6tRXOFN-2qH-WheVoA4-F8-mbjMMoJrdqbQeNpsQ,564887 +fontTools/pens/momentsPen.cpython-310-x86_64-linux-gnu.so,sha256=BjXJaC20P_GT37ZoYYb2eiVJxzMfTVgwPwzqi0sHIZk,916288 +fontTools/pens/momentsPen.py,sha256=kjLVXhGe55Abl__Yr1gob0bl0dHe7fPSwyr7TRJnbug,25658 +fontTools/pens/perimeterPen.py,sha256=lr6NzrIWxi4TXBJPbcJsKzqABWfQeil2Bgm9BgUD3N4,2153 +fontTools/pens/pointInsidePen.py,sha256=noEUvBQIeAheDMJwzvvfnEiKhmwbS1i0RQE9jik6Gl4,6355 +fontTools/pens/pointPen.py,sha256=kc-jhMC-UgXZzKz7r6UL36kosYAQQEJ0xjhm3SaX59Y,22730 +fontTools/pens/qtPen.py,sha256=QRNLIry2rQl4E_7ct2tu10-qLHneQp0XV7FfaZ-tcL8,634 +fontTools/pens/qu2cuPen.py,sha256=pRST43-rUpzlOP83Z_Rr0IvIQBCx6RWI6nnNaitQcLk,3985 +fontTools/pens/quartzPen.py,sha256=EH482Kz_xsqYhVRovv6N_T1CXaSvOzUKPLxTaN956tU,1287 +fontTools/pens/recordingPen.py,sha256=VgFZ4NMhnZt1qSTzFEU0cma-gw3kBe47bfSxPYH73rs,12489 +fontTools/pens/reportLabPen.py,sha256=kpfMfOLXt2vOQ5smPsU82ft80FpCPWJzQLl7ENOH8Ew,2066 +fontTools/pens/reverseContourPen.py,sha256=oz64ZRhLAvT7DYMAwGKoLzZXQK8l81jRiYnTZkW6a-Y,4022 +fontTools/pens/roundingPen.py,sha256=vh_FjikRd82-S4I8glgGMGEuGrj5IkCjRT_wmZ8jfqY,4620 +fontTools/pens/statisticsPen.py,sha256=piWK6NjjWqk9MLROjeE2-4EsxVYMyNU7UQFGD_trE9g,9808 +fontTools/pens/svgPathPen.py,sha256=T3b6SZS9B9sVWMK9mSFDtjHeviQs_yOJOZKq5Sg5Zdg,8572 +fontTools/pens/t2CharStringPen.py,sha256=GgGklb5XsCer0w37ujgRLRXx-EuzdFsyCYuzCx4n-Qs,2931 +fontTools/pens/teePen.py,sha256=P1ARJOCMJ6MxK-PB1yZ-ips3CUfnadWYnQ_do6VIasQ,1290 +fontTools/pens/transformPen.py,sha256=s0kUyQdnemUwHvYr2SFboFmh4WY1S9OHBL8L4PJKRwE,4056 +fontTools/pens/ttGlyphPen.py,sha256=yLtB-E5pTQR59OKVYySttWBu1xC2vR8ezSaRhIMtVwg,11870 +fontTools/pens/wxPen.py,sha256=W9RRHlBWHp-CVC4Exvk3ytBmRaB4-LgJPP5Bv7o9BA0,680 +fontTools/qu2cu/__init__.py,sha256=Jfm1JljXbt91w4gyvZn6jzEmVnhRx50sh2fDongrOsE,618 +fontTools/qu2cu/__main__.py,sha256=9FWf6SIZaRaC8SiL0LhjAWC2yIdY9N_9wlRko8m1l2Q,93 +fontTools/qu2cu/__pycache__/__init__.cpython-310.pyc,, +fontTools/qu2cu/__pycache__/__main__.cpython-310.pyc,, +fontTools/qu2cu/__pycache__/benchmark.cpython-310.pyc,, +fontTools/qu2cu/__pycache__/cli.cpython-310.pyc,, +fontTools/qu2cu/__pycache__/qu2cu.cpython-310.pyc,, +fontTools/qu2cu/benchmark.py,sha256=GMcr_4r7L6K9SmJ13itt-_XKhnKqSVUDPlXUG6IZmmM,1400 +fontTools/qu2cu/cli.py,sha256=U2rooYnVVEalGRAWGFHk-Kp6Okys8wtzdaWLjw1bngY,3714 +fontTools/qu2cu/qu2cu.c,sha256=aAgEd2kICdXMOPZyPWUp6FNP4MekqCJ0A_DO2lzh7yA,689362 +fontTools/qu2cu/qu2cu.cpython-310-x86_64-linux-gnu.so,sha256=Lm5UYlhIShjqcp7oT1WRFD5rN255xxFFTi3p0jrbSw8,1120912 +fontTools/qu2cu/qu2cu.py,sha256=IYtpkwHdfKOXJr65Y_pJ9Lrt_MgJaISAKGMAs5ilFSM,12288 +fontTools/subset/__init__.py,sha256=uOof18oO8Bwl3BDoRqBIG1VvEpY9qSkG0TRmBWLKPFk,137707 +fontTools/subset/__main__.py,sha256=bhtfP2SqP4k799pxtksFgnC-XGNQDr3LcO4lc8T5e5g,95 +fontTools/subset/__pycache__/__init__.cpython-310.pyc,, +fontTools/subset/__pycache__/__main__.cpython-310.pyc,, +fontTools/subset/__pycache__/cff.cpython-310.pyc,, +fontTools/subset/__pycache__/svg.cpython-310.pyc,, +fontTools/subset/__pycache__/util.cpython-310.pyc,, +fontTools/subset/cff.py,sha256=rqMRJOlX5FacV1LW8aDlVOglgEM87TkMA9bdsYenask,6145 +fontTools/subset/svg.py,sha256=8dLBzQlnIt4_fOKEFDAVlKTucdHvcbCcyG9-a6UBZZ0,9384 +fontTools/subset/util.py,sha256=9SXFYb5Ef9Z58uXmYPCQil8B2i3Q7aFB_1fFDFSppdU,754 +fontTools/svgLib/__init__.py,sha256=IGCLwSbU8jLhq6HI2vSdPQgNs6zDUi5774TgX5MCXPY,75 +fontTools/svgLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/svgLib/path/__init__.py,sha256=C82fh7xH6ZHsSFVnV848-xeDezpokx1EwTmayJCouFU,1996 +fontTools/svgLib/path/__pycache__/__init__.cpython-310.pyc,, +fontTools/svgLib/path/__pycache__/arc.cpython-310.pyc,, +fontTools/svgLib/path/__pycache__/parser.cpython-310.pyc,, +fontTools/svgLib/path/__pycache__/shapes.cpython-310.pyc,, +fontTools/svgLib/path/arc.py,sha256=-f5Ym6q4tDWQ76sMNSTUTWgL_7AfgXojvBhtBS7bWwQ,5812 +fontTools/svgLib/path/parser.py,sha256=8T6okMstvgM9ufb2zBcwSzsuuoYbqfnUjNYgb6kjznU,10788 +fontTools/svgLib/path/shapes.py,sha256=xvBUIckKyT9JLy7q_ZP50r6TjvZANyHdZP7wFDzErcI,5322 +fontTools/t1Lib/__init__.py,sha256=p42y70wEIbuX0IIxZG7-b_I-gHto1VLy0gLsDvxCfkw,20865 +fontTools/t1Lib/__pycache__/__init__.cpython-310.pyc,, +fontTools/tfmLib.py,sha256=UMbkM73JXRJVS9t2B-BJc13rSjImaWBuzCoehLwHFhs,14270 +fontTools/ttLib/__init__.py,sha256=1k7qp9z04gA3m6GvxDaINjqrKbzOkdTA_4RnqW_-LrA,661 +fontTools/ttLib/__main__.py,sha256=lHMPWsnzjKPuMFavf6i1gpk9KexiAk4qzgDd50Mbby0,4733 +fontTools/ttLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/ttLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/ttLib/__pycache__/macUtils.cpython-310.pyc,, +fontTools/ttLib/__pycache__/removeOverlaps.cpython-310.pyc,, +fontTools/ttLib/__pycache__/reorderGlyphs.cpython-310.pyc,, +fontTools/ttLib/__pycache__/scaleUpem.cpython-310.pyc,, +fontTools/ttLib/__pycache__/sfnt.cpython-310.pyc,, +fontTools/ttLib/__pycache__/standardGlyphOrder.cpython-310.pyc,, +fontTools/ttLib/__pycache__/ttCollection.cpython-310.pyc,, +fontTools/ttLib/__pycache__/ttFont.cpython-310.pyc,, +fontTools/ttLib/__pycache__/ttGlyphSet.cpython-310.pyc,, +fontTools/ttLib/__pycache__/ttVisitor.cpython-310.pyc,, +fontTools/ttLib/__pycache__/woff2.cpython-310.pyc,, +fontTools/ttLib/macUtils.py,sha256=lj3oeFpyjV7ko_JqnluneITmAtlc119J-vwTTg2s73A,1737 +fontTools/ttLib/removeOverlaps.py,sha256=YBtj1PX-d2jMgCiWGuI6ibghWApUWqH2trJGXNxrbjQ,12612 +fontTools/ttLib/reorderGlyphs.py,sha256=TbxLxqPTUGiKRX3ulGFCwVm2lEisFYlX6caONJr_4oY,10371 +fontTools/ttLib/scaleUpem.py,sha256=U_-NGkwfS9GRIackdEXjGYZ-wSomcUPXQahDneLeArI,14618 +fontTools/ttLib/sfnt.py,sha256=wemkfz93dlAoyo-VVzmg5OLoSSyMFQNzBTTP3Kem-xk,22792 +fontTools/ttLib/standardGlyphOrder.py,sha256=7AY_fVWdtwZ4iv5uWdyKAUcbEQiSDt1lN4sqx9xXwE0,5785 +fontTools/ttLib/tables/B_A_S_E_.py,sha256=H71A9pJ850mvjbrWHqy8iFI2Dxg7102YRtAkfdCooig,369 +fontTools/ttLib/tables/BitmapGlyphMetrics.py,sha256=9gcGPVzsxEYnVBO7YLWfeOuht9PaCl09GmbAqDYqKi0,1769 +fontTools/ttLib/tables/C_B_D_T_.py,sha256=5LNdc8FMir1kC5fvp5iHwWfeuE-RuqdxAArFXaqPjQ0,3646 +fontTools/ttLib/tables/C_B_L_C_.py,sha256=YXlwovoCHYx8THLQD9iBU_VGoaB9LFObEKtL6ddD320,520 +fontTools/ttLib/tables/C_F_F_.py,sha256=yg3mUtYBudgmpG7Bz475j_DNnCelsgrTsM8DH1uR4ek,1978 +fontTools/ttLib/tables/C_F_F__2.py,sha256=YoHfJQdF-ezx4OwRQ2Y2O7rRJEPjOkf3Hx5Y11Xq0AM,807 +fontTools/ttLib/tables/C_O_L_R_.py,sha256=SHwFVNVmoUQR2e87KuTSe-J9LfeegS4f2hEpee29_2o,5993 +fontTools/ttLib/tables/C_P_A_L_.py,sha256=odFjqM4GnjXyQYGEC-e0Gvqms1jQ5zHHG3SDg7y-BI0,11942 +fontTools/ttLib/tables/D_S_I_G_.py,sha256=AgQPM9Cdro1P-ehJjTfsC9mRTTtSc16At0nnpb1XOGI,5517 +fontTools/ttLib/tables/D__e_b_g.py,sha256=KDnfkNOUnm3F13wD_j3YNBOvYadZ40Gf_0170hFkJp0,1134 +fontTools/ttLib/tables/DefaultTable.py,sha256=cOtgkLWPY9qmOH2BSPt4c4IUSdANWTKx2rK1CTxQ4h0,1487 +fontTools/ttLib/tables/E_B_D_T_.py,sha256=uOpmt25gOJQeO1u1IGAyPWgVmh-4vSZqrQEHvOYwbwg,32534 +fontTools/ttLib/tables/E_B_L_C_.py,sha256=LfEVzBg_yWr9dhChzS0U2G-7wNOwzwB0LWoXIUYNKKM,30054 +fontTools/ttLib/tables/F_F_T_M_.py,sha256=_450vdbEH7Y-0_rOwb3Q0hg-Qq2W8C_sHljy7rZtqqM,1683 +fontTools/ttLib/tables/F__e_a_t.py,sha256=ct79Gf__5ALlqfSBn6wvw6fazb31Od71R6vIp6o9XF4,5483 +fontTools/ttLib/tables/G_D_E_F_.py,sha256=QXiILFCRnPNZcwpub6ojN5S9WP6y56LsXi25pUWLgp4,299 +fontTools/ttLib/tables/G_M_A_P_.py,sha256=fvIQumokOCLa8DFeq_xi069F9RROsXSVmDvWtxgyacQ,4720 +fontTools/ttLib/tables/G_P_K_G_.py,sha256=Xi4Hj2OxZ2IZgVyBQ-Qyiie0hPZjpXZkrao-E5EdTWM,4646 +fontTools/ttLib/tables/G_P_O_S_.py,sha256=UkP3mlnyvQg-jj6ZBOh6j-OieVg_goJQ31nlLvoLGSI,397 +fontTools/ttLib/tables/G_S_U_B_.py,sha256=cwFMKO-pgwsn1H8Q9Jb58Z6ZrBrCoN0sqJB0YunBfSk,294 +fontTools/ttLib/tables/G_V_A_R_.py,sha256=13oO2dD-L4yfkrBuR-KN2rc40wh5lLIlx_khwMz5GH4,94 +fontTools/ttLib/tables/G__l_a_t.py,sha256=Xh3IzFgYlvNjrAOn7Ja73DrWrQTJgJxmDFSUKS6yHdM,8645 +fontTools/ttLib/tables/G__l_o_c.py,sha256=5DsxGzaG7HyJVvLlKQeff1lXt-XPWaHNNaf-EYwsKh4,2685 +fontTools/ttLib/tables/H_V_A_R_.py,sha256=6kPLDUGT8EussA3y9gKr_mrgY5PNv7YaK1V0keMXD9w,313 +fontTools/ttLib/tables/J_S_T_F_.py,sha256=Q9TEf3OuyDIxZlmoz9a3c-mDMlJK6YBQ9KcYmiwFRbU,315 +fontTools/ttLib/tables/L_T_S_H_.py,sha256=Iu6syJFuhJj0_7Aan2NPlDuQDIq-AzLwsOQbXVTnlL0,2189 +fontTools/ttLib/tables/M_A_T_H_.py,sha256=-TVu9Nlcs-1shkElbIk-CWtUwXUMdycHFkjvPE8C_fs,342 +fontTools/ttLib/tables/M_E_T_A_.py,sha256=sA6ookcjchw8UYVEuS8QEXc62I9_Rms9cu_jKA6MkNI,11989 +fontTools/ttLib/tables/M_V_A_R_.py,sha256=67cEuiTw5y5W1Zk98L_S_SmJINIfy_mzWCkyHcujz94,308 +fontTools/ttLib/tables/O_S_2f_2.py,sha256=1Pq2Xu4oYOJePTHC_hTKg3RIfKely3j6T1u_lMTEpD8,28030 +fontTools/ttLib/tables/S_I_N_G_.py,sha256=CFDy8R2fDeYn7ocfrZr7Ui7U9D0h4G55CdPfY55g-Bk,3317 +fontTools/ttLib/tables/S_T_A_T_.py,sha256=y9NiWCtnlZtMjw4K9_SdA84Xa-dJk7G5eb2dSe6ciWc,498 +fontTools/ttLib/tables/S_V_G_.py,sha256=vT6QTW5ArtskVUxnPEH_ZxKz4DF4v1pKbylN6DG0R3o,7676 +fontTools/ttLib/tables/S__i_l_f.py,sha256=lPQV2RdhcJRgfDzHp_dkgSxVUUdkcAnY1Bz7V18Gt9U,34985 +fontTools/ttLib/tables/S__i_l_l.py,sha256=Vjtn7SI83vaLGIuQf2e-jhZSFOXb9vXB4jwqznjqnMc,3224 +fontTools/ttLib/tables/T_S_I_B_.py,sha256=3WhEtyNnjYumcowD0GpjubrgnS-RzouZxCxEe4yLDo8,341 +fontTools/ttLib/tables/T_S_I_C_.py,sha256=hAV9Hq_ALsWaducDpw1tDRREvFL7hx7onnUF0sXTelU,381 +fontTools/ttLib/tables/T_S_I_D_.py,sha256=TsdX-G2xxVQO9sSE1wE_xDRx-gor5YiXTHeUthMwCPY,341 +fontTools/ttLib/tables/T_S_I_J_.py,sha256=x8Tlvi6aTxoQcI12UL7muoWF1Q61iBDseAS1mRdOYrg,341 +fontTools/ttLib/tables/T_S_I_P_.py,sha256=-il2ucTBOghVBY7cmleHdLZc3W3CKh7-iPPT0A3KBzk,341 +fontTools/ttLib/tables/T_S_I_S_.py,sha256=tVBnl63vyZUIq93oM6dEjHCXvPn9vt5vvL3jG59b0Lg,341 +fontTools/ttLib/tables/T_S_I_V_.py,sha256=iUWxz2MSrtw7mzuQZj30QAJrCPnyJ4GincFfySFUNAg,855 +fontTools/ttLib/tables/T_S_I__0.py,sha256=O-2oI0eBgt4mP15-UwH0_0r7YWi3EEEhG-4etqDueGI,2505 +fontTools/ttLib/tables/T_S_I__1.py,sha256=nSUhni-fvYmeKXW4zLfP3FG_3LQU2QKPKS1_gKY5lYg,6971 +fontTools/ttLib/tables/T_S_I__2.py,sha256=q2rub-d77iWWiBM6awO0-TCl-Xq7kalPobHYC2QEOfc,496 +fontTools/ttLib/tables/T_S_I__3.py,sha256=0LcvvCzVZJzyz7i4zjIkUuYXEqXwOCs9WeCsgDFqKJ8,543 +fontTools/ttLib/tables/T_S_I__5.py,sha256=hhvJn6jiXs8kuBtun8krNUTXTljH-eKxaxXM1T-7SXM,1905 +fontTools/ttLib/tables/T_T_F_A_.py,sha256=LuT0w__AMtawnsBMobhEMW9gp2yk0mA5ZRzwF45c0UI,392 +fontTools/ttLib/tables/TupleVariation.py,sha256=4XTDTRPZWPg9_1K5SVgdNoxtgQvahtiO4LNO7fk1cK4,32235 +fontTools/ttLib/tables/V_A_R_C_.py,sha256=3jFX50J6X-Cc4dwwiztKKsDTRXVHTXlVdQH328UN1-k,289 +fontTools/ttLib/tables/V_D_M_X_.py,sha256=RbHl7vvO9pcjT_kKvcCmcByQj39n4PmVeq55wD5C14g,10437 +fontTools/ttLib/tables/V_O_R_G_.py,sha256=Cn3OxjVtcO-Uvp61P5c2336V9iEbuGr6vWAXnSIaihk,5965 +fontTools/ttLib/tables/V_V_A_R_.py,sha256=Cstw6tc_U4-EmTriRItBSpvTJODAjMFQjfyTaxLzsbI,319 +fontTools/ttLib/tables/__init__.py,sha256=eQPcuHCfRuGtt6nOa0KwV6vtUNKHnwuQyA7xSN8SPoc,2651 +fontTools/ttLib/tables/__pycache__/B_A_S_E_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/BitmapGlyphMetrics.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_B_D_T_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_B_L_C_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_F_F_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_F_F__2.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_O_L_R_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/C_P_A_L_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/D_S_I_G_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/D__e_b_g.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/DefaultTable.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/E_B_D_T_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/E_B_L_C_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/F_F_T_M_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/F__e_a_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_D_E_F_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_M_A_P_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_P_K_G_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_P_O_S_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_S_U_B_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G_V_A_R_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G__l_a_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/G__l_o_c.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/H_V_A_R_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/J_S_T_F_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/L_T_S_H_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/M_A_T_H_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/M_E_T_A_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/M_V_A_R_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/O_S_2f_2.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/S_I_N_G_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/S_T_A_T_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/S_V_G_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/S__i_l_f.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/S__i_l_l.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_B_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_C_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_D_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_J_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_P_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_S_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I_V_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I__0.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I__1.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I__2.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I__3.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_S_I__5.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/T_T_F_A_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/TupleVariation.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/V_A_R_C_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/V_D_M_X_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/V_O_R_G_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/V_V_A_R_.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/__init__.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_a_n_k_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_a_v_a_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_b_s_l_n.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_c_i_d_g.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_c_m_a_p.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_c_v_a_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_c_v_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_f_e_a_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_f_p_g_m.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_f_v_a_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_g_a_s_p.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_g_c_i_d.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_g_l_y_f.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_g_v_a_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_h_d_m_x.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_h_e_a_d.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_h_h_e_a.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_h_m_t_x.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_k_e_r_n.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_l_c_a_r.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_l_o_c_a.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_l_t_a_g.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_m_a_x_p.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_m_e_t_a.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_m_o_r_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_m_o_r_x.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_n_a_m_e.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_o_p_b_d.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_p_o_s_t.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_p_r_e_p.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_p_r_o_p.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_s_b_i_x.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_t_r_a_k.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_v_h_e_a.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/_v_m_t_x.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/asciiTable.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/grUtils.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/otBase.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/otConverters.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/otData.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/otTables.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/otTraverse.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/sbixGlyph.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/sbixStrike.cpython-310.pyc,, +fontTools/ttLib/tables/__pycache__/ttProgram.cpython-310.pyc,, +fontTools/ttLib/tables/_a_n_k_r.py,sha256=MpAzIifmIi_3gx2oP6PC3R2lu36Ewsr2-W1rXjsz2Ug,483 +fontTools/ttLib/tables/_a_v_a_r.py,sha256=QnjP8oO-hozKcI21_bRC-_SzZxVqcHC2VLDyNs4aH9U,7116 +fontTools/ttLib/tables/_b_s_l_n.py,sha256=_848o7SQqztzBDfHYei-80u9ltxIHVBzXu1dYHLV57M,465 +fontTools/ttLib/tables/_c_i_d_g.py,sha256=yt8rVIadpJSDUCoVH4dZetNiy0Azm5ESAxHjB2BX_eA,913 +fontTools/ttLib/tables/_c_m_a_p.py,sha256=r8-bB_E0EQh5h4TGX5nTnDnwTUtXuRB3iuqEDoN_IOM,62202 +fontTools/ttLib/tables/_c_v_a_r.py,sha256=35ayk2kX1pcLGwyx0y4I1l-r7LHgdKv0ulVx8oBPteI,3527 +fontTools/ttLib/tables/_c_v_t.py,sha256=1_RhEcTmhWQWQp7Hsj8UsByKmXCIppZyIbIArGywEEM,1618 +fontTools/ttLib/tables/_f_e_a_t.py,sha256=Fi1XnjhkCG0tp43AcvpIaivD-YRFpufo6feGIrenQDo,469 +fontTools/ttLib/tables/_f_p_g_m.py,sha256=uZHZzqL6OdLn_Hxskv-xf3XuE4fyaSv_jbALEjwXYug,1633 +fontTools/ttLib/tables/_f_v_a_r.py,sha256=rV33H2BgHUl3Wuydsou1G-Hi4uASBppWaLj3FMmiLjs,8837 +fontTools/ttLib/tables/_g_a_s_p.py,sha256=YvhAVDvdssN2fjPMTfSrO4WBCfTuh9T2cU5zquDVnSw,2203 +fontTools/ttLib/tables/_g_c_i_d.py,sha256=AJ4uV7PTHbnsw4Tfw8c2Ezh0VMox3oAH0qhhq7y8hdM,362 +fontTools/ttLib/tables/_g_l_y_f.py,sha256=dDV65llsEDI9fKcVKC5TOiaXpXSyMHNEytuYOGt7adM,85584 +fontTools/ttLib/tables/_g_v_a_r.py,sha256=sRA_ldRw_IggqTNuyS-IKY0rybj22AeNnYGMazGVZ6Y,12075 +fontTools/ttLib/tables/_h_d_m_x.py,sha256=wMrO4D04QNT8u30p8AV-aG3bndXCq4wlPNvtbd8ip7c,4252 +fontTools/ttLib/tables/_h_e_a_d.py,sha256=yY2GTFq6Mn6nN8EegbMVJRMUWIqDYFln3FhTk3ziw6s,4926 +fontTools/ttLib/tables/_h_h_e_a.py,sha256=X4t1aF1MZMuz3phCVSFwKcNTeoZdx-042wFtHc-nK9w,4767 +fontTools/ttLib/tables/_h_m_t_x.py,sha256=C_-GIrH8rHEqEQtsGeYTc6XLtLeu6ibRl8AAQxkQng8,6042 +fontTools/ttLib/tables/_k_e_r_n.py,sha256=DQNLmD_HEdDKPfp4tamOd9W3T5a1lXFM5tDaWrKl164,10794 +fontTools/ttLib/tables/_l_c_a_r.py,sha256=8W6xFOj-sm003MCXX4bIHxs9ntfVvT0FXYllPxa3z4I,390 +fontTools/ttLib/tables/_l_o_c_a.py,sha256=yxiwLKXLZjNju5XYmLb6EhNLec1d7ezEDDe1dszceHo,2180 +fontTools/ttLib/tables/_l_t_a_g.py,sha256=9YpApjI-rZ4e3HeT8Pj-osiHl3uALD9JXg5O7pqk9L0,2552 +fontTools/ttLib/tables/_m_a_x_p.py,sha256=cIDIZWse9czwwsnlxIh3qwgwaXbt7PQAjXKAcmMDspY,5264 +fontTools/ttLib/tables/_m_e_t_a.py,sha256=A0CZPEAVxYrpytjXUGQJCTddwG8KrvUVbtBe3A1MqgI,3913 +fontTools/ttLib/tables/_m_o_r_t.py,sha256=u35tYqn3cjzKxeCF0FUFeLtaf36mjDDSN08uuk0Kme8,487 +fontTools/ttLib/tables/_m_o_r_x.py,sha256=OwamVpIO7REDnFr95HuFPoY_0U6i9zQPb11K1sFTvDY,548 +fontTools/ttLib/tables/_n_a_m_e.py,sha256=LYySKOxLr_t5dYKeNTqA8pozf8MfI1R_jS0SCHNViq0,41063 +fontTools/ttLib/tables/_o_p_b_d.py,sha256=TNZv_2YTrj4dGzd6wA9Jb-KGZ99un177s5p3LlfxQ74,448 +fontTools/ttLib/tables/_p_o_s_t.py,sha256=hsGOLA10ZwaGSGpvQgeelnj1vj_kTUbiBnskgk2i35w,11701 +fontTools/ttLib/tables/_p_r_e_p.py,sha256=CcKr4HrswkupLmbJdrJLTM-z9XgLefQyv8467j9V0zs,427 +fontTools/ttLib/tables/_p_r_o_p.py,sha256=Eg8x5qWyXDzPezMafFu0s0qyPDHj-sPsFxGtE6h29qo,427 +fontTools/ttLib/tables/_s_b_i_x.py,sha256=tkkKbNKNYkUhZJuN0kl7q37x5KK5OovB06y28obPV6A,4865 +fontTools/ttLib/tables/_t_r_a_k.py,sha256=rrrPZLELFYA5F8PERoafIS9cb_d_i6xtpAzHEbsFHSw,11379 +fontTools/ttLib/tables/_v_h_e_a.py,sha256=FuULIBl4OQyUeLPOFEY8buB0pAnQhGa1-5a6kN9i5Sc,4459 +fontTools/ttLib/tables/_v_m_t_x.py,sha256=AUuxtyQvMWrTBNbOIaL6uKcB_DNpNb0YX28JIuTHw_Y,500 +fontTools/ttLib/tables/asciiTable.py,sha256=4c69jsAirUnDEpylf9CYBoCKTzwbmfbtUAOrtPnpHjY,637 +fontTools/ttLib/tables/grUtils.py,sha256=hcOJ5oJPOd2uJWnWA7qwR7AfL37YZ5zUT7g8o5BBV80,2270 +fontTools/ttLib/tables/otBase.py,sha256=cHdoYX-ICa8GeI-tVhFy1K9-CHaExiKO-HBjmH0OkbU,53330 +fontTools/ttLib/tables/otConverters.py,sha256=ihE_WMSKAKSaBbMvnFYDj2eMxf7PvRMMa8zGwfoYuYc,74202 +fontTools/ttLib/tables/otData.py,sha256=-XXRwdVfP-Wz7oBjMPpku0A0QH9lw_fFGNzZlt9N0mo,197262 +fontTools/ttLib/tables/otTables.py,sha256=2U04ot_2ITlBZx2QtpnIOtBGftPFs9ZX2FWfz4vz1G0,96987 +fontTools/ttLib/tables/otTraverse.py,sha256=HznEVAlVf_8eyqjsO2edgELtMlXnjnUqccK3PytvVUE,5518 +fontTools/ttLib/tables/sbixGlyph.py,sha256=tjEUPVRfx6gr5yme8UytGTtVrimKN5qmbzT1GZPjXiM,5796 +fontTools/ttLib/tables/sbixStrike.py,sha256=dL8O9K8R4S6RVQDP-PVjIPBrvbqbE9zwra0uRL0nLq0,6651 +fontTools/ttLib/tables/table_API_readme.txt,sha256=eZlRTLUkLzc_9Ot3pdfhyMb3ahU0_Iipx0vSbzOVGy8,2748 +fontTools/ttLib/tables/ttProgram.py,sha256=tgtxgd-EnOq-2PUlYEihp-6NHu_7HnE5rxeSAtmXOtU,35888 +fontTools/ttLib/ttCollection.py,sha256=aRph2MkBK3kd9-JCLqhJ1EN9pffN_lVX6WWmOTTewc8,3963 +fontTools/ttLib/ttFont.py,sha256=8I79ksIdtfMf8GV_nhawEhlzOvQMTyB98lrWzoJADh0,40669 +fontTools/ttLib/ttGlyphSet.py,sha256=cUBhMGa5hszeVqOm2KpOdeJh-LsiqE7RNdyIUPZ2vO8,17476 +fontTools/ttLib/ttVisitor.py,sha256=_tah4C42Tv6Pm9QeLNQwwVCxqI4VNEAqYCbmThp6cvY,1025 +fontTools/ttLib/woff2.py,sha256=6LPISeBQ1dubzKjWrUcYm_vgETC46BTLY4XkG52qvSA,60921 +fontTools/ttx.py,sha256=FxuGubujWCGJWSTrJEjoNH--25fVIPy-ZRtYy9H6iTk,17277 +fontTools/ufoLib/__init__.py,sha256=Q_yTPiqbbgFO50ephzOkE0AQITEu0IC24jvIv1dhxds,94339 +fontTools/ufoLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/converters.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/errors.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/etree.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/filenames.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/glifLib.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/kerning.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/plistlib.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/pointPen.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/utils.cpython-310.pyc,, +fontTools/ufoLib/__pycache__/validators.cpython-310.pyc,, +fontTools/ufoLib/converters.py,sha256=9j4BQ7EXVfWiVoB7OZCzRGsRJWi9G9w_27iFuiJNphw,13044 +fontTools/ufoLib/errors.py,sha256=9f8l5NaFAj3BZPa6Bbqt06FL4afffLuMzy4nPf-eOlE,845 +fontTools/ufoLib/etree.py,sha256=T3sjLTgjMAq6VyYRicWPaMIVBJ2YSuwZxV6Vc5yZtQI,231 +fontTools/ufoLib/filenames.py,sha256=o8Kuj0aOSlb8_hoLfJ2D91n9S5JZazFcTjiQYfqQ5ic,10418 +fontTools/ufoLib/glifLib.py,sha256=cUNEGB1xnJyiY4BqXkPjlkXQbB4wjjN4gYPnXpgyxmU,72699 +fontTools/ufoLib/kerning.py,sha256=QF5n7eXwBfc2TJ_XGUkLAnxcgq82vdLOpJEN2Lh_7UY,4233 +fontTools/ufoLib/plistlib.py,sha256=jzMGOGvHO6XvS-IO8hS04ur7r8-v2dnVq-vKMoJZvqQ,1510 +fontTools/ufoLib/pointPen.py,sha256=CuREcm3IYteZNBDAd_ZRAV4XqBsy0s07jdWc4en9r-8,244 +fontTools/ufoLib/utils.py,sha256=mN5GuY21fsr-9N1I3laaSm3IUhILTBlNySfM52p26gQ,1995 +fontTools/ufoLib/validators.py,sha256=EtVfe3fNZAS7q9Yd07S0pF1x_lonFxpYoqLpjiNIwL8,30813 +fontTools/unicode.py,sha256=ZZ7OMmWvIyV1IL1k6ioTzaRAh3tUvm6gvK7QgFbOIHY,1237 +fontTools/unicodedata/Blocks.py,sha256=6BL66vrr5UWylVx3bicy6U2kO-QcNNKpmPKQTGggUEU,32415 +fontTools/unicodedata/Mirrored.py,sha256=kdhwCWOWaArmfNkDah0Thv-67M9wWz45R5IMPhqyzFM,9242 +fontTools/unicodedata/OTTags.py,sha256=wOPpbMsNcp_gdvPFeITtgVMnTN8TJSNAsVEdu_nuPXE,1196 +fontTools/unicodedata/ScriptExtensions.py,sha256=YTZr2bOteHiz_7I4108PRy0Is4kFof-32yFuoKjFHjc,28207 +fontTools/unicodedata/Scripts.py,sha256=I0nY08ovsZ4pHU5wYchjat9DjnxcpoTzaXAn5oFaKNI,130271 +fontTools/unicodedata/__init__.py,sha256=ht5cIwvgKSonvRADzijXzBp6uZjhTvFobBwaba4ogaM,9033 +fontTools/unicodedata/__pycache__/Blocks.cpython-310.pyc,, +fontTools/unicodedata/__pycache__/Mirrored.cpython-310.pyc,, +fontTools/unicodedata/__pycache__/OTTags.cpython-310.pyc,, +fontTools/unicodedata/__pycache__/ScriptExtensions.cpython-310.pyc,, +fontTools/unicodedata/__pycache__/Scripts.cpython-310.pyc,, +fontTools/unicodedata/__pycache__/__init__.cpython-310.pyc,, +fontTools/varLib/__init__.py,sha256=dc1_8itv2pS3q6mO8vMuEe-XdWOiJLIWVsJ8-bhPflI,54236 +fontTools/varLib/__main__.py,sha256=wbdYC5bPjWCxA0I4SKcLO88gl-UMtsYS8MxdW9ySTkY,95 +fontTools/varLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/varLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/varLib/__pycache__/avar.cpython-310.pyc,, +fontTools/varLib/__pycache__/avarPlanner.cpython-310.pyc,, +fontTools/varLib/__pycache__/builder.cpython-310.pyc,, +fontTools/varLib/__pycache__/cff.cpython-310.pyc,, +fontTools/varLib/__pycache__/errors.cpython-310.pyc,, +fontTools/varLib/__pycache__/featureVars.cpython-310.pyc,, +fontTools/varLib/__pycache__/hvar.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolatable.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolatableHelpers.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolatablePlot.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolatableTestContourOrder.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolatableTestStartingPoint.cpython-310.pyc,, +fontTools/varLib/__pycache__/interpolate_layout.cpython-310.pyc,, +fontTools/varLib/__pycache__/iup.cpython-310.pyc,, +fontTools/varLib/__pycache__/merger.cpython-310.pyc,, +fontTools/varLib/__pycache__/models.cpython-310.pyc,, +fontTools/varLib/__pycache__/multiVarStore.cpython-310.pyc,, +fontTools/varLib/__pycache__/mutator.cpython-310.pyc,, +fontTools/varLib/__pycache__/mvar.cpython-310.pyc,, +fontTools/varLib/__pycache__/plot.cpython-310.pyc,, +fontTools/varLib/__pycache__/stat.cpython-310.pyc,, +fontTools/varLib/__pycache__/varStore.cpython-310.pyc,, +fontTools/varLib/avar.py,sha256=Ye_u0HHznaPQaTzufNFKDj_v9o_LxOKJoa_eTK1D1F0,9647 +fontTools/varLib/avarPlanner.py,sha256=uLMGsL6cBbEMq5YItwABG_vXlXV3bxquM93WGDJ1brA,27358 +fontTools/varLib/builder.py,sha256=mSKOCcnnw-WzmZs15FayoqCDh77Ts7o9Tre9psh8CUc,6609 +fontTools/varLib/cff.py,sha256=EVgaQcoROIrYQsRuftnxFuGGldEPYbrIh5yBckylJC4,22901 +fontTools/varLib/errors.py,sha256=dMo8eGj76I7H4hrBEiNbYrGs2J1K1SwdsUyTHpkVOrQ,6934 +fontTools/varLib/featureVars.py,sha256=ZmHPyy4KuamR4bI1PfH-Umk4EN_CfwvNfchu7BOmECg,25695 +fontTools/varLib/hvar.py,sha256=1IvL5BneTkg8jJYicH0TSQViB6D0vBEesLdlfqoLBX4,3695 +fontTools/varLib/instancer/__init__.py,sha256=UxmXi4zpOGxSOMRaWQB-2IsI_tsZIK0O_T4AqXPJXZQ,72266 +fontTools/varLib/instancer/__main__.py,sha256=zfULwcP01FhplS1IlcMgNQnLxk5RVfmOuinWjqeid-g,104 +fontTools/varLib/instancer/__pycache__/__init__.cpython-310.pyc,, +fontTools/varLib/instancer/__pycache__/__main__.cpython-310.pyc,, +fontTools/varLib/instancer/__pycache__/featureVars.cpython-310.pyc,, +fontTools/varLib/instancer/__pycache__/names.cpython-310.pyc,, +fontTools/varLib/instancer/__pycache__/solver.cpython-310.pyc,, +fontTools/varLib/instancer/featureVars.py,sha256=oPqSlnHLMDTtOsmQMi6gkzLox7ymCrqlRAkvC_EJ4bc,7110 +fontTools/varLib/instancer/names.py,sha256=IPRqel_M8zVU0jl30WsfgufxUm9PBBQDQCY3VHapeHc,14950 +fontTools/varLib/instancer/solver.py,sha256=uMePwX0BVT5F94kUvDglsI4_F0nEH67F7RFuJ6tQwQ0,11002 +fontTools/varLib/interpolatable.py,sha256=Bhlq_LhEZ-sXfLNY8aFEChFrsKuT2kzmnuMfG5qi0v4,45221 +fontTools/varLib/interpolatableHelpers.py,sha256=lXd7kwfIVl-4opd-vxCDhf48RnJ7IQKv_uuFQM_6vaU,11496 +fontTools/varLib/interpolatablePlot.py,sha256=w393P6mGLRhYkIjSxMww3qyoYxAUZzCXlmPBbI_84C0,44375 +fontTools/varLib/interpolatableTestContourOrder.py,sha256=mHJ9Ry7Rm7H3zHDwEUQEtEIDseiUzOxjg4MveW_FSiU,3021 +fontTools/varLib/interpolatableTestStartingPoint.py,sha256=K6OYKBspim6BXc91pfLTbGLyi5XZukfMuBc6hRpENG8,4296 +fontTools/varLib/interpolate_layout.py,sha256=22VjGZuV2YiAe2MpdTf0xPVz1x2G84bcOL0vOeBpGQM,3689 +fontTools/varLib/iup.c,sha256=M-eCV04EONUWonJxOEZxVlUqw4zWN6Iy7wpvOPOpMB4,826716 +fontTools/varLib/iup.cpython-310-x86_64-linux-gnu.so,sha256=1drw6v7iIsglS9zMgUpFSVLiCikWjoEedYsJwqRzJ5M,1568136 +fontTools/varLib/iup.py,sha256=mKq_GRWuUg4yTmw2V32nu0v2r-SzzN7xS7rIbV0mYuc,14984 +fontTools/varLib/merger.py,sha256=E59oli4AwqWZ-FgnuStMSBvsB-FHe-55esXTYUqGeJ8,60802 +fontTools/varLib/models.py,sha256=sj_ENljh_qcMbfYzRIOlRgHq6tFOmL02Wv6WO8uofis,22398 +fontTools/varLib/multiVarStore.py,sha256=eQEuWNY01YF5zDpy1UwNtvOYyD6c0FLxpH-QFpX1i78,8305 +fontTools/varLib/mutator.py,sha256=YJkKFFWjwpYZ1MrC7UZYJ1BuYTGiwgi7jHnpqNpKfKg,19278 +fontTools/varLib/mvar.py,sha256=LTV77vH_3Ecg_qKBO5xQzjLOlJir_ppEr7mPVZRgad8,2449 +fontTools/varLib/plot.py,sha256=NoSZkJ5ndxNcDvJIvd5pQ9_jX6X1oM1K2G_tR4sdPVs,7494 +fontTools/varLib/stat.py,sha256=XuNKKZxGlBrl4OGFDAwVXhpBwJi23U3BdHmNTKoJnvE,4811 +fontTools/varLib/varStore.py,sha256=2QA9SDI6jQyQ_zq82OOwa3FBkfl-ksaSo1KGmVFpa9Q,24069 +fontTools/voltLib/__init__.py,sha256=ZZ1AsTx1VlDn40Kupce-fM3meOWugy3RZraBW9LG-9M,151 +fontTools/voltLib/__main__.py,sha256=uVtABLzMeHtvKL8zetf4rpC4aB8BkYr5QLSegNjZZZI,5928 +fontTools/voltLib/__pycache__/__init__.cpython-310.pyc,, +fontTools/voltLib/__pycache__/__main__.cpython-310.pyc,, +fontTools/voltLib/__pycache__/ast.cpython-310.pyc,, +fontTools/voltLib/__pycache__/error.cpython-310.pyc,, +fontTools/voltLib/__pycache__/lexer.cpython-310.pyc,, +fontTools/voltLib/__pycache__/parser.cpython-310.pyc,, +fontTools/voltLib/__pycache__/voltToFea.cpython-310.pyc,, +fontTools/voltLib/ast.py,sha256=arA9W3Gqo6OqljwNNKnMAojz-C5LStbC5SgjJh7buKk,13300 +fontTools/voltLib/error.py,sha256=phcQOQj-xOspCXu9hBJQRhSOBDzxHRgZd3fWQOFNJzw,395 +fontTools/voltLib/lexer.py,sha256=OvuETOSvlS6v7iCVeJ3IdH2Cg71n3OJoEyiB3-h6vhE,3368 +fontTools/voltLib/parser.py,sha256=rkw2IHBZPsrhGVC7Kw7V501m0u52kh1JSM5HXp-xchM,25396 +fontTools/voltLib/voltToFea.py,sha256=Z2yvnaZLQXzPLT86Uta0zRsXIYgj6NnvZtSWt5xmw2s,36549 +fonttools-4.59.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +fonttools-4.59.0.dist-info/METADATA,sha256=LneEbtHYsAQHPecvKT4gNMv-qzS_AwQTJBmslrSnfvs,107891 +fonttools-4.59.0.dist-info/RECORD,, +fonttools-4.59.0.dist-info/WHEEL,sha256=DTnKjM5OInJxWADod3iQyWxWcdG-eRwxzGww236swpY,151 +fonttools-4.59.0.dist-info/entry_points.txt,sha256=8kVHddxfFWA44FSD4mBpmC-4uCynQnkoz_9aNJb227Y,147 +fonttools-4.59.0.dist-info/licenses/LICENSE,sha256=Z4cgj4P2Wcy8IiOy_elS_6b36KymLxqKK_W8UbsbI4M,1072 +fonttools-4.59.0.dist-info/licenses/LICENSE.external,sha256=lKg6ruBymg8wLTSsxKzsvZ1YNm8mJCkHX-VX5KVLLmk,20022 +fonttools-4.59.0.dist-info/top_level.txt,sha256=rRgRylrXzekqWOsrhygzib12pQ7WILf7UGjqEwkIFDM,10 diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/WHEEL b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..d170d6d9582d145f12244c4135d9446d597e1029 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp310-cp310-manylinux_2_17_x86_64 +Tag: cp310-cp310-manylinux2014_x86_64 + diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/entry_points.txt b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..87ae781f169a63f0cf672a9050474035bfa5add4 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +fonttools = fontTools.__main__:main +pyftmerge = fontTools.merge:main +pyftsubset = fontTools.subset:main +ttx = fontTools.ttx:main diff --git a/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/top_level.txt b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..9af65ba39d292309497df4accdc44bd6f8143d10 --- /dev/null +++ b/lib/python3.10/site-packages/fonttools-4.59.0.dist-info/top_level.txt @@ -0,0 +1 @@ +fontTools diff --git a/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/INSTALLER b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/METADATA b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..349b111cae1046c2c6cc268f04dfb2f1df982fba --- /dev/null +++ b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/METADATA @@ -0,0 +1,285 @@ +Metadata-Version: 2.4 +Name: fsspec +Version: 2025.7.0 +Summary: File-system specification +Project-URL: Changelog, https://filesystem-spec.readthedocs.io/en/latest/changelog.html +Project-URL: Documentation, https://filesystem-spec.readthedocs.io/en/latest/ +Project-URL: Homepage, https://github.com/fsspec/filesystem_spec +Maintainer-email: Martin Durant +License: BSD 3-Clause License + + Copyright (c) 2018, Martin Durant + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +License-File: LICENSE +Keywords: file +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +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 +Requires-Python: >=3.9 +Provides-Extra: abfs +Requires-Dist: adlfs; extra == 'abfs' +Provides-Extra: adl +Requires-Dist: adlfs; extra == 'adl' +Provides-Extra: arrow +Requires-Dist: pyarrow>=1; extra == 'arrow' +Provides-Extra: dask +Requires-Dist: dask; extra == 'dask' +Requires-Dist: distributed; extra == 'dask' +Provides-Extra: dev +Requires-Dist: pre-commit; extra == 'dev' +Requires-Dist: ruff>=0.5; extra == 'dev' +Provides-Extra: doc +Requires-Dist: numpydoc; extra == 'doc' +Requires-Dist: sphinx; extra == 'doc' +Requires-Dist: sphinx-design; extra == 'doc' +Requires-Dist: sphinx-rtd-theme; extra == 'doc' +Requires-Dist: yarl; extra == 'doc' +Provides-Extra: dropbox +Requires-Dist: dropbox; extra == 'dropbox' +Requires-Dist: dropboxdrivefs; extra == 'dropbox' +Requires-Dist: requests; extra == 'dropbox' +Provides-Extra: entrypoints +Provides-Extra: full +Requires-Dist: adlfs; extra == 'full' +Requires-Dist: aiohttp!=4.0.0a0,!=4.0.0a1; extra == 'full' +Requires-Dist: dask; extra == 'full' +Requires-Dist: distributed; extra == 'full' +Requires-Dist: dropbox; extra == 'full' +Requires-Dist: dropboxdrivefs; extra == 'full' +Requires-Dist: fusepy; extra == 'full' +Requires-Dist: gcsfs; extra == 'full' +Requires-Dist: libarchive-c; extra == 'full' +Requires-Dist: ocifs; extra == 'full' +Requires-Dist: panel; extra == 'full' +Requires-Dist: paramiko; extra == 'full' +Requires-Dist: pyarrow>=1; extra == 'full' +Requires-Dist: pygit2; extra == 'full' +Requires-Dist: requests; extra == 'full' +Requires-Dist: s3fs; extra == 'full' +Requires-Dist: smbprotocol; extra == 'full' +Requires-Dist: tqdm; extra == 'full' +Provides-Extra: fuse +Requires-Dist: fusepy; extra == 'fuse' +Provides-Extra: gcs +Requires-Dist: gcsfs; extra == 'gcs' +Provides-Extra: git +Requires-Dist: pygit2; extra == 'git' +Provides-Extra: github +Requires-Dist: requests; extra == 'github' +Provides-Extra: gs +Requires-Dist: gcsfs; extra == 'gs' +Provides-Extra: gui +Requires-Dist: panel; extra == 'gui' +Provides-Extra: hdfs +Requires-Dist: pyarrow>=1; extra == 'hdfs' +Provides-Extra: http +Requires-Dist: aiohttp!=4.0.0a0,!=4.0.0a1; extra == 'http' +Provides-Extra: libarchive +Requires-Dist: libarchive-c; extra == 'libarchive' +Provides-Extra: oci +Requires-Dist: ocifs; extra == 'oci' +Provides-Extra: s3 +Requires-Dist: s3fs; extra == 's3' +Provides-Extra: sftp +Requires-Dist: paramiko; extra == 'sftp' +Provides-Extra: smb +Requires-Dist: smbprotocol; extra == 'smb' +Provides-Extra: ssh +Requires-Dist: paramiko; extra == 'ssh' +Provides-Extra: test +Requires-Dist: aiohttp!=4.0.0a0,!=4.0.0a1; extra == 'test' +Requires-Dist: numpy; extra == 'test' +Requires-Dist: pytest; extra == 'test' +Requires-Dist: pytest-asyncio!=0.22.0; extra == 'test' +Requires-Dist: pytest-benchmark; extra == 'test' +Requires-Dist: pytest-cov; extra == 'test' +Requires-Dist: pytest-mock; extra == 'test' +Requires-Dist: pytest-recording; extra == 'test' +Requires-Dist: pytest-rerunfailures; extra == 'test' +Requires-Dist: requests; extra == 'test' +Provides-Extra: test-downstream +Requires-Dist: aiobotocore<3.0.0,>=2.5.4; extra == 'test-downstream' +Requires-Dist: dask[dataframe,test]; extra == 'test-downstream' +Requires-Dist: moto[server]<5,>4; extra == 'test-downstream' +Requires-Dist: pytest-timeout; extra == 'test-downstream' +Requires-Dist: xarray; extra == 'test-downstream' +Provides-Extra: test-full +Requires-Dist: adlfs; extra == 'test-full' +Requires-Dist: aiohttp!=4.0.0a0,!=4.0.0a1; extra == 'test-full' +Requires-Dist: cloudpickle; extra == 'test-full' +Requires-Dist: dask; extra == 'test-full' +Requires-Dist: distributed; extra == 'test-full' +Requires-Dist: dropbox; extra == 'test-full' +Requires-Dist: dropboxdrivefs; extra == 'test-full' +Requires-Dist: fastparquet; extra == 'test-full' +Requires-Dist: fusepy; extra == 'test-full' +Requires-Dist: gcsfs; extra == 'test-full' +Requires-Dist: jinja2; extra == 'test-full' +Requires-Dist: kerchunk; extra == 'test-full' +Requires-Dist: libarchive-c; extra == 'test-full' +Requires-Dist: lz4; extra == 'test-full' +Requires-Dist: notebook; extra == 'test-full' +Requires-Dist: numpy; extra == 'test-full' +Requires-Dist: ocifs; extra == 'test-full' +Requires-Dist: pandas; extra == 'test-full' +Requires-Dist: panel; extra == 'test-full' +Requires-Dist: paramiko; extra == 'test-full' +Requires-Dist: pyarrow; extra == 'test-full' +Requires-Dist: pyarrow>=1; extra == 'test-full' +Requires-Dist: pyftpdlib; extra == 'test-full' +Requires-Dist: pygit2; extra == 'test-full' +Requires-Dist: pytest; extra == 'test-full' +Requires-Dist: pytest-asyncio!=0.22.0; extra == 'test-full' +Requires-Dist: pytest-benchmark; extra == 'test-full' +Requires-Dist: pytest-cov; extra == 'test-full' +Requires-Dist: pytest-mock; extra == 'test-full' +Requires-Dist: pytest-recording; extra == 'test-full' +Requires-Dist: pytest-rerunfailures; extra == 'test-full' +Requires-Dist: python-snappy; extra == 'test-full' +Requires-Dist: requests; extra == 'test-full' +Requires-Dist: smbprotocol; extra == 'test-full' +Requires-Dist: tqdm; extra == 'test-full' +Requires-Dist: urllib3; extra == 'test-full' +Requires-Dist: zarr; extra == 'test-full' +Requires-Dist: zstandard; (python_version < '3.14') and extra == 'test-full' +Provides-Extra: tqdm +Requires-Dist: tqdm; extra == 'tqdm' +Description-Content-Type: text/markdown + +# filesystem_spec + +[![PyPI version](https://badge.fury.io/py/fsspec.svg)](https://pypi.python.org/pypi/fsspec/) +[![Anaconda-Server Badge](https://anaconda.org/conda-forge/fsspec/badges/version.svg)](https://anaconda.org/conda-forge/fsspec) +![Build](https://github.com/fsspec/filesystem_spec/workflows/CI/badge.svg) +[![Docs](https://readthedocs.org/projects/filesystem-spec/badge/?version=latest)](https://filesystem-spec.readthedocs.io/en/latest/?badge=latest) + +A specification for pythonic filesystems. + +## Install + +```bash +pip install fsspec +``` + +would install the base fsspec. Various optionally supported features might require specification of custom +extra require, e.g. `pip install fsspec[ssh]` will install dependencies for `ssh` backends support. +Use `pip install fsspec[full]` for installation of all known extra dependencies. + +Up-to-date package also provided through conda-forge distribution: + +```bash +conda install -c conda-forge fsspec +``` + + +## Purpose + +To produce a template or specification for a file-system interface, that specific implementations should follow, +so that applications making use of them can rely on a common behaviour and not have to worry about the specific +internal implementation decisions with any given backend. Many such implementations are included in this package, +or in sister projects such as `s3fs` and `gcsfs`. + +In addition, if this is well-designed, then additional functionality, such as a key-value store or FUSE +mounting of the file-system implementation may be available for all implementations "for free". + +## Documentation + +Please refer to [RTD](https://filesystem-spec.readthedocs.io/en/latest/?badge=latest) + +## Develop + +fsspec uses GitHub Actions for CI. Environment files can be found +in the "ci/" directory. Note that the main environment is called "py38", +but it is expected that the version of python installed be adjustable at +CI runtime. For local use, pick a version suitable for you. + +```bash +# For a new environment (mamba / conda). +mamba create -n fsspec -c conda-forge python=3.9 -y +conda activate fsspec + +# Standard dev install with docs and tests. +pip install -e ".[dev,doc,test]" + +# Full tests except for downstream +pip install s3fs +pip uninstall s3fs +pip install -e .[dev,doc,test_full] +pip install s3fs --no-deps +pytest -v + +# Downstream tests. +sh install_s3fs.sh +# Windows powershell. +install_s3fs.sh +``` + +### Testing + +Tests can be run in the dev environment, if activated, via ``pytest fsspec``. + +The full fsspec suite requires a system-level docker, docker-compose, and fuse +installation. If only making changes to one backend implementation, it is +not generally necessary to run all tests locally. + +It is expected that contributors ensure that any change to fsspec does not +cause issues or regressions for either other fsspec-related packages such +as gcsfs and s3fs, nor for downstream users of fsspec. The "downstream" CI +run and corresponding environment file run a set of tests from the dask +test suite, and very minimal tests against pandas and zarr from the +test_downstream.py module in this repo. + +### Code Formatting + +fsspec uses [Black](https://black.readthedocs.io/en/stable) to ensure +a consistent code format throughout the project. +Run ``black fsspec`` from the root of the filesystem_spec repository to +auto-format your code. Additionally, many editors have plugins that will apply +``black`` as you edit files. ``black`` is included in the ``tox`` environments. + +Optionally, you may wish to setup [pre-commit hooks](https://pre-commit.com) to +automatically run ``black`` when you make a git commit. +Run ``pre-commit install --install-hooks`` from the root of the +filesystem_spec repository to setup pre-commit hooks. ``black`` will now be run +before you commit, reformatting any changed files. You can format without +committing via ``pre-commit run`` or skip these checks with ``git commit +--no-verify``. + +## Support + +Work on this repository is supported in part by: + +"Anaconda, Inc. - Advancing AI through open source." + +anaconda logo diff --git a/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/RECORD b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..5fe6ad626112c6ad86f95bde1bae68cf442e95be --- /dev/null +++ b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/RECORD @@ -0,0 +1,117 @@ +fsspec-2025.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +fsspec-2025.7.0.dist-info/METADATA,sha256=fPEhTbN6wi6KOz3IkiJQcOTYvQXVEjzTv9gzyX-KRHI,12161 +fsspec-2025.7.0.dist-info/RECORD,, +fsspec-2025.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +fsspec-2025.7.0.dist-info/licenses/LICENSE,sha256=LcNUls5TpzB5FcAIqESq1T53K0mzTN0ARFBnaRQH7JQ,1513 +fsspec/__init__.py,sha256=L7qwNBU1iMNQd8Of87HYSNFT9gWlNMSESaJC8fY0AaQ,2053 +fsspec/__pycache__/__init__.cpython-310.pyc,, +fsspec/__pycache__/_version.cpython-310.pyc,, +fsspec/__pycache__/archive.cpython-310.pyc,, +fsspec/__pycache__/asyn.cpython-310.pyc,, +fsspec/__pycache__/caching.cpython-310.pyc,, +fsspec/__pycache__/callbacks.cpython-310.pyc,, +fsspec/__pycache__/compression.cpython-310.pyc,, +fsspec/__pycache__/config.cpython-310.pyc,, +fsspec/__pycache__/conftest.cpython-310.pyc,, +fsspec/__pycache__/core.cpython-310.pyc,, +fsspec/__pycache__/dircache.cpython-310.pyc,, +fsspec/__pycache__/exceptions.cpython-310.pyc,, +fsspec/__pycache__/fuse.cpython-310.pyc,, +fsspec/__pycache__/generic.cpython-310.pyc,, +fsspec/__pycache__/gui.cpython-310.pyc,, +fsspec/__pycache__/json.cpython-310.pyc,, +fsspec/__pycache__/mapping.cpython-310.pyc,, +fsspec/__pycache__/parquet.cpython-310.pyc,, +fsspec/__pycache__/registry.cpython-310.pyc,, +fsspec/__pycache__/spec.cpython-310.pyc,, +fsspec/__pycache__/transaction.cpython-310.pyc,, +fsspec/__pycache__/utils.cpython-310.pyc,, +fsspec/_version.py,sha256=BxtkhBSbP2A9Z9pLCoNSk_l6NzaY9SDK9dmjDgIXO54,517 +fsspec/archive.py,sha256=vM6t_lgV6lBWbBYwpm3S4ofBQFQxUPr5KkDQrrQcQro,2411 +fsspec/asyn.py,sha256=mE55tO_MmGcxD14cUuaiS3veAqo0h6ZqANfnUuCN3sk,36365 +fsspec/caching.py,sha256=86uSgPa5E55b28XEhuC-dMcKAxJtZZnpQqnHTwaF3hI,34294 +fsspec/callbacks.py,sha256=BDIwLzK6rr_0V5ch557fSzsivCElpdqhXr5dZ9Te-EE,9210 +fsspec/compression.py,sha256=gBK2MV_oTFVW2XDq8bZVbYQKYrl6JDUou6_-kyvmxuk,5086 +fsspec/config.py,sha256=LF4Zmu1vhJW7Je9Q-cwkRc3xP7Rhyy7Xnwj26Z6sv2g,4279 +fsspec/conftest.py,sha256=fVfx-NLrH_OZS1TIpYNoPzM7efEcMoL62reHOdYeFCA,1245 +fsspec/core.py,sha256=1tLctwr7sF1VO3djc_UkjhJ8IAEy0TUMH_bb07Sw17E,23828 +fsspec/dircache.py,sha256=YzogWJrhEastHU7vWz-cJiJ7sdtLXFXhEpInGKd4EcM,2717 +fsspec/exceptions.py,sha256=pauSLDMxzTJMOjvX1WEUK0cMyFkrFxpWJsyFywav7A8,331 +fsspec/fuse.py,sha256=Q-3NOOyLqBfYa4Db5E19z_ZY36zzYHtIs1mOUasItBQ,10177 +fsspec/generic.py,sha256=K-b03ifKidHUo99r8nz2pB6oGyf88RtTKahCuBF9ZVU,13409 +fsspec/gui.py,sha256=CQ7QsrTpaDlWSLNOpwNoJc7khOcYXIZxmrAJN9bHWQU,14002 +fsspec/implementations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +fsspec/implementations/__pycache__/__init__.cpython-310.pyc,, +fsspec/implementations/__pycache__/arrow.cpython-310.pyc,, +fsspec/implementations/__pycache__/asyn_wrapper.cpython-310.pyc,, +fsspec/implementations/__pycache__/cache_mapper.cpython-310.pyc,, +fsspec/implementations/__pycache__/cache_metadata.cpython-310.pyc,, +fsspec/implementations/__pycache__/cached.cpython-310.pyc,, +fsspec/implementations/__pycache__/dask.cpython-310.pyc,, +fsspec/implementations/__pycache__/data.cpython-310.pyc,, +fsspec/implementations/__pycache__/dbfs.cpython-310.pyc,, +fsspec/implementations/__pycache__/dirfs.cpython-310.pyc,, +fsspec/implementations/__pycache__/ftp.cpython-310.pyc,, +fsspec/implementations/__pycache__/gist.cpython-310.pyc,, +fsspec/implementations/__pycache__/git.cpython-310.pyc,, +fsspec/implementations/__pycache__/github.cpython-310.pyc,, +fsspec/implementations/__pycache__/http.cpython-310.pyc,, +fsspec/implementations/__pycache__/http_sync.cpython-310.pyc,, +fsspec/implementations/__pycache__/jupyter.cpython-310.pyc,, +fsspec/implementations/__pycache__/libarchive.cpython-310.pyc,, +fsspec/implementations/__pycache__/local.cpython-310.pyc,, +fsspec/implementations/__pycache__/memory.cpython-310.pyc,, +fsspec/implementations/__pycache__/reference.cpython-310.pyc,, +fsspec/implementations/__pycache__/sftp.cpython-310.pyc,, +fsspec/implementations/__pycache__/smb.cpython-310.pyc,, +fsspec/implementations/__pycache__/tar.cpython-310.pyc,, +fsspec/implementations/__pycache__/webhdfs.cpython-310.pyc,, +fsspec/implementations/__pycache__/zip.cpython-310.pyc,, +fsspec/implementations/arrow.py,sha256=721Dikne_lV_0tlgk9jyKmHL6W-5MT0h2LKGvOYQTPI,8623 +fsspec/implementations/asyn_wrapper.py,sha256=435NV_LyrRic3WvSxMWq7B8QGV_Ovzi-vYd2W1_1YtM,3326 +fsspec/implementations/cache_mapper.py,sha256=W4wlxyPxZbSp9ItJ0pYRVBMh6bw9eFypgP6kUYuuiI4,2421 +fsspec/implementations/cache_metadata.py,sha256=rddh5-0SXIeyWCPpBpOFcaAyWoPyeYmFfeubEWt-nRM,8536 +fsspec/implementations/cached.py,sha256=59lyWvbzvX_yYC9cVASrktOdjmK6w-e7dNtNBJHaONQ,35103 +fsspec/implementations/dask.py,sha256=CXZbJzIVOhKV8ILcxuy3bTvcacCueAbyQxmvAkbPkrk,4466 +fsspec/implementations/data.py,sha256=LDLczxRh8h7x39Zjrd-GgzdQHr78yYxDlrv2C9Uxb5E,1658 +fsspec/implementations/dbfs.py,sha256=2Bp-0m9SqlaroDa0KbXxb5BobCyBJ7_5YQBISf3fxbQ,15145 +fsspec/implementations/dirfs.py,sha256=f1sGnQ9Vf0xTxrXo4jDeBy4Qfq3RTqAEemqBSeb0hwY,12108 +fsspec/implementations/ftp.py,sha256=bzL_TgH77nMMtTMewRGkbq4iObSHGu7YoMRCXBH4nrc,11639 +fsspec/implementations/gist.py,sha256=Ost985hmFr50KsA-QD0shY3hP4KX5qJ9rb5C-X4ehK8,8341 +fsspec/implementations/git.py,sha256=qBDWMz5LNllPqVjr5jf_1FuNha4P5lyQI3IlhYg-wUE,3731 +fsspec/implementations/github.py,sha256=aCsZL8UvXZgdkcB1RUs3DdLeNrjLKcFsFYeQFDWbBFo,11653 +fsspec/implementations/http.py,sha256=3LhYuRU3yw3v3tN8Oqz6EbJRl3ab2Sg_zsGOIv0E2gE,30418 +fsspec/implementations/http_sync.py,sha256=UydDqSdUBdhiJ1KufzV8rKGrTftFR4QmNV0safILb8g,30133 +fsspec/implementations/jupyter.py,sha256=B2uj7OEm7yIk-vRSsO37_ND0t0EBvn4B-Su43ibN4Pg,3811 +fsspec/implementations/libarchive.py,sha256=5_I2DiLXwQ1JC8x-K7jXu-tBwhO9dj7tFLnb0bTnVMQ,7102 +fsspec/implementations/local.py,sha256=DQeK7jRGv4_mJAweLKALO5WzIIkjXxZ_jRvwQ_xadSA,16936 +fsspec/implementations/memory.py,sha256=Kc6TZSbZ4tdi-6cE5ttEPIgMyq9aAt6cDdVLFRTJvf8,10488 +fsspec/implementations/reference.py,sha256=npYj49AmR8rmON9t_BLpfEXqhgsardUeynamqyraOXo,48704 +fsspec/implementations/sftp.py,sha256=fMY9XZcmpjszQ2tCqO_TPaJesaeD_Dv7ptYzgUPGoO0,5631 +fsspec/implementations/smb.py,sha256=5fhu8h06nOLBPh2c48aT7WBRqh9cEcbIwtyu06wTjec,15236 +fsspec/implementations/tar.py,sha256=dam78Tp_CozybNqCY2JYgGBS3Uc9FuJUAT9oB0lolOs,4111 +fsspec/implementations/webhdfs.py,sha256=G9wGywj7BkZk4Mu9zXu6HaDlEqX4F8Gw1i4k46CP_-o,16769 +fsspec/implementations/zip.py,sha256=9LBMHPft2OutJl2Ft-r9u_z3GptLkc2n91ur2A3bCbg,6072 +fsspec/json.py,sha256=3BfNSQ96MB4Xao_ocjheINeqZM2ev7oljUzR5XmNXrE,3814 +fsspec/mapping.py,sha256=m2ndB_gtRBXYmNJg0Ie1-BVR75TFleHmIQBzC-yWhjU,8343 +fsspec/parquet.py,sha256=6ibAmG527L5JNFS0VO8BDNlxHdA3bVYqdByeiFgpUVM,19448 +fsspec/registry.py,sha256=epoYryFFzDWjbkQJfh6xkF3nEu8RTiOzV3-voi8Pshs,12048 +fsspec/spec.py,sha256=7cOUe5PC5Uyf56HtGBUHEoym8ktPj-BI8G4HR8Xd_C8,77298 +fsspec/tests/abstract/__init__.py,sha256=4xUJrv7gDgc85xAOz1p-V_K1hrsdMWTSa0rviALlJk8,10181 +fsspec/tests/abstract/__pycache__/__init__.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/common.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/copy.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/get.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/mv.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/open.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/pipe.cpython-310.pyc,, +fsspec/tests/abstract/__pycache__/put.cpython-310.pyc,, +fsspec/tests/abstract/common.py,sha256=1GQwNo5AONzAnzZj0fWgn8NJPLXALehbsuGxS3FzWVU,4973 +fsspec/tests/abstract/copy.py,sha256=gU5-d97U3RSde35Vp4RxPY4rWwL744HiSrJ8IBOp9-8,19967 +fsspec/tests/abstract/get.py,sha256=vNR4HztvTR7Cj56AMo7_tx7TeYz1Jgr_2Wb8Lv-UiBY,20755 +fsspec/tests/abstract/mv.py,sha256=k8eUEBIrRrGMsBY5OOaDXdGnQUKGwDIfQyduB6YD3Ns,1982 +fsspec/tests/abstract/open.py,sha256=Fi2PBPYLbRqysF8cFm0rwnB41kMdQVYjq8cGyDXp3BU,329 +fsspec/tests/abstract/pipe.py,sha256=LFzIrLCB5GLXf9rzFKJmE8AdG7LQ_h4bJo70r8FLPqM,402 +fsspec/tests/abstract/put.py,sha256=7aih17OKB_IZZh1Mkq1eBDIjobhtMQmI8x-Pw-S_aZk,21201 +fsspec/transaction.py,sha256=xliRG6U2Zf3khG4xcw9WiB-yAoqJSHEGK_VjHOdtgo0,2398 +fsspec/utils.py,sha256=HC8RFbb7KpEDedsYxExvWvsTObEuUcuuWxd0B_MyGpo,22995 diff --git a/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/WHEEL b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..12228d414b6cfed7c39d3781c85c63256a1d7fb5 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec-2025.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/fsspec/__init__.py b/lib/python3.10/site-packages/fsspec/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..452c78a055e72a6d04f1013d1a98fda33fdc449e --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/__init__.py @@ -0,0 +1,71 @@ +from . import caching +from ._version import __version__ # noqa: F401 +from .callbacks import Callback +from .compression import available_compressions +from .core import get_fs_token_paths, open, open_files, open_local, url_to_fs +from .exceptions import FSTimeoutError +from .mapping import FSMap, get_mapper +from .registry import ( + available_protocols, + filesystem, + get_filesystem_class, + register_implementation, + registry, +) +from .spec import AbstractFileSystem + +__all__ = [ + "AbstractFileSystem", + "FSTimeoutError", + "FSMap", + "filesystem", + "register_implementation", + "get_filesystem_class", + "get_fs_token_paths", + "get_mapper", + "open", + "open_files", + "open_local", + "registry", + "caching", + "Callback", + "available_protocols", + "available_compressions", + "url_to_fs", +] + + +def process_entries(): + try: + from importlib.metadata import entry_points + except ImportError: + return + if entry_points is not None: + try: + eps = entry_points() + except TypeError: + pass # importlib-metadata < 0.8 + else: + if hasattr(eps, "select"): # Python 3.10+ / importlib_metadata >= 3.9.0 + specs = eps.select(group="fsspec.specs") + else: + specs = eps.get("fsspec.specs", []) + registered_names = {} + for spec in specs: + err_msg = f"Unable to load filesystem from {spec}" + name = spec.name + if name in registered_names: + continue + registered_names[name] = True + register_implementation( + name, + spec.value.replace(":", "."), + errtxt=err_msg, + # We take our implementations as the ones to overload with if + # for some reason we encounter some, may be the same, already + # registered + clobber=True, + ) + + +process_entries() diff --git a/lib/python3.10/site-packages/fsspec/_version.py b/lib/python3.10/site-packages/fsspec/_version.py new file mode 100644 index 0000000000000000000000000000000000000000..fa46f9e465e2ef9bf881760a6ce215414c4548eb --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/_version.py @@ -0,0 +1,21 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '2025.7.0' +__version_tuple__ = version_tuple = (2025, 7, 0) diff --git a/lib/python3.10/site-packages/fsspec/archive.py b/lib/python3.10/site-packages/fsspec/archive.py new file mode 100644 index 0000000000000000000000000000000000000000..13a4da8df7c9405297cdd7d37476be2f725b2f57 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/archive.py @@ -0,0 +1,75 @@ +import operator + +from fsspec import AbstractFileSystem +from fsspec.utils import tokenize + + +class AbstractArchiveFileSystem(AbstractFileSystem): + """ + A generic superclass for implementing Archive-based filesystems. + + Currently, it is shared amongst + :class:`~fsspec.implementations.zip.ZipFileSystem`, + :class:`~fsspec.implementations.libarchive.LibArchiveFileSystem` and + :class:`~fsspec.implementations.tar.TarFileSystem`. + """ + + def __str__(self): + return f"" + + __repr__ = __str__ + + def ukey(self, path): + return tokenize(path, self.fo, self.protocol) + + def _all_dirnames(self, paths): + """Returns *all* directory names for each path in paths, including intermediate + ones. + + Parameters + ---------- + paths: Iterable of path strings + """ + if len(paths) == 0: + return set() + + dirnames = {self._parent(path) for path in paths} - {self.root_marker} + return dirnames | self._all_dirnames(dirnames) + + def info(self, path, **kwargs): + self._get_dirs() + path = self._strip_protocol(path) + if path in {"", "/"} and self.dir_cache: + return {"name": "", "type": "directory", "size": 0} + if path in self.dir_cache: + return self.dir_cache[path] + elif path + "/" in self.dir_cache: + return self.dir_cache[path + "/"] + else: + raise FileNotFoundError(path) + + def ls(self, path, detail=True, **kwargs): + self._get_dirs() + paths = {} + for p, f in self.dir_cache.items(): + p = p.rstrip("/") + if "/" in p: + root = p.rsplit("/", 1)[0] + else: + root = "" + if root == path.rstrip("/"): + paths[p] = f + elif all( + (a == b) + for a, b in zip(path.split("/"), [""] + p.strip("/").split("/")) + ): + # root directory entry + ppath = p.rstrip("/").split("/", 1)[0] + if ppath not in paths: + out = {"name": ppath, "size": 0, "type": "directory"} + paths[ppath] = out + if detail: + out = sorted(paths.values(), key=operator.itemgetter("name")) + return out + else: + return sorted(paths) diff --git a/lib/python3.10/site-packages/fsspec/asyn.py b/lib/python3.10/site-packages/fsspec/asyn.py new file mode 100644 index 0000000000000000000000000000000000000000..83772839450ec74a8fb37f9be323341f948e3748 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/asyn.py @@ -0,0 +1,1097 @@ +import asyncio +import asyncio.events +import functools +import inspect +import io +import numbers +import os +import re +import threading +from collections.abc import Iterable +from glob import has_magic +from typing import TYPE_CHECKING + +from .callbacks import DEFAULT_CALLBACK +from .exceptions import FSTimeoutError +from .implementations.local import LocalFileSystem, make_path_posix, trailing_sep +from .spec import AbstractBufferedFile, AbstractFileSystem +from .utils import glob_translate, is_exception, other_paths + +private = re.compile("_[^_]") +iothread = [None] # dedicated fsspec IO thread +loop = [None] # global event loop for any non-async instance +_lock = None # global lock placeholder +get_running_loop = asyncio.get_running_loop + + +def get_lock(): + """Allocate or return a threading lock. + + The lock is allocated on first use to allow setting one lock per forked process. + """ + global _lock + if not _lock: + _lock = threading.Lock() + return _lock + + +def reset_lock(): + """Reset the global lock. + + This should be called only on the init of a forked process to reset the lock to + None, enabling the new forked process to get a new lock. + """ + global _lock + + iothread[0] = None + loop[0] = None + _lock = None + + +async def _runner(event, coro, result, timeout=None): + timeout = timeout if timeout else None # convert 0 or 0.0 to None + if timeout is not None: + coro = asyncio.wait_for(coro, timeout=timeout) + try: + result[0] = await coro + except Exception as ex: + result[0] = ex + finally: + event.set() + + +def sync(loop, func, *args, timeout=None, **kwargs): + """ + Make loop run coroutine until it returns. Runs in other thread + + Examples + -------- + >>> fsspec.asyn.sync(fsspec.asyn.get_loop(), func, *args, + timeout=timeout, **kwargs) + """ + timeout = timeout if timeout else None # convert 0 or 0.0 to None + # NB: if the loop is not running *yet*, it is OK to submit work + # and we will wait for it + if loop is None or loop.is_closed(): + raise RuntimeError("Loop is not running") + try: + loop0 = asyncio.events.get_running_loop() + if loop0 is loop: + raise NotImplementedError("Calling sync() from within a running loop") + except NotImplementedError: + raise + except RuntimeError: + pass + coro = func(*args, **kwargs) + result = [None] + event = threading.Event() + asyncio.run_coroutine_threadsafe(_runner(event, coro, result, timeout), loop) + while True: + # this loops allows thread to get interrupted + if event.wait(1): + break + if timeout is not None: + timeout -= 1 + if timeout < 0: + raise FSTimeoutError + + return_result = result[0] + if isinstance(return_result, asyncio.TimeoutError): + # suppress asyncio.TimeoutError, raise FSTimeoutError + raise FSTimeoutError from return_result + elif isinstance(return_result, BaseException): + raise return_result + else: + return return_result + + +def sync_wrapper(func, obj=None): + """Given a function, make so can be called in blocking contexts + + Leave obj=None if defining within a class. Pass the instance if attaching + as an attribute of the instance. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + self = obj or args[0] + return sync(self.loop, func, *args, **kwargs) + + return wrapper + + +def get_loop(): + """Create or return the default fsspec IO loop + + The loop will be running on a separate thread. + """ + if loop[0] is None: + with get_lock(): + # repeat the check just in case the loop got filled between the + # previous two calls from another thread + if loop[0] is None: + loop[0] = asyncio.new_event_loop() + th = threading.Thread(target=loop[0].run_forever, name="fsspecIO") + th.daemon = True + th.start() + iothread[0] = th + return loop[0] + + +def reset_after_fork(): + global lock + loop[0] = None + iothread[0] = None + lock = None + + +if hasattr(os, "register_at_fork"): + # should be posix; this will do nothing for spawn or forkserver subprocesses + os.register_at_fork(after_in_child=reset_after_fork) + + +if TYPE_CHECKING: + import resource + + ResourceError = resource.error +else: + try: + import resource + except ImportError: + resource = None + ResourceError = OSError + else: + ResourceError = getattr(resource, "error", OSError) + +_DEFAULT_BATCH_SIZE = 128 +_NOFILES_DEFAULT_BATCH_SIZE = 1280 + + +def _get_batch_size(nofiles=False): + from fsspec.config import conf + + if nofiles: + if "nofiles_gather_batch_size" in conf: + return conf["nofiles_gather_batch_size"] + else: + if "gather_batch_size" in conf: + return conf["gather_batch_size"] + if nofiles: + return _NOFILES_DEFAULT_BATCH_SIZE + if resource is None: + return _DEFAULT_BATCH_SIZE + + try: + soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE) + except (ImportError, ValueError, ResourceError): + return _DEFAULT_BATCH_SIZE + + if soft_limit == resource.RLIM_INFINITY: + return -1 + else: + return soft_limit // 8 + + +def running_async() -> bool: + """Being executed by an event loop?""" + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + +async def _run_coros_in_chunks( + coros, + batch_size=None, + callback=DEFAULT_CALLBACK, + timeout=None, + return_exceptions=False, + nofiles=False, +): + """Run the given coroutines in chunks. + + Parameters + ---------- + coros: list of coroutines to run + batch_size: int or None + Number of coroutines to submit/wait on simultaneously. + If -1, then it will not be any throttling. If + None, it will be inferred from _get_batch_size() + callback: fsspec.callbacks.Callback instance + Gets a relative_update when each coroutine completes + timeout: number or None + If given, each coroutine times out after this time. Note that, since + there are multiple batches, the total run time of this function will in + general be longer + return_exceptions: bool + Same meaning as in asyncio.gather + nofiles: bool + If inferring the batch_size, does this operation involve local files? + If yes, you normally expect smaller batches. + """ + + if batch_size is None: + batch_size = _get_batch_size(nofiles=nofiles) + + if batch_size == -1: + batch_size = len(coros) + + assert batch_size > 0 + + async def _run_coro(coro, i): + try: + return await asyncio.wait_for(coro, timeout=timeout), i + except Exception as e: + if not return_exceptions: + raise + return e, i + finally: + callback.relative_update(1) + + i = 0 + n = len(coros) + results = [None] * n + pending = set() + + while pending or i < n: + while len(pending) < batch_size and i < n: + pending.add(asyncio.ensure_future(_run_coro(coros[i], i))) + i += 1 + + if not pending: + break + + done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) + while done: + result, k = await done.pop() + results[k] = result + + return results + + +# these methods should be implemented as async by any async-able backend +async_methods = [ + "_ls", + "_cat_file", + "_get_file", + "_put_file", + "_rm_file", + "_cp_file", + "_pipe_file", + "_expand_path", + "_info", + "_isfile", + "_isdir", + "_exists", + "_walk", + "_glob", + "_find", + "_du", + "_size", + "_mkdir", + "_makedirs", +] + + +class AsyncFileSystem(AbstractFileSystem): + """Async file operations, default implementations + + Passes bulk operations to asyncio.gather for concurrent operation. + + Implementations that have concurrent batch operations and/or async methods + should inherit from this class instead of AbstractFileSystem. Docstrings are + copied from the un-underscored method in AbstractFileSystem, if not given. + """ + + # note that methods do not have docstring here; they will be copied + # for _* methods and inferred for overridden methods. + + async_impl = True + mirror_sync_methods = True + disable_throttling = False + + def __init__(self, *args, asynchronous=False, loop=None, batch_size=None, **kwargs): + self.asynchronous = asynchronous + self._pid = os.getpid() + if not asynchronous: + self._loop = loop or get_loop() + else: + self._loop = None + self.batch_size = batch_size + super().__init__(*args, **kwargs) + + @property + def loop(self): + if self._pid != os.getpid(): + raise RuntimeError("This class is not fork-safe") + return self._loop + + async def _rm_file(self, path, **kwargs): + raise NotImplementedError + + async def _rm(self, path, recursive=False, batch_size=None, **kwargs): + # TODO: implement on_error + batch_size = batch_size or self.batch_size + path = await self._expand_path(path, recursive=recursive) + return await _run_coros_in_chunks( + [self._rm_file(p, **kwargs) for p in reversed(path)], + batch_size=batch_size, + nofiles=True, + ) + + async def _cp_file(self, path1, path2, **kwargs): + raise NotImplementedError + + async def _mv_file(self, path1, path2): + await self._cp_file(path1, path2) + await self._rm_file(path1) + + async def _copy( + self, + path1, + path2, + recursive=False, + on_error=None, + maxdepth=None, + batch_size=None, + **kwargs, + ): + if on_error is None and recursive: + on_error = "ignore" + elif on_error is None: + on_error = "raise" + + if isinstance(path1, list) and isinstance(path2, list): + # No need to expand paths when both source and destination + # are provided as lists + paths1 = path1 + paths2 = path2 + else: + source_is_str = isinstance(path1, str) + paths1 = await self._expand_path( + path1, maxdepth=maxdepth, recursive=recursive + ) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + paths1 = [ + p for p in paths1 if not (trailing_sep(p) or await self._isdir(p)) + ] + if not paths1: + return + + source_is_file = len(paths1) == 1 + dest_is_dir = isinstance(path2, str) and ( + trailing_sep(path2) or await self._isdir(path2) + ) + + exists = source_is_str and ( + (has_magic(path1) and source_is_file) + or (not has_magic(path1) and dest_is_dir and not trailing_sep(path1)) + ) + paths2 = other_paths( + paths1, + path2, + exists=exists, + flatten=not source_is_str, + ) + + batch_size = batch_size or self.batch_size + coros = [self._cp_file(p1, p2, **kwargs) for p1, p2 in zip(paths1, paths2)] + result = await _run_coros_in_chunks( + coros, batch_size=batch_size, return_exceptions=True, nofiles=True + ) + + for ex in filter(is_exception, result): + if on_error == "ignore" and isinstance(ex, FileNotFoundError): + continue + raise ex + + async def _pipe_file(self, path, value, mode="overwrite", **kwargs): + raise NotImplementedError + + async def _pipe(self, path, value=None, batch_size=None, **kwargs): + if isinstance(path, str): + path = {path: value} + batch_size = batch_size or self.batch_size + return await _run_coros_in_chunks( + [self._pipe_file(k, v, **kwargs) for k, v in path.items()], + batch_size=batch_size, + nofiles=True, + ) + + async def _process_limits(self, url, start, end): + """Helper for "Range"-based _cat_file""" + size = None + suff = False + if start is not None and start < 0: + # if start is negative and end None, end is the "suffix length" + if end is None: + end = -start + start = "" + suff = True + else: + size = size or (await self._info(url))["size"] + start = size + start + elif start is None: + start = 0 + if not suff: + if end is not None and end < 0: + if start is not None: + size = size or (await self._info(url))["size"] + end = size + end + elif end is None: + end = "" + if isinstance(end, numbers.Integral): + end -= 1 # bytes range is inclusive + return f"bytes={start}-{end}" + + async def _cat_file(self, path, start=None, end=None, **kwargs): + raise NotImplementedError + + async def _cat( + self, path, recursive=False, on_error="raise", batch_size=None, **kwargs + ): + paths = await self._expand_path(path, recursive=recursive) + coros = [self._cat_file(path, **kwargs) for path in paths] + batch_size = batch_size or self.batch_size + out = await _run_coros_in_chunks( + coros, batch_size=batch_size, nofiles=True, return_exceptions=True + ) + if on_error == "raise": + ex = next(filter(is_exception, out), False) + if ex: + raise ex + if ( + len(paths) > 1 + or isinstance(path, list) + or paths[0] != self._strip_protocol(path) + ): + return { + k: v + for k, v in zip(paths, out) + if on_error != "omit" or not is_exception(v) + } + else: + return out[0] + + async def _cat_ranges( + self, + paths, + starts, + ends, + max_gap=None, + batch_size=None, + on_error="return", + **kwargs, + ): + """Get the contents of byte ranges from one or more files + + Parameters + ---------- + paths: list + A list of of filepaths on this filesystems + starts, ends: int or list + Bytes limits of the read. If using a single int, the same value will be + used to read all the specified files. + """ + # TODO: on_error + if max_gap is not None: + # use utils.merge_offset_ranges + raise NotImplementedError + if not isinstance(paths, list): + raise TypeError + if not isinstance(starts, Iterable): + starts = [starts] * len(paths) + if not isinstance(ends, Iterable): + ends = [ends] * len(paths) + if len(starts) != len(paths) or len(ends) != len(paths): + raise ValueError + coros = [ + self._cat_file(p, start=s, end=e, **kwargs) + for p, s, e in zip(paths, starts, ends) + ] + batch_size = batch_size or self.batch_size + return await _run_coros_in_chunks( + coros, batch_size=batch_size, nofiles=True, return_exceptions=True + ) + + async def _put_file(self, lpath, rpath, mode="overwrite", **kwargs): + raise NotImplementedError + + async def _put( + self, + lpath, + rpath, + recursive=False, + callback=DEFAULT_CALLBACK, + batch_size=None, + maxdepth=None, + **kwargs, + ): + """Copy file(s) from local. + + Copies a specific file or tree of files (if recursive=True). If rpath + ends with a "/", it will be assumed to be a directory, and target files + will go within. + + The put_file method will be called concurrently on a batch of files. The + batch_size option can configure the amount of futures that can be executed + at the same time. If it is -1, then all the files will be uploaded concurrently. + The default can be set for this instance by passing "batch_size" in the + constructor, or for all instances by setting the "gather_batch_size" key + in ``fsspec.config.conf``, falling back to 1/8th of the system limit . + """ + if isinstance(lpath, list) and isinstance(rpath, list): + # No need to expand paths when both source and destination + # are provided as lists + rpaths = rpath + lpaths = lpath + else: + source_is_str = isinstance(lpath, str) + if source_is_str: + lpath = make_path_posix(lpath) + fs = LocalFileSystem() + lpaths = fs.expand_path(lpath, recursive=recursive, maxdepth=maxdepth) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))] + if not lpaths: + return + + source_is_file = len(lpaths) == 1 + dest_is_dir = isinstance(rpath, str) and ( + trailing_sep(rpath) or await self._isdir(rpath) + ) + + rpath = self._strip_protocol(rpath) + exists = source_is_str and ( + (has_magic(lpath) and source_is_file) + or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath)) + ) + rpaths = other_paths( + lpaths, + rpath, + exists=exists, + flatten=not source_is_str, + ) + + is_dir = {l: os.path.isdir(l) for l in lpaths} + rdirs = [r for l, r in zip(lpaths, rpaths) if is_dir[l]] + file_pairs = [(l, r) for l, r in zip(lpaths, rpaths) if not is_dir[l]] + + await asyncio.gather(*[self._makedirs(d, exist_ok=True) for d in rdirs]) + batch_size = batch_size or self.batch_size + + coros = [] + callback.set_size(len(file_pairs)) + for lfile, rfile in file_pairs: + put_file = callback.branch_coro(self._put_file) + coros.append(put_file(lfile, rfile, **kwargs)) + + return await _run_coros_in_chunks( + coros, batch_size=batch_size, callback=callback + ) + + async def _get_file(self, rpath, lpath, **kwargs): + raise NotImplementedError + + async def _get( + self, + rpath, + lpath, + recursive=False, + callback=DEFAULT_CALLBACK, + maxdepth=None, + **kwargs, + ): + """Copy file(s) to local. + + Copies a specific file or tree of files (if recursive=True). If lpath + ends with a "/", it will be assumed to be a directory, and target files + will go within. Can submit a list of paths, which may be glob-patterns + and will be expanded. + + The get_file method will be called concurrently on a batch of files. The + batch_size option can configure the amount of futures that can be executed + at the same time. If it is -1, then all the files will be uploaded concurrently. + The default can be set for this instance by passing "batch_size" in the + constructor, or for all instances by setting the "gather_batch_size" key + in ``fsspec.config.conf``, falling back to 1/8th of the system limit . + """ + if isinstance(lpath, list) and isinstance(rpath, list): + # No need to expand paths when both source and destination + # are provided as lists + rpaths = rpath + lpaths = lpath + else: + source_is_str = isinstance(rpath, str) + # First check for rpath trailing slash as _strip_protocol removes it. + source_not_trailing_sep = source_is_str and not trailing_sep(rpath) + rpath = self._strip_protocol(rpath) + rpaths = await self._expand_path( + rpath, recursive=recursive, maxdepth=maxdepth + ) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + rpaths = [ + p for p in rpaths if not (trailing_sep(p) or await self._isdir(p)) + ] + if not rpaths: + return + + lpath = make_path_posix(lpath) + source_is_file = len(rpaths) == 1 + dest_is_dir = isinstance(lpath, str) and ( + trailing_sep(lpath) or LocalFileSystem().isdir(lpath) + ) + + exists = source_is_str and ( + (has_magic(rpath) and source_is_file) + or (not has_magic(rpath) and dest_is_dir and source_not_trailing_sep) + ) + lpaths = other_paths( + rpaths, + lpath, + exists=exists, + flatten=not source_is_str, + ) + + [os.makedirs(os.path.dirname(lp), exist_ok=True) for lp in lpaths] + batch_size = kwargs.pop("batch_size", self.batch_size) + + coros = [] + callback.set_size(len(lpaths)) + for lpath, rpath in zip(lpaths, rpaths): + get_file = callback.branch_coro(self._get_file) + coros.append(get_file(rpath, lpath, **kwargs)) + return await _run_coros_in_chunks( + coros, batch_size=batch_size, callback=callback + ) + + async def _isfile(self, path): + try: + return (await self._info(path))["type"] == "file" + except: # noqa: E722 + return False + + async def _isdir(self, path): + try: + return (await self._info(path))["type"] == "directory" + except OSError: + return False + + async def _size(self, path): + return (await self._info(path)).get("size", None) + + async def _sizes(self, paths, batch_size=None): + batch_size = batch_size or self.batch_size + return await _run_coros_in_chunks( + [self._size(p) for p in paths], batch_size=batch_size + ) + + async def _exists(self, path, **kwargs): + try: + await self._info(path, **kwargs) + return True + except FileNotFoundError: + return False + + async def _info(self, path, **kwargs): + raise NotImplementedError + + async def _ls(self, path, detail=True, **kwargs): + raise NotImplementedError + + async def _walk(self, path, maxdepth=None, on_error="omit", **kwargs): + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + path = self._strip_protocol(path) + full_dirs = {} + dirs = {} + files = {} + + detail = kwargs.pop("detail", False) + try: + listing = await self._ls(path, detail=True, **kwargs) + except (FileNotFoundError, OSError) as e: + if on_error == "raise": + raise + elif callable(on_error): + on_error(e) + if detail: + yield path, {}, {} + else: + yield path, [], [] + return + + for info in listing: + # each info name must be at least [path]/part , but here + # we check also for names like [path]/part/ + pathname = info["name"].rstrip("/") + name = pathname.rsplit("/", 1)[-1] + if info["type"] == "directory" and pathname != path: + # do not include "self" path + full_dirs[name] = pathname + dirs[name] = info + elif pathname == path: + # file-like with same name as give path + files[""] = info + else: + files[name] = info + + if detail: + yield path, dirs, files + else: + yield path, list(dirs), list(files) + + if maxdepth is not None: + maxdepth -= 1 + if maxdepth < 1: + return + + for d in dirs: + async for _ in self._walk( + full_dirs[d], maxdepth=maxdepth, detail=detail, **kwargs + ): + yield _ + + async def _glob(self, path, maxdepth=None, **kwargs): + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + import re + + seps = (os.path.sep, os.path.altsep) if os.path.altsep else (os.path.sep,) + ends_with_sep = path.endswith(seps) # _strip_protocol strips trailing slash + path = self._strip_protocol(path) + append_slash_to_dirname = ends_with_sep or path.endswith( + tuple(sep + "**" for sep in seps) + ) + idx_star = path.find("*") if path.find("*") >= 0 else len(path) + idx_qmark = path.find("?") if path.find("?") >= 0 else len(path) + idx_brace = path.find("[") if path.find("[") >= 0 else len(path) + + min_idx = min(idx_star, idx_qmark, idx_brace) + + detail = kwargs.pop("detail", False) + + if not has_magic(path): + if await self._exists(path, **kwargs): + if not detail: + return [path] + else: + return {path: await self._info(path, **kwargs)} + else: + if not detail: + return [] # glob of non-existent returns empty + else: + return {} + elif "/" in path[:min_idx]: + min_idx = path[:min_idx].rindex("/") + root = path[: min_idx + 1] + depth = path[min_idx + 1 :].count("/") + 1 + else: + root = "" + depth = path[min_idx + 1 :].count("/") + 1 + + if "**" in path: + if maxdepth is not None: + idx_double_stars = path.find("**") + depth_double_stars = path[idx_double_stars:].count("/") + 1 + depth = depth - depth_double_stars + maxdepth + else: + depth = None + + allpaths = await self._find( + root, maxdepth=depth, withdirs=True, detail=True, **kwargs + ) + + pattern = glob_translate(path + ("/" if ends_with_sep else "")) + pattern = re.compile(pattern) + + out = { + p: info + for p, info in sorted(allpaths.items()) + if pattern.match( + p + "/" + if append_slash_to_dirname and info["type"] == "directory" + else p + ) + } + + if detail: + return out + else: + return list(out) + + async def _du(self, path, total=True, maxdepth=None, **kwargs): + sizes = {} + # async for? + for f in await self._find(path, maxdepth=maxdepth, **kwargs): + info = await self._info(f) + sizes[info["name"]] = info["size"] + if total: + return sum(sizes.values()) + else: + return sizes + + async def _find(self, path, maxdepth=None, withdirs=False, **kwargs): + path = self._strip_protocol(path) + out = {} + detail = kwargs.pop("detail", False) + + # Add the root directory if withdirs is requested + # This is needed for posix glob compliance + if withdirs and path != "" and await self._isdir(path): + out[path] = await self._info(path) + + # async for? + async for _, dirs, files in self._walk(path, maxdepth, detail=True, **kwargs): + if withdirs: + files.update(dirs) + out.update({info["name"]: info for name, info in files.items()}) + if not out and (await self._isfile(path)): + # walk works on directories, but find should also return [path] + # when path happens to be a file + out[path] = {} + names = sorted(out) + if not detail: + return names + else: + return {name: out[name] for name in names} + + async def _expand_path(self, path, recursive=False, maxdepth=None): + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + if isinstance(path, str): + out = await self._expand_path([path], recursive, maxdepth) + else: + out = set() + path = [self._strip_protocol(p) for p in path] + for p in path: # can gather here + if has_magic(p): + bit = set(await self._glob(p, maxdepth=maxdepth)) + out |= bit + if recursive: + # glob call above expanded one depth so if maxdepth is defined + # then decrement it in expand_path call below. If it is zero + # after decrementing then avoid expand_path call. + if maxdepth is not None and maxdepth <= 1: + continue + out |= set( + await self._expand_path( + list(bit), + recursive=recursive, + maxdepth=maxdepth - 1 if maxdepth is not None else None, + ) + ) + continue + elif recursive: + rec = set(await self._find(p, maxdepth=maxdepth, withdirs=True)) + out |= rec + if p not in out and (recursive is False or (await self._exists(p))): + # should only check once, for the root + out.add(p) + if not out: + raise FileNotFoundError(path) + return sorted(out) + + async def _mkdir(self, path, create_parents=True, **kwargs): + pass # not necessary to implement, may not have directories + + async def _makedirs(self, path, exist_ok=False): + pass # not necessary to implement, may not have directories + + async def open_async(self, path, mode="rb", **kwargs): + if "b" not in mode or kwargs.get("compression"): + raise ValueError + raise NotImplementedError + + +def mirror_sync_methods(obj): + """Populate sync and async methods for obj + + For each method will create a sync version if the name refers to an async method + (coroutine) and there is no override in the child class; will create an async + method for the corresponding sync method if there is no implementation. + + Uses the methods specified in + - async_methods: the set that an implementation is expected to provide + - default_async_methods: that can be derived from their sync version in + AbstractFileSystem + - AsyncFileSystem: async-specific default coroutines + """ + from fsspec import AbstractFileSystem + + for method in async_methods + dir(AsyncFileSystem): + if not method.startswith("_"): + continue + smethod = method[1:] + if private.match(method): + isco = inspect.iscoroutinefunction(getattr(obj, method, None)) + unsync = getattr(getattr(obj, smethod, False), "__func__", None) + is_default = unsync is getattr(AbstractFileSystem, smethod, "") + if isco and is_default: + mth = sync_wrapper(getattr(obj, method), obj=obj) + setattr(obj, smethod, mth) + if not mth.__doc__: + mth.__doc__ = getattr( + getattr(AbstractFileSystem, smethod, None), "__doc__", "" + ) + + +class FSSpecCoroutineCancel(Exception): + pass + + +def _dump_running_tasks( + printout=True, cancel=True, exc=FSSpecCoroutineCancel, with_task=False +): + import traceback + + tasks = [t for t in asyncio.tasks.all_tasks(loop[0]) if not t.done()] + if printout: + [task.print_stack() for task in tasks] + out = [ + { + "locals": task._coro.cr_frame.f_locals, + "file": task._coro.cr_frame.f_code.co_filename, + "firstline": task._coro.cr_frame.f_code.co_firstlineno, + "linelo": task._coro.cr_frame.f_lineno, + "stack": traceback.format_stack(task._coro.cr_frame), + "task": task if with_task else None, + } + for task in tasks + ] + if cancel: + for t in tasks: + cbs = t._callbacks + t.cancel() + asyncio.futures.Future.set_exception(t, exc) + asyncio.futures.Future.cancel(t) + [cb[0](t) for cb in cbs] # cancels any dependent concurrent.futures + try: + t._coro.throw(exc) # exits coro, unless explicitly handled + except exc: + pass + return out + + +class AbstractAsyncStreamedFile(AbstractBufferedFile): + # no read buffering, and always auto-commit + # TODO: readahead might still be useful here, but needs async version + + async def read(self, length=-1): + """ + Return data from cache, or fetch pieces as necessary + + Parameters + ---------- + length: int (-1) + Number of bytes to read; if <0, all remaining bytes. + """ + length = -1 if length is None else int(length) + if self.mode != "rb": + raise ValueError("File not in read mode") + if length < 0: + length = self.size - self.loc + if self.closed: + raise ValueError("I/O operation on closed file.") + if length == 0: + # don't even bother calling fetch + return b"" + out = await self._fetch_range(self.loc, self.loc + length) + self.loc += len(out) + return out + + async def write(self, data): + """ + Write data to buffer. + + Buffer only sent on flush() or if buffer is greater than + or equal to blocksize. + + Parameters + ---------- + data: bytes + Set of bytes to be written. + """ + if self.mode not in {"wb", "ab"}: + raise ValueError("File not in write mode") + if self.closed: + raise ValueError("I/O operation on closed file.") + if self.forced: + raise ValueError("This file has been force-flushed, can only close") + out = self.buffer.write(data) + self.loc += out + if self.buffer.tell() >= self.blocksize: + await self.flush() + return out + + async def close(self): + """Close file + + Finalizes writes, discards cache + """ + if getattr(self, "_unclosable", False): + return + if self.closed: + return + if self.mode == "rb": + self.cache = None + else: + if not self.forced: + await self.flush(force=True) + + if self.fs is not None: + self.fs.invalidate_cache(self.path) + self.fs.invalidate_cache(self.fs._parent(self.path)) + + self.closed = True + + async def flush(self, force=False): + if self.closed: + raise ValueError("Flush on closed file") + if force and self.forced: + raise ValueError("Force flush cannot be called more than once") + if force: + self.forced = True + + if self.mode not in {"wb", "ab"}: + # no-op to flush on read-mode + return + + if not force and self.buffer.tell() < self.blocksize: + # Defer write on small block + return + + if self.offset is None: + # Initialize a multipart upload + self.offset = 0 + try: + await self._initiate_upload() + except: + self.closed = True + raise + + if await self._upload_chunk(final=force) is not False: + self.offset += self.buffer.seek(0, 2) + self.buffer = io.BytesIO() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + + async def _fetch_range(self, start, end): + raise NotImplementedError + + async def _initiate_upload(self): + pass + + async def _upload_chunk(self, final=False): + raise NotImplementedError diff --git a/lib/python3.10/site-packages/fsspec/caching.py b/lib/python3.10/site-packages/fsspec/caching.py new file mode 100644 index 0000000000000000000000000000000000000000..de6a4e3407a62a1c2123bf2b4a81afb96c54150a --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/caching.py @@ -0,0 +1,1004 @@ +from __future__ import annotations + +import collections +import functools +import logging +import math +import os +import threading +import warnings +from collections import OrderedDict +from concurrent.futures import Future, ThreadPoolExecutor +from itertools import groupby +from operator import itemgetter +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Generic, + NamedTuple, + TypeVar, +) + +if TYPE_CHECKING: + import mmap + + from typing_extensions import ParamSpec + + P = ParamSpec("P") +else: + P = TypeVar("P") + +T = TypeVar("T") + + +logger = logging.getLogger("fsspec") + +Fetcher = Callable[[int, int], bytes] # Maps (start, end) to bytes +MultiFetcher = Callable[[list[int, int]], bytes] # Maps [(start, end)] to bytes + + +class BaseCache: + """Pass-though cache: doesn't keep anything, calls every time + + Acts as base class for other cachers + + Parameters + ---------- + blocksize: int + How far to read ahead in numbers of bytes + fetcher: func + Function of the form f(start, end) which gets bytes from remote as + specified + size: int + How big this file is + """ + + name: ClassVar[str] = "none" + + def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None: + self.blocksize = blocksize + self.nblocks = 0 + self.fetcher = fetcher + self.size = size + self.hit_count = 0 + self.miss_count = 0 + # the bytes that we actually requested + self.total_requested_bytes = 0 + + def _fetch(self, start: int | None, stop: int | None) -> bytes: + if start is None: + start = 0 + if stop is None: + stop = self.size + if start >= self.size or start >= stop: + return b"" + return self.fetcher(start, stop) + + def _reset_stats(self) -> None: + """Reset hit and miss counts for a more ganular report e.g. by file.""" + self.hit_count = 0 + self.miss_count = 0 + self.total_requested_bytes = 0 + + def _log_stats(self) -> str: + """Return a formatted string of the cache statistics.""" + if self.hit_count == 0 and self.miss_count == 0: + # a cache that does nothing, this is for logs only + return "" + return f" , {self.name}: {self.hit_count} hits, {self.miss_count} misses, {self.total_requested_bytes} total requested bytes" + + def __repr__(self) -> str: + # TODO: use rich for better formatting + return f""" + <{self.__class__.__name__}: + block size : {self.blocksize} + block count : {self.nblocks} + file size : {self.size} + cache hits : {self.hit_count} + cache misses: {self.miss_count} + total requested bytes: {self.total_requested_bytes}> + """ + + +class MMapCache(BaseCache): + """memory-mapped sparse file cache + + Opens temporary file, which is filled blocks-wise when data is requested. + Ensure there is enough disc space in the temporary location. + + This cache method might only work on posix + + Parameters + ---------- + blocksize: int + How far to read ahead in numbers of bytes + fetcher: Fetcher + Function of the form f(start, end) which gets bytes from remote as + specified + size: int + How big this file is + location: str + Where to create the temporary file. If None, a temporary file is + created using tempfile.TemporaryFile(). + blocks: set[int] + Set of block numbers that have already been fetched. If None, an empty + set is created. + multi_fetcher: MultiFetcher + Function of the form f([(start, end)]) which gets bytes from remote + as specified. This function is used to fetch multiple blocks at once. + If not specified, the fetcher function is used instead. + """ + + name = "mmap" + + def __init__( + self, + blocksize: int, + fetcher: Fetcher, + size: int, + location: str | None = None, + blocks: set[int] | None = None, + multi_fetcher: MultiFetcher | None = None, + ) -> None: + super().__init__(blocksize, fetcher, size) + self.blocks = set() if blocks is None else blocks + self.location = location + self.multi_fetcher = multi_fetcher + self.cache = self._makefile() + + def _makefile(self) -> mmap.mmap | bytearray: + import mmap + import tempfile + + if self.size == 0: + return bytearray() + + # posix version + if self.location is None or not os.path.exists(self.location): + if self.location is None: + fd = tempfile.TemporaryFile() + self.blocks = set() + else: + fd = open(self.location, "wb+") + fd.seek(self.size - 1) + fd.write(b"1") + fd.flush() + else: + fd = open(self.location, "r+b") + + return mmap.mmap(fd.fileno(), self.size) + + def _fetch(self, start: int | None, end: int | None) -> bytes: + logger.debug(f"MMap cache fetching {start}-{end}") + if start is None: + start = 0 + if end is None: + end = self.size + if start >= self.size or start >= end: + return b"" + start_block = start // self.blocksize + end_block = end // self.blocksize + block_range = range(start_block, end_block + 1) + # Determine which blocks need to be fetched. This sequence is sorted by construction. + need = (i for i in block_range if i not in self.blocks) + # Count the number of blocks already cached + self.hit_count += sum(1 for i in block_range if i in self.blocks) + + ranges = [] + + # Consolidate needed blocks. + # Algorithm adapted from Python 2.x itertools documentation. + # We are grouping an enumerated sequence of blocks. By comparing when the difference + # between an ascending range (provided by enumerate) and the needed block numbers + # we can detect when the block number skips values. The key computes this difference. + # Whenever the difference changes, we know that we have previously cached block(s), + # and a new group is started. In other words, this algorithm neatly groups + # runs of consecutive block numbers so they can be fetched together. + for _, _blocks in groupby(enumerate(need), key=lambda x: x[0] - x[1]): + # Extract the blocks from the enumerated sequence + _blocks = tuple(map(itemgetter(1), _blocks)) + # Compute start of first block + sstart = _blocks[0] * self.blocksize + # Compute the end of the last block. Last block may not be full size. + send = min(_blocks[-1] * self.blocksize + self.blocksize, self.size) + + # Fetch bytes (could be multiple consecutive blocks) + self.total_requested_bytes += send - sstart + logger.debug( + f"MMap get blocks {_blocks[0]}-{_blocks[-1]} ({sstart}-{send})" + ) + ranges.append((sstart, send)) + + # Update set of cached blocks + self.blocks.update(_blocks) + # Update cache statistics with number of blocks we had to cache + self.miss_count += len(_blocks) + + if not ranges: + return self.cache[start:end] + + if self.multi_fetcher: + logger.debug(f"MMap get blocks {ranges}") + for idx, r in enumerate(self.multi_fetcher(ranges)): + (sstart, send) = ranges[idx] + logger.debug(f"MMap copy block ({sstart}-{send}") + self.cache[sstart:send] = r + else: + for sstart, send in ranges: + logger.debug(f"MMap get block ({sstart}-{send}") + self.cache[sstart:send] = self.fetcher(sstart, send) + + return self.cache[start:end] + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__.copy() + # Remove the unpicklable entries. + del state["cache"] + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + # Restore instance attributes + self.__dict__.update(state) + self.cache = self._makefile() + + +class ReadAheadCache(BaseCache): + """Cache which reads only when we get beyond a block of data + + This is a much simpler version of BytesCache, and does not attempt to + fill holes in the cache or keep fragments alive. It is best suited to + many small reads in a sequential order (e.g., reading lines from a file). + """ + + name = "readahead" + + def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None: + super().__init__(blocksize, fetcher, size) + self.cache = b"" + self.start = 0 + self.end = 0 + + def _fetch(self, start: int | None, end: int | None) -> bytes: + if start is None: + start = 0 + if end is None or end > self.size: + end = self.size + if start >= self.size or start >= end: + return b"" + l = end - start + if start >= self.start and end <= self.end: + # cache hit + self.hit_count += 1 + return self.cache[start - self.start : end - self.start] + elif self.start <= start < self.end: + # partial hit + self.miss_count += 1 + part = self.cache[start - self.start :] + l -= len(part) + start = self.end + else: + # miss + self.miss_count += 1 + part = b"" + end = min(self.size, end + self.blocksize) + self.total_requested_bytes += end - start + self.cache = self.fetcher(start, end) # new block replaces old + self.start = start + self.end = self.start + len(self.cache) + return part + self.cache[:l] + + +class FirstChunkCache(BaseCache): + """Caches the first block of a file only + + This may be useful for file types where the metadata is stored in the header, + but is randomly accessed. + """ + + name = "first" + + def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None: + if blocksize > size: + # this will buffer the whole thing + blocksize = size + super().__init__(blocksize, fetcher, size) + self.cache: bytes | None = None + + def _fetch(self, start: int | None, end: int | None) -> bytes: + start = start or 0 + if start > self.size: + logger.debug("FirstChunkCache: requested start > file size") + return b"" + + end = min(end, self.size) + + if start < self.blocksize: + if self.cache is None: + self.miss_count += 1 + if end > self.blocksize: + self.total_requested_bytes += end + data = self.fetcher(0, end) + self.cache = data[: self.blocksize] + return data[start:] + self.cache = self.fetcher(0, self.blocksize) + self.total_requested_bytes += self.blocksize + part = self.cache[start:end] + if end > self.blocksize: + self.total_requested_bytes += end - self.blocksize + part += self.fetcher(self.blocksize, end) + self.hit_count += 1 + return part + else: + self.miss_count += 1 + self.total_requested_bytes += end - start + return self.fetcher(start, end) + + +class BlockCache(BaseCache): + """ + Cache holding memory as a set of blocks. + + Requests are only ever made ``blocksize`` at a time, and are + stored in an LRU cache. The least recently accessed block is + discarded when more than ``maxblocks`` are stored. + + Parameters + ---------- + blocksize : int + The number of bytes to store in each block. + Requests are only ever made for ``blocksize``, so this + should balance the overhead of making a request against + the granularity of the blocks. + fetcher : Callable + size : int + The total size of the file being cached. + maxblocks : int + The maximum number of blocks to cache for. The maximum memory + use for this cache is then ``blocksize * maxblocks``. + """ + + name = "blockcache" + + def __init__( + self, blocksize: int, fetcher: Fetcher, size: int, maxblocks: int = 32 + ) -> None: + super().__init__(blocksize, fetcher, size) + self.nblocks = math.ceil(size / blocksize) + self.maxblocks = maxblocks + self._fetch_block_cached = functools.lru_cache(maxblocks)(self._fetch_block) + + def cache_info(self): + """ + The statistics on the block cache. + + Returns + ------- + NamedTuple + Returned directly from the LRU Cache used internally. + """ + return self._fetch_block_cached.cache_info() + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__ + del state["_fetch_block_cached"] + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + self.__dict__.update(state) + self._fetch_block_cached = functools.lru_cache(state["maxblocks"])( + self._fetch_block + ) + + def _fetch(self, start: int | None, end: int | None) -> bytes: + if start is None: + start = 0 + if end is None: + end = self.size + if start >= self.size or start >= end: + return b"" + + # byte position -> block numbers + start_block_number = start // self.blocksize + end_block_number = end // self.blocksize + + # these are cached, so safe to do multiple calls for the same start and end. + for block_number in range(start_block_number, end_block_number + 1): + self._fetch_block_cached(block_number) + + return self._read_cache( + start, + end, + start_block_number=start_block_number, + end_block_number=end_block_number, + ) + + def _fetch_block(self, block_number: int) -> bytes: + """ + Fetch the block of data for `block_number`. + """ + if block_number > self.nblocks: + raise ValueError( + f"'block_number={block_number}' is greater than " + f"the number of blocks ({self.nblocks})" + ) + + start = block_number * self.blocksize + end = start + self.blocksize + self.total_requested_bytes += end - start + self.miss_count += 1 + logger.info("BlockCache fetching block %d", block_number) + block_contents = super()._fetch(start, end) + return block_contents + + def _read_cache( + self, start: int, end: int, start_block_number: int, end_block_number: int + ) -> bytes: + """ + Read from our block cache. + + Parameters + ---------- + start, end : int + The start and end byte positions. + start_block_number, end_block_number : int + The start and end block numbers. + """ + start_pos = start % self.blocksize + end_pos = end % self.blocksize + + self.hit_count += 1 + if start_block_number == end_block_number: + block: bytes = self._fetch_block_cached(start_block_number) + return block[start_pos:end_pos] + + else: + # read from the initial + out = [self._fetch_block_cached(start_block_number)[start_pos:]] + + # intermediate blocks + # Note: it'd be nice to combine these into one big request. However + # that doesn't play nicely with our LRU cache. + out.extend( + map( + self._fetch_block_cached, + range(start_block_number + 1, end_block_number), + ) + ) + + # final block + out.append(self._fetch_block_cached(end_block_number)[:end_pos]) + + return b"".join(out) + + +class BytesCache(BaseCache): + """Cache which holds data in a in-memory bytes object + + Implements read-ahead by the block size, for semi-random reads progressing + through the file. + + Parameters + ---------- + trim: bool + As we read more data, whether to discard the start of the buffer when + we are more than a blocksize ahead of it. + """ + + name: ClassVar[str] = "bytes" + + def __init__( + self, blocksize: int, fetcher: Fetcher, size: int, trim: bool = True + ) -> None: + super().__init__(blocksize, fetcher, size) + self.cache = b"" + self.start: int | None = None + self.end: int | None = None + self.trim = trim + + def _fetch(self, start: int | None, end: int | None) -> bytes: + # TODO: only set start/end after fetch, in case it fails? + # is this where retry logic might go? + if start is None: + start = 0 + if end is None: + end = self.size + if start >= self.size or start >= end: + return b"" + if ( + self.start is not None + and start >= self.start + and self.end is not None + and end < self.end + ): + # cache hit: we have all the required data + offset = start - self.start + self.hit_count += 1 + return self.cache[offset : offset + end - start] + + if self.blocksize: + bend = min(self.size, end + self.blocksize) + else: + bend = end + + if bend == start or start > self.size: + return b"" + + if (self.start is None or start < self.start) and ( + self.end is None or end > self.end + ): + # First read, or extending both before and after + self.total_requested_bytes += bend - start + self.miss_count += 1 + self.cache = self.fetcher(start, bend) + self.start = start + else: + assert self.start is not None + assert self.end is not None + self.miss_count += 1 + + if start < self.start: + if self.end is None or self.end - end > self.blocksize: + self.total_requested_bytes += bend - start + self.cache = self.fetcher(start, bend) + self.start = start + else: + self.total_requested_bytes += self.start - start + new = self.fetcher(start, self.start) + self.start = start + self.cache = new + self.cache + elif self.end is not None and bend > self.end: + if self.end > self.size: + pass + elif end - self.end > self.blocksize: + self.total_requested_bytes += bend - start + self.cache = self.fetcher(start, bend) + self.start = start + else: + self.total_requested_bytes += bend - self.end + new = self.fetcher(self.end, bend) + self.cache = self.cache + new + + self.end = self.start + len(self.cache) + offset = start - self.start + out = self.cache[offset : offset + end - start] + if self.trim: + num = (self.end - self.start) // (self.blocksize + 1) + if num > 1: + self.start += self.blocksize * num + self.cache = self.cache[self.blocksize * num :] + return out + + def __len__(self) -> int: + return len(self.cache) + + +class AllBytes(BaseCache): + """Cache entire contents of the file""" + + name: ClassVar[str] = "all" + + def __init__( + self, + blocksize: int | None = None, + fetcher: Fetcher | None = None, + size: int | None = None, + data: bytes | None = None, + ) -> None: + super().__init__(blocksize, fetcher, size) # type: ignore[arg-type] + if data is None: + self.miss_count += 1 + self.total_requested_bytes += self.size + data = self.fetcher(0, self.size) + self.data = data + + def _fetch(self, start: int | None, stop: int | None) -> bytes: + self.hit_count += 1 + return self.data[start:stop] + + +class KnownPartsOfAFile(BaseCache): + """ + Cache holding known file parts. + + Parameters + ---------- + blocksize: int + How far to read ahead in numbers of bytes + fetcher: func + Function of the form f(start, end) which gets bytes from remote as + specified + size: int + How big this file is + data: dict + A dictionary mapping explicit `(start, stop)` file-offset tuples + with known bytes. + strict: bool, default True + Whether to fetch reads that go beyond a known byte-range boundary. + If `False`, any read that ends outside a known part will be zero + padded. Note that zero padding will not be used for reads that + begin outside a known byte-range. + """ + + name: ClassVar[str] = "parts" + + def __init__( + self, + blocksize: int, + fetcher: Fetcher, + size: int, + data: dict[tuple[int, int], bytes] | None = None, + strict: bool = True, + **_: Any, + ): + super().__init__(blocksize, fetcher, size) + self.strict = strict + + # simple consolidation of contiguous blocks + if data: + old_offsets = sorted(data.keys()) + offsets = [old_offsets[0]] + blocks = [data.pop(old_offsets[0])] + for start, stop in old_offsets[1:]: + start0, stop0 = offsets[-1] + if start == stop0: + offsets[-1] = (start0, stop) + blocks[-1] += data.pop((start, stop)) + else: + offsets.append((start, stop)) + blocks.append(data.pop((start, stop))) + + self.data = dict(zip(offsets, blocks)) + else: + self.data = {} + + def _fetch(self, start: int | None, stop: int | None) -> bytes: + if start is None: + start = 0 + if stop is None: + stop = self.size + + out = b"" + for (loc0, loc1), data in self.data.items(): + # If self.strict=False, use zero-padded data + # for reads beyond the end of a "known" buffer + if loc0 <= start < loc1: + off = start - loc0 + out = data[off : off + stop - start] + if not self.strict or loc0 <= stop <= loc1: + # The request is within a known range, or + # it begins within a known range, and we + # are allowed to pad reads beyond the + # buffer with zero + out += b"\x00" * (stop - start - len(out)) + self.hit_count += 1 + return out + else: + # The request ends outside a known range, + # and we are being "strict" about reads + # beyond the buffer + start = loc1 + break + + # We only get here if there is a request outside the + # known parts of the file. In an ideal world, this + # should never happen + if self.fetcher is None: + # We cannot fetch the data, so raise an error + raise ValueError(f"Read is outside the known file parts: {(start, stop)}. ") + # We can fetch the data, but should warn the user + # that this may be slow + warnings.warn( + f"Read is outside the known file parts: {(start, stop)}. " + f"IO/caching performance may be poor!" + ) + logger.debug(f"KnownPartsOfAFile cache fetching {start}-{stop}") + self.total_requested_bytes += stop - start + self.miss_count += 1 + return out + super()._fetch(start, stop) + + +class UpdatableLRU(Generic[P, T]): + """ + Custom implementation of LRU cache that allows updating keys + + Used by BackgroudBlockCache + """ + + class CacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int + currsize: int + + def __init__(self, func: Callable[P, T], max_size: int = 128) -> None: + self._cache: OrderedDict[Any, T] = collections.OrderedDict() + self._func = func + self._max_size = max_size + self._hits = 0 + self._misses = 0 + self._lock = threading.Lock() + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + if kwargs: + raise TypeError(f"Got unexpected keyword argument {kwargs.keys()}") + with self._lock: + if args in self._cache: + self._cache.move_to_end(args) + self._hits += 1 + return self._cache[args] + + result = self._func(*args, **kwargs) + + with self._lock: + self._cache[args] = result + self._misses += 1 + if len(self._cache) > self._max_size: + self._cache.popitem(last=False) + + return result + + def is_key_cached(self, *args: Any) -> bool: + with self._lock: + return args in self._cache + + def add_key(self, result: T, *args: Any) -> None: + with self._lock: + self._cache[args] = result + if len(self._cache) > self._max_size: + self._cache.popitem(last=False) + + def cache_info(self) -> UpdatableLRU.CacheInfo: + with self._lock: + return self.CacheInfo( + maxsize=self._max_size, + currsize=len(self._cache), + hits=self._hits, + misses=self._misses, + ) + + +class BackgroundBlockCache(BaseCache): + """ + Cache holding memory as a set of blocks with pre-loading of + the next block in the background. + + Requests are only ever made ``blocksize`` at a time, and are + stored in an LRU cache. The least recently accessed block is + discarded when more than ``maxblocks`` are stored. If the + next block is not in cache, it is loaded in a separate thread + in non-blocking way. + + Parameters + ---------- + blocksize : int + The number of bytes to store in each block. + Requests are only ever made for ``blocksize``, so this + should balance the overhead of making a request against + the granularity of the blocks. + fetcher : Callable + size : int + The total size of the file being cached. + maxblocks : int + The maximum number of blocks to cache for. The maximum memory + use for this cache is then ``blocksize * maxblocks``. + """ + + name: ClassVar[str] = "background" + + def __init__( + self, blocksize: int, fetcher: Fetcher, size: int, maxblocks: int = 32 + ) -> None: + super().__init__(blocksize, fetcher, size) + self.nblocks = math.ceil(size / blocksize) + self.maxblocks = maxblocks + self._fetch_block_cached = UpdatableLRU(self._fetch_block, maxblocks) + + self._thread_executor = ThreadPoolExecutor(max_workers=1) + self._fetch_future_block_number: int | None = None + self._fetch_future: Future[bytes] | None = None + self._fetch_future_lock = threading.Lock() + + def cache_info(self) -> UpdatableLRU.CacheInfo: + """ + The statistics on the block cache. + + Returns + ------- + NamedTuple + Returned directly from the LRU Cache used internally. + """ + return self._fetch_block_cached.cache_info() + + def __getstate__(self) -> dict[str, Any]: + state = self.__dict__ + del state["_fetch_block_cached"] + del state["_thread_executor"] + del state["_fetch_future_block_number"] + del state["_fetch_future"] + del state["_fetch_future_lock"] + return state + + def __setstate__(self, state) -> None: + self.__dict__.update(state) + self._fetch_block_cached = UpdatableLRU(self._fetch_block, state["maxblocks"]) + self._thread_executor = ThreadPoolExecutor(max_workers=1) + self._fetch_future_block_number = None + self._fetch_future = None + self._fetch_future_lock = threading.Lock() + + def _fetch(self, start: int | None, end: int | None) -> bytes: + if start is None: + start = 0 + if end is None: + end = self.size + if start >= self.size or start >= end: + return b"" + + # byte position -> block numbers + start_block_number = start // self.blocksize + end_block_number = end // self.blocksize + + fetch_future_block_number = None + fetch_future = None + with self._fetch_future_lock: + # Background thread is running. Check we we can or must join it. + if self._fetch_future is not None: + assert self._fetch_future_block_number is not None + if self._fetch_future.done(): + logger.info("BlockCache joined background fetch without waiting.") + self._fetch_block_cached.add_key( + self._fetch_future.result(), self._fetch_future_block_number + ) + # Cleanup the fetch variables. Done with fetching the block. + self._fetch_future_block_number = None + self._fetch_future = None + else: + # Must join if we need the block for the current fetch + must_join = bool( + start_block_number + <= self._fetch_future_block_number + <= end_block_number + ) + if must_join: + # Copy to the local variables to release lock + # before waiting for result + fetch_future_block_number = self._fetch_future_block_number + fetch_future = self._fetch_future + + # Cleanup the fetch variables. Have a local copy. + self._fetch_future_block_number = None + self._fetch_future = None + + # Need to wait for the future for the current read + if fetch_future is not None: + logger.info("BlockCache waiting for background fetch.") + # Wait until result and put it in cache + self._fetch_block_cached.add_key( + fetch_future.result(), fetch_future_block_number + ) + + # these are cached, so safe to do multiple calls for the same start and end. + for block_number in range(start_block_number, end_block_number + 1): + self._fetch_block_cached(block_number) + + # fetch next block in the background if nothing is running in the background, + # the block is within file and it is not already cached + end_block_plus_1 = end_block_number + 1 + with self._fetch_future_lock: + if ( + self._fetch_future is None + and end_block_plus_1 <= self.nblocks + and not self._fetch_block_cached.is_key_cached(end_block_plus_1) + ): + self._fetch_future_block_number = end_block_plus_1 + self._fetch_future = self._thread_executor.submit( + self._fetch_block, end_block_plus_1, "async" + ) + + return self._read_cache( + start, + end, + start_block_number=start_block_number, + end_block_number=end_block_number, + ) + + def _fetch_block(self, block_number: int, log_info: str = "sync") -> bytes: + """ + Fetch the block of data for `block_number`. + """ + if block_number > self.nblocks: + raise ValueError( + f"'block_number={block_number}' is greater than " + f"the number of blocks ({self.nblocks})" + ) + + start = block_number * self.blocksize + end = start + self.blocksize + logger.info("BlockCache fetching block (%s) %d", log_info, block_number) + self.total_requested_bytes += end - start + self.miss_count += 1 + block_contents = super()._fetch(start, end) + return block_contents + + def _read_cache( + self, start: int, end: int, start_block_number: int, end_block_number: int + ) -> bytes: + """ + Read from our block cache. + + Parameters + ---------- + start, end : int + The start and end byte positions. + start_block_number, end_block_number : int + The start and end block numbers. + """ + start_pos = start % self.blocksize + end_pos = end % self.blocksize + + # kind of pointless to count this as a hit, but it is + self.hit_count += 1 + + if start_block_number == end_block_number: + block = self._fetch_block_cached(start_block_number) + return block[start_pos:end_pos] + + else: + # read from the initial + out = [self._fetch_block_cached(start_block_number)[start_pos:]] + + # intermediate blocks + # Note: it'd be nice to combine these into one big request. However + # that doesn't play nicely with our LRU cache. + out.extend( + map( + self._fetch_block_cached, + range(start_block_number + 1, end_block_number), + ) + ) + + # final block + out.append(self._fetch_block_cached(end_block_number)[:end_pos]) + + return b"".join(out) + + +caches: dict[str | None, type[BaseCache]] = { + # one custom case + None: BaseCache, +} + + +def register_cache(cls: type[BaseCache], clobber: bool = False) -> None: + """'Register' cache implementation. + + Parameters + ---------- + clobber: bool, optional + If set to True (default is False) - allow to overwrite existing + entry. + + Raises + ------ + ValueError + """ + name = cls.name + if not clobber and name in caches: + raise ValueError(f"Cache with name {name!r} is already known: {caches[name]}") + caches[name] = cls + + +for c in ( + BaseCache, + MMapCache, + BytesCache, + ReadAheadCache, + BlockCache, + FirstChunkCache, + AllBytes, + KnownPartsOfAFile, + BackgroundBlockCache, +): + register_cache(c) diff --git a/lib/python3.10/site-packages/fsspec/callbacks.py b/lib/python3.10/site-packages/fsspec/callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..7ca99ca6ac3cd69b28bcd1550f6550e8e648c5fe --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/callbacks.py @@ -0,0 +1,324 @@ +from functools import wraps + + +class Callback: + """ + Base class and interface for callback mechanism + + This class can be used directly for monitoring file transfers by + providing ``callback=Callback(hooks=...)`` (see the ``hooks`` argument, + below), or subclassed for more specialised behaviour. + + Parameters + ---------- + size: int (optional) + Nominal quantity for the value that corresponds to a complete + transfer, e.g., total number of tiles or total number of + bytes + value: int (0) + Starting internal counter value + hooks: dict or None + A dict of named functions to be called on each update. The signature + of these must be ``f(size, value, **kwargs)`` + """ + + def __init__(self, size=None, value=0, hooks=None, **kwargs): + self.size = size + self.value = value + self.hooks = hooks or {} + self.kw = kwargs + + def __enter__(self): + return self + + def __exit__(self, *exc_args): + self.close() + + def close(self): + """Close callback.""" + + def branched(self, path_1, path_2, **kwargs): + """ + Return callback for child transfers + + If this callback is operating at a higher level, e.g., put, which may + trigger transfers that can also be monitored. The function returns a callback + that has to be passed to the child method, e.g., put_file, + as `callback=` argument. + + The implementation uses `callback.branch` for compatibility. + When implementing callbacks, it is recommended to override this function instead + of `branch` and avoid calling `super().branched(...)`. + + Prefer using this function over `branch`. + + Parameters + ---------- + path_1: str + Child's source path + path_2: str + Child's destination path + **kwargs: + Arbitrary keyword arguments + + Returns + ------- + callback: Callback + A callback instance to be passed to the child method + """ + self.branch(path_1, path_2, kwargs) + # mutate kwargs so that we can force the caller to pass "callback=" explicitly + return kwargs.pop("callback", DEFAULT_CALLBACK) + + def branch_coro(self, fn): + """ + Wraps a coroutine, and pass a new child callback to it. + """ + + @wraps(fn) + async def func(path1, path2: str, **kwargs): + with self.branched(path1, path2, **kwargs) as child: + return await fn(path1, path2, callback=child, **kwargs) + + return func + + def set_size(self, size): + """ + Set the internal maximum size attribute + + Usually called if not initially set at instantiation. Note that this + triggers a ``call()``. + + Parameters + ---------- + size: int + """ + self.size = size + self.call() + + def absolute_update(self, value): + """ + Set the internal value state + + Triggers ``call()`` + + Parameters + ---------- + value: int + """ + self.value = value + self.call() + + def relative_update(self, inc=1): + """ + Delta increment the internal counter + + Triggers ``call()`` + + Parameters + ---------- + inc: int + """ + self.value += inc + self.call() + + def call(self, hook_name=None, **kwargs): + """ + Execute hook(s) with current state + + Each function is passed the internal size and current value + + Parameters + ---------- + hook_name: str or None + If given, execute on this hook + kwargs: passed on to (all) hook(s) + """ + if not self.hooks: + return + kw = self.kw.copy() + kw.update(kwargs) + if hook_name: + if hook_name not in self.hooks: + return + return self.hooks[hook_name](self.size, self.value, **kw) + for hook in self.hooks.values() or []: + hook(self.size, self.value, **kw) + + def wrap(self, iterable): + """ + Wrap an iterable to call ``relative_update`` on each iterations + + Parameters + ---------- + iterable: Iterable + The iterable that is being wrapped + """ + for item in iterable: + self.relative_update() + yield item + + def branch(self, path_1, path_2, kwargs): + """ + Set callbacks for child transfers + + If this callback is operating at a higher level, e.g., put, which may + trigger transfers that can also be monitored. The passed kwargs are + to be *mutated* to add ``callback=``, if this class supports branching + to children. + + Parameters + ---------- + path_1: str + Child's source path + path_2: str + Child's destination path + kwargs: dict + arguments passed to child method, e.g., put_file. + + Returns + ------- + + """ + return None + + def no_op(self, *_, **__): + pass + + def __getattr__(self, item): + """ + If undefined methods are called on this class, nothing happens + """ + return self.no_op + + @classmethod + def as_callback(cls, maybe_callback=None): + """Transform callback=... into Callback instance + + For the special value of ``None``, return the global instance of + ``NoOpCallback``. This is an alternative to including + ``callback=DEFAULT_CALLBACK`` directly in a method signature. + """ + if maybe_callback is None: + return DEFAULT_CALLBACK + return maybe_callback + + +class NoOpCallback(Callback): + """ + This implementation of Callback does exactly nothing + """ + + def call(self, *args, **kwargs): + return None + + +class DotPrinterCallback(Callback): + """ + Simple example Callback implementation + + Almost identical to Callback with a hook that prints a char; here we + demonstrate how the outer layer may print "#" and the inner layer "." + """ + + def __init__(self, chr_to_print="#", **kwargs): + self.chr = chr_to_print + super().__init__(**kwargs) + + def branch(self, path_1, path_2, kwargs): + """Mutate kwargs to add new instance with different print char""" + kwargs["callback"] = DotPrinterCallback(".") + + def call(self, **kwargs): + """Just outputs a character""" + print(self.chr, end="") + + +class TqdmCallback(Callback): + """ + A callback to display a progress bar using tqdm + + Parameters + ---------- + tqdm_kwargs : dict, (optional) + Any argument accepted by the tqdm constructor. + See the `tqdm doc `_. + Will be forwarded to `tqdm_cls`. + tqdm_cls: (optional) + subclass of `tqdm.tqdm`. If not passed, it will default to `tqdm.tqdm`. + + Examples + -------- + >>> import fsspec + >>> from fsspec.callbacks import TqdmCallback + >>> fs = fsspec.filesystem("memory") + >>> path2distant_data = "/your-path" + >>> fs.upload( + ".", + path2distant_data, + recursive=True, + callback=TqdmCallback(), + ) + + You can forward args to tqdm using the ``tqdm_kwargs`` parameter. + + >>> fs.upload( + ".", + path2distant_data, + recursive=True, + callback=TqdmCallback(tqdm_kwargs={"desc": "Your tqdm description"}), + ) + + You can also customize the progress bar by passing a subclass of `tqdm`. + + .. code-block:: python + + class TqdmFormat(tqdm): + '''Provides a `total_time` format parameter''' + @property + def format_dict(self): + d = super().format_dict + total_time = d["elapsed"] * (d["total"] or 0) / max(d["n"], 1) + d.update(total_time=self.format_interval(total_time) + " in total") + return d + + >>> with TqdmCallback( + tqdm_kwargs={ + "desc": "desc", + "bar_format": "{total_time}: {percentage:.0f}%|{bar}{r_bar}", + }, + tqdm_cls=TqdmFormat, + ) as callback: + fs.upload(".", path2distant_data, recursive=True, callback=callback) + """ + + def __init__(self, tqdm_kwargs=None, *args, **kwargs): + try: + from tqdm import tqdm + + except ImportError as exce: + raise ImportError( + "Using TqdmCallback requires tqdm to be installed" + ) from exce + + self._tqdm_cls = kwargs.pop("tqdm_cls", tqdm) + self._tqdm_kwargs = tqdm_kwargs or {} + self.tqdm = None + super().__init__(*args, **kwargs) + + def call(self, *args, **kwargs): + if self.tqdm is None: + self.tqdm = self._tqdm_cls(total=self.size, **self._tqdm_kwargs) + self.tqdm.total = self.size + self.tqdm.update(self.value - self.tqdm.n) + + def close(self): + if self.tqdm is not None: + self.tqdm.close() + self.tqdm = None + + def __del__(self): + return self.close() + + +DEFAULT_CALLBACK = _DEFAULT_CALLBACK = NoOpCallback() diff --git a/lib/python3.10/site-packages/fsspec/compression.py b/lib/python3.10/site-packages/fsspec/compression.py new file mode 100644 index 0000000000000000000000000000000000000000..e21da562bbab49c2ad60e9d9beb546af8dadea45 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/compression.py @@ -0,0 +1,182 @@ +"""Helper functions for a standard streaming compression API""" + +from zipfile import ZipFile + +import fsspec.utils +from fsspec.spec import AbstractBufferedFile + + +def noop_file(file, mode, **kwargs): + return file + + +# TODO: files should also be available as contexts +# should be functions of the form func(infile, mode=, **kwargs) -> file-like +compr = {None: noop_file} + + +def register_compression(name, callback, extensions, force=False): + """Register an "inferable" file compression type. + + Registers transparent file compression type for use with fsspec.open. + Compression can be specified by name in open, or "infer"-ed for any files + ending with the given extensions. + + Args: + name: (str) The compression type name. Eg. "gzip". + callback: A callable of form (infile, mode, **kwargs) -> file-like. + Accepts an input file-like object, the target mode and kwargs. + Returns a wrapped file-like object. + extensions: (str, Iterable[str]) A file extension, or list of file + extensions for which to infer this compression scheme. Eg. "gz". + force: (bool) Force re-registration of compression type or extensions. + + Raises: + ValueError: If name or extensions already registered, and not force. + + """ + if isinstance(extensions, str): + extensions = [extensions] + + # Validate registration + if name in compr and not force: + raise ValueError(f"Duplicate compression registration: {name}") + + for ext in extensions: + if ext in fsspec.utils.compressions and not force: + raise ValueError(f"Duplicate compression file extension: {ext} ({name})") + + compr[name] = callback + + for ext in extensions: + fsspec.utils.compressions[ext] = name + + +def unzip(infile, mode="rb", filename=None, **kwargs): + if "r" not in mode: + filename = filename or "file" + z = ZipFile(infile, mode="w", **kwargs) + fo = z.open(filename, mode="w") + fo.close = lambda closer=fo.close: closer() or z.close() + return fo + z = ZipFile(infile) + if filename is None: + filename = z.namelist()[0] + return z.open(filename, mode="r", **kwargs) + + +register_compression("zip", unzip, "zip") + +try: + from bz2 import BZ2File +except ImportError: + pass +else: + register_compression("bz2", BZ2File, "bz2") + +try: # pragma: no cover + from isal import igzip + + def isal(infile, mode="rb", **kwargs): + return igzip.IGzipFile(fileobj=infile, mode=mode, **kwargs) + + register_compression("gzip", isal, "gz") +except ImportError: + from gzip import GzipFile + + register_compression( + "gzip", lambda f, **kwargs: GzipFile(fileobj=f, **kwargs), "gz" + ) + +try: + from lzma import LZMAFile + + register_compression("lzma", LZMAFile, "lzma") + register_compression("xz", LZMAFile, "xz") +except ImportError: + pass + +try: + import lzmaffi + + register_compression("lzma", lzmaffi.LZMAFile, "lzma", force=True) + register_compression("xz", lzmaffi.LZMAFile, "xz", force=True) +except ImportError: + pass + + +class SnappyFile(AbstractBufferedFile): + def __init__(self, infile, mode, **kwargs): + import snappy + + super().__init__( + fs=None, path="snappy", mode=mode.strip("b") + "b", size=999999999, **kwargs + ) + self.infile = infile + if "r" in mode: + self.codec = snappy.StreamDecompressor() + else: + self.codec = snappy.StreamCompressor() + + def _upload_chunk(self, final=False): + self.buffer.seek(0) + out = self.codec.add_chunk(self.buffer.read()) + self.infile.write(out) + return True + + def seek(self, loc, whence=0): + raise NotImplementedError("SnappyFile is not seekable") + + def seekable(self): + return False + + def _fetch_range(self, start, end): + """Get the specified set of bytes from remote""" + data = self.infile.read(end - start) + return self.codec.decompress(data) + + +try: + import snappy + + snappy.compress(b"") + # Snappy may use the .sz file extension, but this is not part of the + # standard implementation. + register_compression("snappy", SnappyFile, []) + +except (ImportError, NameError, AttributeError): + pass + +try: + import lz4.frame + + register_compression("lz4", lz4.frame.open, "lz4") +except ImportError: + pass + +try: + # zstd in the standard library for python >= 3.14 + from compression.zstd import ZstdFile + + register_compression("zstd", ZstdFile, "zst") + +except ImportError: + try: + import zstandard as zstd + + def zstandard_file(infile, mode="rb"): + if "r" in mode: + cctx = zstd.ZstdDecompressor() + return cctx.stream_reader(infile) + else: + cctx = zstd.ZstdCompressor(level=10) + return cctx.stream_writer(infile) + + register_compression("zstd", zstandard_file, "zst") + except ImportError: + pass + + +def available_compressions(): + """Return a list of the implemented compressions.""" + return list(compr) diff --git a/lib/python3.10/site-packages/fsspec/config.py b/lib/python3.10/site-packages/fsspec/config.py new file mode 100644 index 0000000000000000000000000000000000000000..76d9af14aaf7df47c4551c169f27b05abf9c269e --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/config.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import configparser +import json +import os +import warnings +from typing import Any + +conf: dict[str, dict[str, Any]] = {} +default_conf_dir = os.path.join(os.path.expanduser("~"), ".config/fsspec") +conf_dir = os.environ.get("FSSPEC_CONFIG_DIR", default_conf_dir) + + +def set_conf_env(conf_dict, envdict=os.environ): + """Set config values from environment variables + + Looks for variables of the form ``FSSPEC_`` and + ``FSSPEC__``. For ``FSSPEC_`` the value is parsed + as a json dictionary and used to ``update`` the config of the + corresponding protocol. For ``FSSPEC__`` there is no + attempt to convert the string value, but the kwarg keys will be lower-cased. + + The ``FSSPEC__`` variables are applied after the + ``FSSPEC_`` ones. + + Parameters + ---------- + conf_dict : dict(str, dict) + This dict will be mutated + envdict : dict-like(str, str) + Source for the values - usually the real environment + """ + kwarg_keys = [] + for key in envdict: + if key.startswith("FSSPEC_") and len(key) > 7 and key[7] != "_": + if key.count("_") > 1: + kwarg_keys.append(key) + continue + try: + value = json.loads(envdict[key]) + except json.decoder.JSONDecodeError as ex: + warnings.warn( + f"Ignoring environment variable {key} due to a parse failure: {ex}" + ) + else: + if isinstance(value, dict): + _, proto = key.split("_", 1) + conf_dict.setdefault(proto.lower(), {}).update(value) + else: + warnings.warn( + f"Ignoring environment variable {key} due to not being a dict:" + f" {type(value)}" + ) + elif key.startswith("FSSPEC"): + warnings.warn( + f"Ignoring environment variable {key} due to having an unexpected name" + ) + + for key in kwarg_keys: + _, proto, kwarg = key.split("_", 2) + conf_dict.setdefault(proto.lower(), {})[kwarg.lower()] = envdict[key] + + +def set_conf_files(cdir, conf_dict): + """Set config values from files + + Scans for INI and JSON files in the given dictionary, and uses their + contents to set the config. In case of repeated values, later values + win. + + In the case of INI files, all values are strings, and these will not + be converted. + + Parameters + ---------- + cdir : str + Directory to search + conf_dict : dict(str, dict) + This dict will be mutated + """ + if not os.path.isdir(cdir): + return + allfiles = sorted(os.listdir(cdir)) + for fn in allfiles: + if fn.endswith(".ini"): + ini = configparser.ConfigParser() + ini.read(os.path.join(cdir, fn)) + for key in ini: + if key == "DEFAULT": + continue + conf_dict.setdefault(key, {}).update(dict(ini[key])) + if fn.endswith(".json"): + with open(os.path.join(cdir, fn)) as f: + js = json.load(f) + for key in js: + conf_dict.setdefault(key, {}).update(dict(js[key])) + + +def apply_config(cls, kwargs, conf_dict=None): + """Supply default values for kwargs when instantiating class + + Augments the passed kwargs, by finding entries in the config dict + which match the classes ``.protocol`` attribute (one or more str) + + Parameters + ---------- + cls : file system implementation + kwargs : dict + conf_dict : dict of dict + Typically this is the global configuration + + Returns + ------- + dict : the modified set of kwargs + """ + if conf_dict is None: + conf_dict = conf + protos = cls.protocol if isinstance(cls.protocol, (tuple, list)) else [cls.protocol] + kw = {} + for proto in protos: + # default kwargs from the current state of the config + if proto in conf_dict: + kw.update(conf_dict[proto]) + # explicit kwargs always win + kw.update(**kwargs) + kwargs = kw + return kwargs + + +set_conf_files(conf_dir, conf) +set_conf_env(conf) diff --git a/lib/python3.10/site-packages/fsspec/conftest.py b/lib/python3.10/site-packages/fsspec/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..6874a42c4895c3c7b973dc5d63fd4488a4e60b44 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/conftest.py @@ -0,0 +1,55 @@ +import os +import shutil +import subprocess +import sys +import time + +import pytest + +import fsspec +from fsspec.implementations.cached import CachingFileSystem + + +@pytest.fixture() +def m(): + """ + Fixture providing a memory filesystem. + """ + m = fsspec.filesystem("memory") + m.store.clear() + m.pseudo_dirs.clear() + m.pseudo_dirs.append("") + try: + yield m + finally: + m.store.clear() + m.pseudo_dirs.clear() + m.pseudo_dirs.append("") + + +@pytest.fixture +def ftp_writable(tmpdir): + """ + Fixture providing a writable FTP filesystem. + """ + pytest.importorskip("pyftpdlib") + from fsspec.implementations.ftp import FTPFileSystem + + FTPFileSystem.clear_instance_cache() # remove lingering connections + CachingFileSystem.clear_instance_cache() + d = str(tmpdir) + with open(os.path.join(d, "out"), "wb") as f: + f.write(b"hello" * 10000) + P = subprocess.Popen( + [sys.executable, "-m", "pyftpdlib", "-d", d, "-u", "user", "-P", "pass", "-w"] + ) + try: + time.sleep(1) + yield "localhost", 2121, "user", "pass" + finally: + P.terminate() + P.wait() + try: + shutil.rmtree(tmpdir) + except Exception: + pass diff --git a/lib/python3.10/site-packages/fsspec/core.py b/lib/python3.10/site-packages/fsspec/core.py new file mode 100644 index 0000000000000000000000000000000000000000..d8e75572bc0e31b5a12faee73275760e421aadb0 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/core.py @@ -0,0 +1,743 @@ +from __future__ import annotations + +import io +import logging +import os +import re +from glob import has_magic +from pathlib import Path + +# for backwards compat, we export cache things from here too +from fsspec.caching import ( # noqa: F401 + BaseCache, + BlockCache, + BytesCache, + MMapCache, + ReadAheadCache, + caches, +) +from fsspec.compression import compr +from fsspec.config import conf +from fsspec.registry import filesystem, get_filesystem_class +from fsspec.utils import ( + _unstrip_protocol, + build_name_function, + infer_compression, + stringify_path, +) + +logger = logging.getLogger("fsspec") + + +class OpenFile: + """ + File-like object to be used in a context + + Can layer (buffered) text-mode and compression over any file-system, which + are typically binary-only. + + These instances are safe to serialize, as the low-level file object + is not created until invoked using ``with``. + + Parameters + ---------- + fs: FileSystem + The file system to use for opening the file. Should be a subclass or duck-type + with ``fsspec.spec.AbstractFileSystem`` + path: str + Location to open + mode: str like 'rb', optional + Mode of the opened file + compression: str or None, optional + Compression to apply + encoding: str or None, optional + The encoding to use if opened in text mode. + errors: str or None, optional + How to handle encoding errors if opened in text mode. + newline: None or str + Passed to TextIOWrapper in text mode, how to handle line endings. + autoopen: bool + If True, calls open() immediately. Mostly used by pickle + pos: int + If given and autoopen is True, seek to this location immediately + """ + + def __init__( + self, + fs, + path, + mode="rb", + compression=None, + encoding=None, + errors=None, + newline=None, + ): + self.fs = fs + self.path = path + self.mode = mode + self.compression = get_compression(path, compression) + self.encoding = encoding + self.errors = errors + self.newline = newline + self.fobjects = [] + + def __reduce__(self): + return ( + OpenFile, + ( + self.fs, + self.path, + self.mode, + self.compression, + self.encoding, + self.errors, + self.newline, + ), + ) + + def __repr__(self): + return f"" + + def __enter__(self): + mode = self.mode.replace("t", "").replace("b", "") + "b" + + try: + f = self.fs.open(self.path, mode=mode) + except FileNotFoundError as e: + if has_magic(self.path): + raise FileNotFoundError( + "%s not found. The URL contains glob characters: you maybe needed\n" + "to pass expand=True in fsspec.open() or the storage_options of \n" + "your library. You can also set the config value 'open_expand'\n" + "before import, or fsspec.core.DEFAULT_EXPAND at runtime, to True.", + self.path, + ) from e + raise + + self.fobjects = [f] + + if self.compression is not None: + compress = compr[self.compression] + f = compress(f, mode=mode[0]) + self.fobjects.append(f) + + if "b" not in self.mode: + # assume, for example, that 'r' is equivalent to 'rt' as in builtin + f = PickleableTextIOWrapper( + f, encoding=self.encoding, errors=self.errors, newline=self.newline + ) + self.fobjects.append(f) + + return self.fobjects[-1] + + def __exit__(self, *args): + self.close() + + @property + def full_name(self): + return _unstrip_protocol(self.path, self.fs) + + def open(self): + """Materialise this as a real open file without context + + The OpenFile object should be explicitly closed to avoid enclosed file + instances persisting. You must, therefore, keep a reference to the OpenFile + during the life of the file-like it generates. + """ + return self.__enter__() + + def close(self): + """Close all encapsulated file objects""" + for f in reversed(self.fobjects): + if "r" not in self.mode and not f.closed: + f.flush() + f.close() + self.fobjects.clear() + + +class OpenFiles(list): + """List of OpenFile instances + + Can be used in a single context, which opens and closes all of the + contained files. Normal list access to get the elements works as + normal. + + A special case is made for caching filesystems - the files will + be down/uploaded together at the start or end of the context, and + this may happen concurrently, if the target filesystem supports it. + """ + + def __init__(self, *args, mode="rb", fs=None): + self.mode = mode + self.fs = fs + self.files = [] + super().__init__(*args) + + def __enter__(self): + if self.fs is None: + raise ValueError("Context has already been used") + + fs = self.fs + while True: + if hasattr(fs, "open_many"): + # check for concurrent cache download; or set up for upload + self.files = fs.open_many(self) + return self.files + if hasattr(fs, "fs") and fs.fs is not None: + fs = fs.fs + else: + break + return [s.__enter__() for s in self] + + def __exit__(self, *args): + fs = self.fs + [s.__exit__(*args) for s in self] + if "r" not in self.mode: + while True: + if hasattr(fs, "open_many"): + # check for concurrent cache upload + fs.commit_many(self.files) + return + if hasattr(fs, "fs") and fs.fs is not None: + fs = fs.fs + else: + break + + def __getitem__(self, item): + out = super().__getitem__(item) + if isinstance(item, slice): + return OpenFiles(out, mode=self.mode, fs=self.fs) + return out + + def __repr__(self): + return f"" + + +def open_files( + urlpath, + mode="rb", + compression=None, + encoding="utf8", + errors=None, + name_function=None, + num=1, + protocol=None, + newline=None, + auto_mkdir=True, + expand=True, + **kwargs, +): + """Given a path or paths, return a list of ``OpenFile`` objects. + + For writing, a str path must contain the "*" character, which will be filled + in by increasing numbers, e.g., "part*" -> "part1", "part2" if num=2. + + For either reading or writing, can instead provide explicit list of paths. + + Parameters + ---------- + urlpath: string or list + Absolute or relative filepath(s). Prefix with a protocol like ``s3://`` + to read from alternative filesystems. To read from multiple files you + can pass a globstring or a list of paths, with the caveat that they + must all have the same protocol. + mode: 'rb', 'wt', etc. + compression: string or None + If given, open file using compression codec. Can either be a compression + name (a key in ``fsspec.compression.compr``) or "infer" to guess the + compression from the filename suffix. + encoding: str + For text mode only + errors: None or str + Passed to TextIOWrapper in text mode + name_function: function or None + if opening a set of files for writing, those files do not yet exist, + so we need to generate their names by formatting the urlpath for + each sequence number + num: int [1] + if writing mode, number of files we expect to create (passed to + name+function) + protocol: str or None + If given, overrides the protocol found in the URL. + newline: bytes or None + Used for line terminator in text mode. If None, uses system default; + if blank, uses no translation. + auto_mkdir: bool (True) + If in write mode, this will ensure the target directory exists before + writing, by calling ``fs.mkdirs(exist_ok=True)``. + expand: bool + **kwargs: dict + Extra options that make sense to a particular storage connection, e.g. + host, port, username, password, etc. + + Examples + -------- + >>> files = open_files('2015-*-*.csv') # doctest: +SKIP + >>> files = open_files( + ... 's3://bucket/2015-*-*.csv.gz', compression='gzip' + ... ) # doctest: +SKIP + + Returns + ------- + An ``OpenFiles`` instance, which is a list of ``OpenFile`` objects that can + be used as a single context + + Notes + ----- + For a full list of the available protocols and the implementations that + they map across to see the latest online documentation: + + - For implementations built into ``fsspec`` see + https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations + - For implementations in separate packages see + https://filesystem-spec.readthedocs.io/en/latest/api.html#other-known-implementations + """ + fs, fs_token, paths = get_fs_token_paths( + urlpath, + mode, + num=num, + name_function=name_function, + storage_options=kwargs, + protocol=protocol, + expand=expand, + ) + if fs.protocol == "file": + fs.auto_mkdir = auto_mkdir + elif "r" not in mode and auto_mkdir: + parents = {fs._parent(path) for path in paths} + for parent in parents: + try: + fs.makedirs(parent, exist_ok=True) + except PermissionError: + pass + return OpenFiles( + [ + OpenFile( + fs, + path, + mode=mode, + compression=compression, + encoding=encoding, + errors=errors, + newline=newline, + ) + for path in paths + ], + mode=mode, + fs=fs, + ) + + +def _un_chain(path, kwargs): + # Avoid a circular import + from fsspec.implementations.cached import CachingFileSystem + + if "::" in path: + x = re.compile(".*[^a-z]+.*") # test for non protocol-like single word + bits = [] + for p in path.split("::"): + if "://" in p or x.match(p): + bits.append(p) + else: + bits.append(p + "://") + else: + bits = [path] + # [[url, protocol, kwargs], ...] + out = [] + previous_bit = None + kwargs = kwargs.copy() + for bit in reversed(bits): + protocol = kwargs.pop("protocol", None) or split_protocol(bit)[0] or "file" + cls = get_filesystem_class(protocol) + extra_kwargs = cls._get_kwargs_from_urls(bit) + kws = kwargs.pop(protocol, {}) + if bit is bits[0]: + kws.update(kwargs) + kw = dict( + **{k: v for k, v in extra_kwargs.items() if k not in kws or v != kws[k]}, + **kws, + ) + bit = cls._strip_protocol(bit) + if "target_protocol" not in kw and issubclass(cls, CachingFileSystem): + bit = previous_bit + out.append((bit, protocol, kw)) + previous_bit = bit + out.reverse() + return out + + +def url_to_fs(url, **kwargs): + """ + Turn fully-qualified and potentially chained URL into filesystem instance + + Parameters + ---------- + url : str + The fsspec-compatible URL + **kwargs: dict + Extra options that make sense to a particular storage connection, e.g. + host, port, username, password, etc. + + Returns + ------- + filesystem : FileSystem + The new filesystem discovered from ``url`` and created with + ``**kwargs``. + urlpath : str + The file-systems-specific URL for ``url``. + """ + url = stringify_path(url) + # non-FS arguments that appear in fsspec.open() + # inspect could keep this in sync with open()'s signature + known_kwargs = { + "compression", + "encoding", + "errors", + "expand", + "mode", + "name_function", + "newline", + "num", + } + kwargs = {k: v for k, v in kwargs.items() if k not in known_kwargs} + chain = _un_chain(url, kwargs) + inkwargs = {} + # Reverse iterate the chain, creating a nested target_* structure + for i, ch in enumerate(reversed(chain)): + urls, protocol, kw = ch + if i == len(chain) - 1: + inkwargs = dict(**kw, **inkwargs) + continue + inkwargs["target_options"] = dict(**kw, **inkwargs) + inkwargs["target_protocol"] = protocol + inkwargs["fo"] = urls + urlpath, protocol, _ = chain[0] + fs = filesystem(protocol, **inkwargs) + return fs, urlpath + + +DEFAULT_EXPAND = conf.get("open_expand", False) + + +def open( + urlpath, + mode="rb", + compression=None, + encoding="utf8", + errors=None, + protocol=None, + newline=None, + expand=None, + **kwargs, +): + """Given a path or paths, return one ``OpenFile`` object. + + Parameters + ---------- + urlpath: string or list + Absolute or relative filepath. Prefix with a protocol like ``s3://`` + to read from alternative filesystems. Should not include glob + character(s). + mode: 'rb', 'wt', etc. + compression: string or None + If given, open file using compression codec. Can either be a compression + name (a key in ``fsspec.compression.compr``) or "infer" to guess the + compression from the filename suffix. + encoding: str + For text mode only + errors: None or str + Passed to TextIOWrapper in text mode + protocol: str or None + If given, overrides the protocol found in the URL. + newline: bytes or None + Used for line terminator in text mode. If None, uses system default; + if blank, uses no translation. + expand: bool or None + Whether to regard file paths containing special glob characters as needing + expansion (finding the first match) or absolute. Setting False allows using + paths which do embed such characters. If None (default), this argument + takes its value from the DEFAULT_EXPAND module variable, which takes + its initial value from the "open_expand" config value at startup, which will + be False if not set. + **kwargs: dict + Extra options that make sense to a particular storage connection, e.g. + host, port, username, password, etc. + + Examples + -------- + >>> openfile = open('2015-01-01.csv') # doctest: +SKIP + >>> openfile = open( + ... 's3://bucket/2015-01-01.csv.gz', compression='gzip' + ... ) # doctest: +SKIP + >>> with openfile as f: + ... df = pd.read_csv(f) # doctest: +SKIP + ... + + Returns + ------- + ``OpenFile`` object. + + Notes + ----- + For a full list of the available protocols and the implementations that + they map across to see the latest online documentation: + + - For implementations built into ``fsspec`` see + https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations + - For implementations in separate packages see + https://filesystem-spec.readthedocs.io/en/latest/api.html#other-known-implementations + """ + expand = DEFAULT_EXPAND if expand is None else expand + out = open_files( + urlpath=[urlpath], + mode=mode, + compression=compression, + encoding=encoding, + errors=errors, + protocol=protocol, + newline=newline, + expand=expand, + **kwargs, + ) + if not out: + raise FileNotFoundError(urlpath) + return out[0] + + +def open_local( + url: str | list[str] | Path | list[Path], + mode: str = "rb", + **storage_options: dict, +) -> str | list[str]: + """Open file(s) which can be resolved to local + + For files which either are local, or get downloaded upon open + (e.g., by file caching) + + Parameters + ---------- + url: str or list(str) + mode: str + Must be read mode + storage_options: + passed on to FS for or used by open_files (e.g., compression) + """ + if "r" not in mode: + raise ValueError("Can only ensure local files when reading") + of = open_files(url, mode=mode, **storage_options) + if not getattr(of[0].fs, "local_file", False): + raise ValueError( + "open_local can only be used on a filesystem which" + " has attribute local_file=True" + ) + with of as files: + paths = [f.name for f in files] + if (isinstance(url, str) and not has_magic(url)) or isinstance(url, Path): + return paths[0] + return paths + + +def get_compression(urlpath, compression): + if compression == "infer": + compression = infer_compression(urlpath) + if compression is not None and compression not in compr: + raise ValueError(f"Compression type {compression} not supported") + return compression + + +def split_protocol(urlpath): + """Return protocol, path pair""" + urlpath = stringify_path(urlpath) + if "://" in urlpath: + protocol, path = urlpath.split("://", 1) + if len(protocol) > 1: + # excludes Windows paths + return protocol, path + if urlpath.startswith("data:"): + return urlpath.split(":", 1) + return None, urlpath + + +def strip_protocol(urlpath): + """Return only path part of full URL, according to appropriate backend""" + protocol, _ = split_protocol(urlpath) + cls = get_filesystem_class(protocol) + return cls._strip_protocol(urlpath) + + +def expand_paths_if_needed(paths, mode, num, fs, name_function): + """Expand paths if they have a ``*`` in them (write mode) or any of ``*?[]`` + in them (read mode). + + :param paths: list of paths + mode: str + Mode in which to open files. + num: int + If opening in writing mode, number of files we expect to create. + fs: filesystem object + name_function: callable + If opening in writing mode, this callable is used to generate path + names. Names are generated for each partition by + ``urlpath.replace('*', name_function(partition_index))``. + :return: list of paths + """ + expanded_paths = [] + paths = list(paths) + + if "w" in mode: # read mode + if sum(1 for p in paths if "*" in p) > 1: + raise ValueError( + "When writing data, only one filename mask can be specified." + ) + num = max(num, len(paths)) + + for curr_path in paths: + if "*" in curr_path: + # expand using name_function + expanded_paths.extend(_expand_paths(curr_path, name_function, num)) + else: + expanded_paths.append(curr_path) + # if we generated more paths that asked for, trim the list + if len(expanded_paths) > num: + expanded_paths = expanded_paths[:num] + + else: # read mode + for curr_path in paths: + if has_magic(curr_path): + # expand using glob + expanded_paths.extend(fs.glob(curr_path)) + else: + expanded_paths.append(curr_path) + + return expanded_paths + + +def get_fs_token_paths( + urlpath, + mode="rb", + num=1, + name_function=None, + storage_options=None, + protocol=None, + expand=True, +): + """Filesystem, deterministic token, and paths from a urlpath and options. + + Parameters + ---------- + urlpath: string or iterable + Absolute or relative filepath, URL (may include protocols like + ``s3://``), or globstring pointing to data. + mode: str, optional + Mode in which to open files. + num: int, optional + If opening in writing mode, number of files we expect to create. + name_function: callable, optional + If opening in writing mode, this callable is used to generate path + names. Names are generated for each partition by + ``urlpath.replace('*', name_function(partition_index))``. + storage_options: dict, optional + Additional keywords to pass to the filesystem class. + protocol: str or None + To override the protocol specifier in the URL + expand: bool + Expand string paths for writing, assuming the path is a directory + """ + if isinstance(urlpath, (list, tuple, set)): + if not urlpath: + raise ValueError("empty urlpath sequence") + urlpath0 = stringify_path(next(iter(urlpath))) + else: + urlpath0 = stringify_path(urlpath) + storage_options = storage_options or {} + if protocol: + storage_options["protocol"] = protocol + chain = _un_chain(urlpath0, storage_options or {}) + inkwargs = {} + # Reverse iterate the chain, creating a nested target_* structure + for i, ch in enumerate(reversed(chain)): + urls, nested_protocol, kw = ch + if i == len(chain) - 1: + inkwargs = dict(**kw, **inkwargs) + continue + inkwargs["target_options"] = dict(**kw, **inkwargs) + inkwargs["target_protocol"] = nested_protocol + inkwargs["fo"] = urls + paths, protocol, _ = chain[0] + fs = filesystem(protocol, **inkwargs) + if isinstance(urlpath, (list, tuple, set)): + pchains = [ + _un_chain(stringify_path(u), storage_options or {})[0] for u in urlpath + ] + if len({pc[1] for pc in pchains}) > 1: + raise ValueError("Protocol mismatch getting fs from %s", urlpath) + paths = [pc[0] for pc in pchains] + else: + paths = fs._strip_protocol(paths) + if isinstance(paths, (list, tuple, set)): + if expand: + paths = expand_paths_if_needed(paths, mode, num, fs, name_function) + elif not isinstance(paths, list): + paths = list(paths) + else: + if ("w" in mode or "x" in mode) and expand: + paths = _expand_paths(paths, name_function, num) + elif "*" in paths: + paths = [f for f in sorted(fs.glob(paths)) if not fs.isdir(f)] + else: + paths = [paths] + + return fs, fs._fs_token, paths + + +def _expand_paths(path, name_function, num): + if isinstance(path, str): + if path.count("*") > 1: + raise ValueError("Output path spec must contain exactly one '*'.") + elif "*" not in path: + path = os.path.join(path, "*.part") + + if name_function is None: + name_function = build_name_function(num - 1) + + paths = [path.replace("*", name_function(i)) for i in range(num)] + if paths != sorted(paths): + logger.warning( + "In order to preserve order between partitions" + " paths created with ``name_function`` should " + "sort to partition order" + ) + elif isinstance(path, (tuple, list)): + assert len(path) == num + paths = list(path) + else: + raise ValueError( + "Path should be either\n" + "1. A list of paths: ['foo.json', 'bar.json', ...]\n" + "2. A directory: 'foo/\n" + "3. A path with a '*' in it: 'foo.*.json'" + ) + return paths + + +class PickleableTextIOWrapper(io.TextIOWrapper): + """TextIOWrapper cannot be pickled. This solves it. + + Requires that ``buffer`` be pickleable, which all instances of + AbstractBufferedFile are. + """ + + def __init__( + self, + buffer, + encoding=None, + errors=None, + newline=None, + line_buffering=False, + write_through=False, + ): + self.args = buffer, encoding, errors, newline, line_buffering, write_through + super().__init__(*self.args) + + def __reduce__(self): + return PickleableTextIOWrapper, self.args diff --git a/lib/python3.10/site-packages/fsspec/dircache.py b/lib/python3.10/site-packages/fsspec/dircache.py new file mode 100644 index 0000000000000000000000000000000000000000..eca19566b135e5a7a4f6e7407d56411ec58bfe44 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/dircache.py @@ -0,0 +1,98 @@ +import time +from collections.abc import MutableMapping +from functools import lru_cache + + +class DirCache(MutableMapping): + """ + Caching of directory listings, in a structure like:: + + {"path0": [ + {"name": "path0/file0", + "size": 123, + "type": "file", + ... + }, + {"name": "path0/file1", + }, + ... + ], + "path1": [...] + } + + Parameters to this class control listing expiry or indeed turn + caching off + """ + + def __init__( + self, + use_listings_cache=True, + listings_expiry_time=None, + max_paths=None, + **kwargs, + ): + """ + + Parameters + ---------- + use_listings_cache: bool + If False, this cache never returns items, but always reports KeyError, + and setting items has no effect + listings_expiry_time: int or float (optional) + Time in seconds that a listing is considered valid. If None, + listings do not expire. + max_paths: int (optional) + The number of most recent listings that are considered valid; 'recent' + refers to when the entry was set. + """ + self._cache = {} + self._times = {} + if max_paths: + self._q = lru_cache(max_paths + 1)(lambda key: self._cache.pop(key, None)) + self.use_listings_cache = use_listings_cache + self.listings_expiry_time = listings_expiry_time + self.max_paths = max_paths + + def __getitem__(self, item): + if self.listings_expiry_time is not None: + if self._times.get(item, 0) - time.time() < -self.listings_expiry_time: + del self._cache[item] + if self.max_paths: + self._q(item) + return self._cache[item] # maybe raises KeyError + + def clear(self): + self._cache.clear() + + def __len__(self): + return len(self._cache) + + def __contains__(self, item): + try: + self[item] + return True + except KeyError: + return False + + def __setitem__(self, key, value): + if not self.use_listings_cache: + return + if self.max_paths: + self._q(key) + self._cache[key] = value + if self.listings_expiry_time is not None: + self._times[key] = time.time() + + def __delitem__(self, key): + del self._cache[key] + + def __iter__(self): + entries = list(self._cache) + + return (k for k in entries if k in self) + + def __reduce__(self): + return ( + DirCache, + (self.use_listings_cache, self.listings_expiry_time, self.max_paths), + ) diff --git a/lib/python3.10/site-packages/fsspec/exceptions.py b/lib/python3.10/site-packages/fsspec/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..ae8905475f02655f4fc5863931d99ca9da55db78 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/exceptions.py @@ -0,0 +1,18 @@ +""" +fsspec user-defined exception classes +""" + +import asyncio + + +class BlocksizeMismatchError(ValueError): + """ + Raised when a cached file is opened with a different blocksize than it was + written with + """ + + +class FSTimeoutError(asyncio.TimeoutError): + """ + Raised when a fsspec function timed out occurs + """ diff --git a/lib/python3.10/site-packages/fsspec/fuse.py b/lib/python3.10/site-packages/fsspec/fuse.py new file mode 100644 index 0000000000000000000000000000000000000000..566d520fce3e94e3bbaee48c3c6acc9f1db315a8 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/fuse.py @@ -0,0 +1,324 @@ +import argparse +import logging +import os +import stat +import threading +import time +from errno import EIO, ENOENT + +from fuse import FUSE, FuseOSError, LoggingMixIn, Operations + +from fsspec import __version__ +from fsspec.core import url_to_fs + +logger = logging.getLogger("fsspec.fuse") + + +class FUSEr(Operations): + def __init__(self, fs, path, ready_file=False): + self.fs = fs + self.cache = {} + self.root = path.rstrip("/") + "/" + self.counter = 0 + logger.info("Starting FUSE at %s", path) + self._ready_file = ready_file + + def getattr(self, path, fh=None): + logger.debug("getattr %s", path) + if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]: + return {"type": "file", "st_size": 5} + + path = "".join([self.root, path.lstrip("/")]).rstrip("/") + try: + info = self.fs.info(path) + except FileNotFoundError as exc: + raise FuseOSError(ENOENT) from exc + + data = {"st_uid": info.get("uid", 1000), "st_gid": info.get("gid", 1000)} + perm = info.get("mode", 0o777) + + if info["type"] != "file": + data["st_mode"] = stat.S_IFDIR | perm + data["st_size"] = 0 + data["st_blksize"] = 0 + else: + data["st_mode"] = stat.S_IFREG | perm + data["st_size"] = info["size"] + data["st_blksize"] = 5 * 2**20 + data["st_nlink"] = 1 + data["st_atime"] = info["atime"] if "atime" in info else time.time() + data["st_ctime"] = info["ctime"] if "ctime" in info else time.time() + data["st_mtime"] = info["mtime"] if "mtime" in info else time.time() + return data + + def readdir(self, path, fh): + logger.debug("readdir %s", path) + path = "".join([self.root, path.lstrip("/")]) + files = self.fs.ls(path, False) + files = [os.path.basename(f.rstrip("/")) for f in files] + return [".", ".."] + files + + def mkdir(self, path, mode): + path = "".join([self.root, path.lstrip("/")]) + self.fs.mkdir(path) + return 0 + + def rmdir(self, path): + path = "".join([self.root, path.lstrip("/")]) + self.fs.rmdir(path) + return 0 + + def read(self, path, size, offset, fh): + logger.debug("read %s", (path, size, offset)) + if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]: + # status indicator + return b"ready" + + f = self.cache[fh] + f.seek(offset) + out = f.read(size) + return out + + def write(self, path, data, offset, fh): + logger.debug("write %s", (path, offset)) + f = self.cache[fh] + f.seek(offset) + f.write(data) + return len(data) + + def create(self, path, flags, fi=None): + logger.debug("create %s", (path, flags)) + fn = "".join([self.root, path.lstrip("/")]) + self.fs.touch(fn) # OS will want to get attributes immediately + f = self.fs.open(fn, "wb") + self.cache[self.counter] = f + self.counter += 1 + return self.counter - 1 + + def open(self, path, flags): + logger.debug("open %s", (path, flags)) + fn = "".join([self.root, path.lstrip("/")]) + if flags % 2 == 0: + # read + mode = "rb" + else: + # write/create + mode = "wb" + self.cache[self.counter] = self.fs.open(fn, mode) + self.counter += 1 + return self.counter - 1 + + def truncate(self, path, length, fh=None): + fn = "".join([self.root, path.lstrip("/")]) + if length != 0: + raise NotImplementedError + # maybe should be no-op since open with write sets size to zero anyway + self.fs.touch(fn) + + def unlink(self, path): + fn = "".join([self.root, path.lstrip("/")]) + try: + self.fs.rm(fn, False) + except (OSError, FileNotFoundError) as exc: + raise FuseOSError(EIO) from exc + + def release(self, path, fh): + try: + if fh in self.cache: + f = self.cache[fh] + f.close() + self.cache.pop(fh) + except Exception as e: + print(e) + return 0 + + def chmod(self, path, mode): + if hasattr(self.fs, "chmod"): + path = "".join([self.root, path.lstrip("/")]) + return self.fs.chmod(path, mode) + raise NotImplementedError + + +def run( + fs, + path, + mount_point, + foreground=True, + threads=False, + ready_file=False, + ops_class=FUSEr, +): + """Mount stuff in a local directory + + This uses fusepy to make it appear as if a given path on an fsspec + instance is in fact resident within the local file-system. + + This requires that fusepy by installed, and that FUSE be available on + the system (typically requiring a package to be installed with + apt, yum, brew, etc.). + + Parameters + ---------- + fs: file-system instance + From one of the compatible implementations + path: str + Location on that file-system to regard as the root directory to + mount. Note that you typically should include the terminating "/" + character. + mount_point: str + An empty directory on the local file-system where the contents of + the remote path will appear. + foreground: bool + Whether or not calling this function will block. Operation will + typically be more stable if True. + threads: bool + Whether or not to create threads when responding to file operations + within the mounter directory. Operation will typically be more + stable if False. + ready_file: bool + Whether the FUSE process is ready. The ``.fuse_ready`` file will + exist in the ``mount_point`` directory if True. Debugging purpose. + ops_class: FUSEr or Subclass of FUSEr + To override the default behavior of FUSEr. For Example, logging + to file. + + """ + func = lambda: FUSE( + ops_class(fs, path, ready_file=ready_file), + mount_point, + nothreads=not threads, + foreground=foreground, + ) + if not foreground: + th = threading.Thread(target=func) + th.daemon = True + th.start() + return th + else: # pragma: no cover + try: + func() + except KeyboardInterrupt: + pass + + +def main(args): + """Mount filesystem from chained URL to MOUNT_POINT. + + Examples: + + python3 -m fsspec.fuse memory /usr/share /tmp/mem + + python3 -m fsspec.fuse local /tmp/source /tmp/local \\ + -l /tmp/fsspecfuse.log + + You can also mount chained-URLs and use special settings: + + python3 -m fsspec.fuse 'filecache::zip::file://data.zip' \\ + / /tmp/zip \\ + -o 'filecache-cache_storage=/tmp/simplecache' + + You can specify the type of the setting by using `[int]` or `[bool]`, + (`true`, `yes`, `1` represents the Boolean value `True`): + + python3 -m fsspec.fuse 'simplecache::ftp://ftp1.at.proftpd.org' \\ + /historic/packages/RPMS /tmp/ftp \\ + -o 'simplecache-cache_storage=/tmp/simplecache' \\ + -o 'simplecache-check_files=false[bool]' \\ + -o 'ftp-listings_expiry_time=60[int]' \\ + -o 'ftp-username=anonymous' \\ + -o 'ftp-password=xieyanbo' + """ + + class RawDescriptionArgumentParser(argparse.ArgumentParser): + def format_help(self): + usage = super().format_help() + parts = usage.split("\n\n") + parts[1] = self.description.rstrip() + return "\n\n".join(parts) + + parser = RawDescriptionArgumentParser(prog="fsspec.fuse", description=main.__doc__) + parser.add_argument("--version", action="version", version=__version__) + parser.add_argument("url", type=str, help="fs url") + parser.add_argument("source_path", type=str, help="source directory in fs") + parser.add_argument("mount_point", type=str, help="local directory") + parser.add_argument( + "-o", + "--option", + action="append", + help="Any options of protocol included in the chained URL", + ) + parser.add_argument( + "-l", "--log-file", type=str, help="Logging FUSE debug info (Default: '')" + ) + parser.add_argument( + "-f", + "--foreground", + action="store_false", + help="Running in foreground or not (Default: False)", + ) + parser.add_argument( + "-t", + "--threads", + action="store_false", + help="Running with threads support (Default: False)", + ) + parser.add_argument( + "-r", + "--ready-file", + action="store_false", + help="The `.fuse_ready` file will exist after FUSE is ready. " + "(Debugging purpose, Default: False)", + ) + args = parser.parse_args(args) + + kwargs = {} + for item in args.option or []: + key, sep, value = item.partition("=") + if not sep: + parser.error(message=f"Wrong option: {item!r}") + val = value.lower() + if val.endswith("[int]"): + value = int(value[: -len("[int]")]) + elif val.endswith("[bool]"): + value = val[: -len("[bool]")] in ["1", "yes", "true"] + + if "-" in key: + fs_name, setting_name = key.split("-", 1) + if fs_name in kwargs: + kwargs[fs_name][setting_name] = value + else: + kwargs[fs_name] = {setting_name: value} + else: + kwargs[key] = value + + if args.log_file: + logging.basicConfig( + level=logging.DEBUG, + filename=args.log_file, + format="%(asctime)s %(message)s", + ) + + class LoggingFUSEr(FUSEr, LoggingMixIn): + pass + + fuser = LoggingFUSEr + else: + fuser = FUSEr + + fs, url_path = url_to_fs(args.url, **kwargs) + logger.debug("Mounting %s to %s", url_path, str(args.mount_point)) + run( + fs, + args.source_path, + args.mount_point, + foreground=args.foreground, + threads=args.threads, + ready_file=args.ready_file, + ops_class=fuser, + ) + + +if __name__ == "__main__": + import sys + + main(sys.argv[1:]) diff --git a/lib/python3.10/site-packages/fsspec/generic.py b/lib/python3.10/site-packages/fsspec/generic.py new file mode 100644 index 0000000000000000000000000000000000000000..2156d354a87bcf044bef04a62b4d61f5864cf33c --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/generic.py @@ -0,0 +1,394 @@ +from __future__ import annotations + +import inspect +import logging +import os +import shutil +import uuid + +from .asyn import AsyncFileSystem, _run_coros_in_chunks, sync_wrapper +from .callbacks import DEFAULT_CALLBACK +from .core import filesystem, get_filesystem_class, split_protocol, url_to_fs + +_generic_fs = {} +logger = logging.getLogger("fsspec.generic") + + +def set_generic_fs(protocol, **storage_options): + """Populate the dict used for method=="generic" lookups""" + _generic_fs[protocol] = filesystem(protocol, **storage_options) + + +def _resolve_fs(url, method, protocol=None, storage_options=None): + """Pick instance of backend FS""" + url = url[0] if isinstance(url, (list, tuple)) else url + protocol = protocol or split_protocol(url)[0] + storage_options = storage_options or {} + if method == "default": + return filesystem(protocol) + if method == "generic": + return _generic_fs[protocol] + if method == "current": + cls = get_filesystem_class(protocol) + return cls.current() + if method == "options": + fs, _ = url_to_fs(url, **storage_options.get(protocol, {})) + return fs + raise ValueError(f"Unknown FS resolution method: {method}") + + +def rsync( + source, + destination, + delete_missing=False, + source_field="size", + dest_field="size", + update_cond="different", + inst_kwargs=None, + fs=None, + **kwargs, +): + """Sync files between two directory trees + + (experimental) + + Parameters + ---------- + source: str + Root of the directory tree to take files from. This must be a directory, but + do not include any terminating "/" character + destination: str + Root path to copy into. The contents of this location should be + identical to the contents of ``source`` when done. This will be made a + directory, and the terminal "/" should not be included. + delete_missing: bool + If there are paths in the destination that don't exist in the + source and this is True, delete them. Otherwise, leave them alone. + source_field: str | callable + If ``update_field`` is "different", this is the key in the info + of source files to consider for difference. Maybe a function of the + info dict. + dest_field: str | callable + If ``update_field`` is "different", this is the key in the info + of destination files to consider for difference. May be a function of + the info dict. + update_cond: "different"|"always"|"never" + If "always", every file is copied, regardless of whether it exists in + the destination. If "never", files that exist in the destination are + not copied again. If "different" (default), only copy if the info + fields given by ``source_field`` and ``dest_field`` (usually "size") + are different. Other comparisons may be added in the future. + inst_kwargs: dict|None + If ``fs`` is None, use this set of keyword arguments to make a + GenericFileSystem instance + fs: GenericFileSystem|None + Instance to use if explicitly given. The instance defines how to + to make downstream file system instances from paths. + + Returns + ------- + dict of the copy operations that were performed, {source: destination} + """ + fs = fs or GenericFileSystem(**(inst_kwargs or {})) + source = fs._strip_protocol(source) + destination = fs._strip_protocol(destination) + allfiles = fs.find(source, withdirs=True, detail=True) + if not fs.isdir(source): + raise ValueError("Can only rsync on a directory") + otherfiles = fs.find(destination, withdirs=True, detail=True) + dirs = [ + a + for a, v in allfiles.items() + if v["type"] == "directory" and a.replace(source, destination) not in otherfiles + ] + logger.debug(f"{len(dirs)} directories to create") + if dirs: + fs.make_many_dirs( + [dirn.replace(source, destination) for dirn in dirs], exist_ok=True + ) + allfiles = {a: v for a, v in allfiles.items() if v["type"] == "file"} + logger.debug(f"{len(allfiles)} files to consider for copy") + to_delete = [ + o + for o, v in otherfiles.items() + if o.replace(destination, source) not in allfiles and v["type"] == "file" + ] + for k, v in allfiles.copy().items(): + otherfile = k.replace(source, destination) + if otherfile in otherfiles: + if update_cond == "always": + allfiles[k] = otherfile + elif update_cond == "different": + inf1 = source_field(v) if callable(source_field) else v[source_field] + v2 = otherfiles[otherfile] + inf2 = dest_field(v2) if callable(dest_field) else v2[dest_field] + if inf1 != inf2: + # details mismatch, make copy + allfiles[k] = otherfile + else: + # details match, don't copy + allfiles.pop(k) + else: + # file not in target yet + allfiles[k] = otherfile + logger.debug(f"{len(allfiles)} files to copy") + if allfiles: + source_files, target_files = zip(*allfiles.items()) + fs.cp(source_files, target_files, **kwargs) + logger.debug(f"{len(to_delete)} files to delete") + if delete_missing and to_delete: + fs.rm(to_delete) + return allfiles + + +class GenericFileSystem(AsyncFileSystem): + """Wrapper over all other FS types + + + + This implementation is a single unified interface to be able to run FS operations + over generic URLs, and dispatch to the specific implementations using the URL + protocol prefix. + + Note: instances of this FS are always async, even if you never use it with any async + backend. + """ + + protocol = "generic" # there is no real reason to ever use a protocol with this FS + + def __init__(self, default_method="default", storage_options=None, **kwargs): + """ + + Parameters + ---------- + default_method: str (optional) + Defines how to configure backend FS instances. Options are: + - "default": instantiate like FSClass(), with no + extra arguments; this is the default instance of that FS, and can be + configured via the config system + - "generic": takes instances from the `_generic_fs` dict in this module, + which you must populate before use. Keys are by protocol + - "options": expects storage_options, a dict mapping protocol to + kwargs to use when constructing the filesystem + - "current": takes the most recently instantiated version of each FS + """ + self.method = default_method + self.st_opts = storage_options + super().__init__(**kwargs) + + def _parent(self, path): + fs = _resolve_fs(path, self.method, storage_options=self.st_opts) + return fs.unstrip_protocol(fs._parent(path)) + + def _strip_protocol(self, path): + # normalization only + fs = _resolve_fs(path, self.method, storage_options=self.st_opts) + return fs.unstrip_protocol(fs._strip_protocol(path)) + + async def _find(self, path, maxdepth=None, withdirs=False, detail=False, **kwargs): + fs = _resolve_fs(path, self.method, storage_options=self.st_opts) + if fs.async_impl: + out = await fs._find( + path, maxdepth=maxdepth, withdirs=withdirs, detail=True, **kwargs + ) + else: + out = fs.find( + path, maxdepth=maxdepth, withdirs=withdirs, detail=True, **kwargs + ) + result = {} + for k, v in out.items(): + v = v.copy() # don't corrupt target FS dircache + name = fs.unstrip_protocol(k) + v["name"] = name + result[name] = v + if detail: + return result + return list(result) + + async def _info(self, url, **kwargs): + fs = _resolve_fs(url, self.method) + if fs.async_impl: + out = await fs._info(url, **kwargs) + else: + out = fs.info(url, **kwargs) + out = out.copy() # don't edit originals + out["name"] = fs.unstrip_protocol(out["name"]) + return out + + async def _ls( + self, + url, + detail=True, + **kwargs, + ): + fs = _resolve_fs(url, self.method) + if fs.async_impl: + out = await fs._ls(url, detail=True, **kwargs) + else: + out = fs.ls(url, detail=True, **kwargs) + out = [o.copy() for o in out] # don't edit originals + for o in out: + o["name"] = fs.unstrip_protocol(o["name"]) + if detail: + return out + else: + return [o["name"] for o in out] + + async def _cat_file( + self, + url, + **kwargs, + ): + fs = _resolve_fs(url, self.method) + if fs.async_impl: + return await fs._cat_file(url, **kwargs) + else: + return fs.cat_file(url, **kwargs) + + async def _pipe_file( + self, + path, + value, + **kwargs, + ): + fs = _resolve_fs(path, self.method, storage_options=self.st_opts) + if fs.async_impl: + return await fs._pipe_file(path, value, **kwargs) + else: + return fs.pipe_file(path, value, **kwargs) + + async def _rm(self, url, **kwargs): + urls = url + if isinstance(urls, str): + urls = [urls] + fs = _resolve_fs(urls[0], self.method) + if fs.async_impl: + await fs._rm(urls, **kwargs) + else: + fs.rm(url, **kwargs) + + async def _makedirs(self, path, exist_ok=False): + logger.debug("Make dir %s", path) + fs = _resolve_fs(path, self.method, storage_options=self.st_opts) + if fs.async_impl: + await fs._makedirs(path, exist_ok=exist_ok) + else: + fs.makedirs(path, exist_ok=exist_ok) + + def rsync(self, source, destination, **kwargs): + """Sync files between two directory trees + + See `func:rsync` for more details. + """ + rsync(source, destination, fs=self, **kwargs) + + async def _cp_file( + self, + url, + url2, + blocksize=2**20, + callback=DEFAULT_CALLBACK, + tempdir: str | None = None, + **kwargs, + ): + fs = _resolve_fs(url, self.method) + fs2 = _resolve_fs(url2, self.method) + if fs is fs2: + # pure remote + if fs.async_impl: + return await fs._copy(url, url2, **kwargs) + else: + return fs.copy(url, url2, **kwargs) + await copy_file_op(fs, [url], fs2, [url2], tempdir, 1, on_error="raise") + + async def _make_many_dirs(self, urls, exist_ok=True): + fs = _resolve_fs(urls[0], self.method) + if fs.async_impl: + coros = [fs._makedirs(u, exist_ok=exist_ok) for u in urls] + await _run_coros_in_chunks(coros) + else: + for u in urls: + fs.makedirs(u, exist_ok=exist_ok) + + make_many_dirs = sync_wrapper(_make_many_dirs) + + async def _copy( + self, + path1: list[str], + path2: list[str], + recursive: bool = False, + on_error: str = "ignore", + maxdepth: int | None = None, + batch_size: int | None = None, + tempdir: str | None = None, + **kwargs, + ): + # TODO: special case for one FS being local, which can use get/put + # TODO: special case for one being memFS, which can use cat/pipe + if recursive: + raise NotImplementedError("Please use fsspec.generic.rsync") + path1 = [path1] if isinstance(path1, str) else path1 + path2 = [path2] if isinstance(path2, str) else path2 + + fs = _resolve_fs(path1, self.method) + fs2 = _resolve_fs(path2, self.method) + + if fs is fs2: + if fs.async_impl: + return await fs._copy(path1, path2, **kwargs) + else: + return fs.copy(path1, path2, **kwargs) + + await copy_file_op( + fs, path1, fs2, path2, tempdir, batch_size, on_error=on_error + ) + + +async def copy_file_op( + fs1, url1, fs2, url2, tempdir=None, batch_size=20, on_error="ignore" +): + import tempfile + + tempdir = tempdir or tempfile.mkdtemp() + try: + coros = [ + _copy_file_op( + fs1, + u1, + fs2, + u2, + os.path.join(tempdir, uuid.uuid4().hex), + ) + for u1, u2 in zip(url1, url2) + ] + out = await _run_coros_in_chunks( + coros, batch_size=batch_size, return_exceptions=True + ) + finally: + shutil.rmtree(tempdir) + if on_error == "return": + return out + elif on_error == "raise": + for o in out: + if isinstance(o, Exception): + raise o + + +async def _copy_file_op(fs1, url1, fs2, url2, local, on_error="ignore"): + if fs1.async_impl: + await fs1._get_file(url1, local) + else: + fs1.get_file(url1, local) + if fs2.async_impl: + await fs2._put_file(local, url2) + else: + fs2.put_file(local, url2) + os.unlink(local) + logger.debug("Copy %s -> %s; done", url1, url2) + + +async def maybe_await(cor): + if inspect.iscoroutine(cor): + return await cor + else: + return cor diff --git a/lib/python3.10/site-packages/fsspec/gui.py b/lib/python3.10/site-packages/fsspec/gui.py new file mode 100644 index 0000000000000000000000000000000000000000..9d914c8beb6cabb2c2700eb8eee31028559be2bd --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/gui.py @@ -0,0 +1,417 @@ +import ast +import contextlib +import logging +import os +import re +from collections.abc import Sequence +from typing import ClassVar + +import panel as pn + +from .core import OpenFile, get_filesystem_class, split_protocol +from .registry import known_implementations + +pn.extension() +logger = logging.getLogger("fsspec.gui") + + +class SigSlot: + """Signal-slot mixin, for Panel event passing + + Include this class in a widget manager's superclasses to be able to + register events and callbacks on Panel widgets managed by that class. + + The method ``_register`` should be called as widgets are added, and external + code should call ``connect`` to associate callbacks. + + By default, all signals emit a DEBUG logging statement. + """ + + # names of signals that this class may emit each of which must be + # set by _register for any new instance + signals: ClassVar[Sequence[str]] = [] + # names of actions that this class may respond to + slots: ClassVar[Sequence[str]] = [] + + # each of which must be a method name + + def __init__(self): + self._ignoring_events = False + self._sigs = {} + self._map = {} + self._setup() + + def _setup(self): + """Create GUI elements and register signals""" + self.panel = pn.pane.PaneBase() + # no signals to set up in the base class + + def _register( + self, widget, name, thing="value", log_level=logging.DEBUG, auto=False + ): + """Watch the given attribute of a widget and assign it a named event + + This is normally called at the time a widget is instantiated, in the + class which owns it. + + Parameters + ---------- + widget : pn.layout.Panel or None + Widget to watch. If None, an anonymous signal not associated with + any widget. + name : str + Name of this event + thing : str + Attribute of the given widget to watch + log_level : int + When the signal is triggered, a logging event of the given level + will be fired in the dfviz logger. + auto : bool + If True, automatically connects with a method in this class of the + same name. + """ + if name not in self.signals: + raise ValueError(f"Attempt to assign an undeclared signal: {name}") + self._sigs[name] = { + "widget": widget, + "callbacks": [], + "thing": thing, + "log": log_level, + } + wn = "-".join( + [ + getattr(widget, "name", str(widget)) if widget is not None else "none", + thing, + ] + ) + self._map[wn] = name + if widget is not None: + widget.param.watch(self._signal, thing, onlychanged=True) + if auto and hasattr(self, name): + self.connect(name, getattr(self, name)) + + def _repr_mimebundle_(self, *args, **kwargs): + """Display in a notebook or a server""" + try: + return self.panel._repr_mimebundle_(*args, **kwargs) + except (ValueError, AttributeError) as exc: + raise NotImplementedError( + "Panel does not seem to be set up properly" + ) from exc + + def connect(self, signal, slot): + """Associate call back with given event + + The callback must be a function which takes the "new" value of the + watched attribute as the only parameter. If the callback return False, + this cancels any further processing of the given event. + + Alternatively, the callback can be a string, in which case it means + emitting the correspondingly-named event (i.e., connect to self) + """ + self._sigs[signal]["callbacks"].append(slot) + + def _signal(self, event): + """This is called by a an action on a widget + + Within an self.ignore_events context, nothing happens. + + Tests can execute this method by directly changing the values of + widget components. + """ + if not self._ignoring_events: + wn = "-".join([event.obj.name, event.name]) + if wn in self._map and self._map[wn] in self._sigs: + self._emit(self._map[wn], event.new) + + @contextlib.contextmanager + def ignore_events(self): + """Temporarily turn off events processing in this instance + + (does not propagate to children) + """ + self._ignoring_events = True + try: + yield + finally: + self._ignoring_events = False + + def _emit(self, sig, value=None): + """An event happened, call its callbacks + + This method can be used in tests to simulate message passing without + directly changing visual elements. + + Calling of callbacks will halt whenever one returns False. + """ + logger.log(self._sigs[sig]["log"], f"{sig}: {value}") + for callback in self._sigs[sig]["callbacks"]: + if isinstance(callback, str): + self._emit(callback) + else: + try: + # running callbacks should not break the interface + ret = callback(value) + if ret is False: + break + except Exception as e: + logger.exception( + "Exception (%s) while executing callback for signal: %s", + e, + sig, + ) + + def show(self, threads=False): + """Open a new browser tab and display this instance's interface""" + self.panel.show(threads=threads, verbose=False) + return self + + +class SingleSelect(SigSlot): + """A multiselect which only allows you to select one item for an event""" + + signals = ["_selected", "selected"] # the first is internal + slots = ["set_options", "set_selection", "add", "clear", "select"] + + def __init__(self, **kwargs): + self.kwargs = kwargs + super().__init__() + + def _setup(self): + self.panel = pn.widgets.MultiSelect(**self.kwargs) + self._register(self.panel, "_selected", "value") + self._register(None, "selected") + self.connect("_selected", self.select_one) + + def _signal(self, *args, **kwargs): + super()._signal(*args, **kwargs) + + def select_one(self, *_): + with self.ignore_events(): + val = [self.panel.value[-1]] if self.panel.value else [] + self.panel.value = val + self._emit("selected", self.panel.value) + + def set_options(self, options): + self.panel.options = options + + def clear(self): + self.panel.options = [] + + @property + def value(self): + return self.panel.value + + def set_selection(self, selection): + self.panel.value = [selection] + + +class FileSelector(SigSlot): + """Panel-based graphical file selector widget + + Instances of this widget are interactive and can be displayed in jupyter by having + them as the output of a cell, or in a separate browser tab using ``.show()``. + """ + + signals = [ + "protocol_changed", + "selection_changed", + "directory_entered", + "home_clicked", + "up_clicked", + "go_clicked", + "filters_changed", + ] + slots = ["set_filters", "go_home"] + + def __init__(self, url=None, filters=None, ignore=None, kwargs=None): + """ + + Parameters + ---------- + url : str (optional) + Initial value of the URL to populate the dialog; should include protocol + filters : list(str) (optional) + File endings to include in the listings. If not included, all files are + allowed. Does not affect directories. + If given, the endings will appear as checkboxes in the interface + ignore : list(str) (optional) + Regex(s) of file basename patterns to ignore, e.g., "\\." for typical + hidden files on posix + kwargs : dict (optional) + To pass to file system instance + """ + if url: + self.init_protocol, url = split_protocol(url) + else: + self.init_protocol, url = "file", os.getcwd() + self.init_url = url + self.init_kwargs = (kwargs if isinstance(kwargs, str) else str(kwargs)) or "{}" + self.filters = filters + self.ignore = [re.compile(i) for i in ignore or []] + self._fs = None + super().__init__() + + def _setup(self): + self.url = pn.widgets.TextInput( + name="url", + value=self.init_url, + align="end", + sizing_mode="stretch_width", + width_policy="max", + ) + self.protocol = pn.widgets.Select( + options=sorted(known_implementations), + value=self.init_protocol, + name="protocol", + align="center", + ) + self.kwargs = pn.widgets.TextInput( + name="kwargs", value=self.init_kwargs, align="center" + ) + self.go = pn.widgets.Button(name="⇨", align="end", width=45) + self.main = SingleSelect(size=10) + self.home = pn.widgets.Button(name="🏠", width=40, height=30, align="end") + self.up = pn.widgets.Button(name="‹", width=30, height=30, align="end") + + self._register(self.protocol, "protocol_changed", auto=True) + self._register(self.go, "go_clicked", "clicks", auto=True) + self._register(self.up, "up_clicked", "clicks", auto=True) + self._register(self.home, "home_clicked", "clicks", auto=True) + self._register(None, "selection_changed") + self.main.connect("selected", self.selection_changed) + self._register(None, "directory_entered") + self.prev_protocol = self.protocol.value + self.prev_kwargs = self.storage_options + + self.filter_sel = pn.widgets.CheckBoxGroup( + value=[], options=[], inline=False, align="end", width_policy="min" + ) + self._register(self.filter_sel, "filters_changed", auto=True) + + self.panel = pn.Column( + pn.Row(self.protocol, self.kwargs), + pn.Row(self.home, self.up, self.url, self.go, self.filter_sel), + self.main.panel, + ) + self.set_filters(self.filters) + self.go_clicked() + + def set_filters(self, filters=None): + self.filters = filters + if filters: + self.filter_sel.options = filters + self.filter_sel.value = filters + else: + self.filter_sel.options = [] + self.filter_sel.value = [] + + @property + def storage_options(self): + """Value of the kwargs box as a dictionary""" + return ast.literal_eval(self.kwargs.value) or {} + + @property + def fs(self): + """Current filesystem instance""" + if self._fs is None: + cls = get_filesystem_class(self.protocol.value) + self._fs = cls(**self.storage_options) + return self._fs + + @property + def urlpath(self): + """URL of currently selected item""" + return ( + (f"{self.protocol.value}://{self.main.value[0]}") + if self.main.value + else None + ) + + def open_file(self, mode="rb", compression=None, encoding=None): + """Create OpenFile instance for the currently selected item + + For example, in a notebook you might do something like + + .. code-block:: + + [ ]: sel = FileSelector(); sel + + # user selects their file + + [ ]: with sel.open_file('rb') as f: + ... out = f.read() + + Parameters + ---------- + mode: str (optional) + Open mode for the file. + compression: str (optional) + The interact with the file as compressed. Set to 'infer' to guess + compression from the file ending + encoding: str (optional) + If using text mode, use this encoding; defaults to UTF8. + """ + if self.urlpath is None: + raise ValueError("No file selected") + return OpenFile(self.fs, self.urlpath, mode, compression, encoding) + + def filters_changed(self, values): + self.filters = values + self.go_clicked() + + def selection_changed(self, *_): + if self.urlpath is None: + return + if self.fs.isdir(self.urlpath): + self.url.value = self.fs._strip_protocol(self.urlpath) + self.go_clicked() + + def go_clicked(self, *_): + if ( + self.prev_protocol != self.protocol.value + or self.prev_kwargs != self.storage_options + ): + self._fs = None # causes fs to be recreated + self.prev_protocol = self.protocol.value + self.prev_kwargs = self.storage_options + listing = sorted( + self.fs.ls(self.url.value, detail=True), key=lambda x: x["name"] + ) + listing = [ + l + for l in listing + if not any(i.match(l["name"].rsplit("/", 1)[-1]) for i in self.ignore) + ] + folders = { + "📁 " + o["name"].rsplit("/", 1)[-1]: o["name"] + for o in listing + if o["type"] == "directory" + } + files = { + "📄 " + o["name"].rsplit("/", 1)[-1]: o["name"] + for o in listing + if o["type"] == "file" + } + if self.filters: + files = { + k: v + for k, v in files.items() + if any(v.endswith(ext) for ext in self.filters) + } + self.main.set_options(dict(**folders, **files)) + + def protocol_changed(self, *_): + self._fs = None + self.main.options = [] + self.url.value = "" + + def home_clicked(self, *_): + self.protocol.value = self.init_protocol + self.kwargs.value = self.init_kwargs + self.url.value = self.init_url + self.go_clicked() + + def up_clicked(self, *_): + self.url.value = self.fs._parent(self.url.value) + self.go_clicked() diff --git a/lib/python3.10/site-packages/fsspec/json.py b/lib/python3.10/site-packages/fsspec/json.py new file mode 100644 index 0000000000000000000000000000000000000000..3bd2485ef1cc581d608f8627cb4133c198e35293 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/json.py @@ -0,0 +1,117 @@ +import json +from collections.abc import Mapping, Sequence +from contextlib import suppress +from pathlib import PurePath +from typing import ( + Any, + Callable, + ClassVar, + Optional, +) + +from .registry import _import_class, get_filesystem_class +from .spec import AbstractFileSystem + + +class FilesystemJSONEncoder(json.JSONEncoder): + include_password: ClassVar[bool] = True + + def default(self, o: Any) -> Any: + if isinstance(o, AbstractFileSystem): + return o.to_dict(include_password=self.include_password) + if isinstance(o, PurePath): + cls = type(o) + return {"cls": f"{cls.__module__}.{cls.__name__}", "str": str(o)} + + return super().default(o) + + def make_serializable(self, obj: Any) -> Any: + """ + Recursively converts an object so that it can be JSON serialized via + :func:`json.dumps` and :func:`json.dump`, without actually calling + said functions. + """ + if isinstance(obj, (str, int, float, bool)): + return obj + if isinstance(obj, Mapping): + return {k: self.make_serializable(v) for k, v in obj.items()} + if isinstance(obj, Sequence): + return [self.make_serializable(v) for v in obj] + + return self.default(obj) + + +class FilesystemJSONDecoder(json.JSONDecoder): + def __init__( + self, + *, + object_hook: Optional[Callable[[dict[str, Any]], Any]] = None, + parse_float: Optional[Callable[[str], Any]] = None, + parse_int: Optional[Callable[[str], Any]] = None, + parse_constant: Optional[Callable[[str], Any]] = None, + strict: bool = True, + object_pairs_hook: Optional[Callable[[list[tuple[str, Any]]], Any]] = None, + ) -> None: + self.original_object_hook = object_hook + + super().__init__( + object_hook=self.custom_object_hook, + parse_float=parse_float, + parse_int=parse_int, + parse_constant=parse_constant, + strict=strict, + object_pairs_hook=object_pairs_hook, + ) + + @classmethod + def try_resolve_path_cls(cls, dct: dict[str, Any]): + with suppress(Exception): + fqp = dct["cls"] + + path_cls = _import_class(fqp) + + if issubclass(path_cls, PurePath): + return path_cls + + return None + + @classmethod + def try_resolve_fs_cls(cls, dct: dict[str, Any]): + with suppress(Exception): + if "cls" in dct: + try: + fs_cls = _import_class(dct["cls"]) + if issubclass(fs_cls, AbstractFileSystem): + return fs_cls + except Exception: + if "protocol" in dct: # Fallback if cls cannot be imported + return get_filesystem_class(dct["protocol"]) + + raise + + return None + + def custom_object_hook(self, dct: dict[str, Any]): + if "cls" in dct: + if (obj_cls := self.try_resolve_fs_cls(dct)) is not None: + return AbstractFileSystem.from_dict(dct) + if (obj_cls := self.try_resolve_path_cls(dct)) is not None: + return obj_cls(dct["str"]) + + if self.original_object_hook is not None: + return self.original_object_hook(dct) + + return dct + + def unmake_serializable(self, obj: Any) -> Any: + """ + Inverse function of :meth:`FilesystemJSONEncoder.make_serializable`. + """ + if isinstance(obj, dict): + obj = self.custom_object_hook(obj) + if isinstance(obj, dict): + return {k: self.unmake_serializable(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [self.unmake_serializable(v) for v in obj] + + return obj diff --git a/lib/python3.10/site-packages/fsspec/mapping.py b/lib/python3.10/site-packages/fsspec/mapping.py new file mode 100644 index 0000000000000000000000000000000000000000..752eef35273b13eded7297e2e801b58e436a25b1 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/mapping.py @@ -0,0 +1,251 @@ +import array +import logging +import posixpath +import warnings +from collections.abc import MutableMapping +from functools import cached_property + +from fsspec.core import url_to_fs + +logger = logging.getLogger("fsspec.mapping") + + +class FSMap(MutableMapping): + """Wrap a FileSystem instance as a mutable wrapping. + + The keys of the mapping become files under the given root, and the + values (which must be bytes) the contents of those files. + + Parameters + ---------- + root: string + prefix for all the files + fs: FileSystem instance + check: bool (=True) + performs a touch at the location, to check for write access. + + Examples + -------- + >>> fs = FileSystem(**parameters) # doctest: +SKIP + >>> d = FSMap('my-data/path/', fs) # doctest: +SKIP + or, more likely + >>> d = fs.get_mapper('my-data/path/') + + >>> d['loc1'] = b'Hello World' # doctest: +SKIP + >>> list(d.keys()) # doctest: +SKIP + ['loc1'] + >>> d['loc1'] # doctest: +SKIP + b'Hello World' + """ + + def __init__(self, root, fs, check=False, create=False, missing_exceptions=None): + self.fs = fs + self.root = fs._strip_protocol(root) + self._root_key_to_str = fs._strip_protocol(posixpath.join(root, "x"))[:-1] + if missing_exceptions is None: + missing_exceptions = ( + FileNotFoundError, + IsADirectoryError, + NotADirectoryError, + ) + self.missing_exceptions = missing_exceptions + self.check = check + self.create = create + if create: + if not self.fs.exists(root): + self.fs.mkdir(root) + if check: + if not self.fs.exists(root): + raise ValueError( + f"Path {root} does not exist. Create " + f" with the ``create=True`` keyword" + ) + self.fs.touch(root + "/a") + self.fs.rm(root + "/a") + + @cached_property + def dirfs(self): + """dirfs instance that can be used with the same keys as the mapper""" + from .implementations.dirfs import DirFileSystem + + return DirFileSystem(path=self._root_key_to_str, fs=self.fs) + + def clear(self): + """Remove all keys below root - empties out mapping""" + logger.info("Clear mapping at %s", self.root) + try: + self.fs.rm(self.root, True) + self.fs.mkdir(self.root) + except: # noqa: E722 + pass + + def getitems(self, keys, on_error="raise"): + """Fetch multiple items from the store + + If the backend is async-able, this might proceed concurrently + + Parameters + ---------- + keys: list(str) + They keys to be fetched + on_error : "raise", "omit", "return" + If raise, an underlying exception will be raised (converted to KeyError + if the type is in self.missing_exceptions); if omit, keys with exception + will simply not be included in the output; if "return", all keys are + included in the output, but the value will be bytes or an exception + instance. + + Returns + ------- + dict(key, bytes|exception) + """ + keys2 = [self._key_to_str(k) for k in keys] + oe = on_error if on_error == "raise" else "return" + try: + out = self.fs.cat(keys2, on_error=oe) + if isinstance(out, bytes): + out = {keys2[0]: out} + except self.missing_exceptions as e: + raise KeyError from e + out = { + k: (KeyError() if isinstance(v, self.missing_exceptions) else v) + for k, v in out.items() + } + return { + key: out[k2] if on_error == "raise" else out.get(k2, KeyError(k2)) + for key, k2 in zip(keys, keys2) + if on_error == "return" or not isinstance(out[k2], BaseException) + } + + def setitems(self, values_dict): + """Set the values of multiple items in the store + + Parameters + ---------- + values_dict: dict(str, bytes) + """ + values = {self._key_to_str(k): maybe_convert(v) for k, v in values_dict.items()} + self.fs.pipe(values) + + def delitems(self, keys): + """Remove multiple keys from the store""" + self.fs.rm([self._key_to_str(k) for k in keys]) + + def _key_to_str(self, key): + """Generate full path for the key""" + if not isinstance(key, str): + # raise TypeError("key must be of type `str`, got `{type(key).__name__}`" + warnings.warn( + "from fsspec 2023.5 onward FSMap non-str keys will raise TypeError", + DeprecationWarning, + ) + if isinstance(key, list): + key = tuple(key) + key = str(key) + return f"{self._root_key_to_str}{key}".rstrip("/") + + def _str_to_key(self, s): + """Strip path of to leave key name""" + return s[len(self.root) :].lstrip("/") + + def __getitem__(self, key, default=None): + """Retrieve data""" + k = self._key_to_str(key) + try: + result = self.fs.cat(k) + except self.missing_exceptions as exc: + if default is not None: + return default + raise KeyError(key) from exc + return result + + def pop(self, key, default=None): + """Pop data""" + result = self.__getitem__(key, default) + try: + del self[key] + except KeyError: + pass + return result + + def __setitem__(self, key, value): + """Store value in key""" + key = self._key_to_str(key) + self.fs.mkdirs(self.fs._parent(key), exist_ok=True) + self.fs.pipe_file(key, maybe_convert(value)) + + def __iter__(self): + return (self._str_to_key(x) for x in self.fs.find(self.root)) + + def __len__(self): + return len(self.fs.find(self.root)) + + def __delitem__(self, key): + """Remove key""" + try: + self.fs.rm(self._key_to_str(key)) + except Exception as exc: + raise KeyError from exc + + def __contains__(self, key): + """Does key exist in mapping?""" + path = self._key_to_str(key) + return self.fs.isfile(path) + + def __reduce__(self): + return FSMap, (self.root, self.fs, False, False, self.missing_exceptions) + + +def maybe_convert(value): + if isinstance(value, array.array) or hasattr(value, "__array__"): + # bytes-like things + if hasattr(value, "dtype") and value.dtype.kind in "Mm": + # The buffer interface doesn't support datetime64/timdelta64 numpy + # arrays + value = value.view("int64") + value = bytes(memoryview(value)) + return value + + +def get_mapper( + url="", + check=False, + create=False, + missing_exceptions=None, + alternate_root=None, + **kwargs, +): + """Create key-value interface for given URL and options + + The URL will be of the form "protocol://location" and point to the root + of the mapper required. All keys will be file-names below this location, + and their values the contents of each key. + + Also accepts compound URLs like zip::s3://bucket/file.zip , see ``fsspec.open``. + + Parameters + ---------- + url: str + Root URL of mapping + check: bool + Whether to attempt to read from the location before instantiation, to + check that the mapping does exist + create: bool + Whether to make the directory corresponding to the root before + instantiating + missing_exceptions: None or tuple + If given, these exception types will be regarded as missing keys and + return KeyError when trying to read data. By default, you get + (FileNotFoundError, IsADirectoryError, NotADirectoryError) + alternate_root: None or str + In cases of complex URLs, the parser may fail to pick the correct part + for the mapper root, so this arg can override + + Returns + ------- + ``FSMap`` instance, the dict-like key-value store. + """ + # Removing protocol here - could defer to each open() on the backend + fs, urlpath = url_to_fs(url, **kwargs) + root = alternate_root if alternate_root is not None else urlpath + return FSMap(root, fs, check, create, missing_exceptions=missing_exceptions) diff --git a/lib/python3.10/site-packages/fsspec/parquet.py b/lib/python3.10/site-packages/fsspec/parquet.py new file mode 100644 index 0000000000000000000000000000000000000000..faedb7b9e0aa90b6fb7cba33e794c4b4fb35eb77 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/parquet.py @@ -0,0 +1,541 @@ +import io +import json +import warnings + +from .core import url_to_fs +from .utils import merge_offset_ranges + +# Parquet-Specific Utilities for fsspec +# +# Most of the functions defined in this module are NOT +# intended for public consumption. The only exception +# to this is `open_parquet_file`, which should be used +# place of `fs.open()` to open parquet-formatted files +# on remote file systems. + + +def open_parquet_file( + path, + mode="rb", + fs=None, + metadata=None, + columns=None, + row_groups=None, + storage_options=None, + strict=False, + engine="auto", + max_gap=64_000, + max_block=256_000_000, + footer_sample_size=1_000_000, + **kwargs, +): + """ + Return a file-like object for a single Parquet file. + + The specified parquet `engine` will be used to parse the + footer metadata, and determine the required byte ranges + from the file. The target path will then be opened with + the "parts" (`KnownPartsOfAFile`) caching strategy. + + Note that this method is intended for usage with remote + file systems, and is unlikely to improve parquet-read + performance on local file systems. + + Parameters + ---------- + path: str + Target file path. + mode: str, optional + Mode option to be passed through to `fs.open`. Default is "rb". + metadata: Any, optional + Parquet metadata object. Object type must be supported + by the backend parquet engine. For now, only the "fastparquet" + engine supports an explicit `ParquetFile` metadata object. + If a metadata object is supplied, the remote footer metadata + will not need to be transferred into local memory. + fs: AbstractFileSystem, optional + Filesystem object to use for opening the file. If nothing is + specified, an `AbstractFileSystem` object will be inferred. + engine : str, default "auto" + Parquet engine to use for metadata parsing. Allowed options + include "fastparquet", "pyarrow", and "auto". The specified + engine must be installed in the current environment. If + "auto" is specified, and both engines are installed, + "fastparquet" will take precedence over "pyarrow". + columns: list, optional + List of all column names that may be read from the file. + row_groups : list, optional + List of all row-groups that may be read from the file. This + may be a list of row-group indices (integers), or it may be + a list of `RowGroup` metadata objects (if the "fastparquet" + engine is used). + storage_options : dict, optional + Used to generate an `AbstractFileSystem` object if `fs` was + not specified. + strict : bool, optional + Whether the resulting `KnownPartsOfAFile` cache should + fetch reads that go beyond a known byte-range boundary. + If `False` (the default), any read that ends outside a + known part will be zero padded. Note that using + `strict=True` may be useful for debugging. + max_gap : int, optional + Neighboring byte ranges will only be merged when their + inter-range gap is <= `max_gap`. Default is 64KB. + max_block : int, optional + Neighboring byte ranges will only be merged when the size of + the aggregated range is <= `max_block`. Default is 256MB. + footer_sample_size : int, optional + Number of bytes to read from the end of the path to look + for the footer metadata. If the sampled bytes do not contain + the footer, a second read request will be required, and + performance will suffer. Default is 1MB. + **kwargs : + Optional key-word arguments to pass to `fs.open` + """ + + # Make sure we have an `AbstractFileSystem` object + # to work with + if fs is None: + fs = url_to_fs(path, **(storage_options or {}))[0] + + # For now, `columns == []` not supported. Just use + # default `open` command with `path` input + if columns is not None and len(columns) == 0: + return fs.open(path, mode=mode) + + # Set the engine + engine = _set_engine(engine) + + # Fetch the known byte ranges needed to read + # `columns` and/or `row_groups` + data = _get_parquet_byte_ranges( + [path], + fs, + metadata=metadata, + columns=columns, + row_groups=row_groups, + engine=engine, + max_gap=max_gap, + max_block=max_block, + footer_sample_size=footer_sample_size, + ) + + # Extract file name from `data` + fn = next(iter(data)) if data else path + + # Call self.open with "parts" caching + options = kwargs.pop("cache_options", {}).copy() + return fs.open( + fn, + mode=mode, + cache_type="parts", + cache_options={ + **options, + "data": data.get(fn, {}), + "strict": strict, + }, + **kwargs, + ) + + +def _get_parquet_byte_ranges( + paths, + fs, + metadata=None, + columns=None, + row_groups=None, + max_gap=64_000, + max_block=256_000_000, + footer_sample_size=1_000_000, + engine="auto", +): + """Get a dictionary of the known byte ranges needed + to read a specific column/row-group selection from a + Parquet dataset. Each value in the output dictionary + is intended for use as the `data` argument for the + `KnownPartsOfAFile` caching strategy of a single path. + """ + + # Set engine if necessary + if isinstance(engine, str): + engine = _set_engine(engine) + + # Pass to specialized function if metadata is defined + if metadata is not None: + # Use the provided parquet metadata object + # to avoid transferring/parsing footer metadata + return _get_parquet_byte_ranges_from_metadata( + metadata, + fs, + engine, + columns=columns, + row_groups=row_groups, + max_gap=max_gap, + max_block=max_block, + ) + + # Get file sizes asynchronously + file_sizes = fs.sizes(paths) + + # Populate global paths, starts, & ends + result = {} + data_paths = [] + data_starts = [] + data_ends = [] + add_header_magic = True + if columns is None and row_groups is None: + # We are NOT selecting specific columns or row-groups. + # + # We can avoid sampling the footers, and just transfer + # all file data with cat_ranges + for i, path in enumerate(paths): + result[path] = {} + for b in range(0, file_sizes[i], max_block): + data_paths.append(path) + data_starts.append(b) + data_ends.append(min(b + max_block, file_sizes[i])) + add_header_magic = False # "Magic" should already be included + else: + # We ARE selecting specific columns or row-groups. + # + # Gather file footers. + # We just take the last `footer_sample_size` bytes of each + # file (or the entire file if it is smaller than that) + footer_starts = [] + footer_ends = [] + for i, path in enumerate(paths): + footer_ends.append(file_sizes[i]) + sample_size = max(0, file_sizes[i] - footer_sample_size) + footer_starts.append(sample_size) + footer_samples = fs.cat_ranges(paths, footer_starts, footer_ends) + + # Check our footer samples and re-sample if necessary. + missing_footer_starts = footer_starts.copy() + large_footer = 0 + for i, path in enumerate(paths): + footer_size = int.from_bytes(footer_samples[i][-8:-4], "little") + real_footer_start = file_sizes[i] - (footer_size + 8) + if real_footer_start < footer_starts[i]: + missing_footer_starts[i] = real_footer_start + large_footer = max(large_footer, (footer_size + 8)) + if large_footer: + warnings.warn( + f"Not enough data was used to sample the parquet footer. " + f"Try setting footer_sample_size >= {large_footer}." + ) + for i, block in enumerate( + fs.cat_ranges( + paths, + missing_footer_starts, + footer_starts, + ) + ): + footer_samples[i] = block + footer_samples[i] + footer_starts[i] = missing_footer_starts[i] + + # Calculate required byte ranges for each path + for i, path in enumerate(paths): + # Deal with small-file case. + # Just include all remaining bytes of the file + # in a single range. + if file_sizes[i] < max_block: + if footer_starts[i] > 0: + # Only need to transfer the data if the + # footer sample isn't already the whole file + data_paths.append(path) + data_starts.append(0) + data_ends.append(footer_starts[i]) + continue + + # Use "engine" to collect data byte ranges + path_data_starts, path_data_ends = engine._parquet_byte_ranges( + columns, + row_groups=row_groups, + footer=footer_samples[i], + footer_start=footer_starts[i], + ) + + data_paths += [path] * len(path_data_starts) + data_starts += path_data_starts + data_ends += path_data_ends + + # Merge adjacent offset ranges + data_paths, data_starts, data_ends = merge_offset_ranges( + data_paths, + data_starts, + data_ends, + max_gap=max_gap, + max_block=max_block, + sort=False, # Should already be sorted + ) + + # Start by populating `result` with footer samples + for i, path in enumerate(paths): + result[path] = {(footer_starts[i], footer_ends[i]): footer_samples[i]} + + # Transfer the data byte-ranges into local memory + _transfer_ranges(fs, result, data_paths, data_starts, data_ends) + + # Add b"PAR1" to header if necessary + if add_header_magic: + _add_header_magic(result) + + return result + + +def _get_parquet_byte_ranges_from_metadata( + metadata, + fs, + engine, + columns=None, + row_groups=None, + max_gap=64_000, + max_block=256_000_000, +): + """Simplified version of `_get_parquet_byte_ranges` for + the case that an engine-specific `metadata` object is + provided, and the remote footer metadata does not need to + be transferred before calculating the required byte ranges. + """ + + # Use "engine" to collect data byte ranges + data_paths, data_starts, data_ends = engine._parquet_byte_ranges( + columns, + row_groups=row_groups, + metadata=metadata, + ) + + # Merge adjacent offset ranges + data_paths, data_starts, data_ends = merge_offset_ranges( + data_paths, + data_starts, + data_ends, + max_gap=max_gap, + max_block=max_block, + sort=False, # Should be sorted + ) + + # Transfer the data byte-ranges into local memory + result = {fn: {} for fn in list(set(data_paths))} + _transfer_ranges(fs, result, data_paths, data_starts, data_ends) + + # Add b"PAR1" to header + _add_header_magic(result) + + return result + + +def _transfer_ranges(fs, blocks, paths, starts, ends): + # Use cat_ranges to gather the data byte_ranges + ranges = (paths, starts, ends) + for path, start, stop, data in zip(*ranges, fs.cat_ranges(*ranges)): + blocks[path][(start, stop)] = data + + +def _add_header_magic(data): + # Add b"PAR1" to file headers + for path in list(data.keys()): + add_magic = True + for k in data[path]: + if k[0] == 0 and k[1] >= 4: + add_magic = False + break + if add_magic: + data[path][(0, 4)] = b"PAR1" + + +def _set_engine(engine_str): + # Define a list of parquet engines to try + if engine_str == "auto": + try_engines = ("fastparquet", "pyarrow") + elif not isinstance(engine_str, str): + raise ValueError( + "Failed to set parquet engine! " + "Please pass 'fastparquet', 'pyarrow', or 'auto'" + ) + elif engine_str not in ("fastparquet", "pyarrow"): + raise ValueError(f"{engine_str} engine not supported by `fsspec.parquet`") + else: + try_engines = [engine_str] + + # Try importing the engines in `try_engines`, + # and choose the first one that succeeds + for engine in try_engines: + try: + if engine == "fastparquet": + return FastparquetEngine() + elif engine == "pyarrow": + return PyarrowEngine() + except ImportError: + pass + + # Raise an error if a supported parquet engine + # was not found + raise ImportError( + f"The following parquet engines are not installed " + f"in your python environment: {try_engines}." + f"Please install 'fastparquert' or 'pyarrow' to " + f"utilize the `fsspec.parquet` module." + ) + + +class FastparquetEngine: + # The purpose of the FastparquetEngine class is + # to check if fastparquet can be imported (on initialization) + # and to define a `_parquet_byte_ranges` method. In the + # future, this class may also be used to define other + # methods/logic that are specific to fastparquet. + + def __init__(self): + import fastparquet as fp + + self.fp = fp + + def _row_group_filename(self, row_group, pf): + return pf.row_group_filename(row_group) + + def _parquet_byte_ranges( + self, + columns, + row_groups=None, + metadata=None, + footer=None, + footer_start=None, + ): + # Initialize offset ranges and define ParqetFile metadata + pf = metadata + data_paths, data_starts, data_ends = [], [], [] + if pf is None: + pf = self.fp.ParquetFile(io.BytesIO(footer)) + + # Convert columns to a set and add any index columns + # specified in the pandas metadata (just in case) + column_set = None if columns is None else set(columns) + if column_set is not None and hasattr(pf, "pandas_metadata"): + md_index = [ + ind + for ind in pf.pandas_metadata.get("index_columns", []) + # Ignore RangeIndex information + if not isinstance(ind, dict) + ] + column_set |= set(md_index) + + # Check if row_groups is a list of integers + # or a list of row-group metadata + if row_groups and not isinstance(row_groups[0], int): + # Input row_groups contains row-group metadata + row_group_indices = None + else: + # Input row_groups contains row-group indices + row_group_indices = row_groups + row_groups = pf.row_groups + + # Loop through column chunks to add required byte ranges + for r, row_group in enumerate(row_groups): + # Skip this row-group if we are targeting + # specific row-groups + if row_group_indices is None or r in row_group_indices: + # Find the target parquet-file path for `row_group` + fn = self._row_group_filename(row_group, pf) + + for column in row_group.columns: + name = column.meta_data.path_in_schema[0] + # Skip this column if we are targeting a + # specific columns + if column_set is None or name in column_set: + file_offset0 = column.meta_data.dictionary_page_offset + if file_offset0 is None: + file_offset0 = column.meta_data.data_page_offset + num_bytes = column.meta_data.total_compressed_size + if footer_start is None or file_offset0 < footer_start: + data_paths.append(fn) + data_starts.append(file_offset0) + data_ends.append( + min( + file_offset0 + num_bytes, + footer_start or (file_offset0 + num_bytes), + ) + ) + + if metadata: + # The metadata in this call may map to multiple + # file paths. Need to include `data_paths` + return data_paths, data_starts, data_ends + return data_starts, data_ends + + +class PyarrowEngine: + # The purpose of the PyarrowEngine class is + # to check if pyarrow can be imported (on initialization) + # and to define a `_parquet_byte_ranges` method. In the + # future, this class may also be used to define other + # methods/logic that are specific to pyarrow. + + def __init__(self): + import pyarrow.parquet as pq + + self.pq = pq + + def _row_group_filename(self, row_group, metadata): + raise NotImplementedError + + def _parquet_byte_ranges( + self, + columns, + row_groups=None, + metadata=None, + footer=None, + footer_start=None, + ): + if metadata is not None: + raise ValueError("metadata input not supported for PyarrowEngine") + + data_starts, data_ends = [], [] + md = self.pq.ParquetFile(io.BytesIO(footer)).metadata + + # Convert columns to a set and add any index columns + # specified in the pandas metadata (just in case) + column_set = None if columns is None else set(columns) + if column_set is not None: + schema = md.schema.to_arrow_schema() + has_pandas_metadata = ( + schema.metadata is not None and b"pandas" in schema.metadata + ) + if has_pandas_metadata: + md_index = [ + ind + for ind in json.loads( + schema.metadata[b"pandas"].decode("utf8") + ).get("index_columns", []) + # Ignore RangeIndex information + if not isinstance(ind, dict) + ] + column_set |= set(md_index) + + # Loop through column chunks to add required byte ranges + for r in range(md.num_row_groups): + # Skip this row-group if we are targeting + # specific row-groups + if row_groups is None or r in row_groups: + row_group = md.row_group(r) + for c in range(row_group.num_columns): + column = row_group.column(c) + name = column.path_in_schema + # Skip this column if we are targeting a + # specific columns + split_name = name.split(".")[0] + if ( + column_set is None + or name in column_set + or split_name in column_set + ): + file_offset0 = column.dictionary_page_offset + if file_offset0 is None: + file_offset0 = column.data_page_offset + num_bytes = column.total_compressed_size + if file_offset0 < footer_start: + data_starts.append(file_offset0) + data_ends.append( + min(file_offset0 + num_bytes, footer_start) + ) + return data_starts, data_ends diff --git a/lib/python3.10/site-packages/fsspec/registry.py b/lib/python3.10/site-packages/fsspec/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..96ffad7f4a889662c8fb4a006e1c497652992430 --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/registry.py @@ -0,0 +1,330 @@ +from __future__ import annotations + +import importlib +import types +import warnings + +__all__ = ["registry", "get_filesystem_class", "default"] + +# internal, mutable +_registry: dict[str, type] = {} + +# external, immutable +registry = types.MappingProxyType(_registry) +default = "file" + + +def register_implementation(name, cls, clobber=False, errtxt=None): + """Add implementation class to the registry + + Parameters + ---------- + name: str + Protocol name to associate with the class + cls: class or str + if a class: fsspec-compliant implementation class (normally inherits from + ``fsspec.AbstractFileSystem``, gets added straight to the registry. If a + str, the full path to an implementation class like package.module.class, + which gets added to known_implementations, + so the import is deferred until the filesystem is actually used. + clobber: bool (optional) + Whether to overwrite a protocol with the same name; if False, will raise + instead. + errtxt: str (optional) + If given, then a failure to import the given class will result in this + text being given. + """ + if isinstance(cls, str): + if name in known_implementations and clobber is False: + if cls != known_implementations[name]["class"]: + raise ValueError( + f"Name ({name}) already in the known_implementations and clobber " + f"is False" + ) + else: + known_implementations[name] = { + "class": cls, + "err": errtxt or f"{cls} import failed for protocol {name}", + } + + else: + if name in registry and clobber is False: + if _registry[name] is not cls: + raise ValueError( + f"Name ({name}) already in the registry and clobber is False" + ) + else: + _registry[name] = cls + + +# protocols mapped to the class which implements them. This dict can be +# updated with register_implementation +known_implementations = { + "abfs": { + "class": "adlfs.AzureBlobFileSystem", + "err": "Install adlfs to access Azure Datalake Gen2 and Azure Blob Storage", + }, + "adl": { + "class": "adlfs.AzureDatalakeFileSystem", + "err": "Install adlfs to access Azure Datalake Gen1", + }, + "arrow_hdfs": { + "class": "fsspec.implementations.arrow.HadoopFileSystem", + "err": "pyarrow and local java libraries required for HDFS", + }, + "asynclocal": { + "class": "morefs.asyn_local.AsyncLocalFileSystem", + "err": "Install 'morefs[asynclocalfs]' to use AsyncLocalFileSystem", + }, + "asyncwrapper": { + "class": "fsspec.implementations.asyn_wrapper.AsyncFileSystemWrapper", + }, + "az": { + "class": "adlfs.AzureBlobFileSystem", + "err": "Install adlfs to access Azure Datalake Gen2 and Azure Blob Storage", + }, + "blockcache": {"class": "fsspec.implementations.cached.CachingFileSystem"}, + "box": { + "class": "boxfs.BoxFileSystem", + "err": "Please install boxfs to access BoxFileSystem", + }, + "cached": {"class": "fsspec.implementations.cached.CachingFileSystem"}, + "dask": { + "class": "fsspec.implementations.dask.DaskWorkerFileSystem", + "err": "Install dask distributed to access worker file system", + }, + "data": {"class": "fsspec.implementations.data.DataFileSystem"}, + "dbfs": { + "class": "fsspec.implementations.dbfs.DatabricksFileSystem", + "err": "Install the requests package to use the DatabricksFileSystem", + }, + "dir": {"class": "fsspec.implementations.dirfs.DirFileSystem"}, + "dropbox": { + "class": "dropboxdrivefs.DropboxDriveFileSystem", + "err": ( + 'DropboxFileSystem requires "dropboxdrivefs","requests" and "' + '"dropbox" to be installed' + ), + }, + "dvc": { + "class": "dvc.api.DVCFileSystem", + "err": "Install dvc to access DVCFileSystem", + }, + "file": {"class": "fsspec.implementations.local.LocalFileSystem"}, + "filecache": {"class": "fsspec.implementations.cached.WholeFileCacheFileSystem"}, + "ftp": {"class": "fsspec.implementations.ftp.FTPFileSystem"}, + "gcs": { + "class": "gcsfs.GCSFileSystem", + "err": "Please install gcsfs to access Google Storage", + }, + "gdrive": { + "class": "gdrive_fsspec.GoogleDriveFileSystem", + "err": "Please install gdrive_fs for access to Google Drive", + }, + "generic": {"class": "fsspec.generic.GenericFileSystem"}, + "gist": { + "class": "fsspec.implementations.gist.GistFileSystem", + "err": "Install the requests package to use the gist FS", + }, + "git": { + "class": "fsspec.implementations.git.GitFileSystem", + "err": "Install pygit2 to browse local git repos", + }, + "github": { + "class": "fsspec.implementations.github.GithubFileSystem", + "err": "Install the requests package to use the github FS", + }, + "gs": { + "class": "gcsfs.GCSFileSystem", + "err": "Please install gcsfs to access Google Storage", + }, + "hdfs": { + "class": "fsspec.implementations.arrow.HadoopFileSystem", + "err": "pyarrow and local java libraries required for HDFS", + }, + "hf": { + "class": "huggingface_hub.HfFileSystem", + "err": "Install huggingface_hub to access HfFileSystem", + }, + "http": { + "class": "fsspec.implementations.http.HTTPFileSystem", + "err": 'HTTPFileSystem requires "requests" and "aiohttp" to be installed', + }, + "https": { + "class": "fsspec.implementations.http.HTTPFileSystem", + "err": 'HTTPFileSystem requires "requests" and "aiohttp" to be installed', + }, + "jlab": { + "class": "fsspec.implementations.jupyter.JupyterFileSystem", + "err": "Jupyter FS requires requests to be installed", + }, + "jupyter": { + "class": "fsspec.implementations.jupyter.JupyterFileSystem", + "err": "Jupyter FS requires requests to be installed", + }, + "lakefs": { + "class": "lakefs_spec.LakeFSFileSystem", + "err": "Please install lakefs-spec to access LakeFSFileSystem", + }, + "libarchive": { + "class": "fsspec.implementations.libarchive.LibArchiveFileSystem", + "err": "LibArchive requires to be installed", + }, + "local": {"class": "fsspec.implementations.local.LocalFileSystem"}, + "memory": {"class": "fsspec.implementations.memory.MemoryFileSystem"}, + "oci": { + "class": "ocifs.OCIFileSystem", + "err": "Install ocifs to access OCI Object Storage", + }, + "ocilake": { + "class": "ocifs.OCIFileSystem", + "err": "Install ocifs to access OCI Data Lake", + }, + "oss": { + "class": "ossfs.OSSFileSystem", + "err": "Install ossfs to access Alibaba Object Storage System", + }, + "pyscript": { + "class": "pyscript_fsspec_client.client.PyscriptFileSystem", + "err": "Install requests (cpython) or run in pyscript", + }, + "reference": {"class": "fsspec.implementations.reference.ReferenceFileSystem"}, + "root": { + "class": "fsspec_xrootd.XRootDFileSystem", + "err": ( + "Install fsspec-xrootd to access xrootd storage system. " + "Note: 'root' is the protocol name for xrootd storage systems, " + "not referring to root directories" + ), + }, + "s3": {"class": "s3fs.S3FileSystem", "err": "Install s3fs to access S3"}, + "s3a": {"class": "s3fs.S3FileSystem", "err": "Install s3fs to access S3"}, + "sftp": { + "class": "fsspec.implementations.sftp.SFTPFileSystem", + "err": 'SFTPFileSystem requires "paramiko" to be installed', + }, + "simplecache": {"class": "fsspec.implementations.cached.SimpleCacheFileSystem"}, + "smb": { + "class": "fsspec.implementations.smb.SMBFileSystem", + "err": 'SMB requires "smbprotocol" or "smbprotocol[kerberos]" installed', + }, + "ssh": { + "class": "fsspec.implementations.sftp.SFTPFileSystem", + "err": 'SFTPFileSystem requires "paramiko" to be installed', + }, + "tar": {"class": "fsspec.implementations.tar.TarFileSystem"}, + "tos": { + "class": "tosfs.TosFileSystem", + "err": "Install tosfs to access ByteDance volcano engine Tinder Object Storage", + }, + "tosfs": { + "class": "tosfs.TosFileSystem", + "err": "Install tosfs to access ByteDance volcano engine Tinder Object Storage", + }, + "wandb": {"class": "wandbfs.WandbFS", "err": "Install wandbfs to access wandb"}, + "webdav": { + "class": "webdav4.fsspec.WebdavFileSystem", + "err": "Install webdav4 to access WebDAV", + }, + "webhdfs": { + "class": "fsspec.implementations.webhdfs.WebHDFS", + "err": 'webHDFS access requires "requests" to be installed', + }, + "zip": {"class": "fsspec.implementations.zip.ZipFileSystem"}, +} + +assert list(known_implementations) == sorted(known_implementations), ( + "Not in alphabetical order" +) + + +def get_filesystem_class(protocol): + """Fetch named protocol implementation from the registry + + The dict ``known_implementations`` maps protocol names to the locations + of classes implementing the corresponding file-system. When used for the + first time, appropriate imports will happen and the class will be placed in + the registry. All subsequent calls will fetch directly from the registry. + + Some protocol implementations require additional dependencies, and so the + import may fail. In this case, the string in the "err" field of the + ``known_implementations`` will be given as the error message. + """ + if not protocol: + protocol = default + + if protocol not in registry: + if protocol not in known_implementations: + raise ValueError(f"Protocol not known: {protocol}") + bit = known_implementations[protocol] + try: + register_implementation(protocol, _import_class(bit["class"])) + except ImportError as e: + raise ImportError(bit.get("err")) from e + cls = registry[protocol] + if getattr(cls, "protocol", None) in ("abstract", None): + cls.protocol = protocol + + return cls + + +s3_msg = """Your installed version of s3fs is very old and known to cause +severe performance issues, see also https://github.com/dask/dask/issues/10276 + +To fix, you should specify a lower version bound on s3fs, or +update the current installation. +""" + + +def _import_class(fqp: str): + """Take a fully-qualified path and return the imported class or identifier. + + ``fqp`` is of the form "package.module.klass" or + "package.module:subobject.klass". + + Warnings + -------- + This can import arbitrary modules. Make sure you haven't installed any modules + that may execute malicious code at import time. + """ + if ":" in fqp: + mod, name = fqp.rsplit(":", 1) + else: + mod, name = fqp.rsplit(".", 1) + + is_s3 = mod == "s3fs" + mod = importlib.import_module(mod) + if is_s3 and mod.__version__.split(".") < ["0", "5"]: + warnings.warn(s3_msg) + for part in name.split("."): + mod = getattr(mod, part) + + if not isinstance(mod, type): + raise TypeError(f"{fqp} is not a class") + + return mod + + +def filesystem(protocol, **storage_options): + """Instantiate filesystems for given protocol and arguments + + ``storage_options`` are specific to the protocol being chosen, and are + passed directly to the class. + """ + if protocol == "arrow_hdfs": + warnings.warn( + "The 'arrow_hdfs' protocol has been deprecated and will be " + "removed in the future. Specify it as 'hdfs'.", + DeprecationWarning, + ) + + cls = get_filesystem_class(protocol) + return cls(**storage_options) + + +def available_protocols(): + """Return a list of the implemented protocols. + + Note that any given protocol may require extra packages to be importable. + """ + return list(known_implementations) diff --git a/lib/python3.10/site-packages/fsspec/spec.py b/lib/python3.10/site-packages/fsspec/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..5f6f9a10441c12ef8db4870c4e5dc72262037c6e --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/spec.py @@ -0,0 +1,2270 @@ +from __future__ import annotations + +import io +import json +import logging +import os +import threading +import warnings +import weakref +from errno import ESPIPE +from glob import has_magic +from hashlib import sha256 +from typing import Any, ClassVar + +from .callbacks import DEFAULT_CALLBACK +from .config import apply_config, conf +from .dircache import DirCache +from .transaction import Transaction +from .utils import ( + _unstrip_protocol, + glob_translate, + isfilelike, + other_paths, + read_block, + stringify_path, + tokenize, +) + +logger = logging.getLogger("fsspec") + + +def make_instance(cls, args, kwargs): + return cls(*args, **kwargs) + + +class _Cached(type): + """ + Metaclass for caching file system instances. + + Notes + ----- + Instances are cached according to + + * The values of the class attributes listed in `_extra_tokenize_attributes` + * The arguments passed to ``__init__``. + + This creates an additional reference to the filesystem, which prevents the + filesystem from being garbage collected when all *user* references go away. + A call to the :meth:`AbstractFileSystem.clear_instance_cache` must *also* + be made for a filesystem instance to be garbage collected. + """ + + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + # Note: we intentionally create a reference here, to avoid garbage + # collecting instances when all other references are gone. To really + # delete a FileSystem, the cache must be cleared. + if conf.get("weakref_instance_cache"): # pragma: no cover + # debug option for analysing fork/spawn conditions + cls._cache = weakref.WeakValueDictionary() + else: + cls._cache = {} + cls._pid = os.getpid() + + def __call__(cls, *args, **kwargs): + kwargs = apply_config(cls, kwargs) + extra_tokens = tuple( + getattr(cls, attr, None) for attr in cls._extra_tokenize_attributes + ) + token = tokenize( + cls, cls._pid, threading.get_ident(), *args, *extra_tokens, **kwargs + ) + skip = kwargs.pop("skip_instance_cache", False) + if os.getpid() != cls._pid: + cls._cache.clear() + cls._pid = os.getpid() + if not skip and cls.cachable and token in cls._cache: + cls._latest = token + return cls._cache[token] + else: + obj = super().__call__(*args, **kwargs) + # Setting _fs_token here causes some static linters to complain. + obj._fs_token_ = token + obj.storage_args = args + obj.storage_options = kwargs + if obj.async_impl and obj.mirror_sync_methods: + from .asyn import mirror_sync_methods + + mirror_sync_methods(obj) + + if cls.cachable and not skip: + cls._latest = token + cls._cache[token] = obj + return obj + + +class AbstractFileSystem(metaclass=_Cached): + """ + An abstract super-class for pythonic file-systems + + Implementations are expected to be compatible with or, better, subclass + from here. + """ + + cachable = True # this class can be cached, instances reused + _cached = False + blocksize = 2**22 + sep = "/" + protocol: ClassVar[str | tuple[str, ...]] = "abstract" + _latest = None + async_impl = False + mirror_sync_methods = False + root_marker = "" # For some FSs, may require leading '/' or other character + transaction_type = Transaction + + #: Extra *class attributes* that should be considered when hashing. + _extra_tokenize_attributes = () + + # Set by _Cached metaclass + storage_args: tuple[Any, ...] + storage_options: dict[str, Any] + + def __init__(self, *args, **storage_options): + """Create and configure file-system instance + + Instances may be cachable, so if similar enough arguments are seen + a new instance is not required. The token attribute exists to allow + implementations to cache instances if they wish. + + A reasonable default should be provided if there are no arguments. + + Subclasses should call this method. + + Parameters + ---------- + use_listings_cache, listings_expiry_time, max_paths: + passed to ``DirCache``, if the implementation supports + directory listing caching. Pass use_listings_cache=False + to disable such caching. + skip_instance_cache: bool + If this is a cachable implementation, pass True here to force + creating a new instance even if a matching instance exists, and prevent + storing this instance. + asynchronous: bool + loop: asyncio-compatible IOLoop or None + """ + if self._cached: + # reusing instance, don't change + return + self._cached = True + self._intrans = False + self._transaction = None + self._invalidated_caches_in_transaction = [] + self.dircache = DirCache(**storage_options) + + if storage_options.pop("add_docs", None): + warnings.warn("add_docs is no longer supported.", FutureWarning) + + if storage_options.pop("add_aliases", None): + warnings.warn("add_aliases has been removed.", FutureWarning) + # This is set in _Cached + self._fs_token_ = None + + @property + def fsid(self): + """Persistent filesystem id that can be used to compare filesystems + across sessions. + """ + raise NotImplementedError + + @property + def _fs_token(self): + return self._fs_token_ + + def __dask_tokenize__(self): + return self._fs_token + + def __hash__(self): + return int(self._fs_token, 16) + + def __eq__(self, other): + return isinstance(other, type(self)) and self._fs_token == other._fs_token + + def __reduce__(self): + return make_instance, (type(self), self.storage_args, self.storage_options) + + @classmethod + def _strip_protocol(cls, path): + """Turn path from fully-qualified to file-system-specific + + May require FS-specific handling, e.g., for relative paths or links. + """ + if isinstance(path, list): + return [cls._strip_protocol(p) for p in path] + path = stringify_path(path) + protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol + for protocol in protos: + if path.startswith(protocol + "://"): + path = path[len(protocol) + 3 :] + elif path.startswith(protocol + "::"): + path = path[len(protocol) + 2 :] + path = path.rstrip("/") + # use of root_marker to make minimum required path, e.g., "/" + return path or cls.root_marker + + def unstrip_protocol(self, name: str) -> str: + """Format FS-specific path to generic, including protocol""" + protos = (self.protocol,) if isinstance(self.protocol, str) else self.protocol + for protocol in protos: + if name.startswith(f"{protocol}://"): + return name + return f"{protos[0]}://{name}" + + @staticmethod + def _get_kwargs_from_urls(path): + """If kwargs can be encoded in the paths, extract them here + + This should happen before instantiation of the class; incoming paths + then should be amended to strip the options in methods. + + Examples may look like an sftp path "sftp://user@host:/my/path", where + the user and host should become kwargs and later get stripped. + """ + # by default, nothing happens + return {} + + @classmethod + def current(cls): + """Return the most recently instantiated FileSystem + + If no instance has been created, then create one with defaults + """ + if cls._latest in cls._cache: + return cls._cache[cls._latest] + return cls() + + @property + def transaction(self): + """A context within which files are committed together upon exit + + Requires the file class to implement `.commit()` and `.discard()` + for the normal and exception cases. + """ + if self._transaction is None: + self._transaction = self.transaction_type(self) + return self._transaction + + def start_transaction(self): + """Begin write transaction for deferring files, non-context version""" + self._intrans = True + self._transaction = self.transaction_type(self) + return self.transaction + + def end_transaction(self): + """Finish write transaction, non-context version""" + self.transaction.complete() + self._transaction = None + # The invalid cache must be cleared after the transaction is completed. + for path in self._invalidated_caches_in_transaction: + self.invalidate_cache(path) + self._invalidated_caches_in_transaction.clear() + + def invalidate_cache(self, path=None): + """ + Discard any cached directory information + + Parameters + ---------- + path: string or None + If None, clear all listings cached else listings at or under given + path. + """ + # Not necessary to implement invalidation mechanism, may have no cache. + # But if have, you should call this method of parent class from your + # subclass to ensure expiring caches after transacations correctly. + # See the implementation of FTPFileSystem in ftp.py + if self._intrans: + self._invalidated_caches_in_transaction.append(path) + + def mkdir(self, path, create_parents=True, **kwargs): + """ + Create directory entry at path + + For systems that don't have true directories, may create an for + this instance only and not touch the real filesystem + + Parameters + ---------- + path: str + location + create_parents: bool + if True, this is equivalent to ``makedirs`` + kwargs: + may be permissions, etc. + """ + pass # not necessary to implement, may not have directories + + def makedirs(self, path, exist_ok=False): + """Recursively make directories + + Creates directory at path and any intervening required directories. + Raises exception if, for instance, the path already exists but is a + file. + + Parameters + ---------- + path: str + leaf directory name + exist_ok: bool (False) + If False, will error if the target already exists + """ + pass # not necessary to implement, may not have directories + + def rmdir(self, path): + """Remove a directory, if empty""" + pass # not necessary to implement, may not have directories + + def ls(self, path, detail=True, **kwargs): + """List objects at path. + + This should include subdirectories and files at that location. The + difference between a file and a directory must be clear when details + are requested. + + The specific keys, or perhaps a FileInfo class, or similar, is TBD, + but must be consistent across implementations. + Must include: + + - full path to the entry (without protocol) + - size of the entry, in bytes. If the value cannot be determined, will + be ``None``. + - type of entry, "file", "directory" or other + + Additional information + may be present, appropriate to the file-system, e.g., generation, + checksum, etc. + + May use refresh=True|False to allow use of self._ls_from_cache to + check for a saved listing and avoid calling the backend. This would be + common where listing may be expensive. + + Parameters + ---------- + path: str + detail: bool + if True, gives a list of dictionaries, where each is the same as + the result of ``info(path)``. If False, gives a list of paths + (str). + kwargs: may have additional backend-specific options, such as version + information + + Returns + ------- + List of strings if detail is False, or list of directory information + dicts if detail is True. + """ + raise NotImplementedError + + def _ls_from_cache(self, path): + """Check cache for listing + + Returns listing, if found (may be empty list for a directly that exists + but contains nothing), None if not in cache. + """ + parent = self._parent(path) + try: + return self.dircache[path.rstrip("/")] + except KeyError: + pass + try: + files = [ + f + for f in self.dircache[parent] + if f["name"] == path + or (f["name"] == path.rstrip("/") and f["type"] == "directory") + ] + if len(files) == 0: + # parent dir was listed but did not contain this file + raise FileNotFoundError(path) + return files + except KeyError: + pass + + def walk(self, path, maxdepth=None, topdown=True, on_error="omit", **kwargs): + """Return all files under the given path. + + List all files, recursing into subdirectories; output is iterator-style, + like ``os.walk()``. For a simple list of files, ``find()`` is available. + + When topdown is True, the caller can modify the dirnames list in-place (perhaps + using del or slice assignment), and walk() will + only recurse into the subdirectories whose names remain in dirnames; + this can be used to prune the search, impose a specific order of visiting, + or even to inform walk() about directories the caller creates or renames before + it resumes walk() again. + Modifying dirnames when topdown is False has no effect. (see os.walk) + + Note that the "files" outputted will include anything that is not + a directory, such as links. + + Parameters + ---------- + path: str + Root to recurse into + maxdepth: int + Maximum recursion depth. None means limitless, but not recommended + on link-based file-systems. + topdown: bool (True) + Whether to walk the directory tree from the top downwards or from + the bottom upwards. + on_error: "omit", "raise", a callable + if omit (default), path with exception will simply be empty; + If raise, an underlying exception will be raised; + if callable, it will be called with a single OSError instance as argument + kwargs: passed to ``ls`` + """ + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + path = self._strip_protocol(path) + full_dirs = {} + dirs = {} + files = {} + + detail = kwargs.pop("detail", False) + try: + listing = self.ls(path, detail=True, **kwargs) + except (FileNotFoundError, OSError) as e: + if on_error == "raise": + raise + if callable(on_error): + on_error(e) + return + + for info in listing: + # each info name must be at least [path]/part , but here + # we check also for names like [path]/part/ + pathname = info["name"].rstrip("/") + name = pathname.rsplit("/", 1)[-1] + if info["type"] == "directory" and pathname != path: + # do not include "self" path + full_dirs[name] = pathname + dirs[name] = info + elif pathname == path: + # file-like with same name as give path + files[""] = info + else: + files[name] = info + + if not detail: + dirs = list(dirs) + files = list(files) + + if topdown: + # Yield before recursion if walking top down + yield path, dirs, files + + if maxdepth is not None: + maxdepth -= 1 + if maxdepth < 1: + if not topdown: + yield path, dirs, files + return + + for d in dirs: + yield from self.walk( + full_dirs[d], + maxdepth=maxdepth, + detail=detail, + topdown=topdown, + **kwargs, + ) + + if not topdown: + # Yield after recursion if walking bottom up + yield path, dirs, files + + def find(self, path, maxdepth=None, withdirs=False, detail=False, **kwargs): + """List all files below path. + + Like posix ``find`` command without conditions + + Parameters + ---------- + path : str + maxdepth: int or None + If not None, the maximum number of levels to descend + withdirs: bool + Whether to include directory paths in the output. This is True + when used by glob, but users usually only want files. + kwargs are passed to ``ls``. + """ + # TODO: allow equivalent of -name parameter + path = self._strip_protocol(path) + out = {} + + # Add the root directory if withdirs is requested + # This is needed for posix glob compliance + if withdirs and path != "" and self.isdir(path): + out[path] = self.info(path) + + for _, dirs, files in self.walk(path, maxdepth, detail=True, **kwargs): + if withdirs: + files.update(dirs) + out.update({info["name"]: info for name, info in files.items()}) + if not out and self.isfile(path): + # walk works on directories, but find should also return [path] + # when path happens to be a file + out[path] = {} + names = sorted(out) + if not detail: + return names + else: + return {name: out[name] for name in names} + + def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs): + """Space used by files and optionally directories within a path + + Directory size does not include the size of its contents. + + Parameters + ---------- + path: str + total: bool + Whether to sum all the file sizes + maxdepth: int or None + Maximum number of directory levels to descend, None for unlimited. + withdirs: bool + Whether to include directory paths in the output. + kwargs: passed to ``find`` + + Returns + ------- + Dict of {path: size} if total=False, or int otherwise, where numbers + refer to bytes used. + """ + sizes = {} + if withdirs and self.isdir(path): + # Include top-level directory in output + info = self.info(path) + sizes[info["name"]] = info["size"] + for f in self.find(path, maxdepth=maxdepth, withdirs=withdirs, **kwargs): + info = self.info(f) + sizes[info["name"]] = info["size"] + if total: + return sum(sizes.values()) + else: + return sizes + + def glob(self, path, maxdepth=None, **kwargs): + """Find files by glob-matching. + + Pattern matching capabilities for finding files that match the given pattern. + + Parameters + ---------- + path: str + The glob pattern to match against + maxdepth: int or None + Maximum depth for ``'**'`` patterns. Applied on the first ``'**'`` found. + Must be at least 1 if provided. + kwargs: + Additional arguments passed to ``find`` (e.g., detail=True) + + Returns + ------- + List of matched paths, or dict of paths and their info if detail=True + + Notes + ----- + Supported patterns: + - '*': Matches any sequence of characters within a single directory level + - ``'**'``: Matches any number of directory levels (must be an entire path component) + - '?': Matches exactly one character + - '[abc]': Matches any character in the set + - '[a-z]': Matches any character in the range + - '[!abc]': Matches any character NOT in the set + + Special behaviors: + - If the path ends with '/', only folders are returned + - Consecutive '*' characters are compressed into a single '*' + - Empty brackets '[]' never match anything + - Negated empty brackets '[!]' match any single character + - Special characters in character classes are escaped properly + + Limitations: + - ``'**'`` must be a complete path component (e.g., ``'a/**/b'``, not ``'a**b'``) + - No brace expansion ('{a,b}.txt') + - No extended glob patterns ('+(pattern)', '!(pattern)') + """ + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + import re + + seps = (os.path.sep, os.path.altsep) if os.path.altsep else (os.path.sep,) + ends_with_sep = path.endswith(seps) # _strip_protocol strips trailing slash + path = self._strip_protocol(path) + append_slash_to_dirname = ends_with_sep or path.endswith( + tuple(sep + "**" for sep in seps) + ) + idx_star = path.find("*") if path.find("*") >= 0 else len(path) + idx_qmark = path.find("?") if path.find("?") >= 0 else len(path) + idx_brace = path.find("[") if path.find("[") >= 0 else len(path) + + min_idx = min(idx_star, idx_qmark, idx_brace) + + detail = kwargs.pop("detail", False) + + if not has_magic(path): + if self.exists(path, **kwargs): + if not detail: + return [path] + else: + return {path: self.info(path, **kwargs)} + else: + if not detail: + return [] # glob of non-existent returns empty + else: + return {} + elif "/" in path[:min_idx]: + min_idx = path[:min_idx].rindex("/") + root = path[: min_idx + 1] + depth = path[min_idx + 1 :].count("/") + 1 + else: + root = "" + depth = path[min_idx + 1 :].count("/") + 1 + + if "**" in path: + if maxdepth is not None: + idx_double_stars = path.find("**") + depth_double_stars = path[idx_double_stars:].count("/") + 1 + depth = depth - depth_double_stars + maxdepth + else: + depth = None + + allpaths = self.find(root, maxdepth=depth, withdirs=True, detail=True, **kwargs) + + pattern = glob_translate(path + ("/" if ends_with_sep else "")) + pattern = re.compile(pattern) + + out = { + p: info + for p, info in sorted(allpaths.items()) + if pattern.match( + p + "/" + if append_slash_to_dirname and info["type"] == "directory" + else p + ) + } + + if detail: + return out + else: + return list(out) + + def exists(self, path, **kwargs): + """Is there a file at the given path""" + try: + self.info(path, **kwargs) + return True + except: # noqa: E722 + # any exception allowed bar FileNotFoundError? + return False + + def lexists(self, path, **kwargs): + """If there is a file at the given path (including + broken links)""" + return self.exists(path) + + def info(self, path, **kwargs): + """Give details of entry at path + + Returns a single dictionary, with exactly the same information as ``ls`` + would with ``detail=True``. + + The default implementation calls ls and could be overridden by a + shortcut. kwargs are passed on to ```ls()``. + + Some file systems might not be able to measure the file's size, in + which case, the returned dict will include ``'size': None``. + + Returns + ------- + dict with keys: name (full path in the FS), size (in bytes), type (file, + directory, or something else) and other FS-specific keys. + """ + path = self._strip_protocol(path) + out = self.ls(self._parent(path), detail=True, **kwargs) + out = [o for o in out if o["name"].rstrip("/") == path] + if out: + return out[0] + out = self.ls(path, detail=True, **kwargs) + path = path.rstrip("/") + out1 = [o for o in out if o["name"].rstrip("/") == path] + if len(out1) == 1: + if "size" not in out1[0]: + out1[0]["size"] = None + return out1[0] + elif len(out1) > 1 or out: + return {"name": path, "size": 0, "type": "directory"} + else: + raise FileNotFoundError(path) + + def checksum(self, path): + """Unique value for current version of file + + If the checksum is the same from one moment to another, the contents + are guaranteed to be the same. If the checksum changes, the contents + *might* have changed. + + This should normally be overridden; default will probably capture + creation/modification timestamp (which would be good) or maybe + access timestamp (which would be bad) + """ + return int(tokenize(self.info(path)), 16) + + def size(self, path): + """Size in bytes of file""" + return self.info(path).get("size", None) + + def sizes(self, paths): + """Size in bytes of each file in a list of paths""" + return [self.size(p) for p in paths] + + def isdir(self, path): + """Is this entry directory-like?""" + try: + return self.info(path)["type"] == "directory" + except OSError: + return False + + def isfile(self, path): + """Is this entry file-like?""" + try: + return self.info(path)["type"] == "file" + except: # noqa: E722 + return False + + def read_text(self, path, encoding=None, errors=None, newline=None, **kwargs): + """Get the contents of the file as a string. + + Parameters + ---------- + path: str + URL of file on this filesystems + encoding, errors, newline: same as `open`. + """ + with self.open( + path, + mode="r", + encoding=encoding, + errors=errors, + newline=newline, + **kwargs, + ) as f: + return f.read() + + def write_text( + self, path, value, encoding=None, errors=None, newline=None, **kwargs + ): + """Write the text to the given file. + + An existing file will be overwritten. + + Parameters + ---------- + path: str + URL of file on this filesystems + value: str + Text to write. + encoding, errors, newline: same as `open`. + """ + with self.open( + path, + mode="w", + encoding=encoding, + errors=errors, + newline=newline, + **kwargs, + ) as f: + return f.write(value) + + def cat_file(self, path, start=None, end=None, **kwargs): + """Get the content of a file + + Parameters + ---------- + path: URL of file on this filesystems + start, end: int + Bytes limits of the read. If negative, backwards from end, + like usual python slices. Either can be None for start or + end of file, respectively + kwargs: passed to ``open()``. + """ + # explicitly set buffering off? + with self.open(path, "rb", **kwargs) as f: + if start is not None: + if start >= 0: + f.seek(start) + else: + f.seek(max(0, f.size + start)) + if end is not None: + if end < 0: + end = f.size + end + return f.read(end - f.tell()) + return f.read() + + def pipe_file(self, path, value, mode="overwrite", **kwargs): + """Set the bytes of given file""" + if mode == "create" and self.exists(path): + # non-atomic but simple way; or could use "xb" in open(), which is likely + # not as well supported + raise FileExistsError + with self.open(path, "wb", **kwargs) as f: + f.write(value) + + def pipe(self, path, value=None, **kwargs): + """Put value into path + + (counterpart to ``cat``) + + Parameters + ---------- + path: string or dict(str, bytes) + If a string, a single remote location to put ``value`` bytes; if a dict, + a mapping of {path: bytesvalue}. + value: bytes, optional + If using a single path, these are the bytes to put there. Ignored if + ``path`` is a dict + """ + if isinstance(path, str): + self.pipe_file(self._strip_protocol(path), value, **kwargs) + elif isinstance(path, dict): + for k, v in path.items(): + self.pipe_file(self._strip_protocol(k), v, **kwargs) + else: + raise ValueError("path must be str or dict") + + def cat_ranges( + self, paths, starts, ends, max_gap=None, on_error="return", **kwargs + ): + """Get the contents of byte ranges from one or more files + + Parameters + ---------- + paths: list + A list of of filepaths on this filesystems + starts, ends: int or list + Bytes limits of the read. If using a single int, the same value will be + used to read all the specified files. + """ + if max_gap is not None: + raise NotImplementedError + if not isinstance(paths, list): + raise TypeError + if not isinstance(starts, list): + starts = [starts] * len(paths) + if not isinstance(ends, list): + ends = [ends] * len(paths) + if len(starts) != len(paths) or len(ends) != len(paths): + raise ValueError + out = [] + for p, s, e in zip(paths, starts, ends): + try: + out.append(self.cat_file(p, s, e)) + except Exception as e: + if on_error == "return": + out.append(e) + else: + raise + return out + + def cat(self, path, recursive=False, on_error="raise", **kwargs): + """Fetch (potentially multiple) paths' contents + + Parameters + ---------- + recursive: bool + If True, assume the path(s) are directories, and get all the + contained files + on_error : "raise", "omit", "return" + If raise, an underlying exception will be raised (converted to KeyError + if the type is in self.missing_exceptions); if omit, keys with exception + will simply not be included in the output; if "return", all keys are + included in the output, but the value will be bytes or an exception + instance. + kwargs: passed to cat_file + + Returns + ------- + dict of {path: contents} if there are multiple paths + or the path has been otherwise expanded + """ + paths = self.expand_path(path, recursive=recursive) + if ( + len(paths) > 1 + or isinstance(path, list) + or paths[0] != self._strip_protocol(path) + ): + out = {} + for path in paths: + try: + out[path] = self.cat_file(path, **kwargs) + except Exception as e: + if on_error == "raise": + raise + if on_error == "return": + out[path] = e + return out + else: + return self.cat_file(paths[0], **kwargs) + + def get_file(self, rpath, lpath, callback=DEFAULT_CALLBACK, outfile=None, **kwargs): + """Copy single remote file to local""" + from .implementations.local import LocalFileSystem + + if isfilelike(lpath): + outfile = lpath + elif self.isdir(rpath): + os.makedirs(lpath, exist_ok=True) + return None + + fs = LocalFileSystem(auto_mkdir=True) + fs.makedirs(fs._parent(lpath), exist_ok=True) + + with self.open(rpath, "rb", **kwargs) as f1: + if outfile is None: + outfile = open(lpath, "wb") + + try: + callback.set_size(getattr(f1, "size", None)) + data = True + while data: + data = f1.read(self.blocksize) + segment_len = outfile.write(data) + if segment_len is None: + segment_len = len(data) + callback.relative_update(segment_len) + finally: + if not isfilelike(lpath): + outfile.close() + + def get( + self, + rpath, + lpath, + recursive=False, + callback=DEFAULT_CALLBACK, + maxdepth=None, + **kwargs, + ): + """Copy file(s) to local. + + Copies a specific file or tree of files (if recursive=True). If lpath + ends with a "/", it will be assumed to be a directory, and target files + will go within. Can submit a list of paths, which may be glob-patterns + and will be expanded. + + Calls get_file for each source. + """ + if isinstance(lpath, list) and isinstance(rpath, list): + # No need to expand paths when both source and destination + # are provided as lists + rpaths = rpath + lpaths = lpath + else: + from .implementations.local import ( + LocalFileSystem, + make_path_posix, + trailing_sep, + ) + + source_is_str = isinstance(rpath, str) + rpaths = self.expand_path(rpath, recursive=recursive, maxdepth=maxdepth) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + rpaths = [p for p in rpaths if not (trailing_sep(p) or self.isdir(p))] + if not rpaths: + return + + if isinstance(lpath, str): + lpath = make_path_posix(lpath) + + source_is_file = len(rpaths) == 1 + dest_is_dir = isinstance(lpath, str) and ( + trailing_sep(lpath) or LocalFileSystem().isdir(lpath) + ) + + exists = source_is_str and ( + (has_magic(rpath) and source_is_file) + or (not has_magic(rpath) and dest_is_dir and not trailing_sep(rpath)) + ) + lpaths = other_paths( + rpaths, + lpath, + exists=exists, + flatten=not source_is_str, + ) + + callback.set_size(len(lpaths)) + for lpath, rpath in callback.wrap(zip(lpaths, rpaths)): + with callback.branched(rpath, lpath) as child: + self.get_file(rpath, lpath, callback=child, **kwargs) + + def put_file( + self, lpath, rpath, callback=DEFAULT_CALLBACK, mode="overwrite", **kwargs + ): + """Copy single file to remote""" + if mode == "create" and self.exists(rpath): + raise FileExistsError + if os.path.isdir(lpath): + self.makedirs(rpath, exist_ok=True) + return None + + with open(lpath, "rb") as f1: + size = f1.seek(0, 2) + callback.set_size(size) + f1.seek(0) + + self.mkdirs(self._parent(os.fspath(rpath)), exist_ok=True) + with self.open(rpath, "wb", **kwargs) as f2: + while f1.tell() < size: + data = f1.read(self.blocksize) + segment_len = f2.write(data) + if segment_len is None: + segment_len = len(data) + callback.relative_update(segment_len) + + def put( + self, + lpath, + rpath, + recursive=False, + callback=DEFAULT_CALLBACK, + maxdepth=None, + **kwargs, + ): + """Copy file(s) from local. + + Copies a specific file or tree of files (if recursive=True). If rpath + ends with a "/", it will be assumed to be a directory, and target files + will go within. + + Calls put_file for each source. + """ + if isinstance(lpath, list) and isinstance(rpath, list): + # No need to expand paths when both source and destination + # are provided as lists + rpaths = rpath + lpaths = lpath + else: + from .implementations.local import ( + LocalFileSystem, + make_path_posix, + trailing_sep, + ) + + source_is_str = isinstance(lpath, str) + if source_is_str: + lpath = make_path_posix(lpath) + fs = LocalFileSystem() + lpaths = fs.expand_path(lpath, recursive=recursive, maxdepth=maxdepth) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))] + if not lpaths: + return + + source_is_file = len(lpaths) == 1 + dest_is_dir = isinstance(rpath, str) and ( + trailing_sep(rpath) or self.isdir(rpath) + ) + + rpath = ( + self._strip_protocol(rpath) + if isinstance(rpath, str) + else [self._strip_protocol(p) for p in rpath] + ) + exists = source_is_str and ( + (has_magic(lpath) and source_is_file) + or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath)) + ) + rpaths = other_paths( + lpaths, + rpath, + exists=exists, + flatten=not source_is_str, + ) + + callback.set_size(len(rpaths)) + for lpath, rpath in callback.wrap(zip(lpaths, rpaths)): + with callback.branched(lpath, rpath) as child: + self.put_file(lpath, rpath, callback=child, **kwargs) + + def head(self, path, size=1024): + """Get the first ``size`` bytes from file""" + with self.open(path, "rb") as f: + return f.read(size) + + def tail(self, path, size=1024): + """Get the last ``size`` bytes from file""" + with self.open(path, "rb") as f: + f.seek(max(-size, -f.size), 2) + return f.read() + + def cp_file(self, path1, path2, **kwargs): + raise NotImplementedError + + def copy( + self, path1, path2, recursive=False, maxdepth=None, on_error=None, **kwargs + ): + """Copy within two locations in the filesystem + + on_error : "raise", "ignore" + If raise, any not-found exceptions will be raised; if ignore any + not-found exceptions will cause the path to be skipped; defaults to + raise unless recursive is true, where the default is ignore + """ + if on_error is None and recursive: + on_error = "ignore" + elif on_error is None: + on_error = "raise" + + if isinstance(path1, list) and isinstance(path2, list): + # No need to expand paths when both source and destination + # are provided as lists + paths1 = path1 + paths2 = path2 + else: + from .implementations.local import trailing_sep + + source_is_str = isinstance(path1, str) + paths1 = self.expand_path(path1, recursive=recursive, maxdepth=maxdepth) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + paths1 = [p for p in paths1 if not (trailing_sep(p) or self.isdir(p))] + if not paths1: + return + + source_is_file = len(paths1) == 1 + dest_is_dir = isinstance(path2, str) and ( + trailing_sep(path2) or self.isdir(path2) + ) + + exists = source_is_str and ( + (has_magic(path1) and source_is_file) + or (not has_magic(path1) and dest_is_dir and not trailing_sep(path1)) + ) + paths2 = other_paths( + paths1, + path2, + exists=exists, + flatten=not source_is_str, + ) + + for p1, p2 in zip(paths1, paths2): + try: + self.cp_file(p1, p2, **kwargs) + except FileNotFoundError: + if on_error == "raise": + raise + + def expand_path(self, path, recursive=False, maxdepth=None, **kwargs): + """Turn one or more globs or directories into a list of all matching paths + to files or directories. + + kwargs are passed to ``glob`` or ``find``, which may in turn call ``ls`` + """ + + if maxdepth is not None and maxdepth < 1: + raise ValueError("maxdepth must be at least 1") + + if isinstance(path, (str, os.PathLike)): + out = self.expand_path([path], recursive, maxdepth) + else: + out = set() + path = [self._strip_protocol(p) for p in path] + for p in path: + if has_magic(p): + bit = set(self.glob(p, maxdepth=maxdepth, **kwargs)) + out |= bit + if recursive: + # glob call above expanded one depth so if maxdepth is defined + # then decrement it in expand_path call below. If it is zero + # after decrementing then avoid expand_path call. + if maxdepth is not None and maxdepth <= 1: + continue + out |= set( + self.expand_path( + list(bit), + recursive=recursive, + maxdepth=maxdepth - 1 if maxdepth is not None else None, + **kwargs, + ) + ) + continue + elif recursive: + rec = set( + self.find( + p, maxdepth=maxdepth, withdirs=True, detail=False, **kwargs + ) + ) + out |= rec + if p not in out and (recursive is False or self.exists(p)): + # should only check once, for the root + out.add(p) + if not out: + raise FileNotFoundError(path) + return sorted(out) + + def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs): + """Move file(s) from one location to another""" + if path1 == path2: + logger.debug("%s mv: The paths are the same, so no files were moved.", self) + else: + # explicitly raise exception to prevent data corruption + self.copy( + path1, path2, recursive=recursive, maxdepth=maxdepth, onerror="raise" + ) + self.rm(path1, recursive=recursive) + + def rm_file(self, path): + """Delete a file""" + self._rm(path) + + def _rm(self, path): + """Delete one file""" + # this is the old name for the method, prefer rm_file + raise NotImplementedError + + def rm(self, path, recursive=False, maxdepth=None): + """Delete files. + + Parameters + ---------- + path: str or list of str + File(s) to delete. + recursive: bool + If file(s) are directories, recursively delete contents and then + also remove the directory + maxdepth: int or None + Depth to pass to walk for finding files to delete, if recursive. + If None, there will be no limit and infinite recursion may be + possible. + """ + path = self.expand_path(path, recursive=recursive, maxdepth=maxdepth) + for p in reversed(path): + self.rm_file(p) + + @classmethod + def _parent(cls, path): + path = cls._strip_protocol(path) + if "/" in path: + parent = path.rsplit("/", 1)[0].lstrip(cls.root_marker) + return cls.root_marker + parent + else: + return cls.root_marker + + def _open( + self, + path, + mode="rb", + block_size=None, + autocommit=True, + cache_options=None, + **kwargs, + ): + """Return raw bytes-mode file-like from the file-system""" + return AbstractBufferedFile( + self, + path, + mode, + block_size, + autocommit, + cache_options=cache_options, + **kwargs, + ) + + def open( + self, + path, + mode="rb", + block_size=None, + cache_options=None, + compression=None, + **kwargs, + ): + """ + Return a file-like object from the filesystem + + The resultant instance must function correctly in a context ``with`` + block. + + Parameters + ---------- + path: str + Target file + mode: str like 'rb', 'w' + See builtin ``open()`` + Mode "x" (exclusive write) may be implemented by the backend. Even if + it is, whether it is checked up front or on commit, and whether it is + atomic is implementation-dependent. + block_size: int + Some indication of buffering - this is a value in bytes + cache_options : dict, optional + Extra arguments to pass through to the cache. + compression: string or None + If given, open file using compression codec. Can either be a compression + name (a key in ``fsspec.compression.compr``) or "infer" to guess the + compression from the filename suffix. + encoding, errors, newline: passed on to TextIOWrapper for text mode + """ + import io + + path = self._strip_protocol(path) + if "b" not in mode: + mode = mode.replace("t", "") + "b" + + text_kwargs = { + k: kwargs.pop(k) + for k in ["encoding", "errors", "newline"] + if k in kwargs + } + return io.TextIOWrapper( + self.open( + path, + mode, + block_size=block_size, + cache_options=cache_options, + compression=compression, + **kwargs, + ), + **text_kwargs, + ) + else: + ac = kwargs.pop("autocommit", not self._intrans) + f = self._open( + path, + mode=mode, + block_size=block_size, + autocommit=ac, + cache_options=cache_options, + **kwargs, + ) + if compression is not None: + from fsspec.compression import compr + from fsspec.core import get_compression + + compression = get_compression(path, compression) + compress = compr[compression] + f = compress(f, mode=mode[0]) + + if not ac and "r" not in mode: + self.transaction.files.append(f) + return f + + def touch(self, path, truncate=True, **kwargs): + """Create empty file, or update timestamp + + Parameters + ---------- + path: str + file location + truncate: bool + If True, always set file size to 0; if False, update timestamp and + leave file unchanged, if backend allows this + """ + if truncate or not self.exists(path): + with self.open(path, "wb", **kwargs): + pass + else: + raise NotImplementedError # update timestamp, if possible + + def ukey(self, path): + """Hash of file properties, to tell if it has changed""" + return sha256(str(self.info(path)).encode()).hexdigest() + + def read_block(self, fn, offset, length, delimiter=None): + """Read a block of bytes from + + Starting at ``offset`` of the file, read ``length`` bytes. If + ``delimiter`` is set then we ensure that the read starts and stops at + delimiter boundaries that follow the locations ``offset`` and ``offset + + length``. If ``offset`` is zero then we start at zero. The + bytestring returned WILL include the end delimiter string. + + If offset+length is beyond the eof, reads to eof. + + Parameters + ---------- + fn: string + Path to filename + offset: int + Byte offset to start read + length: int + Number of bytes to read. If None, read to end. + delimiter: bytes (optional) + Ensure reading starts and stops at delimiter bytestring + + Examples + -------- + >>> fs.read_block('data/file.csv', 0, 13) # doctest: +SKIP + b'Alice, 100\\nBo' + >>> fs.read_block('data/file.csv', 0, 13, delimiter=b'\\n') # doctest: +SKIP + b'Alice, 100\\nBob, 200\\n' + + Use ``length=None`` to read to the end of the file. + >>> fs.read_block('data/file.csv', 0, None, delimiter=b'\\n') # doctest: +SKIP + b'Alice, 100\\nBob, 200\\nCharlie, 300' + + See Also + -------- + :func:`fsspec.utils.read_block` + """ + with self.open(fn, "rb") as f: + size = f.size + if length is None: + length = size + if size is not None and offset + length > size: + length = size - offset + return read_block(f, offset, length, delimiter) + + def to_json(self, *, include_password: bool = True) -> str: + """ + JSON representation of this filesystem instance. + + Parameters + ---------- + include_password: bool, default True + Whether to include the password (if any) in the output. + + Returns + ------- + JSON string with keys ``cls`` (the python location of this class), + protocol (text name of this class's protocol, first one in case of + multiple), ``args`` (positional args, usually empty), and all other + keyword arguments as their own keys. + + Warnings + -------- + Serialized filesystems may contain sensitive information which have been + passed to the constructor, such as passwords and tokens. Make sure you + store and send them in a secure environment! + """ + from .json import FilesystemJSONEncoder + + return json.dumps( + self, + cls=type( + "_FilesystemJSONEncoder", + (FilesystemJSONEncoder,), + {"include_password": include_password}, + ), + ) + + @staticmethod + def from_json(blob: str) -> AbstractFileSystem: + """ + Recreate a filesystem instance from JSON representation. + + See ``.to_json()`` for the expected structure of the input. + + Parameters + ---------- + blob: str + + Returns + ------- + file system instance, not necessarily of this particular class. + + Warnings + -------- + This can import arbitrary modules (as determined by the ``cls`` key). + Make sure you haven't installed any modules that may execute malicious code + at import time. + """ + from .json import FilesystemJSONDecoder + + return json.loads(blob, cls=FilesystemJSONDecoder) + + def to_dict(self, *, include_password: bool = True) -> dict[str, Any]: + """ + JSON-serializable dictionary representation of this filesystem instance. + + Parameters + ---------- + include_password: bool, default True + Whether to include the password (if any) in the output. + + Returns + ------- + Dictionary with keys ``cls`` (the python location of this class), + protocol (text name of this class's protocol, first one in case of + multiple), ``args`` (positional args, usually empty), and all other + keyword arguments as their own keys. + + Warnings + -------- + Serialized filesystems may contain sensitive information which have been + passed to the constructor, such as passwords and tokens. Make sure you + store and send them in a secure environment! + """ + from .json import FilesystemJSONEncoder + + json_encoder = FilesystemJSONEncoder() + + cls = type(self) + proto = self.protocol + + storage_options = dict(self.storage_options) + if not include_password: + storage_options.pop("password", None) + + return dict( + cls=f"{cls.__module__}:{cls.__name__}", + protocol=proto[0] if isinstance(proto, (tuple, list)) else proto, + args=json_encoder.make_serializable(self.storage_args), + **json_encoder.make_serializable(storage_options), + ) + + @staticmethod + def from_dict(dct: dict[str, Any]) -> AbstractFileSystem: + """ + Recreate a filesystem instance from dictionary representation. + + See ``.to_dict()`` for the expected structure of the input. + + Parameters + ---------- + dct: Dict[str, Any] + + Returns + ------- + file system instance, not necessarily of this particular class. + + Warnings + -------- + This can import arbitrary modules (as determined by the ``cls`` key). + Make sure you haven't installed any modules that may execute malicious code + at import time. + """ + from .json import FilesystemJSONDecoder + + json_decoder = FilesystemJSONDecoder() + + dct = dict(dct) # Defensive copy + + cls = FilesystemJSONDecoder.try_resolve_fs_cls(dct) + if cls is None: + raise ValueError("Not a serialized AbstractFileSystem") + + dct.pop("cls", None) + dct.pop("protocol", None) + + return cls( + *json_decoder.unmake_serializable(dct.pop("args", ())), + **json_decoder.unmake_serializable(dct), + ) + + def _get_pyarrow_filesystem(self): + """ + Make a version of the FS instance which will be acceptable to pyarrow + """ + # all instances already also derive from pyarrow + return self + + def get_mapper(self, root="", check=False, create=False, missing_exceptions=None): + """Create key/value store based on this file-system + + Makes a MutableMapping interface to the FS at the given root path. + See ``fsspec.mapping.FSMap`` for further details. + """ + from .mapping import FSMap + + return FSMap( + root, + self, + check=check, + create=create, + missing_exceptions=missing_exceptions, + ) + + @classmethod + def clear_instance_cache(cls): + """ + Clear the cache of filesystem instances. + + Notes + ----- + Unless overridden by setting the ``cachable`` class attribute to False, + the filesystem class stores a reference to newly created instances. This + prevents Python's normal rules around garbage collection from working, + since the instances refcount will not drop to zero until + ``clear_instance_cache`` is called. + """ + cls._cache.clear() + + def created(self, path): + """Return the created timestamp of a file as a datetime.datetime""" + raise NotImplementedError + + def modified(self, path): + """Return the modified timestamp of a file as a datetime.datetime""" + raise NotImplementedError + + def tree( + self, + path: str = "/", + recursion_limit: int = 2, + max_display: int = 25, + display_size: bool = False, + prefix: str = "", + is_last: bool = True, + first: bool = True, + indent_size: int = 4, + ) -> str: + """ + Return a tree-like structure of the filesystem starting from the given path as a string. + + Parameters + ---------- + path: Root path to start traversal from + recursion_limit: Maximum depth of directory traversal + max_display: Maximum number of items to display per directory + display_size: Whether to display file sizes + prefix: Current line prefix for visual tree structure + is_last: Whether current item is last in its level + first: Whether this is the first call (displays root path) + indent_size: Number of spaces by indent + + Returns + ------- + str: A string representing the tree structure. + + Example + ------- + >>> from fsspec import filesystem + + >>> fs = filesystem('ftp', host='test.rebex.net', user='demo', password='password') + >>> tree = fs.tree(display_size=True, recursion_limit=3, indent_size=8, max_display=10) + >>> print(tree) + """ + + def format_bytes(n: int) -> str: + """Format bytes as text.""" + for prefix, k in ( + ("P", 2**50), + ("T", 2**40), + ("G", 2**30), + ("M", 2**20), + ("k", 2**10), + ): + if n >= 0.9 * k: + return f"{n / k:.2f} {prefix}b" + return f"{n}B" + + result = [] + + if first: + result.append(path) + + if recursion_limit: + indent = " " * indent_size + contents = self.ls(path, detail=True) + contents.sort( + key=lambda x: (x.get("type") != "directory", x.get("name", "")) + ) + + if max_display is not None and len(contents) > max_display: + displayed_contents = contents[:max_display] + remaining_count = len(contents) - max_display + else: + displayed_contents = contents + remaining_count = 0 + + for i, item in enumerate(displayed_contents): + is_last_item = (i == len(displayed_contents) - 1) and ( + remaining_count == 0 + ) + + branch = ( + "└" + ("─" * (indent_size - 2)) + if is_last_item + else "├" + ("─" * (indent_size - 2)) + ) + branch += " " + new_prefix = prefix + ( + indent if is_last_item else "│" + " " * (indent_size - 1) + ) + + name = os.path.basename(item.get("name", "")) + + if display_size and item.get("type") == "directory": + sub_contents = self.ls(item.get("name", ""), detail=True) + num_files = sum( + 1 for sub_item in sub_contents if sub_item.get("type") == "file" + ) + num_folders = sum( + 1 + for sub_item in sub_contents + if sub_item.get("type") == "directory" + ) + + if num_files == 0 and num_folders == 0: + size = " (empty folder)" + elif num_files == 0: + size = f" ({num_folders} subfolder{'s' if num_folders > 1 else ''})" + elif num_folders == 0: + size = f" ({num_files} file{'s' if num_files > 1 else ''})" + else: + size = f" ({num_files} file{'s' if num_files > 1 else ''}, {num_folders} subfolder{'s' if num_folders > 1 else ''})" + elif display_size and item.get("type") == "file": + size = f" ({format_bytes(item.get('size', 0))})" + else: + size = "" + + result.append(f"{prefix}{branch}{name}{size}") + + if item.get("type") == "directory" and recursion_limit > 0: + result.append( + self.tree( + path=item.get("name", ""), + recursion_limit=recursion_limit - 1, + max_display=max_display, + display_size=display_size, + prefix=new_prefix, + is_last=is_last_item, + first=False, + indent_size=indent_size, + ) + ) + + if remaining_count > 0: + more_message = f"{remaining_count} more item(s) not displayed." + result.append( + f"{prefix}{'└' + ('─' * (indent_size - 2))} {more_message}" + ) + + return "\n".join(_ for _ in result if _) + + # ------------------------------------------------------------------------ + # Aliases + + def read_bytes(self, path, start=None, end=None, **kwargs): + """Alias of `AbstractFileSystem.cat_file`.""" + return self.cat_file(path, start=start, end=end, **kwargs) + + def write_bytes(self, path, value, **kwargs): + """Alias of `AbstractFileSystem.pipe_file`.""" + self.pipe_file(path, value, **kwargs) + + def makedir(self, path, create_parents=True, **kwargs): + """Alias of `AbstractFileSystem.mkdir`.""" + return self.mkdir(path, create_parents=create_parents, **kwargs) + + def mkdirs(self, path, exist_ok=False): + """Alias of `AbstractFileSystem.makedirs`.""" + return self.makedirs(path, exist_ok=exist_ok) + + def listdir(self, path, detail=True, **kwargs): + """Alias of `AbstractFileSystem.ls`.""" + return self.ls(path, detail=detail, **kwargs) + + def cp(self, path1, path2, **kwargs): + """Alias of `AbstractFileSystem.copy`.""" + return self.copy(path1, path2, **kwargs) + + def move(self, path1, path2, **kwargs): + """Alias of `AbstractFileSystem.mv`.""" + return self.mv(path1, path2, **kwargs) + + def stat(self, path, **kwargs): + """Alias of `AbstractFileSystem.info`.""" + return self.info(path, **kwargs) + + def disk_usage(self, path, total=True, maxdepth=None, **kwargs): + """Alias of `AbstractFileSystem.du`.""" + return self.du(path, total=total, maxdepth=maxdepth, **kwargs) + + def rename(self, path1, path2, **kwargs): + """Alias of `AbstractFileSystem.mv`.""" + return self.mv(path1, path2, **kwargs) + + def delete(self, path, recursive=False, maxdepth=None): + """Alias of `AbstractFileSystem.rm`.""" + return self.rm(path, recursive=recursive, maxdepth=maxdepth) + + def upload(self, lpath, rpath, recursive=False, **kwargs): + """Alias of `AbstractFileSystem.put`.""" + return self.put(lpath, rpath, recursive=recursive, **kwargs) + + def download(self, rpath, lpath, recursive=False, **kwargs): + """Alias of `AbstractFileSystem.get`.""" + return self.get(rpath, lpath, recursive=recursive, **kwargs) + + def sign(self, path, expiration=100, **kwargs): + """Create a signed URL representing the given path + + Some implementations allow temporary URLs to be generated, as a + way of delegating credentials. + + Parameters + ---------- + path : str + The path on the filesystem + expiration : int + Number of seconds to enable the URL for (if supported) + + Returns + ------- + URL : str + The signed URL + + Raises + ------ + NotImplementedError : if method is not implemented for a filesystem + """ + raise NotImplementedError("Sign is not implemented for this filesystem") + + def _isfilestore(self): + # Originally inherited from pyarrow DaskFileSystem. Keeping this + # here for backwards compatibility as long as pyarrow uses its + # legacy fsspec-compatible filesystems and thus accepts fsspec + # filesystems as well + return False + + +class AbstractBufferedFile(io.IOBase): + """Convenient class to derive from to provide buffering + + In the case that the backend does not provide a pythonic file-like object + already, this class contains much of the logic to build one. The only + methods that need to be overridden are ``_upload_chunk``, + ``_initiate_upload`` and ``_fetch_range``. + """ + + DEFAULT_BLOCK_SIZE = 5 * 2**20 + _details = None + + def __init__( + self, + fs, + path, + mode="rb", + block_size="default", + autocommit=True, + cache_type="readahead", + cache_options=None, + size=None, + **kwargs, + ): + """ + Template for files with buffered reading and writing + + Parameters + ---------- + fs: instance of FileSystem + path: str + location in file-system + mode: str + Normal file modes. Currently only 'wb', 'ab' or 'rb'. Some file + systems may be read-only, and some may not support append. + block_size: int + Buffer size for reading or writing, 'default' for class default + autocommit: bool + Whether to write to final destination; may only impact what + happens when file is being closed. + cache_type: {"readahead", "none", "mmap", "bytes"}, default "readahead" + Caching policy in read mode. See the definitions in ``core``. + cache_options : dict + Additional options passed to the constructor for the cache specified + by `cache_type`. + size: int + If given and in read mode, suppressed having to look up the file size + kwargs: + Gets stored as self.kwargs + """ + from .core import caches + + self.path = path + self.fs = fs + self.mode = mode + self.blocksize = ( + self.DEFAULT_BLOCK_SIZE if block_size in ["default", None] else block_size + ) + self.loc = 0 + self.autocommit = autocommit + self.end = None + self.start = None + self.closed = False + + if cache_options is None: + cache_options = {} + + if "trim" in kwargs: + warnings.warn( + "Passing 'trim' to control the cache behavior has been deprecated. " + "Specify it within the 'cache_options' argument instead.", + FutureWarning, + ) + cache_options["trim"] = kwargs.pop("trim") + + self.kwargs = kwargs + + if mode not in {"ab", "rb", "wb", "xb"}: + raise NotImplementedError("File mode not supported") + if mode == "rb": + if size is not None: + self.size = size + else: + self.size = self.details["size"] + self.cache = caches[cache_type]( + self.blocksize, self._fetch_range, self.size, **cache_options + ) + else: + self.buffer = io.BytesIO() + self.offset = None + self.forced = False + self.location = None + + @property + def details(self): + if self._details is None: + self._details = self.fs.info(self.path) + return self._details + + @details.setter + def details(self, value): + self._details = value + self.size = value["size"] + + @property + def full_name(self): + return _unstrip_protocol(self.path, self.fs) + + @property + def closed(self): + # get around this attr being read-only in IOBase + # use getattr here, since this can be called during del + return getattr(self, "_closed", True) + + @closed.setter + def closed(self, c): + self._closed = c + + def __hash__(self): + if "w" in self.mode: + return id(self) + else: + return int(tokenize(self.details), 16) + + def __eq__(self, other): + """Files are equal if they have the same checksum, only in read mode""" + if self is other: + return True + return ( + isinstance(other, type(self)) + and self.mode == "rb" + and other.mode == "rb" + and hash(self) == hash(other) + ) + + def commit(self): + """Move from temp to final destination""" + + def discard(self): + """Throw away temporary file""" + + def info(self): + """File information about this path""" + if self.readable(): + return self.details + else: + raise ValueError("Info not available while writing") + + def tell(self): + """Current file location""" + return self.loc + + def seek(self, loc, whence=0): + """Set current file location + + Parameters + ---------- + loc: int + byte location + whence: {0, 1, 2} + from start of file, current location or end of file, resp. + """ + loc = int(loc) + if not self.mode == "rb": + raise OSError(ESPIPE, "Seek only available in read mode") + if whence == 0: + nloc = loc + elif whence == 1: + nloc = self.loc + loc + elif whence == 2: + nloc = self.size + loc + else: + raise ValueError(f"invalid whence ({whence}, should be 0, 1 or 2)") + if nloc < 0: + raise ValueError("Seek before start of file") + self.loc = nloc + return self.loc + + def write(self, data): + """ + Write data to buffer. + + Buffer only sent on flush() or if buffer is greater than + or equal to blocksize. + + Parameters + ---------- + data: bytes + Set of bytes to be written. + """ + if not self.writable(): + raise ValueError("File not in write mode") + if self.closed: + raise ValueError("I/O operation on closed file.") + if self.forced: + raise ValueError("This file has been force-flushed, can only close") + out = self.buffer.write(data) + self.loc += out + if self.buffer.tell() >= self.blocksize: + self.flush() + return out + + def flush(self, force=False): + """ + Write buffered data to backend store. + + Writes the current buffer, if it is larger than the block-size, or if + the file is being closed. + + Parameters + ---------- + force: bool + When closing, write the last block even if it is smaller than + blocks are allowed to be. Disallows further writing to this file. + """ + + if self.closed: + raise ValueError("Flush on closed file") + if force and self.forced: + raise ValueError("Force flush cannot be called more than once") + if force: + self.forced = True + + if self.readable(): + # no-op to flush on read-mode + return + + if not force and self.buffer.tell() < self.blocksize: + # Defer write on small block + return + + if self.offset is None: + # Initialize a multipart upload + self.offset = 0 + try: + self._initiate_upload() + except: + self.closed = True + raise + + if self._upload_chunk(final=force) is not False: + self.offset += self.buffer.seek(0, 2) + self.buffer = io.BytesIO() + + def _upload_chunk(self, final=False): + """Write one part of a multi-block file upload + + Parameters + ========== + final: bool + This is the last block, so should complete file, if + self.autocommit is True. + """ + # may not yet have been initialized, may need to call _initialize_upload + + def _initiate_upload(self): + """Create remote file/upload""" + pass + + def _fetch_range(self, start, end): + """Get the specified set of bytes from remote""" + return self.fs.cat_file(self.path, start=start, end=end) + + def read(self, length=-1): + """ + Return data from cache, or fetch pieces as necessary + + Parameters + ---------- + length: int (-1) + Number of bytes to read; if <0, all remaining bytes. + """ + length = -1 if length is None else int(length) + if self.mode != "rb": + raise ValueError("File not in read mode") + if length < 0: + length = self.size - self.loc + if self.closed: + raise ValueError("I/O operation on closed file.") + if length == 0: + # don't even bother calling fetch + return b"" + out = self.cache._fetch(self.loc, self.loc + length) + + logger.debug( + "%s read: %i - %i %s", + self, + self.loc, + self.loc + length, + self.cache._log_stats(), + ) + self.loc += len(out) + return out + + def readinto(self, b): + """mirrors builtin file's readinto method + + https://docs.python.org/3/library/io.html#io.RawIOBase.readinto + """ + out = memoryview(b).cast("B") + data = self.read(out.nbytes) + out[: len(data)] = data + return len(data) + + def readuntil(self, char=b"\n", blocks=None): + """Return data between current position and first occurrence of char + + char is included in the output, except if the end of the tile is + encountered first. + + Parameters + ---------- + char: bytes + Thing to find + blocks: None or int + How much to read in each go. Defaults to file blocksize - which may + mean a new read on every call. + """ + out = [] + while True: + start = self.tell() + part = self.read(blocks or self.blocksize) + if len(part) == 0: + break + found = part.find(char) + if found > -1: + out.append(part[: found + len(char)]) + self.seek(start + found + len(char)) + break + out.append(part) + return b"".join(out) + + def readline(self): + """Read until and including the first occurrence of newline character + + Note that, because of character encoding, this is not necessarily a + true line ending. + """ + return self.readuntil(b"\n") + + def __next__(self): + out = self.readline() + if out: + return out + raise StopIteration + + def __iter__(self): + return self + + def readlines(self): + """Return all data, split by the newline character, including the newline character""" + data = self.read() + lines = data.split(b"\n") + out = [l + b"\n" for l in lines[:-1]] + if data.endswith(b"\n"): + return out + else: + return out + [lines[-1]] + # return list(self) ??? + + def readinto1(self, b): + return self.readinto(b) + + def close(self): + """Close file + + Finalizes writes, discards cache + """ + if getattr(self, "_unclosable", False): + return + if self.closed: + return + try: + if self.mode == "rb": + self.cache = None + else: + if not self.forced: + self.flush(force=True) + + if self.fs is not None: + self.fs.invalidate_cache(self.path) + self.fs.invalidate_cache(self.fs._parent(self.path)) + finally: + self.closed = True + + def readable(self): + """Whether opened for reading""" + return "r" in self.mode and not self.closed + + def seekable(self): + """Whether is seekable (only in read mode)""" + return self.readable() + + def writable(self): + """Whether opened for writing""" + return self.mode in {"wb", "ab", "xb"} and not self.closed + + def __reduce__(self): + if self.mode != "rb": + raise RuntimeError("Pickling a writeable file is not supported") + + return reopen, ( + self.fs, + self.path, + self.mode, + self.blocksize, + self.loc, + self.size, + self.autocommit, + self.cache.name if self.cache else "none", + self.kwargs, + ) + + def __del__(self): + if not self.closed: + self.close() + + def __str__(self): + return f"" + + __repr__ = __str__ + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +def reopen(fs, path, mode, blocksize, loc, size, autocommit, cache_type, kwargs): + file = fs.open( + path, + mode=mode, + block_size=blocksize, + autocommit=autocommit, + cache_type=cache_type, + size=size, + **kwargs, + ) + if loc > 0: + file.seek(loc) + return file diff --git a/lib/python3.10/site-packages/fsspec/transaction.py b/lib/python3.10/site-packages/fsspec/transaction.py new file mode 100644 index 0000000000000000000000000000000000000000..77293f63ecc5f611e19d849ef236d53e9c258efc --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/transaction.py @@ -0,0 +1,90 @@ +from collections import deque + + +class Transaction: + """Filesystem transaction write context + + Gathers files for deferred commit or discard, so that several write + operations can be finalized semi-atomically. This works by having this + instance as the ``.transaction`` attribute of the given filesystem + """ + + def __init__(self, fs, **kwargs): + """ + Parameters + ---------- + fs: FileSystem instance + """ + self.fs = fs + self.files = deque() + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """End transaction and commit, if exit is not due to exception""" + # only commit if there was no exception + self.complete(commit=exc_type is None) + if self.fs: + self.fs._intrans = False + self.fs._transaction = None + self.fs = None + + def start(self): + """Start a transaction on this FileSystem""" + self.files = deque() # clean up after previous failed completions + self.fs._intrans = True + + def complete(self, commit=True): + """Finish transaction: commit or discard all deferred files""" + while self.files: + f = self.files.popleft() + if commit: + f.commit() + else: + f.discard() + self.fs._intrans = False + self.fs._transaction = None + self.fs = None + + +class FileActor: + def __init__(self): + self.files = [] + + def commit(self): + for f in self.files: + f.commit() + self.files.clear() + + def discard(self): + for f in self.files: + f.discard() + self.files.clear() + + def append(self, f): + self.files.append(f) + + +class DaskTransaction(Transaction): + def __init__(self, fs): + """ + Parameters + ---------- + fs: FileSystem instance + """ + import distributed + + super().__init__(fs) + client = distributed.default_client() + self.files = client.submit(FileActor, actor=True).result() + + def complete(self, commit=True): + """Finish transaction: commit or discard all deferred files""" + if commit: + self.files.commit().result() + else: + self.files.discard().result() + self.fs._intrans = False + self.fs = None diff --git a/lib/python3.10/site-packages/fsspec/utils.py b/lib/python3.10/site-packages/fsspec/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6441c5b1d3a766e7911161e54c6d7fba8eb3032e --- /dev/null +++ b/lib/python3.10/site-packages/fsspec/utils.py @@ -0,0 +1,737 @@ +from __future__ import annotations + +import contextlib +import logging +import math +import os +import re +import sys +import tempfile +from collections.abc import Iterable, Iterator, Sequence +from functools import partial +from hashlib import md5 +from importlib.metadata import version +from typing import ( + IO, + TYPE_CHECKING, + Any, + Callable, + TypeVar, +) +from urllib.parse import urlsplit + +if TYPE_CHECKING: + import pathlib + + from typing_extensions import TypeGuard + + from fsspec.spec import AbstractFileSystem + + +DEFAULT_BLOCK_SIZE = 5 * 2**20 + +T = TypeVar("T") + + +def infer_storage_options( + urlpath: str, inherit_storage_options: dict[str, Any] | None = None +) -> dict[str, Any]: + """Infer storage options from URL path and merge it with existing storage + options. + + Parameters + ---------- + urlpath: str or unicode + Either local absolute file path or URL (hdfs://namenode:8020/file.csv) + inherit_storage_options: dict (optional) + Its contents will get merged with the inferred information from the + given path + + Returns + ------- + Storage options dict. + + Examples + -------- + >>> infer_storage_options('/mnt/datasets/test.csv') # doctest: +SKIP + {"protocol": "file", "path", "/mnt/datasets/test.csv"} + >>> infer_storage_options( + ... 'hdfs://username:pwd@node:123/mnt/datasets/test.csv?q=1', + ... inherit_storage_options={'extra': 'value'}, + ... ) # doctest: +SKIP + {"protocol": "hdfs", "username": "username", "password": "pwd", + "host": "node", "port": 123, "path": "/mnt/datasets/test.csv", + "url_query": "q=1", "extra": "value"} + """ + # Handle Windows paths including disk name in this special case + if ( + re.match(r"^[a-zA-Z]:[\\/]", urlpath) + or re.match(r"^[a-zA-Z0-9]+://", urlpath) is None + ): + return {"protocol": "file", "path": urlpath} + + parsed_path = urlsplit(urlpath) + protocol = parsed_path.scheme or "file" + if parsed_path.fragment: + path = "#".join([parsed_path.path, parsed_path.fragment]) + else: + path = parsed_path.path + if protocol == "file": + # Special case parsing file protocol URL on Windows according to: + # https://msdn.microsoft.com/en-us/library/jj710207.aspx + windows_path = re.match(r"^/([a-zA-Z])[:|]([\\/].*)$", path) + if windows_path: + drive, path = windows_path.groups() + path = f"{drive}:{path}" + + if protocol in ["http", "https"]: + # for HTTP, we don't want to parse, as requests will anyway + return {"protocol": protocol, "path": urlpath} + + options: dict[str, Any] = {"protocol": protocol, "path": path} + + if parsed_path.netloc: + # Parse `hostname` from netloc manually because `parsed_path.hostname` + # lowercases the hostname which is not always desirable (e.g. in S3): + # https://github.com/dask/dask/issues/1417 + options["host"] = parsed_path.netloc.rsplit("@", 1)[-1].rsplit(":", 1)[0] + + if protocol in ("s3", "s3a", "gcs", "gs"): + options["path"] = options["host"] + options["path"] + else: + options["host"] = options["host"] + if parsed_path.port: + options["port"] = parsed_path.port + if parsed_path.username: + options["username"] = parsed_path.username + if parsed_path.password: + options["password"] = parsed_path.password + + if parsed_path.query: + options["url_query"] = parsed_path.query + if parsed_path.fragment: + options["url_fragment"] = parsed_path.fragment + + if inherit_storage_options: + update_storage_options(options, inherit_storage_options) + + return options + + +def update_storage_options( + options: dict[str, Any], inherited: dict[str, Any] | None = None +) -> None: + if not inherited: + inherited = {} + collisions = set(options) & set(inherited) + if collisions: + for collision in collisions: + if options.get(collision) != inherited.get(collision): + raise KeyError( + f"Collision between inferred and specified storage " + f"option:\n{collision}" + ) + options.update(inherited) + + +# Compression extensions registered via fsspec.compression.register_compression +compressions: dict[str, str] = {} + + +def infer_compression(filename: str) -> str | None: + """Infer compression, if available, from filename. + + Infer a named compression type, if registered and available, from filename + extension. This includes builtin (gz, bz2, zip) compressions, as well as + optional compressions. See fsspec.compression.register_compression. + """ + extension = os.path.splitext(filename)[-1].strip(".").lower() + if extension in compressions: + return compressions[extension] + return None + + +def build_name_function(max_int: float) -> Callable[[int], str]: + """Returns a function that receives a single integer + and returns it as a string padded by enough zero characters + to align with maximum possible integer + + >>> name_f = build_name_function(57) + + >>> name_f(7) + '07' + >>> name_f(31) + '31' + >>> build_name_function(1000)(42) + '0042' + >>> build_name_function(999)(42) + '042' + >>> build_name_function(0)(0) + '0' + """ + # handle corner cases max_int is 0 or exact power of 10 + max_int += 1e-8 + + pad_length = int(math.ceil(math.log10(max_int))) + + def name_function(i: int) -> str: + return str(i).zfill(pad_length) + + return name_function + + +def seek_delimiter(file: IO[bytes], delimiter: bytes, blocksize: int) -> bool: + r"""Seek current file to file start, file end, or byte after delimiter seq. + + Seeks file to next chunk delimiter, where chunks are defined on file start, + a delimiting sequence, and file end. Use file.tell() to see location afterwards. + Note that file start is a valid split, so must be at offset > 0 to seek for + delimiter. + + Parameters + ---------- + file: a file + delimiter: bytes + a delimiter like ``b'\n'`` or message sentinel, matching file .read() type + blocksize: int + Number of bytes to read from the file at once. + + + Returns + ------- + Returns True if a delimiter was found, False if at file start or end. + + """ + + if file.tell() == 0: + # beginning-of-file, return without seek + return False + + # Interface is for binary IO, with delimiter as bytes, but initialize last + # with result of file.read to preserve compatibility with text IO. + last: bytes | None = None + while True: + current = file.read(blocksize) + if not current: + # end-of-file without delimiter + return False + full = last + current if last else current + try: + if delimiter in full: + i = full.index(delimiter) + file.seek(file.tell() - (len(full) - i) + len(delimiter)) + return True + elif len(current) < blocksize: + # end-of-file without delimiter + return False + except (OSError, ValueError): + pass + last = full[-len(delimiter) :] + + +def read_block( + f: IO[bytes], + offset: int, + length: int | None, + delimiter: bytes | None = None, + split_before: bool = False, +) -> bytes: + """Read a block of bytes from a file + + Parameters + ---------- + f: File + Open file + offset: int + Byte offset to start read + length: int + Number of bytes to read, read through end of file if None + delimiter: bytes (optional) + Ensure reading starts and stops at delimiter bytestring + split_before: bool (optional) + Start/stop read *before* delimiter bytestring. + + + If using the ``delimiter=`` keyword argument we ensure that the read + starts and stops at delimiter boundaries that follow the locations + ``offset`` and ``offset + length``. If ``offset`` is zero then we + start at zero, regardless of delimiter. The bytestring returned WILL + include the terminating delimiter string. + + Examples + -------- + + >>> from io import BytesIO # doctest: +SKIP + >>> f = BytesIO(b'Alice, 100\\nBob, 200\\nCharlie, 300') # doctest: +SKIP + >>> read_block(f, 0, 13) # doctest: +SKIP + b'Alice, 100\\nBo' + + >>> read_block(f, 0, 13, delimiter=b'\\n') # doctest: +SKIP + b'Alice, 100\\nBob, 200\\n' + + >>> read_block(f, 10, 10, delimiter=b'\\n') # doctest: +SKIP + b'Bob, 200\\nCharlie, 300' + """ + if delimiter: + f.seek(offset) + found_start_delim = seek_delimiter(f, delimiter, 2**16) + if length is None: + return f.read() + start = f.tell() + length -= start - offset + + f.seek(start + length) + found_end_delim = seek_delimiter(f, delimiter, 2**16) + end = f.tell() + + # Adjust split location to before delimiter if seek found the + # delimiter sequence, not start or end of file. + if found_start_delim and split_before: + start -= len(delimiter) + + if found_end_delim and split_before: + end -= len(delimiter) + + offset = start + length = end - start + + f.seek(offset) + + # TODO: allow length to be None and read to the end of the file? + assert length is not None + b = f.read(length) + return b + + +def tokenize(*args: Any, **kwargs: Any) -> str: + """Deterministic token + + (modified from dask.base) + + >>> tokenize([1, 2, '3']) + '9d71491b50023b06fc76928e6eddb952' + + >>> tokenize('Hello') == tokenize('Hello') + True + """ + if kwargs: + args += (kwargs,) + try: + h = md5(str(args).encode()) + except ValueError: + # FIPS systems: https://github.com/fsspec/filesystem_spec/issues/380 + h = md5(str(args).encode(), usedforsecurity=False) + return h.hexdigest() + + +def stringify_path(filepath: str | os.PathLike[str] | pathlib.Path) -> str: + """Attempt to convert a path-like object to a string. + + Parameters + ---------- + filepath: object to be converted + + Returns + ------- + filepath_str: maybe a string version of the object + + Notes + ----- + Objects supporting the fspath protocol are coerced according to its + __fspath__ method. + + For backwards compatibility with older Python version, pathlib.Path + objects are specially coerced. + + Any other object is passed through unchanged, which includes bytes, + strings, buffers, or anything else that's not even path-like. + """ + if isinstance(filepath, str): + return filepath + elif hasattr(filepath, "__fspath__"): + return filepath.__fspath__() + elif hasattr(filepath, "path"): + return filepath.path + else: + return filepath # type: ignore[return-value] + + +def make_instance( + cls: Callable[..., T], args: Sequence[Any], kwargs: dict[str, Any] +) -> T: + inst = cls(*args, **kwargs) + inst._determine_worker() # type: ignore[attr-defined] + return inst + + +def common_prefix(paths: Iterable[str]) -> str: + """For a list of paths, find the shortest prefix common to all""" + parts = [p.split("/") for p in paths] + lmax = min(len(p) for p in parts) + end = 0 + for i in range(lmax): + end = all(p[i] == parts[0][i] for p in parts) + if not end: + break + i += end + return "/".join(parts[0][:i]) + + +def other_paths( + paths: list[str], + path2: str | list[str], + exists: bool = False, + flatten: bool = False, +) -> list[str]: + """In bulk file operations, construct a new file tree from a list of files + + Parameters + ---------- + paths: list of str + The input file tree + path2: str or list of str + Root to construct the new list in. If this is already a list of str, we just + assert it has the right number of elements. + exists: bool (optional) + For a str destination, it is already exists (and is a dir), files should + end up inside. + flatten: bool (optional) + Whether to flatten the input directory tree structure so that the output files + are in the same directory. + + Returns + ------- + list of str + """ + + if isinstance(path2, str): + path2 = path2.rstrip("/") + + if flatten: + path2 = ["/".join((path2, p.split("/")[-1])) for p in paths] + else: + cp = common_prefix(paths) + if exists: + cp = cp.rsplit("/", 1)[0] + if not cp and all(not s.startswith("/") for s in paths): + path2 = ["/".join([path2, p]) for p in paths] + else: + path2 = [p.replace(cp, path2, 1) for p in paths] + else: + assert len(paths) == len(path2) + return path2 + + +def is_exception(obj: Any) -> bool: + return isinstance(obj, BaseException) + + +def isfilelike(f: Any) -> TypeGuard[IO[bytes]]: + return all(hasattr(f, attr) for attr in ["read", "close", "tell"]) + + +def get_protocol(url: str) -> str: + url = stringify_path(url) + parts = re.split(r"(\:\:|\://)", url, maxsplit=1) + if len(parts) > 1: + return parts[0] + return "file" + + +def can_be_local(path: str) -> bool: + """Can the given URL be used with open_local?""" + from fsspec import get_filesystem_class + + try: + return getattr(get_filesystem_class(get_protocol(path)), "local_file", False) + except (ValueError, ImportError): + # not in registry or import failed + return False + + +def get_package_version_without_import(name: str) -> str | None: + """For given package name, try to find the version without importing it + + Import and package.__version__ is still the backup here, so an import + *might* happen. + + Returns either the version string, or None if the package + or the version was not readily found. + """ + if name in sys.modules: + mod = sys.modules[name] + if hasattr(mod, "__version__"): + return mod.__version__ + try: + return version(name) + except: # noqa: E722 + pass + try: + import importlib + + mod = importlib.import_module(name) + return mod.__version__ + except (ImportError, AttributeError): + return None + + +def setup_logging( + logger: logging.Logger | None = None, + logger_name: str | None = None, + level: str = "DEBUG", + clear: bool = True, +) -> logging.Logger: + if logger is None and logger_name is None: + raise ValueError("Provide either logger object or logger name") + logger = logger or logging.getLogger(logger_name) + handle = logging.StreamHandler() + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s -- %(message)s" + ) + handle.setFormatter(formatter) + if clear: + logger.handlers.clear() + logger.addHandler(handle) + logger.setLevel(level) + return logger + + +def _unstrip_protocol(name: str, fs: AbstractFileSystem) -> str: + return fs.unstrip_protocol(name) + + +def mirror_from( + origin_name: str, methods: Iterable[str] +) -> Callable[[type[T]], type[T]]: + """Mirror attributes and methods from the given + origin_name attribute of the instance to the + decorated class""" + + def origin_getter(method: str, self: Any) -> Any: + origin = getattr(self, origin_name) + return getattr(origin, method) + + def wrapper(cls: type[T]) -> type[T]: + for method in methods: + wrapped_method = partial(origin_getter, method) + setattr(cls, method, property(wrapped_method)) + return cls + + return wrapper + + +@contextlib.contextmanager +def nullcontext(obj: T) -> Iterator[T]: + yield obj + + +def merge_offset_ranges( + paths: list[str], + starts: list[int] | int, + ends: list[int] | int, + max_gap: int = 0, + max_block: int | None = None, + sort: bool = True, +) -> tuple[list[str], list[int], list[int]]: + """Merge adjacent byte-offset ranges when the inter-range + gap is <= `max_gap`, and when the merged byte range does not + exceed `max_block` (if specified). By default, this function + will re-order the input paths and byte ranges to ensure sorted + order. If the user can guarantee that the inputs are already + sorted, passing `sort=False` will skip the re-ordering. + """ + # Check input + if not isinstance(paths, list): + raise TypeError + if not isinstance(starts, list): + starts = [starts] * len(paths) + if not isinstance(ends, list): + ends = [ends] * len(paths) + if len(starts) != len(paths) or len(ends) != len(paths): + raise ValueError + + # Early Return + if len(starts) <= 1: + return paths, starts, ends + + starts = [s or 0 for s in starts] + # Sort by paths and then ranges if `sort=True` + if sort: + paths, starts, ends = ( + list(v) + for v in zip( + *sorted( + zip(paths, starts, ends), + ) + ) + ) + + if paths: + # Loop through the coupled `paths`, `starts`, and + # `ends`, and merge adjacent blocks when appropriate + new_paths = paths[:1] + new_starts = starts[:1] + new_ends = ends[:1] + for i in range(1, len(paths)): + if paths[i] == paths[i - 1] and new_ends[-1] is None: + continue + elif ( + paths[i] != paths[i - 1] + or ((starts[i] - new_ends[-1]) > max_gap) + or (max_block is not None and (ends[i] - new_starts[-1]) > max_block) + ): + # Cannot merge with previous block. + # Add new `paths`, `starts`, and `ends` elements + new_paths.append(paths[i]) + new_starts.append(starts[i]) + new_ends.append(ends[i]) + else: + # Merge with previous block by updating the + # last element of `ends` + new_ends[-1] = ends[i] + return new_paths, new_starts, new_ends + + # `paths` is empty. Just return input lists + return paths, starts, ends + + +def file_size(filelike: IO[bytes]) -> int: + """Find length of any open read-mode file-like""" + pos = filelike.tell() + try: + return filelike.seek(0, 2) + finally: + filelike.seek(pos) + + +@contextlib.contextmanager +def atomic_write(path: str, mode: str = "wb"): + """ + A context manager that opens a temporary file next to `path` and, on exit, + replaces `path` with the temporary file, thereby updating `path` + atomically. + """ + fd, fn = tempfile.mkstemp( + dir=os.path.dirname(path), prefix=os.path.basename(path) + "-" + ) + try: + with open(fd, mode) as fp: + yield fp + except BaseException: + with contextlib.suppress(FileNotFoundError): + os.unlink(fn) + raise + else: + os.replace(fn, path) + + +def _translate(pat, STAR, QUESTION_MARK): + # Copied from: https://github.com/python/cpython/pull/106703. + res: list[str] = [] + add = res.append + i, n = 0, len(pat) + while i < n: + c = pat[i] + i = i + 1 + if c == "*": + # compress consecutive `*` into one + if (not res) or res[-1] is not STAR: + add(STAR) + elif c == "?": + add(QUESTION_MARK) + elif c == "[": + j = i + if j < n and pat[j] == "!": + j = j + 1 + if j < n and pat[j] == "]": + j = j + 1 + while j < n and pat[j] != "]": + j = j + 1 + if j >= n: + add("\\[") + else: + stuff = pat[i:j] + if "-" not in stuff: + stuff = stuff.replace("\\", r"\\") + else: + chunks = [] + k = i + 2 if pat[i] == "!" else i + 1 + while True: + k = pat.find("-", k, j) + if k < 0: + break + chunks.append(pat[i:k]) + i = k + 1 + k = k + 3 + chunk = pat[i:j] + if chunk: + chunks.append(chunk) + else: + chunks[-1] += "-" + # Remove empty ranges -- invalid in RE. + for k in range(len(chunks) - 1, 0, -1): + if chunks[k - 1][-1] > chunks[k][0]: + chunks[k - 1] = chunks[k - 1][:-1] + chunks[k][1:] + del chunks[k] + # Escape backslashes and hyphens for set difference (--). + # Hyphens that create ranges shouldn't be escaped. + stuff = "-".join( + s.replace("\\", r"\\").replace("-", r"\-") for s in chunks + ) + # Escape set operations (&&, ~~ and ||). + stuff = re.sub(r"([&~|])", r"\\\1", stuff) + i = j + 1 + if not stuff: + # Empty range: never match. + add("(?!)") + elif stuff == "!": + # Negated empty range: match any character. + add(".") + else: + if stuff[0] == "!": + stuff = "^" + stuff[1:] + elif stuff[0] in ("^", "["): + stuff = "\\" + stuff + add(f"[{stuff}]") + else: + add(re.escape(c)) + assert i == n + return res + + +def glob_translate(pat): + # Copied from: https://github.com/python/cpython/pull/106703. + # The keyword parameters' values are fixed to: + # recursive=True, include_hidden=True, seps=None + """Translate a pathname with shell wildcards to a regular expression.""" + if os.path.altsep: + seps = os.path.sep + os.path.altsep + else: + seps = os.path.sep + escaped_seps = "".join(map(re.escape, seps)) + any_sep = f"[{escaped_seps}]" if len(seps) > 1 else escaped_seps + not_sep = f"[^{escaped_seps}]" + one_last_segment = f"{not_sep}+" + one_segment = f"{one_last_segment}{any_sep}" + any_segments = f"(?:.+{any_sep})?" + any_last_segments = ".*" + results = [] + parts = re.split(any_sep, pat) + last_part_idx = len(parts) - 1 + for idx, part in enumerate(parts): + if part == "*": + results.append(one_segment if idx < last_part_idx else one_last_segment) + continue + if part == "**": + results.append(any_segments if idx < last_part_idx else any_last_segments) + continue + elif "**" in part: + raise ValueError( + "Invalid pattern: '**' can only be an entire path component" + ) + if part: + results.extend(_translate(part, f"{not_sep}*", not_sep)) + if idx < last_part_idx: + results.append(any_sep) + res = "".join(results) + return rf"(?s:{res})\Z" diff --git a/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/INSTALLER b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/METADATA b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..1eaaae955f155091e4516629699af17fcfb8e57a --- /dev/null +++ b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/METADATA @@ -0,0 +1,151 @@ +Metadata-Version: 2.3 +Name: ftfy +Version: 6.3.1 +Summary: Fixes mojibake and other problems with Unicode, after the fact +Project-URL: Homepage, https://ftfy.readthedocs.io/en/latest/ +Project-URL: Documentation, https://ftfy.readthedocs.io/en/latest/ +Project-URL: Repository, https://github.com/rspeer/python-ftfy +Project-URL: Issues, https://github.com/rspeer/python-ftfy/issues/ +Project-URL: Changelog, https://github.com/rspeer/python-ftfy/blob/main/CHANGELOG.md +Project-URL: Blog, https://posts.arborelia.net +Author-email: Robyn Speer +License: Apache-2.0 +License-File: LICENSE.txt +Requires-Python: >=3.9 +Requires-Dist: wcwidth +Description-Content-Type: text/markdown + +# ftfy: fixes text for you + +[![PyPI package](https://badge.fury.io/py/ftfy.svg)](https://badge.fury.io/py/ftfy) +[![Docs](https://readthedocs.org/projects/ftfy/badge/?version=latest)](https://ftfy.readthedocs.org/en/latest/) + +```python + +>>> from ftfy import fix_encoding +>>> print(fix_encoding("(ง'⌣')ง")) +(ง'⌣')ง + +``` + +The full documentation of ftfy is available at [ftfy.readthedocs.org](https://ftfy.readthedocs.org). The documentation covers a lot more than this README, so here are some links into it: + +- [Fixing problems and getting explanations](https://ftfy.readthedocs.io/en/latest/explain.html) +- [Configuring ftfy](https://ftfy.readthedocs.io/en/latest/config.html) +- [Encodings ftfy can handle](https://ftfy.readthedocs.io/en/latest/encodings.html) +- [“Fixer” functions](https://ftfy.readthedocs.io/en/latest/fixes.html) +- [Is ftfy an encoding detector?](https://ftfy.readthedocs.io/en/latest/detect.html) +- [Heuristics for detecting mojibake](https://ftfy.readthedocs.io/en/latest/heuristic.html) +- [Support for “bad” encodings](https://ftfy.readthedocs.io/en/latest/bad_encodings.html) +- [Command-line usage](https://ftfy.readthedocs.io/en/latest/cli.html) +- [Citing ftfy](https://ftfy.readthedocs.io/en/latest/cite.html) + +## Testimonials + +- “My life is livable again!” + — [@planarrowspace](https://twitter.com/planarrowspace) +- “A handy piece of magic” + — [@simonw](https://twitter.com/simonw) +- “Saved me a large amount of frustrating dev work” + — [@iancal](https://twitter.com/iancal) +- “ftfy did the right thing right away, with no faffing about. Excellent work, solving a very tricky real-world (whole-world!) problem.” + — Brennan Young +- “I have no idea when I’m gonna need this, but I’m definitely bookmarking it.” + — [/u/ocrow](https://reddit.com/u/ocrow) + +## What it does + +Here are some examples (found in the real world) of what ftfy can do: + +ftfy can fix mojibake (encoding mix-ups), by detecting patterns of characters that were clearly meant to be UTF-8 but were decoded as something else: + + >>> import ftfy + >>> ftfy.fix_text('✔ No problems') + '✔ No problems' + +Does this sound impossible? It's really not. UTF-8 is a well-designed encoding that makes it obvious when it's being misused, and a string of mojibake usually contains all the information we need to recover the original string. + +ftfy can fix multiple layers of mojibake simultaneously: + + >>> ftfy.fix_text('The Mona Lisa doesn’t have eyebrows.') + "The Mona Lisa doesn't have eyebrows." + +It can fix mojibake that has had "curly quotes" applied on top of it, which cannot be consistently decoded until the quotes are uncurled: + + >>> ftfy.fix_text("l’humanité") + "l'humanité" + +ftfy can fix mojibake that would have included the character U+A0 (non-breaking space), but the U+A0 was turned into an ASCII space and then combined with another following space: + + >>> ftfy.fix_text('Ã\xa0 perturber la réflexion') + 'à perturber la réflexion' + >>> ftfy.fix_text('à perturber la réflexion') + 'à perturber la réflexion' + +ftfy can also decode HTML entities that appear outside of HTML, even in cases where the entity has been incorrectly capitalized: + + >>> # by the HTML 5 standard, only 'PÉREZ' is acceptable + >>> ftfy.fix_text('P&EACUTE;REZ') + 'PÉREZ' + +These fixes are not applied in all cases, because ftfy has a strongly-held goal of avoiding false positives -- it should never change correctly-decoded text to something else. + +The following text could be encoded in Windows-1252 and decoded in UTF-8, and it would decode as 'MARQUɅ'. However, the original text is already sensible, so it is unchanged. + + >>> ftfy.fix_text('IL Y MARQUÉ…') + 'IL Y MARQUÉ…' + +## Installing + +ftfy is a Python 3 package that can be installed using `pip` or `uv pip`: + + pip install ftfy + +(Or use `pip3 install ftfy` on systems where Python 2 and 3 are both globally installed and `pip` refers to Python 2.) + +If you use `poetry`, you can use ftfy as a dependency in the usual way (such as `poetry add ftfy`). + +### Local development + +ftfy is developed using [uv](https://github.com/astral-sh/uv). You can build a virtual environment with its local dependencies by running `uv venv`, and test it with `uv run pytest`. + +## Who maintains ftfy? + +I'm Robyn Speer, also known as Elia Robyn Lake. You can find my projects +[on GitHub](https://github.com/rspeer) and my posts on [my own blog](https://posts.arborelia.net). + +## Citing ftfy + +ftfy has been used as a crucial data processing step in major NLP research. + +It's important to give credit appropriately to everyone whose work you build on in research. This includes software, not just high-status contributions such as mathematical models. All I ask when you use ftfy for research is that you cite it. + +ftfy has a citable record [on Zenodo](https://zenodo.org/record/2591652). A citation of ftfy may look like this: + + Robyn Speer. (2019). ftfy (Version 5.5). Zenodo. + http://doi.org/10.5281/zenodo.2591652 + +In BibTeX format, the citation is:: + + @misc{speer-2019-ftfy, + author = {Robyn Speer}, + title = {ftfy}, + note = {Version 5.5}, + year = 2019, + howpublished = {Zenodo}, + doi = {10.5281/zenodo.2591652}, + url = {https://doi.org/10.5281/zenodo.2591652} + } + +## Important license clarifications + +If you do not follow ftfy's license, you do not have a license to ftfy. + +This sounds obvious and tautological, but there are people who think open source licenses mean that they can just do what they want, especially in the field of generative AI. It's a permissive license but you still have to follow it. The [Apache license](https://www.apache.org/licenses/LICENSE-2.0) is the only thing that gives you permission to use and copy ftfy; otherwise, all rights are reserved. + +If you use or distribute ftfy, you must follow the terms of the [Apache license](https://www.apache.org/licenses/LICENSE-2.0), including that you must attribute the author of ftfy (Robyn Speer) correctly. + +You _may not_ make a derived work of ftfy that obscures its authorship, such as by putting its code in an AI training dataset, including the code in AI training at runtime, or using a generative AI that copies code from such a dataset. + +At my discretion, I may notify you of a license violation, and give you a chance to either remedy it or delete all copies of ftfy in your possession. + diff --git a/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/RECORD b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..693332db0f3ab35f2e0371dc84bd0c586d75bc64 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/RECORD @@ -0,0 +1,26 @@ +../../../bin/ftfy,sha256=cpu28V6GNrkGsd90nCCcCCFzDrY8y2NVySxVK8GSNXo,262 +ftfy-6.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ftfy-6.3.1.dist-info/METADATA,sha256=g_Z8YoA3djfFYLXogCoAum7lErCh6h2uXf8edFsZ0d8,7257 +ftfy-6.3.1.dist-info/RECORD,, +ftfy-6.3.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87 +ftfy-6.3.1.dist-info/entry_points.txt,sha256=sy4Ei29IqZhQRgYFC17GVrT1JkE6l1-ooJheKulSqO8,39 +ftfy-6.3.1.dist-info/licenses/LICENSE.txt,sha256=oL_FAgvC4eggVRoGJccMlfqomMFec3EA_0iAqhAZeFc,552 +ftfy/__init__.py,sha256=ekX6ZbJnR3UxmgWwpM2_0Eoizn82ukCYQRv57Bsiyzw,29593 +ftfy/__pycache__/__init__.cpython-310.pyc,, +ftfy/__pycache__/badness.cpython-310.pyc,, +ftfy/__pycache__/chardata.cpython-310.pyc,, +ftfy/__pycache__/cli.cpython-310.pyc,, +ftfy/__pycache__/fixes.cpython-310.pyc,, +ftfy/__pycache__/formatting.cpython-310.pyc,, +ftfy/bad_codecs/__init__.py,sha256=sPKWae974vjc3JeOsGhZ8ZFNUoPTISx988zBbxJ7SIQ,3282 +ftfy/bad_codecs/__pycache__/__init__.cpython-310.pyc,, +ftfy/bad_codecs/__pycache__/sloppy.cpython-310.pyc,, +ftfy/bad_codecs/__pycache__/utf8_variants.cpython-310.pyc,, +ftfy/bad_codecs/sloppy.py,sha256=bzgoZAQIrzRUxb-qJmDVX6_Eh2l143IqstdFFFayTII,6814 +ftfy/bad_codecs/utf8_variants.py,sha256=WlyMbOtUFftePEj_SpxDTXirPAj8HcuxVZ1aSYApVh0,9964 +ftfy/badness.py,sha256=jOR-ucAfX6LTjmUP4EdHS95VN766WXG3IsprQHnfb9M,15561 +ftfy/chardata.py,sha256=tAXqrX2Y3zBslAZoj6tFBy-Yn-_-UwF1uszvKN7cVck,34911 +ftfy/cli.py,sha256=q9DNi4LuhKGrleoH_Z3aB6J0oST34a2WWatap7Odr7s,4137 +ftfy/fixes.py,sha256=G8XQKzMlcsw4aPhRrQgcwAMcxqatf7EDmNaOrsY-C6U,18233 +ftfy/formatting.py,sha256=pbmBunYydgPs3bxMhCXFtaTM0_3qT_yp6esZvjvOKZk,5882 +ftfy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/WHEEL b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..cdd68a497cdfa8d3f2b837225beacef711b85047 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.25.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/entry_points.txt b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..99476a44fd1015a1c391431110d1773503340096 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy-6.3.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +ftfy = ftfy.cli:main diff --git a/lib/python3.10/site-packages/ftfy/__init__.py b/lib/python3.10/site-packages/ftfy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cc0a12049eb1a3093fd921b18bb9dbcef5a7584e --- /dev/null +++ b/lib/python3.10/site-packages/ftfy/__init__.py @@ -0,0 +1,802 @@ +""" +ftfy: fixes text for you + +This is a module for making text less broken. See the `fix_text` function +for more information. +""" + +from __future__ import annotations + +import unicodedata +import warnings +from collections.abc import Iterator +from typing import ( + Any, + BinaryIO, + Callable, + Literal, + NamedTuple, + TextIO, + cast, +) + +from ftfy import bad_codecs, chardata, fixes +from ftfy.badness import is_bad +from ftfy.formatting import display_ljust + +__version__ = "6.3.1" + + +# Though this function does nothing, it lets linters know that we're using +# ftfy.bad_codecs. See the docstring in `bad_codecs/__init__.py` for more. +bad_codecs.ok() + + +class ExplanationStep(NamedTuple): + """ + A step in an ExplainedText, explaining how to decode text. + + The possible actions are: + + - "encode": take in a string and encode it as bytes, with the given encoding + - "decode": take in bytes and decode them as a string, with the given encoding + - "transcode": convert bytes to bytes with a particular named function + - "apply": convert str to str with a particular named function + + The `parameter` is the name of the encoding or function to use. If it's a + function, it must appear in the FIXERS dictionary. + """ + + action: str + parameter: str + + def __repr__(self) -> str: + """ + Get the string representation of an ExplanationStep. We output the + representation of the equivalent tuple, for simplicity. + """ + return repr(tuple(self)) + + +class ExplainedText(NamedTuple): + """ + The return type from ftfy's functions that provide an "explanation" of which + steps it applied to fix the text, such as :func:`fix_and_explain()`. + + When the 'explain' option is disabled, these functions return the same + type, but the `explanation` will be None. + """ + + text: str + explanation: list[ExplanationStep] | None + + +# Functions that can be applied using `apply_plan`. +FIXERS: dict[str, Callable] = { # type: ignore[type-arg] + "unescape_html": fixes.unescape_html, + "remove_terminal_escapes": fixes.remove_terminal_escapes, + "restore_byte_a0": fixes.restore_byte_a0, + "replace_lossy_sequences": fixes.replace_lossy_sequences, + "decode_inconsistent_utf8": fixes.decode_inconsistent_utf8, + "fix_c1_controls": fixes.fix_c1_controls, + "fix_latin_ligatures": fixes.fix_latin_ligatures, + "fix_character_width": fixes.fix_character_width, + "uncurl_quotes": fixes.uncurl_quotes, + "fix_line_breaks": fixes.fix_line_breaks, + "fix_surrogates": fixes.fix_surrogates, + "remove_control_chars": fixes.remove_control_chars, +} + + +class TextFixerConfig(NamedTuple): + r""" + A TextFixerConfig object stores configuration options for ftfy. + + It's implemented as a namedtuple with defaults, so you can instantiate + it by providing the values to change from their defaults as keyword arguments. + For example, to disable 'unescape_html' and keep the rest of the defaults:: + + TextFixerConfig(unescape_html=False) + + Here are the options and their default values: + + - `unescape_html`: "auto" + + Configures whether to replace HTML entities such as & with the character + they represent. "auto" says to do this by default, but disable it when a + literal < character appears, indicating that the input is actual HTML and + entities should be preserved. The value can be True, to always enable this + fixer, or False, to always disable it. + + - `remove_terminal_escapes`: True + + Removes "ANSI" terminal escapes, such as for changing the color of text in a + terminal window. + + - `fix_encoding`: True + + Detect mojibake and attempt to fix it by decoding the text in a different + encoding standard. + + The following four options affect `fix_encoding` works, and do nothing if + `fix_encoding` is False: + + - `restore_byte_a0`: True + + Allow a literal space (U+20) to be interpreted as a non-breaking space + (U+A0) when that would make it part of a fixable mojibake string. + + Because spaces are very common characters, this could lead to false + positives, but we try to apply it only when there's strong evidence for + mojibake. Disabling `restore_byte_a0` is safer from false positives, + but creates false negatives. + + - `replace_lossy_sequences`: True + + Detect mojibake that has been partially replaced by the characters + '�' or '?'. If the mojibake could be decoded otherwise, replace the + detected sequence with '�'. + + - `decode_inconsistent_utf8`: True + + When we see sequences that distinctly look like UTF-8 mojibake, but + there's no consistent way to reinterpret the string in a new encoding, + replace the mojibake with the appropriate UTF-8 characters anyway. + + This helps to decode strings that are concatenated from different + encodings. + + - `fix_c1_controls`: True + + Replace C1 control characters (the useless characters U+80 - U+9B that + come from Latin-1) with their Windows-1252 equivalents, like HTML5 does, + even if the whole string doesn't decode as Latin-1. + + - `fix_latin_ligatures`: True + + Replace common Latin-alphabet ligatures, such as ``fi``, with the + letters they're made of. + + - `fix_character_width`: True + + Replace fullwidth Latin characters and halfwidth Katakana with + their more standard widths. + + - `uncurl_quotes`: True + + Replace curly quotes with straight quotes. + + - `fix_line_breaks`: True + + Replace various forms of line breaks with the standard Unix line + break, ``\n``. + + - `fix_surrogates`: True + + Replace sequences of UTF-16 surrogate codepoints with the character + they were meant to encode. This fixes text that was decoded with the + obsolete UCS-2 standard, and allows it to support high-numbered + codepoints such as emoji. + + - `remove_control_chars`: True + + Remove certain control characters that have no displayed effect on text. + + - `normalization`: "NFC" + + Choose what kind of Unicode normalization is applied. Usually, we apply + NFC normalization, so that letters followed by combining characters become + single combined characters. + + Changing this to "NFKC" applies more compatibility conversions, such as + replacing the 'micro sign' with a standard Greek lowercase mu, which looks + identical. However, some NFKC normalizations change the meaning of text, + such as converting "10³" to "103". + + `normalization` can be None, to apply no normalization. + + - `max_decode_length`: 1_000_000 + + The maximum size of "segment" that ftfy will try to fix all at once. + + - `explain`: True + + Whether to compute 'explanations', lists describing what ftfy changed. + When this is False, the explanation will be None, and the code that + builds the explanation will be skipped, possibly saving time. + + Functions that accept TextFixerConfig and don't return an explanation + will automatically set `explain` to False. + """ + + unescape_html: str | bool = "auto" + remove_terminal_escapes: bool = True + fix_encoding: bool = True + restore_byte_a0: bool = True + replace_lossy_sequences: bool = True + decode_inconsistent_utf8: bool = True + fix_c1_controls: bool = True + fix_latin_ligatures: bool = True + fix_character_width: bool = True + uncurl_quotes: bool = True + fix_line_breaks: bool = True + fix_surrogates: bool = True + remove_control_chars: bool = True + normalization: Literal["NFC", "NFD", "NFKC", "NFKD"] | None = "NFC" + max_decode_length: int = 1000000 + explain: bool = True + + +def _config_from_kwargs( + config: TextFixerConfig, kwargs: dict[str, Any] +) -> TextFixerConfig: + """ + Handle parameters provided as keyword arguments to ftfy's top-level + functions, converting them into a TextFixerConfig. + """ + if "fix_entities" in kwargs: + warnings.warn( + "`fix_entities` has been renamed to `unescape_html`", + DeprecationWarning, + stacklevel=2, + ) + kwargs = kwargs.copy() + kwargs["unescape_html"] = kwargs["fix_entities"] + del kwargs["fix_entities"] + config = config._replace(**kwargs) + return config + + +BYTES_ERROR_TEXT = """Hey wait, this isn't Unicode. + +ftfy is designed to fix problems with text. Treating bytes like they're +interchangeable with Unicode text is usually something that introduces +problems with text. + +You should first decode these bytes from the encoding you think they're in. +If you're not sure what encoding they're in: + +- First, try to find out. 'utf-8' is a good assumption. +- If the encoding is simply unknowable, try running your bytes through + ftfy.guess_bytes. As the name implies, this may not always be accurate. + +For more information on the distinction between bytes and text, read the +Python Unicode HOWTO: + + http://docs.python.org/3/howto/unicode.html +""" + + +def _try_fix( + fixer_name: str, + text: str, + config: TextFixerConfig, + steps: list[ExplanationStep] | None, +) -> str: + """ + A helper function used across several 'fixer' steps, deciding whether to + apply the fix and whether to record the fix in `steps`. + """ + if getattr(config, fixer_name): + fixer = FIXERS[fixer_name] + fixed = fixer(text) + if steps is not None and fixed != text: + steps.append(ExplanationStep("apply", fixer_name)) + return cast(str, fixed) + + return text + + +def fix_text(text: str, config: TextFixerConfig | None = None, **kwargs: Any) -> str: + r""" + Given Unicode text as input, fix inconsistencies and glitches in it, + such as mojibake (text that was decoded in the wrong encoding). + + Let's start with some examples: + + >>> fix_text('✔ No problems') + '✔ No problems' + + >>> print(fix_text("¯\\_(ã\x83\x84)_/¯")) + ¯\_(ツ)_/¯ + + >>> fix_text('Broken text… it’s flubberific!') + "Broken text... it's flubberific!" + + >>> fix_text('LOUD NOISES') + 'LOUD NOISES' + + ftfy applies a number of different fixes to the text, and can accept + configuration to select which fixes to apply. + + The configuration takes the form of a :class:`TextFixerConfig` object, + and you can see a description of the options in that class's docstring + or in the full documentation at ftfy.readthedocs.org. + + For convenience and backward compatibility, the configuration can also + take the form of keyword arguments, which will set the equivalently-named + fields of the TextFixerConfig object. + + For example, here are two ways to fix text but skip the "uncurl_quotes" + step:: + + fix_text(text, TextFixerConfig(uncurl_quotes=False)) + fix_text(text, uncurl_quotes=False) + + This function fixes text in independent segments, which are usually lines + of text, or arbitrarily broken up every 1 million codepoints (configurable + with `config.max_decode_length`) if there aren't enough line breaks. The + bound on segment lengths helps to avoid unbounded slowdowns. + + ftfy can also provide an 'explanation', a list of transformations it applied + to the text that would fix more text like it. This function doesn't provide + explanations (because there may be different fixes for different segments + of text). + + To get an explanation, use the :func:`fix_and_explain()` function, which + fixes the string in one segment and explains what it fixed. + """ + + if config is None: + config = TextFixerConfig(explain=False) + config = _config_from_kwargs(config, kwargs) + if isinstance(text, bytes): + raise UnicodeError(BYTES_ERROR_TEXT) + + out = [] + pos = 0 + while pos < len(text): + textbreak = text.find("\n", pos) + 1 + if textbreak == 0: + textbreak = len(text) + if (textbreak - pos) > config.max_decode_length: + textbreak = pos + config.max_decode_length + + segment = text[pos:textbreak] + if config.unescape_html == "auto" and "<" in segment: + config = config._replace(unescape_html=False) + fixed_segment, _ = fix_and_explain(segment, config) + out.append(fixed_segment) + pos = textbreak + return "".join(out) + + +def fix_and_explain( + text: str, config: TextFixerConfig | None = None, **kwargs: Any +) -> ExplainedText: + """ + Fix text as a single segment, returning the fixed text and an explanation + of what was fixed. + + The explanation is a list of steps that can be applied with + :func:`apply_plan`, or if config.explain is False, it will be None. + """ + if config is None: + config = TextFixerConfig() + if isinstance(text, bytes): + raise UnicodeError(BYTES_ERROR_TEXT) + config = _config_from_kwargs(config, kwargs) + + if config.unescape_html == "auto" and "<" in text: + config = config._replace(unescape_html=False) + + if config.explain: + steps: list[ExplanationStep] | None = [] + else: + # If explanations aren't desired, `steps` will be None + steps = None + + while True: + origtext = text + + text = _try_fix("unescape_html", text, config, steps) + + if config.fix_encoding: + if steps is None: + text = fix_encoding(text) + else: + text, encoding_steps = fix_encoding_and_explain(text, config) + if encoding_steps is not None: + steps.extend(encoding_steps) + + for fixer in [ + "fix_c1_controls", + "fix_latin_ligatures", + "fix_character_width", + "uncurl_quotes", + "fix_line_breaks", + "fix_surrogates", + "remove_terminal_escapes", + "remove_control_chars", + ]: + text = _try_fix(fixer, text, config, steps) + + if config.normalization is not None: + fixed = unicodedata.normalize(config.normalization, text) + if steps is not None and fixed != text: + steps.append(ExplanationStep("normalize", config.normalization)) + text = fixed + + if text == origtext: + return ExplainedText(text, steps) + + +def fix_encoding_and_explain( + text: str, config: TextFixerConfig | None = None, **kwargs: Any +) -> ExplainedText: + """ + Apply the steps of ftfy that detect mojibake and fix it. Returns the fixed + text and a list explaining what was fixed. + + This includes fixing text by encoding and decoding it in different encodings, + as well as the subordinate fixes `restore_byte_a0`, `replace_lossy_sequences`, + `decode_inconsistent_utf8`, and `fix_c1_controls`. + + Examples:: + + >>> fix_encoding_and_explain("só") + ExplainedText(text='só', explanation=[('encode', 'latin-1'), ('decode', 'utf-8')]) + + >>> result = fix_encoding_and_explain("voilà le travail") + >>> result.text + 'voilà le travail' + >>> result.explanation + [('encode', 'latin-1'), ('transcode', 'restore_byte_a0'), ('decode', 'utf-8')] + + """ + if config is None: + config = TextFixerConfig() + if isinstance(text, bytes): + raise UnicodeError(BYTES_ERROR_TEXT) + config = _config_from_kwargs(config, kwargs) + + if not config.fix_encoding: + # A weird trivial case: we're asked to fix the encoding, but skip + # fixing the encoding + return ExplainedText(text, []) + + plan_so_far: list[ExplanationStep] = [] + while True: + prevtext = text + text, plan = _fix_encoding_one_step_and_explain(text, config) + if plan is not None: + plan_so_far.extend(plan) + if text == prevtext: + return ExplainedText(text, plan_so_far) + + +def _fix_encoding_one_step_and_explain( + text: str, config: TextFixerConfig +) -> ExplainedText: + """ + Perform one step of fixing the encoding of text. + """ + if config is None: + config = TextFixerConfig() + + if len(text) == 0: + return ExplainedText(text, []) + + # The first plan is to return ASCII text unchanged, as well as text + # that doesn't look like it contains mojibake + if chardata.possible_encoding(text, "ascii") or not is_bad(text): + return ExplainedText(text, []) + + # As we go through the next step, remember the possible encodings + # that we encounter but don't successfully fix yet. We may need them + # later. + possible_1byte_encodings = [] + + # Suppose the text was supposed to be UTF-8, but it was decoded using + # a single-byte encoding instead. When these cases can be fixed, they + # are usually the correct thing to do, so try them next. + for encoding in chardata.CHARMAP_ENCODINGS: + if chardata.possible_encoding(text, encoding): + possible_1byte_encodings.append(encoding) + encoded_bytes = text.encode(encoding) + encode_step = ExplanationStep("encode", encoding) + transcode_steps = [] + + # Now, find out if it's UTF-8 (or close enough). Otherwise, + # remember the encoding for later. + try: + decoding = "utf-8" + # Check encoded_bytes for sequences that would be UTF-8, + # except they have b' ' where b'\xa0' would belong. + # + # Don't do this in the macroman encoding, where it would match + # an en dash followed by a space, leading to false positives. + if ( + config.restore_byte_a0 + and encoding != "macroman" + and chardata.ALTERED_UTF8_RE.search(encoded_bytes) + ): + replaced_bytes = fixes.restore_byte_a0(encoded_bytes) + if replaced_bytes != encoded_bytes: + transcode_steps.append( + ExplanationStep("transcode", "restore_byte_a0") + ) + encoded_bytes = replaced_bytes + + # Replace sequences where information has been lost + if config.replace_lossy_sequences and encoding.startswith("sloppy"): + replaced_bytes = fixes.replace_lossy_sequences(encoded_bytes) + if replaced_bytes != encoded_bytes: + transcode_steps.append( + ExplanationStep("transcode", "replace_lossy_sequences") + ) + encoded_bytes = replaced_bytes + + if 0xED in encoded_bytes or 0xC0 in encoded_bytes: + decoding = "utf-8-variants" + + decode_step = ExplanationStep("decode", decoding) + steps = [encode_step] + transcode_steps + [decode_step] + fixed = encoded_bytes.decode(decoding) + return ExplainedText(fixed, steps) + + except UnicodeDecodeError: + pass + + # Look for a-hat-euro sequences that remain, and fix them in isolation. + if config.decode_inconsistent_utf8 and chardata.UTF8_DETECTOR_RE.search(text): + steps = [ExplanationStep("apply", "decode_inconsistent_utf8")] + fixed = fixes.decode_inconsistent_utf8(text) + if fixed != text: + return ExplainedText(fixed, steps) + + # The next most likely case is that this is Latin-1 that was intended to + # be read as Windows-1252, because those two encodings in particular are + # easily confused. + if "latin-1" in possible_1byte_encodings: + if "windows-1252" in possible_1byte_encodings: + # This text is in the intersection of Latin-1 and + # Windows-1252, so it's probably legit. + return ExplainedText(text, []) + else: + # Otherwise, it means we have characters that are in Latin-1 but + # not in Windows-1252. Those are C1 control characters. Nobody + # wants those. Assume they were meant to be Windows-1252. + try: + fixed = text.encode("latin-1").decode("windows-1252") + if fixed != text: + steps = [ + ExplanationStep("encode", "latin-1"), + ExplanationStep("decode", "windows-1252"), + ] + return ExplainedText(fixed, steps) + except UnicodeDecodeError: + pass + + # Fix individual characters of Latin-1 with a less satisfying explanation + if config.fix_c1_controls and chardata.C1_CONTROL_RE.search(text): + steps = [ExplanationStep("transcode", "fix_c1_controls")] + fixed = fixes.fix_c1_controls(text) + return ExplainedText(fixed, steps) + + # The cases that remain are mixups between two different single-byte + # encodings, and not the common case of Latin-1 vs. Windows-1252. + # + # With the new heuristic in 6.0, it's possible that we're closer to solving + # these in some cases. It would require a lot of testing and tuning, though. + # For now, we leave the text unchanged in these cases. + return ExplainedText(text, []) + + +def fix_encoding( + text: str, config: TextFixerConfig | None = None, **kwargs: Any +) -> str: + """ + Apply just the encoding-fixing steps of ftfy to this text. Returns the + fixed text, discarding the explanation. + + >>> fix_encoding("ó") + 'ó' + >>> fix_encoding("&ATILDE;&SUP3;") + '&ATILDE;&SUP3;' + """ + if config is None: + config = TextFixerConfig(explain=False) + config = _config_from_kwargs(config, kwargs) + fixed, _explan = fix_encoding_and_explain(text, config) + return fixed + + +# Some alternate names for the main functions +ftfy = fix_text + + +def fix_text_segment( + text: str, config: TextFixerConfig | None = None, **kwargs: Any +) -> str: + """ + Fix text as a single segment, with a consistent sequence of steps that + are applied to fix the text. Discard the explanation. + """ + if config is None: + config = TextFixerConfig(explain=False) + config = _config_from_kwargs(config, kwargs) + fixed, _explan = fix_and_explain(text, config) + return fixed + + +def fix_file( + input_file: TextIO | BinaryIO, + encoding: str | None = None, + config: TextFixerConfig | None = None, + **kwargs: Any, +) -> Iterator[str]: + """ + Fix text that is found in a file. + + If the file is being read as Unicode text, use that. If it's being read as + bytes, then we hope an encoding was supplied. If not, unfortunately, we + have to guess what encoding it is. We'll try a few common encodings, but we + make no promises. See the `guess_bytes` function for how this is done. + + The output is a stream of fixed lines of text. + """ + if config is None: + config = TextFixerConfig() + config = _config_from_kwargs(config, kwargs) + + for line in input_file: + if isinstance(line, bytes): + if encoding is None: + line, encoding = guess_bytes(line) + else: + line = line.decode(encoding) + if config.unescape_html == "auto" and "<" in line: + config = config._replace(unescape_html=False) + + fixed_line, _explan = fix_and_explain(line, config) + yield fixed_line + + +def guess_bytes(bstring: bytes) -> tuple[str, str]: + """ + NOTE: Using `guess_bytes` is not the recommended way of using ftfy. ftfy + is not designed to be an encoding detector. + + In the unfortunate situation that you have some bytes in an unknown + encoding, ftfy can guess a reasonable strategy for decoding them, by trying + a few common encodings that can be distinguished from each other. + + Unlike the rest of ftfy, this may not be accurate, and it may *create* + Unicode problems instead of solving them! + + The encodings we try here are: + + - UTF-16 with a byte order mark, because a UTF-16 byte order mark looks + like nothing else + - UTF-8, because it's the global standard, which has been used by a + majority of the Web since 2008 + - "utf-8-variants", or buggy implementations of UTF-8 + - MacRoman, because Microsoft Office thinks it's still a thing, and it + can be distinguished by its line breaks. (If there are no line breaks in + the string, though, you're out of luck.) + - "sloppy-windows-1252", the Latin-1-like encoding that is the most common + single-byte encoding. + """ + if isinstance(bstring, str): + raise UnicodeError( + "This string was already decoded as Unicode. You should pass " + "bytes to guess_bytes, not Unicode." + ) + + if bstring.startswith(b"\xfe\xff") or bstring.startswith(b"\xff\xfe"): + return bstring.decode("utf-16"), "utf-16" + + byteset = set(bstring) + try: + if 0xED in byteset or 0xC0 in byteset: + # Byte 0xed can be used to encode a range of codepoints that + # are UTF-16 surrogates. UTF-8 does not use UTF-16 surrogates, + # so when we see 0xed, it's very likely we're being asked to + # decode CESU-8, the variant that encodes UTF-16 surrogates + # instead of the original characters themselves. + # + # This will occasionally trigger on standard UTF-8, as there + # are some Korean characters that also use byte 0xed, but that's + # not harmful because standard UTF-8 characters will decode the + # same way in our 'utf-8-variants' codec. + # + # Byte 0xc0 is impossible because, numerically, it would only + # encode characters lower than U+0040. Those already have + # single-byte representations, and UTF-8 requires using the + # shortest possible representation. However, Java hides the null + # codepoint, U+0000, in a non-standard longer representation -- it + # encodes it as 0xc0 0x80 instead of 0x00, guaranteeing that 0x00 + # will never appear in the encoded bytes. + # + # The 'utf-8-variants' decoder can handle both of these cases, as + # well as standard UTF-8, at the cost of a bit of speed. + return bstring.decode("utf-8-variants"), "utf-8-variants" + else: + return bstring.decode("utf-8"), "utf-8" + except UnicodeDecodeError: + pass + + if 0x0D in byteset and 0x0A not in byteset: + # Files that contain CR and not LF are likely to be MacRoman. + return bstring.decode("macroman"), "macroman" + + return bstring.decode("sloppy-windows-1252"), "sloppy-windows-1252" + + +def apply_plan(text: str, plan: list[tuple[str, str]]) -> str: + """ + Apply a plan for fixing the encoding of text. + + The plan is a list of tuples of the form (operation, arg). + + `operation` is one of: + + - `'encode'`: convert a string to bytes, using `arg` as the encoding + - `'decode'`: convert bytes to a string, using `arg` as the encoding + - `'transcode'`: convert bytes to bytes, using the function named `arg` + - `'apply'`: convert a string to a string, using the function named `arg` + + The functions that can be applied by 'transcode' and 'apply' are + specifically those that appear in the dictionary named `FIXERS`. They + can also can be imported from the `ftfy.fixes` module. + + Example:: + + >>> mojibake = "schön" + >>> text, plan = fix_and_explain(mojibake) + >>> apply_plan(mojibake, plan) + 'schön' + """ + obj = text + for operation, encoding in plan: + if operation == "encode": + obj = obj.encode(encoding) # type: ignore + elif operation == "decode": + obj = obj.decode(encoding) # type: ignore + elif operation in ("transcode", "apply"): + if encoding in FIXERS: + obj = FIXERS[encoding](obj) + else: + raise ValueError(f"Unknown function to apply: {encoding}") + else: + raise ValueError(f"Unknown plan step: {operation}") + + return obj + + +def explain_unicode(text: str) -> None: + """ + A utility method that's useful for debugging mysterious Unicode. + + It breaks down a string, showing you for each codepoint its number in + hexadecimal, its glyph, its category in the Unicode standard, and its name + in the Unicode standard. + + >>> explain_unicode('(╯°□°)╯︵ ┻━┻') + U+0028 ( [Ps] LEFT PARENTHESIS + U+256F ╯ [So] BOX DRAWINGS LIGHT ARC UP AND LEFT + U+00B0 ° [So] DEGREE SIGN + U+25A1 □ [So] WHITE SQUARE + U+00B0 ° [So] DEGREE SIGN + U+0029 ) [Pe] RIGHT PARENTHESIS + U+256F ╯ [So] BOX DRAWINGS LIGHT ARC UP AND LEFT + U+FE35 ︵ [Ps] PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS + U+0020 [Zs] SPACE + U+253B ┻ [So] BOX DRAWINGS HEAVY UP AND HORIZONTAL + U+2501 ━ [So] BOX DRAWINGS HEAVY HORIZONTAL + U+253B ┻ [So] BOX DRAWINGS HEAVY UP AND HORIZONTAL + """ + for char in text: + if char.isprintable(): + display = char + else: + display = char.encode("unicode-escape").decode("ascii") + print( + "U+{code:04X} {display} [{category}] {name}".format( + display=display_ljust(display, 7), + code=ord(char), + category=unicodedata.category(char), + name=unicodedata.name(char, ""), + ) + ) diff --git a/lib/python3.10/site-packages/ftfy/badness.py b/lib/python3.10/site-packages/ftfy/badness.py new file mode 100644 index 0000000000000000000000000000000000000000..38ec1f44c44cdd3eba35eaa0aaf823ea37fbe0d8 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy/badness.py @@ -0,0 +1,420 @@ +""" +`ftfy.badness` contains a heuristic that detects likely mojibake. + +This heuristic signals to ftfy which segments of text need to be fixed, and +also indicates when the text can stop being fixed. + +The design of this heuristic is that we categorize the approximately 400 +Unicode characters that occur in UTF-8 mojibake, specifically the characters +that come from mixing up UTF-8 with the other encodings we support. We +identify sequences and contexts of these characters that are much more likely +to be mojibake than intended strings, such as lowercase accented letters +followed immediately by currency symbols. +""" + +import warnings +import re + + +# There are only a few hundred characters that occur in known UTF-8 mojibake, and we can +# characterize them: + +MOJIBAKE_CATEGORIES = { + # Characters that appear in many different contexts. Sequences that contain + # them are not inherently mojibake + "common": ( + "\N{NO-BREAK SPACE}" + "\N{SOFT HYPHEN}" + "\N{MIDDLE DOT}" + "\N{ACUTE ACCENT}" + "\N{EN DASH}" + "\N{EM DASH}" + "\N{HORIZONTAL BAR}" + "\N{HORIZONTAL ELLIPSIS}" + "\N{RIGHT SINGLE QUOTATION MARK}" + ), + # the C1 control character range, which have no uses outside of mojibake anymore + "c1": "\x80-\x9f", + # Characters that are nearly 100% used in mojibake + "bad": ( + "\N{BROKEN BAR}" + "\N{CURRENCY SIGN}" + "\N{DIAERESIS}" + "\N{NOT SIGN}" + "\N{MACRON}" + "\N{CEDILLA}" + "\N{LATIN SMALL LETTER F WITH HOOK}" + "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}" # it's not a modifier + "\N{CARON}" + "\N{BREVE}" + "\N{OGONEK}" + "\N{SMALL TILDE}" + "\N{DAGGER}" + "\N{DOUBLE DAGGER}" + "\N{PER MILLE SIGN}" + "\N{REVERSED NOT SIGN}" + "\N{LOZENGE}" + "\ufffd" + # Theoretically these would appear in 'numeric' contexts, but when they + # co-occur with other mojibake characters, it's not really ambiguous + "\N{FEMININE ORDINAL INDICATOR}" + "\N{MASCULINE ORDINAL INDICATOR}" + ), + # Characters used in legalese + "law": ( + "\N{PILCROW SIGN}" + "\N{SECTION SIGN}" + ), + "currency": ( + "\N{CENT SIGN}" + "\N{POUND SIGN}" + "\N{YEN SIGN}" + "\N{PESETA SIGN}" + "\N{EURO SIGN}" + ), + "start_punctuation": ( + "\N{INVERTED EXCLAMATION MARK}" + "\N{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}" + "\N{INVERTED QUESTION MARK}" + "\N{COPYRIGHT SIGN}" + "\N{GREEK TONOS}" + "\N{GREEK DIALYTIKA TONOS}" + "\N{LEFT SINGLE QUOTATION MARK}" + "\N{SINGLE LOW-9 QUOTATION MARK}" + "\N{LEFT DOUBLE QUOTATION MARK}" + "\N{DOUBLE LOW-9 QUOTATION MARK}" + "\N{BULLET}" + "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}" + "\uf8ff" # OS-specific symbol, usually the Apple logo + ), + "end_punctuation": ( + "\N{REGISTERED SIGN}" + "\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" + "\N{DOUBLE ACUTE ACCENT}" + "\N{RIGHT DOUBLE QUOTATION MARK}" + "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}" + "\N{TRADE MARK SIGN}" + ), + "numeric": ( + "\N{SUPERSCRIPT TWO}" + "\N{SUPERSCRIPT THREE}" + "\N{SUPERSCRIPT ONE}" + "\N{PLUS-MINUS SIGN}" + "\N{VULGAR FRACTION ONE QUARTER}" + "\N{VULGAR FRACTION ONE HALF}" + "\N{VULGAR FRACTION THREE QUARTERS}" + "\N{MULTIPLICATION SIGN}" + "\N{MICRO SIGN}" + "\N{DIVISION SIGN}" + "\N{FRACTION SLASH}" + "\N{PARTIAL DIFFERENTIAL}" + "\N{INCREMENT}" + "\N{N-ARY PRODUCT}" + "\N{N-ARY SUMMATION}" + "\N{SQUARE ROOT}" + "\N{INFINITY}" + "\N{INTERSECTION}" + "\N{INTEGRAL}" + "\N{ALMOST EQUAL TO}" + "\N{NOT EQUAL TO}" + "\N{IDENTICAL TO}" + "\N{LESS-THAN OR EQUAL TO}" + "\N{GREATER-THAN OR EQUAL TO}" + "\N{NUMERO SIGN}" + ), + # Letters that might be used to make emoticon faces (kaomoji), and + # therefore might need to appear in more improbable-looking contexts. + # + # These are concatenated character ranges for use in a regex. I know + # they look like faces themselves. I think expressing the ranges like + # this helps to illustrate why we need to be careful with these + # characters. + "kaomoji": ( + "Ò-Ö" + "Ù-Ü" + "ò-ö" + "ø-ü" + "\N{LATIN CAPITAL LETTER O WITH DOUBLE ACUTE}" + "\N{LATIN CAPITAL LETTER O WITH MACRON}" + "\N{LATIN CAPITAL LETTER U WITH MACRON}" + "\N{LATIN CAPITAL LETTER U WITH OGONEK}" + "\N{DEGREE SIGN}" + ), + "upper_accented": ( + # LATIN CAPITAL LETTER A WITH GRAVE - LATIN CAPITAL LETTER N WITH TILDE + "\xc0-\xd1" + # skip capital O's and U's that could be used in kaomoji, but + # include Ø because it's very common in Arabic mojibake: + "\N{LATIN CAPITAL LETTER O WITH STROKE}" + "\N{LATIN CAPITAL LETTER U WITH DIAERESIS}" + "\N{LATIN CAPITAL LETTER Y WITH ACUTE}" + "\N{LATIN CAPITAL LETTER A WITH BREVE}" + "\N{LATIN CAPITAL LETTER A WITH MACRON}" + "\N{LATIN CAPITAL LETTER A WITH OGONEK}" + "\N{LATIN CAPITAL LETTER C WITH ACUTE}" + "\N{LATIN CAPITAL LETTER C WITH CARON}" + "\N{LATIN CAPITAL LETTER D WITH CARON}" + "\N{LATIN CAPITAL LETTER D WITH STROKE}" + "\N{LATIN CAPITAL LETTER E WITH OGONEK}" + "\N{LATIN CAPITAL LETTER E WITH CARON}" + "\N{LATIN CAPITAL LETTER E WITH MACRON}" + "\N{LATIN CAPITAL LETTER E WITH DOT ABOVE}" + "\N{LATIN CAPITAL LETTER G WITH BREVE}" + "\N{LATIN CAPITAL LETTER G WITH CEDILLA}" + "\N{LATIN CAPITAL LETTER I WITH DOT ABOVE}" + "\N{LATIN CAPITAL LETTER I WITH MACRON}" + "\N{LATIN CAPITAL LETTER K WITH CEDILLA}" + "\N{LATIN CAPITAL LETTER L WITH ACUTE}" + "\N{LATIN CAPITAL LETTER L WITH CARON}" + "\N{LATIN CAPITAL LETTER L WITH STROKE}" + "\N{LATIN CAPITAL LETTER L WITH CEDILLA}" + "\N{LATIN CAPITAL LETTER N WITH ACUTE}" + "\N{LATIN CAPITAL LETTER N WITH CARON}" + "\N{LATIN CAPITAL LETTER N WITH CEDILLA}" + "\N{LATIN CAPITAL LIGATURE OE}" + "\N{LATIN CAPITAL LETTER R WITH CARON}" + "\N{LATIN CAPITAL LETTER S WITH ACUTE}" + "\N{LATIN CAPITAL LETTER S WITH CEDILLA}" + "\N{LATIN CAPITAL LETTER S WITH CARON}" + "\N{LATIN CAPITAL LETTER T WITH CEDILLA}" + "\N{LATIN CAPITAL LETTER T WITH CARON}" + "\N{LATIN CAPITAL LETTER U WITH RING ABOVE}" + "\N{LATIN CAPITAL LETTER U WITH DOUBLE ACUTE}" + "\N{LATIN CAPITAL LETTER Y WITH DIAERESIS}" + "\N{LATIN CAPITAL LETTER Z WITH ACUTE}" + "\N{LATIN CAPITAL LETTER Z WITH DOT ABOVE}" + "\N{LATIN CAPITAL LETTER Z WITH CARON}" + "\N{CYRILLIC CAPITAL LETTER GHE WITH UPTURN}" + ), + "lower_accented": ( + "\N{LATIN SMALL LETTER SHARP S}" + # LATIN SMALL LETTER A WITH GRAVE - LATIN SMALL LETTER N WITH TILDE + "\xe0-\xf1" + # skip o's and u's that could be used in kaomoji + "\N{LATIN SMALL LETTER A WITH BREVE}" + "\N{LATIN SMALL LETTER A WITH OGONEK}" + "\N{LATIN SMALL LETTER A WITH MACRON}" + "\N{LATIN SMALL LETTER C WITH ACUTE}" + "\N{LATIN SMALL LETTER C WITH CARON}" + "\N{LATIN SMALL LETTER D WITH CARON}" + "\N{LATIN SMALL LETTER D WITH STROKE}" + "\N{LATIN SMALL LETTER E WITH OGONEK}" + "\N{LATIN SMALL LETTER E WITH CARON}" + "\N{LATIN SMALL LETTER E WITH MACRON}" + "\N{LATIN SMALL LETTER E WITH DOT ABOVE}" + "\N{LATIN SMALL LETTER G WITH BREVE}" + "\N{LATIN SMALL LETTER G WITH CEDILLA}" + "\N{LATIN SMALL LETTER I WITH OGONEK}" + "\N{LATIN SMALL LETTER I WITH MACRON}" + "\N{LATIN SMALL LETTER K WITH CEDILLA}" + "\N{LATIN SMALL LETTER L WITH ACUTE}" + "\N{LATIN SMALL LETTER L WITH CARON}" + "\N{LATIN SMALL LETTER L WITH STROKE}" + "\N{LATIN SMALL LETTER L WITH CEDILLA}" + "\N{LATIN SMALL LIGATURE OE}" + "\N{LATIN SMALL LETTER R WITH ACUTE}" + "\N{LATIN SMALL LETTER S WITH ACUTE}" + "\N{LATIN SMALL LETTER S WITH CEDILLA}" + "\N{LATIN SMALL LETTER S WITH CARON}" + "\N{LATIN SMALL LETTER T WITH CARON}" + "\N{LATIN SMALL LETTER U WITH DIAERESIS}" + "\N{LATIN SMALL LETTER Z WITH ACUTE}" + "\N{LATIN SMALL LETTER Z WITH DOT ABOVE}" + "\N{LATIN SMALL LETTER Z WITH CARON}" + "\N{CYRILLIC SMALL LETTER GHE WITH UPTURN}" + "\N{LATIN SMALL LIGATURE FI}" + "\N{LATIN SMALL LIGATURE FL}" + ), + "upper_common": ( + "\N{LATIN CAPITAL LETTER THORN}" + "\N{GREEK CAPITAL LETTER ALPHA}-\N{GREEK CAPITAL LETTER OMEGA}" + # not included under 'accented' because these can commonly + # occur at ends of words, in positions where they'd be detected + # as mojibake + "\N{GREEK CAPITAL LETTER ALPHA WITH TONOS}" + "\N{GREEK CAPITAL LETTER EPSILON WITH TONOS}" + "\N{GREEK CAPITAL LETTER ETA WITH TONOS}" + "\N{GREEK CAPITAL LETTER IOTA WITH TONOS}" + "\N{GREEK CAPITAL LETTER OMICRON WITH TONOS}" + "\N{GREEK CAPITAL LETTER UPSILON WITH TONOS}" + "\N{GREEK CAPITAL LETTER OMEGA WITH TONOS}" + "\N{GREEK CAPITAL LETTER IOTA WITH DIALYTIKA}" + "\N{GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA}" + "\N{CYRILLIC CAPITAL LETTER IO}-\N{CYRILLIC CAPITAL LETTER YA}" + ), + "lower_common": ( + # lowercase thorn does not appear in mojibake + "\N{GREEK SMALL LETTER ALPHA}-\N{GREEK SMALL LETTER OMEGA}" + "\N{GREEK SMALL LETTER ALPHA WITH TONOS}" + "\N{GREEK SMALL LETTER EPSILON WITH TONOS}" + "\N{GREEK SMALL LETTER ETA WITH TONOS}" + "\N{GREEK SMALL LETTER IOTA WITH TONOS}" + "\N{GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS}" + "\N{CYRILLIC SMALL LETTER A}-\N{CYRILLIC SMALL LETTER DZHE}" + ), + "box": ( + # omit the single horizontal line, might be used in kaomoji + "│┌┐┘├┤┬┼" + "\N{BOX DRAWINGS DOUBLE HORIZONTAL}-\N{BOX DRAWINGS DOUBLE VERTICAL AND HORIZONTAL}" + "▀▄█▌▐░▒▓" + ), +} + + +# We can now build a regular expression that detects unlikely juxtapositions +# of characters, mostly based on their categories. +# +# Another regular expression, which detects sequences that look more specifically +# like UTF-8 mojibake, appears in chardata.py. +# +# This is a verbose regular expression, with whitespace added for somewhat more +# readability. Remember that the only spaces that count as literal spaces in this +# expression are ones inside character classes (square brackets). + +BADNESS_RE = re.compile( + r""" + [{c1}] + | + [{bad}{lower_accented}{upper_accented}{box}{start_punctuation}{end_punctuation}{currency}{numeric}{law}] [{bad}] + | + [a-zA-Z] [{lower_common}{upper_common}] [{bad}] + | + [{bad}] [{lower_accented}{upper_accented}{box}{start_punctuation}{end_punctuation}{currency}{numeric}{law}] + | + [{lower_accented}{lower_common}{box}{end_punctuation}{currency}{numeric}] [{upper_accented}] + | + [{box}{end_punctuation}{currency}{numeric}] [{lower_accented}] + | + [{lower_accented}{box}{end_punctuation}] [{currency}] + | + \s [{upper_accented}] [{currency}] + | + [{upper_accented}{box}] [{numeric}{law}] + | + [{lower_accented}{upper_accented}{box}{currency}{end_punctuation}] [{start_punctuation}] [{numeric}] + | + [{lower_accented}{upper_accented}{currency}{numeric}{box}{law}] [{end_punctuation}] [{start_punctuation}] + | + [{currency}{numeric}{box}] [{start_punctuation}] + | + [a-z] [{upper_accented}] [{start_punctuation}{currency}] + | + [{box}] [{kaomoji}] + | + [{lower_accented}{upper_accented}{currency}{numeric}{start_punctuation}{end_punctuation}{law}] [{box}] + | + [{box}] [{end_punctuation}] + | + [{lower_accented}{upper_accented}] [{start_punctuation}{end_punctuation}] \w + | + + # The ligature œ when not followed by an unaccented Latin letter + [Œœ][^A-Za-z] + | + + # Degree signs after capital letters + [{upper_accented}]° + | + + # Common Windows-1252 2-character mojibake that isn't covered by the cases above + [ÂÃÎÐ][€œŠš¢£Ÿž\xa0\xad®©°·»{start_punctuation}{end_punctuation}–—´] + | + × [²³] + | + # Windows-1252 mojibake of Arabic words needs to include the 'common' characters. + # To compensate, we require four characters to be matched. + [ØÙ] [{common}{currency}{bad}{numeric}{start_punctuation}ŸŠ®°µ»] + [ØÙ] [{common}{currency}{bad}{numeric}{start_punctuation}ŸŠ®°µ»] + | + + # Windows-1252 mojibake that starts 3-character sequences for some South Asian + # alphabets + à[²µ¹¼½¾] + | + + # MacRoman mojibake that isn't covered by the cases above + √[±∂†≠®™´≤≥¥µø] + | + ≈[°¢] + | + ‚Ä[ìîïòôúùû†°¢π] + | + ‚[âó][àä°ê] + | + + # Windows-1251 mojibake of characters in the U+2000 range + †+ | + + # Windows-1251 mojibake of Latin-1 characters and/or the Cyrillic alphabet. + # Because the 2-character sequences involved here may be common, we require + # seeing a 3-character sequence. + [ВГРС][{c1}{bad}{start_punctuation}{end_punctuation}{currency}°µ][ВГРС] + | + # A distinctive five-character sequence of Cyrillic letters, which can be + # Windows-1251 mojibake on top of Latin-1 mojibake of Windows-1252 characters. + # Require a Latin letter nearby. + ГўВЂВ.[A-Za-z ] + | + + # Windows-1252 encodings of 'à' and 'á', as well as \xa0 itself + Ã[\xa0¡] + | + [a-z]\s?[ÃÂ][ ] + | + ^[ÃÂ][ ] + | + + # Cases where  precedes a character as an encoding of exactly the same + # character, and the character is common enough + [a-z.,?!{end_punctuation}]  [ {start_punctuation}{end_punctuation}] + | + + # Windows-1253 mojibake of characters in the U+2000 range + β€[™\xa0Ά\xad®°] + | + + # Windows-1253 mojibake of Latin-1 characters and/or the Greek alphabet + [ΒΓΞΟ][{c1}{bad}{start_punctuation}{end_punctuation}{currency}°][ΒΓΞΟ] + | + + # Windows-1257 mojibake of characters in the U+2000 range + †+ """.format( + **MOJIBAKE_CATEGORIES + ), + re.VERBOSE, +) + + +def sequence_weirdness(text: str) -> int: + """ + This was the name of the heuristic used in ftfy 2.x through 5.x. As an + attempt at compatibility with external code that calls the heuristic + directly, we redirect to our new heuristic, :func:`badness`. + """ + warnings.warn( + "`sequence_weirdness()` is an old heuristic, and the current " + "closest equivalent is `ftfy.badness.badness()`" + ) + return badness(text) + + +def badness(text: str) -> int: + """ + Get the 'badness' of a sequence of text, counting the number of unlikely + character sequences. A badness greater than 0 indicates that some of it + seems to be mojibake. + """ + return len(BADNESS_RE.findall(text)) + + +def is_bad(text: str) -> bool: + """ + Returns true iff the given text looks like it contains mojibake. + + This can be faster than `badness`, because it returns when the first match + is found to a regex instead of counting matches. Note that as strings get + longer, they have a higher chance of returning True for `is_bad(string)`. + """ + return bool(BADNESS_RE.search(text)) diff --git a/lib/python3.10/site-packages/ftfy/chardata.py b/lib/python3.10/site-packages/ftfy/chardata.py new file mode 100644 index 0000000000000000000000000000000000000000..afcc76715707694d4fdf25ca3d33247731e21c00 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy/chardata.py @@ -0,0 +1,691 @@ +""" +This gives other modules access to the gritty details about characters and the +encodings that use them. +""" + +from __future__ import annotations + +import html +import itertools +import re +import unicodedata + +# These are the encodings we will try to fix in ftfy, in the +# order that they should be tried. +CHARMAP_ENCODINGS = [ + "latin-1", + "sloppy-windows-1252", + "sloppy-windows-1251", + "sloppy-windows-1250", + "sloppy-windows-1253", + "sloppy-windows-1254", + "sloppy-windows-1257", + "iso-8859-2", + "macroman", + "cp437", +] + +SINGLE_QUOTE_RE = re.compile("[\u02bc\u2018-\u201b]") +DOUBLE_QUOTE_RE = re.compile("[\u201c-\u201f]") + + +def _build_regexes() -> dict[str, re.Pattern[str]]: + """ + ENCODING_REGEXES contain reasonably fast ways to detect if we + could represent a given string in a given encoding. The simplest one is + the 'ascii' detector, which of course just determines if all characters + are between U+0000 and U+007F. + """ + # Define a regex that matches ASCII text. + encoding_regexes = {"ascii": re.compile("^[\x00-\x7f]*$")} + + for encoding in CHARMAP_ENCODINGS: + # Make a sequence of characters that bytes \x80 to \xFF decode to + # in each encoding, as well as byte \x1A, which is used to represent + # the replacement character � in the sloppy-* encodings. + byte_range = bytes(list(range(0x80, 0x100)) + [0x1A]) + charlist = byte_range.decode(encoding) + + # The rest of the ASCII bytes -- bytes \x00 to \x19 and \x1B + # to \x7F -- will decode as those ASCII characters in any encoding we + # support, so we can just include them as ranges. This also lets us + # not worry about escaping regex special characters, because all of + # them are in the \x1B to \x7F range. + regex = f"^[\x00-\x19\x1b-\x7f{charlist}]*$" + encoding_regexes[encoding] = re.compile(regex) + return encoding_regexes + + +ENCODING_REGEXES = _build_regexes() + + +def _build_html_entities() -> dict[str, str]: + entities = {} + # Create a dictionary based on the built-in HTML5 entity dictionary. + # Add a limited set of HTML entities that we'll also decode if they've + # been case-folded to uppercase, such as decoding &NTILDE; as "Ñ". + for name, char in html.entities.html5.items(): # type: ignore + if name.endswith(";"): + entities["&" + name] = char + + # Restrict the set of characters we can attempt to decode if their + # name has been uppercased. If we tried to handle all entity names, + # the results would be ambiguous. + if name == name.lower(): + name_upper = name.upper() + entity_upper = "&" + name_upper + if html.unescape(entity_upper) == entity_upper: + entities[entity_upper] = char.upper() + return entities + + +HTML_ENTITY_RE = re.compile(r"&#?[0-9A-Za-z]{1,24};") +HTML_ENTITIES = _build_html_entities() + + +def possible_encoding(text: str, encoding: str) -> bool: + """ + Given text and a single-byte encoding, check whether that text could have + been decoded from that single-byte encoding. + + In other words, check whether it can be encoded in that encoding, possibly + sloppily. + """ + return bool(ENCODING_REGEXES[encoding].match(text)) + + +def _build_control_char_mapping() -> dict[int, None]: + """ + Build a translate mapping that strips likely-unintended control characters. + See :func:`ftfy.fixes.remove_control_chars` for a description of these + codepoint ranges and why they should be removed. + """ + control_chars: dict[int, None] = {} + + for i in itertools.chain( + range(0x00, 0x09), + [0x0B], + range(0x0E, 0x20), + [0x7F], + range(0x206A, 0x2070), + [0xFEFF], + range(0xFFF9, 0xFFFD), + ): + control_chars[i] = None + + return control_chars + + +CONTROL_CHARS = _build_control_char_mapping() + + +# Recognize UTF-8 sequences that would be valid if it weren't for a b'\xa0' +# that some Windows-1252 program converted to a plain space. +# +# The smaller values are included on a case-by-case basis, because we don't want +# to decode likely input sequences to unlikely characters. These are the ones +# that *do* form likely characters before 0xa0: +# +# 0xc2 -> U+A0 NO-BREAK SPACE +# 0xc3 -> U+E0 LATIN SMALL LETTER A WITH GRAVE +# 0xc5 -> U+160 LATIN CAPITAL LETTER S WITH CARON +# 0xce -> U+3A0 GREEK CAPITAL LETTER PI +# 0xd0 -> U+420 CYRILLIC CAPITAL LETTER ER +# 0xd9 -> U+660 ARABIC-INDIC DIGIT ZERO +# +# In three-character sequences, we exclude some lead bytes in some cases. +# +# When the lead byte is immediately followed by 0xA0, we shouldn't accept +# a space there, because it leads to some less-likely character ranges: +# +# 0xe0 -> Samaritan script +# 0xe1 -> Mongolian script (corresponds to Latin-1 'á' which is too common) +# +# We accept 0xe2 and 0xe3, which cover many scripts. Bytes 0xe4 and +# higher point mostly to CJK characters, which we generally don't want to +# decode near Latin lowercase letters. +# +# In four-character sequences, the lead byte must be F0, because that accounts +# for almost all of the usage of high-numbered codepoints (tag characters whose +# UTF-8 starts with the byte F3 are only used in some rare new emoji sequences). +# +# This is meant to be applied to encodings of text that tests true for `is_bad`. +# Any of these could represent characters that legitimately appear surrounded by +# spaces, particularly U+C5 (Å), which is a word in multiple languages! +# +# We should consider checking for b'\x85' being converted to ... in the future. +# I've seen it once, but the text still wasn't recoverable. + +ALTERED_UTF8_RE = re.compile( + b"[\xc2\xc3\xc5\xce\xd0\xd9][ ]" + b"|[\xe2\xe3][ ][\x80-\x84\x86-\x9f\xa1-\xbf]" + b"|[\xe0-\xe3][\x80-\x84\x86-\x9f\xa1-\xbf][ ]" + b"|[\xf0][ ][\x80-\xbf][\x80-\xbf]" + b"|[\xf0][\x80-\xbf][ ][\x80-\xbf]" + b"|[\xf0][\x80-\xbf][\x80-\xbf][ ]" +) + + +# This expression matches UTF-8 and CESU-8 sequences where some of the +# continuation bytes have been lost. The byte 0x1a (sometimes written as ^Z) is +# used within ftfy to represent a byte that produced the replacement character +# \ufffd. We don't know which byte it was, but we can at least decode the UTF-8 +# sequence as \ufffd instead of failing to re-decode it at all. +# +# In some cases, we allow the ASCII '?' in place of \ufffd, but at most once per +# sequence. +LOSSY_UTF8_RE = re.compile( + b"[\xc2-\xdf][\x1a]" + b"|[\xc2-\xc3][?]" + b"|\xed[\xa0-\xaf][\x1a?]\xed[\xb0-\xbf][\x1a?\x80-\xbf]" + b"|\xed[\xa0-\xaf][\x1a?\x80-\xbf]\xed[\xb0-\xbf][\x1a?]" + b"|[\xe0-\xef][\x1a?][\x1a\x80-\xbf]" + b"|[\xe0-\xef][\x1a\x80-\xbf][\x1a?]" + b"|[\xf0-\xf4][\x1a?][\x1a\x80-\xbf][\x1a\x80-\xbf]" + b"|[\xf0-\xf4][\x1a\x80-\xbf][\x1a?][\x1a\x80-\xbf]" + b"|[\xf0-\xf4][\x1a\x80-\xbf][\x1a\x80-\xbf][\x1a?]" + b"|\x1a" +) + + +# This regex matches C1 control characters, which occupy some of the positions +# in the Latin-1 character map that Windows assigns to other characters instead. +C1_CONTROL_RE = re.compile(r"[\x80-\x9f]") + + +# A translate mapping that breaks ligatures made of Latin letters. While +# ligatures may be important to the representation of other languages, in Latin +# letters they tend to represent a copy/paste error. It omits ligatures such +# as æ that are frequently used intentionally. +# +# This list additionally includes some Latin digraphs that represent two +# characters for legacy encoding reasons, not for typographical reasons. +# +# Ligatures and digraphs may also be separated by NFKC normalization, but that +# is sometimes more normalization than you want. + +LIGATURES = { + ord("IJ"): "IJ", # Dutch ligatures + ord("ij"): "ij", + ord("ʼn"): "ʼn", # Afrikaans digraph meant to avoid auto-curled quote + ord("DZ"): "DZ", # Serbian/Croatian digraphs for Cyrillic conversion + ord("Dz"): "Dz", + ord("dz"): "dz", + ord("DŽ"): "DŽ", + ord("Dž"): "Dž", + ord("dž"): "dž", + ord("LJ"): "LJ", + ord("Lj"): "Lj", + ord("lj"): "lj", + ord("NJ"): "NJ", + ord("Nj"): "Nj", + ord("nj"): "nj", + ord("ff"): "ff", # Latin typographical ligatures + ord("fi"): "fi", + ord("fl"): "fl", + ord("ffi"): "ffi", + ord("ffl"): "ffl", + ord("ſt"): "ſt", + ord("st"): "st", +} + + +def _build_width_map() -> dict[int, str]: + """ + Build a translate mapping that replaces halfwidth and fullwidth forms + with their standard-width forms. + """ + # Though it's not listed as a fullwidth character, we'll want to convert + # U+3000 IDEOGRAPHIC SPACE to U+20 SPACE on the same principle, so start + # with that in the dictionary. + width_map = {0x3000: " "} + for i in range(0xFF01, 0xFFF0): + char = chr(i) + alternate = unicodedata.normalize("NFKC", char) + if alternate != char: + width_map[i] = alternate + return width_map + + +WIDTH_MAP = _build_width_map() + + +# Character classes that help us pinpoint embedded mojibake. These can +# include common characters, because we'll also check them for 'badness'. +# +# Though they go on for many lines, the members of this dictionary are +# single concatenated strings. +# +# This code is generated using scripts/char_data_table.py. +UTF8_CLUES: dict[str, str] = { + # Letters that decode to 0xC2 - 0xDF in a Latin-1-like encoding + "utf8_first_of_2": ( + "\N{LATIN CAPITAL LETTER A WITH BREVE}" # windows-1250:C3 + "\N{LATIN CAPITAL LETTER A WITH CIRCUMFLEX}" # latin-1:C2 + "\N{LATIN CAPITAL LETTER A WITH DIAERESIS}" # latin-1:C4 + "\N{LATIN CAPITAL LETTER A WITH MACRON}" # windows-1257:C2 + "\N{LATIN CAPITAL LETTER A WITH RING ABOVE}" # latin-1:C5 + "\N{LATIN CAPITAL LETTER A WITH TILDE}" # latin-1:C3 + "\N{LATIN CAPITAL LETTER AE}" # latin-1:C6 + "\N{LATIN CAPITAL LETTER C WITH ACUTE}" # windows-1250:C6 + "\N{LATIN CAPITAL LETTER C WITH CARON}" # windows-1250:C8 + "\N{LATIN CAPITAL LETTER C WITH CEDILLA}" # latin-1:C7 + "\N{LATIN CAPITAL LETTER D WITH CARON}" # windows-1250:CF + "\N{LATIN CAPITAL LETTER D WITH STROKE}" # windows-1250:D0 + "\N{LATIN CAPITAL LETTER E WITH ACUTE}" # latin-1:C9 + "\N{LATIN CAPITAL LETTER E WITH CARON}" # windows-1250:CC + "\N{LATIN CAPITAL LETTER E WITH CIRCUMFLEX}" # latin-1:CA + "\N{LATIN CAPITAL LETTER E WITH DIAERESIS}" # latin-1:CB + "\N{LATIN CAPITAL LETTER E WITH DOT ABOVE}" # windows-1257:CB + "\N{LATIN CAPITAL LETTER E WITH GRAVE}" # latin-1:C8 + "\N{LATIN CAPITAL LETTER E WITH MACRON}" # windows-1257:C7 + "\N{LATIN CAPITAL LETTER E WITH OGONEK}" # windows-1250:CA + "\N{LATIN CAPITAL LETTER ETH}" # latin-1:D0 + "\N{LATIN CAPITAL LETTER G WITH BREVE}" # windows-1254:D0 + "\N{LATIN CAPITAL LETTER G WITH CEDILLA}" # windows-1257:CC + "\N{LATIN CAPITAL LETTER I WITH ACUTE}" # latin-1:CD + "\N{LATIN CAPITAL LETTER I WITH CIRCUMFLEX}" # latin-1:CE + "\N{LATIN CAPITAL LETTER I WITH DIAERESIS}" # latin-1:CF + "\N{LATIN CAPITAL LETTER I WITH DOT ABOVE}" # windows-1254:DD + "\N{LATIN CAPITAL LETTER I WITH GRAVE}" # latin-1:CC + "\N{LATIN CAPITAL LETTER I WITH MACRON}" # windows-1257:CE + "\N{LATIN CAPITAL LETTER K WITH CEDILLA}" # windows-1257:CD + "\N{LATIN CAPITAL LETTER L WITH ACUTE}" # windows-1250:C5 + "\N{LATIN CAPITAL LETTER L WITH CEDILLA}" # windows-1257:CF + "\N{LATIN CAPITAL LETTER L WITH STROKE}" # windows-1257:D9 + "\N{LATIN CAPITAL LETTER N WITH ACUTE}" # windows-1250:D1 + "\N{LATIN CAPITAL LETTER N WITH CARON}" # windows-1250:D2 + "\N{LATIN CAPITAL LETTER N WITH CEDILLA}" # windows-1257:D2 + "\N{LATIN CAPITAL LETTER N WITH TILDE}" # latin-1:D1 + "\N{LATIN CAPITAL LETTER O WITH ACUTE}" # latin-1:D3 + "\N{LATIN CAPITAL LETTER O WITH CIRCUMFLEX}" # latin-1:D4 + "\N{LATIN CAPITAL LETTER O WITH DIAERESIS}" # latin-1:D6 + "\N{LATIN CAPITAL LETTER O WITH DOUBLE ACUTE}" # windows-1250:D5 + "\N{LATIN CAPITAL LETTER O WITH GRAVE}" # latin-1:D2 + "\N{LATIN CAPITAL LETTER O WITH MACRON}" # windows-1257:D4 + "\N{LATIN CAPITAL LETTER O WITH STROKE}" # latin-1:D8 + "\N{LATIN CAPITAL LETTER O WITH TILDE}" # latin-1:D5 + "\N{LATIN CAPITAL LETTER R WITH CARON}" # windows-1250:D8 + "\N{LATIN CAPITAL LETTER S WITH ACUTE}" # windows-1257:DA + "\N{LATIN CAPITAL LETTER S WITH CARON}" # windows-1257:D0 + "\N{LATIN CAPITAL LETTER S WITH CEDILLA}" # windows-1254:DE + "\N{LATIN CAPITAL LETTER T WITH CEDILLA}" # windows-1250:DE + "\N{LATIN CAPITAL LETTER THORN}" # latin-1:DE + "\N{LATIN CAPITAL LETTER U WITH ACUTE}" # latin-1:DA + "\N{LATIN CAPITAL LETTER U WITH CIRCUMFLEX}" # latin-1:DB + "\N{LATIN CAPITAL LETTER U WITH DIAERESIS}" # latin-1:DC + "\N{LATIN CAPITAL LETTER U WITH DOUBLE ACUTE}" # windows-1250:DB + "\N{LATIN CAPITAL LETTER U WITH GRAVE}" # latin-1:D9 + "\N{LATIN CAPITAL LETTER U WITH MACRON}" # windows-1257:DB + "\N{LATIN CAPITAL LETTER U WITH OGONEK}" # windows-1257:D8 + "\N{LATIN CAPITAL LETTER U WITH RING ABOVE}" # windows-1250:D9 + "\N{LATIN CAPITAL LETTER Y WITH ACUTE}" # latin-1:DD + "\N{LATIN CAPITAL LETTER Z WITH ACUTE}" # windows-1257:CA + "\N{LATIN CAPITAL LETTER Z WITH CARON}" # windows-1257:DE + "\N{LATIN CAPITAL LETTER Z WITH DOT ABOVE}" # windows-1257:DD + "\N{LATIN SMALL LETTER SHARP S}" # latin-1:DF + "\N{MULTIPLICATION SIGN}" # latin-1:D7 + "\N{GREEK CAPITAL LETTER BETA}" # windows-1253:C2 + "\N{GREEK CAPITAL LETTER GAMMA}" # windows-1253:C3 + "\N{GREEK CAPITAL LETTER DELTA}" # windows-1253:C4 + "\N{GREEK CAPITAL LETTER EPSILON}" # windows-1253:C5 + "\N{GREEK CAPITAL LETTER ZETA}" # windows-1253:C6 + "\N{GREEK CAPITAL LETTER ETA}" # windows-1253:C7 + "\N{GREEK CAPITAL LETTER THETA}" # windows-1253:C8 + "\N{GREEK CAPITAL LETTER IOTA}" # windows-1253:C9 + "\N{GREEK CAPITAL LETTER KAPPA}" # windows-1253:CA + "\N{GREEK CAPITAL LETTER LAMDA}" # windows-1253:CB + "\N{GREEK CAPITAL LETTER MU}" # windows-1253:CC + "\N{GREEK CAPITAL LETTER NU}" # windows-1253:CD + "\N{GREEK CAPITAL LETTER XI}" # windows-1253:CE + "\N{GREEK CAPITAL LETTER OMICRON}" # windows-1253:CF + "\N{GREEK CAPITAL LETTER PI}" # windows-1253:D0 + "\N{GREEK CAPITAL LETTER RHO}" # windows-1253:D1 + "\N{GREEK CAPITAL LETTER SIGMA}" # windows-1253:D3 + "\N{GREEK CAPITAL LETTER TAU}" # windows-1253:D4 + "\N{GREEK CAPITAL LETTER UPSILON}" # windows-1253:D5 + "\N{GREEK CAPITAL LETTER PHI}" # windows-1253:D6 + "\N{GREEK CAPITAL LETTER CHI}" # windows-1253:D7 + "\N{GREEK CAPITAL LETTER PSI}" # windows-1253:D8 + "\N{GREEK CAPITAL LETTER OMEGA}" # windows-1253:D9 + "\N{GREEK CAPITAL LETTER IOTA WITH DIALYTIKA}" # windows-1253:DA + "\N{GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA}" # windows-1253:DB + "\N{GREEK SMALL LETTER ALPHA WITH TONOS}" # windows-1253:DC + "\N{GREEK SMALL LETTER EPSILON WITH TONOS}" # windows-1253:DD + "\N{GREEK SMALL LETTER ETA WITH TONOS}" # windows-1253:DE + "\N{GREEK SMALL LETTER IOTA WITH TONOS}" # windows-1253:DF + "\N{CYRILLIC CAPITAL LETTER VE}" # windows-1251:C2 + "\N{CYRILLIC CAPITAL LETTER GHE}" # windows-1251:C3 + "\N{CYRILLIC CAPITAL LETTER DE}" # windows-1251:C4 + "\N{CYRILLIC CAPITAL LETTER IE}" # windows-1251:C5 + "\N{CYRILLIC CAPITAL LETTER ZHE}" # windows-1251:C6 + "\N{CYRILLIC CAPITAL LETTER ZE}" # windows-1251:C7 + "\N{CYRILLIC CAPITAL LETTER I}" # windows-1251:C8 + "\N{CYRILLIC CAPITAL LETTER SHORT I}" # windows-1251:C9 + "\N{CYRILLIC CAPITAL LETTER KA}" # windows-1251:CA + "\N{CYRILLIC CAPITAL LETTER EL}" # windows-1251:CB + "\N{CYRILLIC CAPITAL LETTER EM}" # windows-1251:CC + "\N{CYRILLIC CAPITAL LETTER EN}" # windows-1251:CD + "\N{CYRILLIC CAPITAL LETTER O}" # windows-1251:CE + "\N{CYRILLIC CAPITAL LETTER PE}" # windows-1251:CF + "\N{CYRILLIC CAPITAL LETTER ER}" # windows-1251:D0 + "\N{CYRILLIC CAPITAL LETTER ES}" # windows-1251:D1 + "\N{CYRILLIC CAPITAL LETTER TE}" # windows-1251:D2 + "\N{CYRILLIC CAPITAL LETTER U}" # windows-1251:D3 + "\N{CYRILLIC CAPITAL LETTER EF}" # windows-1251:D4 + "\N{CYRILLIC CAPITAL LETTER HA}" # windows-1251:D5 + "\N{CYRILLIC CAPITAL LETTER TSE}" # windows-1251:D6 + "\N{CYRILLIC CAPITAL LETTER CHE}" # windows-1251:D7 + "\N{CYRILLIC CAPITAL LETTER SHA}" # windows-1251:D8 + "\N{CYRILLIC CAPITAL LETTER SHCHA}" # windows-1251:D9 + "\N{CYRILLIC CAPITAL LETTER HARD SIGN}" # windows-1251:DA + "\N{CYRILLIC CAPITAL LETTER YERU}" # windows-1251:DB + "\N{CYRILLIC CAPITAL LETTER SOFT SIGN}" # windows-1251:DC + "\N{CYRILLIC CAPITAL LETTER E}" # windows-1251:DD + "\N{CYRILLIC CAPITAL LETTER YU}" # windows-1251:DE + "\N{CYRILLIC CAPITAL LETTER YA}" # windows-1251:DF + ), + # Letters that decode to 0xE0 - 0xEF in a Latin-1-like encoding + "utf8_first_of_3": ( + "\N{LATIN SMALL LETTER A WITH ACUTE}" # latin-1:E1 + "\N{LATIN SMALL LETTER A WITH BREVE}" # windows-1250:E3 + "\N{LATIN SMALL LETTER A WITH CIRCUMFLEX}" # latin-1:E2 + "\N{LATIN SMALL LETTER A WITH DIAERESIS}" # latin-1:E4 + "\N{LATIN SMALL LETTER A WITH GRAVE}" # latin-1:E0 + "\N{LATIN SMALL LETTER A WITH MACRON}" # windows-1257:E2 + "\N{LATIN SMALL LETTER A WITH OGONEK}" # windows-1257:E0 + "\N{LATIN SMALL LETTER A WITH RING ABOVE}" # latin-1:E5 + "\N{LATIN SMALL LETTER A WITH TILDE}" # latin-1:E3 + "\N{LATIN SMALL LETTER AE}" # latin-1:E6 + "\N{LATIN SMALL LETTER C WITH ACUTE}" # windows-1250:E6 + "\N{LATIN SMALL LETTER C WITH CARON}" # windows-1250:E8 + "\N{LATIN SMALL LETTER C WITH CEDILLA}" # latin-1:E7 + "\N{LATIN SMALL LETTER D WITH CARON}" # windows-1250:EF + "\N{LATIN SMALL LETTER E WITH ACUTE}" # latin-1:E9 + "\N{LATIN SMALL LETTER E WITH CARON}" # windows-1250:EC + "\N{LATIN SMALL LETTER E WITH CIRCUMFLEX}" # latin-1:EA + "\N{LATIN SMALL LETTER E WITH DIAERESIS}" # latin-1:EB + "\N{LATIN SMALL LETTER E WITH DOT ABOVE}" # windows-1257:EB + "\N{LATIN SMALL LETTER E WITH GRAVE}" # latin-1:E8 + "\N{LATIN SMALL LETTER E WITH MACRON}" # windows-1257:E7 + "\N{LATIN SMALL LETTER E WITH OGONEK}" # windows-1250:EA + "\N{LATIN SMALL LETTER E WITH OGONEK}" # windows-1250:EA + "\N{LATIN SMALL LETTER G WITH CEDILLA}" # windows-1257:EC + "\N{LATIN SMALL LETTER I WITH ACUTE}" # latin-1:ED + "\N{LATIN SMALL LETTER I WITH CIRCUMFLEX}" # latin-1:EE + "\N{LATIN SMALL LETTER I WITH DIAERESIS}" # latin-1:EF + "\N{LATIN SMALL LETTER I WITH GRAVE}" # latin-1:EC + "\N{LATIN SMALL LETTER I WITH MACRON}" # windows-1257:EE + "\N{LATIN SMALL LETTER I WITH OGONEK}" # windows-1257:E1 + "\N{LATIN SMALL LETTER K WITH CEDILLA}" # windows-1257:ED + "\N{LATIN SMALL LETTER L WITH ACUTE}" # windows-1250:E5 + "\N{LATIN SMALL LETTER L WITH CEDILLA}" # windows-1257:EF + "\N{LATIN SMALL LETTER R WITH ACUTE}" # windows-1250:E0 + "\N{LATIN SMALL LETTER Z WITH ACUTE}" # windows-1257:EA + "\N{GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS}" # windows-1253:E0 + "\N{GREEK SMALL LETTER ALPHA}" # windows-1253:E1 + "\N{GREEK SMALL LETTER BETA}" # windows-1253:E2 + "\N{GREEK SMALL LETTER GAMMA}" # windows-1253:E3 + "\N{GREEK SMALL LETTER DELTA}" # windows-1253:E4 + "\N{GREEK SMALL LETTER EPSILON}" # windows-1253:E5 + "\N{GREEK SMALL LETTER ZETA}" # windows-1253:E6 + "\N{GREEK SMALL LETTER ETA}" # windows-1253:E7 + "\N{GREEK SMALL LETTER THETA}" # windows-1253:E8 + "\N{GREEK SMALL LETTER IOTA}" # windows-1253:E9 + "\N{GREEK SMALL LETTER KAPPA}" # windows-1253:EA + "\N{GREEK SMALL LETTER LAMDA}" # windows-1253:EB + "\N{GREEK SMALL LETTER MU}" # windows-1253:EC + "\N{GREEK SMALL LETTER NU}" # windows-1253:ED + "\N{GREEK SMALL LETTER XI}" # windows-1253:EE + "\N{GREEK SMALL LETTER OMICRON}" # windows-1253:EF + "\N{CYRILLIC SMALL LETTER A}" # windows-1251:E0 + "\N{CYRILLIC SMALL LETTER BE}" # windows-1251:E1 + "\N{CYRILLIC SMALL LETTER VE}" # windows-1251:E2 + "\N{CYRILLIC SMALL LETTER GHE}" # windows-1251:E3 + "\N{CYRILLIC SMALL LETTER DE}" # windows-1251:E4 + "\N{CYRILLIC SMALL LETTER IE}" # windows-1251:E5 + "\N{CYRILLIC SMALL LETTER ZHE}" # windows-1251:E6 + "\N{CYRILLIC SMALL LETTER ZE}" # windows-1251:E7 + "\N{CYRILLIC SMALL LETTER I}" # windows-1251:E8 + "\N{CYRILLIC SMALL LETTER SHORT I}" # windows-1251:E9 + "\N{CYRILLIC SMALL LETTER KA}" # windows-1251:EA + "\N{CYRILLIC SMALL LETTER EL}" # windows-1251:EB + "\N{CYRILLIC SMALL LETTER EM}" # windows-1251:EC + "\N{CYRILLIC SMALL LETTER EN}" # windows-1251:ED + "\N{CYRILLIC SMALL LETTER O}" # windows-1251:EE + "\N{CYRILLIC SMALL LETTER PE}" # windows-1251:EF + ), + # Letters that decode to 0xF0 or 0xF3 in a Latin-1-like encoding. + # (Other leading bytes correspond only to unassigned codepoints) + "utf8_first_of_4": ( + "\N{LATIN SMALL LETTER D WITH STROKE}" # windows-1250:F0 + "\N{LATIN SMALL LETTER ETH}" # latin-1:F0 + "\N{LATIN SMALL LETTER G WITH BREVE}" # windows-1254:F0 + "\N{LATIN SMALL LETTER O WITH ACUTE}" # latin-1:F3 + "\N{LATIN SMALL LETTER S WITH CARON}" # windows-1257:F0 + "\N{GREEK SMALL LETTER PI}" # windows-1253:F0 + "\N{GREEK SMALL LETTER SIGMA}" # windows-1253:F3 + "\N{CYRILLIC SMALL LETTER ER}" # windows-1251:F0 + "\N{CYRILLIC SMALL LETTER U}" # windows-1251:F3 + ), + # Letters that decode to 0x80 - 0xBF in a Latin-1-like encoding, + # including a space standing in for 0xA0 + "utf8_continuation": ( + "\x80-\xbf" + "\N{SPACE}" # modification of latin-1:A0, NO-BREAK SPACE + "\N{LATIN CAPITAL LETTER A WITH OGONEK}" # windows-1250:A5 + "\N{LATIN CAPITAL LETTER AE}" # windows-1257:AF + "\N{LATIN CAPITAL LETTER L WITH CARON}" # windows-1250:BC + "\N{LATIN CAPITAL LETTER L WITH STROKE}" # windows-1250:A3 + "\N{LATIN CAPITAL LETTER O WITH STROKE}" # windows-1257:A8 + "\N{LATIN CAPITAL LETTER R WITH CEDILLA}" # windows-1257:AA + "\N{LATIN CAPITAL LETTER S WITH ACUTE}" # windows-1250:8C + "\N{LATIN CAPITAL LETTER S WITH CARON}" # windows-1252:8A + "\N{LATIN CAPITAL LETTER S WITH CEDILLA}" # windows-1250:AA + "\N{LATIN CAPITAL LETTER T WITH CARON}" # windows-1250:8D + "\N{LATIN CAPITAL LETTER Y WITH DIAERESIS}" # windows-1252:9F + "\N{LATIN CAPITAL LETTER Z WITH ACUTE}" # windows-1250:8F + "\N{LATIN CAPITAL LETTER Z WITH CARON}" # windows-1252:8E + "\N{LATIN CAPITAL LETTER Z WITH DOT ABOVE}" # windows-1250:AF + "\N{LATIN CAPITAL LIGATURE OE}" # windows-1252:8C + "\N{LATIN SMALL LETTER A WITH OGONEK}" # windows-1250:B9 + "\N{LATIN SMALL LETTER AE}" # windows-1257:BF + "\N{LATIN SMALL LETTER F WITH HOOK}" # windows-1252:83 + "\N{LATIN SMALL LETTER L WITH CARON}" # windows-1250:BE + "\N{LATIN SMALL LETTER L WITH STROKE}" # windows-1250:B3 + "\N{LATIN SMALL LETTER O WITH STROKE}" # windows-1257:B8 + "\N{LATIN SMALL LETTER R WITH CEDILLA}" # windows-1257:BA + "\N{LATIN SMALL LETTER S WITH ACUTE}" # windows-1250:9C + "\N{LATIN SMALL LETTER S WITH CARON}" # windows-1252:9A + "\N{LATIN SMALL LETTER S WITH CEDILLA}" # windows-1250:BA + "\N{LATIN SMALL LETTER T WITH CARON}" # windows-1250:9D + "\N{LATIN SMALL LETTER Z WITH ACUTE}" # windows-1250:9F + "\N{LATIN SMALL LETTER Z WITH CARON}" # windows-1252:9E + "\N{LATIN SMALL LETTER Z WITH DOT ABOVE}" # windows-1250:BF + "\N{LATIN SMALL LIGATURE OE}" # windows-1252:9C + "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}" # windows-1252:88 + "\N{CARON}" # windows-1250:A1 + "\N{BREVE}" # windows-1250:A2 + "\N{OGONEK}" # windows-1250:B2 + "\N{SMALL TILDE}" # windows-1252:98 + "\N{DOUBLE ACUTE ACCENT}" # windows-1250:BD + "\N{GREEK TONOS}" # windows-1253:B4 + "\N{GREEK DIALYTIKA TONOS}" # windows-1253:A1 + "\N{GREEK CAPITAL LETTER ALPHA WITH TONOS}" # windows-1253:A2 + "\N{GREEK CAPITAL LETTER EPSILON WITH TONOS}" # windows-1253:B8 + "\N{GREEK CAPITAL LETTER ETA WITH TONOS}" # windows-1253:B9 + "\N{GREEK CAPITAL LETTER IOTA WITH TONOS}" # windows-1253:BA + "\N{GREEK CAPITAL LETTER OMICRON WITH TONOS}" # windows-1253:BC + "\N{GREEK CAPITAL LETTER UPSILON WITH TONOS}" # windows-1253:BE + "\N{GREEK CAPITAL LETTER OMEGA WITH TONOS}" # windows-1253:BF + "\N{CYRILLIC CAPITAL LETTER IO}" # windows-1251:A8 + "\N{CYRILLIC CAPITAL LETTER DJE}" # windows-1251:80 + "\N{CYRILLIC CAPITAL LETTER GJE}" # windows-1251:81 + "\N{CYRILLIC CAPITAL LETTER UKRAINIAN IE}" # windows-1251:AA + "\N{CYRILLIC CAPITAL LETTER DZE}" # windows-1251:BD + "\N{CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I}" # windows-1251:B2 + "\N{CYRILLIC CAPITAL LETTER YI}" # windows-1251:AF + "\N{CYRILLIC CAPITAL LETTER JE}" # windows-1251:A3 + "\N{CYRILLIC CAPITAL LETTER LJE}" # windows-1251:8A + "\N{CYRILLIC CAPITAL LETTER NJE}" # windows-1251:8C + "\N{CYRILLIC CAPITAL LETTER TSHE}" # windows-1251:8E + "\N{CYRILLIC CAPITAL LETTER KJE}" # windows-1251:8D + "\N{CYRILLIC CAPITAL LETTER SHORT U}" # windows-1251:A1 + "\N{CYRILLIC CAPITAL LETTER DZHE}" # windows-1251:8F + "\N{CYRILLIC SMALL LETTER IO}" # windows-1251:B8 + "\N{CYRILLIC SMALL LETTER DJE}" # windows-1251:90 + "\N{CYRILLIC SMALL LETTER GJE}" # windows-1251:83 + "\N{CYRILLIC SMALL LETTER UKRAINIAN IE}" # windows-1251:BA + "\N{CYRILLIC SMALL LETTER DZE}" # windows-1251:BE + "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}" # windows-1251:B3 + "\N{CYRILLIC SMALL LETTER YI}" # windows-1251:BF + "\N{CYRILLIC SMALL LETTER JE}" # windows-1251:BC + "\N{CYRILLIC SMALL LETTER LJE}" # windows-1251:9A + "\N{CYRILLIC SMALL LETTER NJE}" # windows-1251:9C + "\N{CYRILLIC SMALL LETTER TSHE}" # windows-1251:9E + "\N{CYRILLIC SMALL LETTER KJE}" # windows-1251:9D + "\N{CYRILLIC SMALL LETTER SHORT U}" # windows-1251:A2 + "\N{CYRILLIC SMALL LETTER DZHE}" # windows-1251:9F + "\N{CYRILLIC CAPITAL LETTER GHE WITH UPTURN}" # windows-1251:A5 + "\N{CYRILLIC SMALL LETTER GHE WITH UPTURN}" # windows-1251:B4 + "\N{EN DASH}" # windows-1252:96 + "\N{EM DASH}" # windows-1252:97 + "\N{HORIZONTAL BAR}" # windows-1253:AF + "\N{LEFT SINGLE QUOTATION MARK}" # windows-1252:91 + "\N{RIGHT SINGLE QUOTATION MARK}" # windows-1252:92 + "\N{SINGLE LOW-9 QUOTATION MARK}" # windows-1252:82 + "\N{LEFT DOUBLE QUOTATION MARK}" # windows-1252:93 + "\N{RIGHT DOUBLE QUOTATION MARK}" # windows-1252:94 + "\N{DOUBLE LOW-9 QUOTATION MARK}" # windows-1252:84 + "\N{DAGGER}" # windows-1252:86 + "\N{DOUBLE DAGGER}" # windows-1252:87 + "\N{BULLET}" # windows-1252:95 + "\N{HORIZONTAL ELLIPSIS}" # windows-1252:85 + "\N{PER MILLE SIGN}" # windows-1252:89 + "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}" # windows-1252:8B + "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}" # windows-1252:9B + "\N{EURO SIGN}" # windows-1252:80 + "\N{NUMERO SIGN}" # windows-1251:B9 + "\N{TRADE MARK SIGN}" # windows-1252:99 + ), + # Letters that decode to 0x80 - 0xBF in a Latin-1-like encoding, + # and don't usually stand for themselves when adjacent to mojibake. + # This excludes spaces, dashes, 'bullet', quotation marks, and ellipses. + "utf8_continuation_strict": ( + "\x80-\xbf" + "\N{LATIN CAPITAL LETTER A WITH OGONEK}" # windows-1250:A5 + "\N{LATIN CAPITAL LETTER AE}" # windows-1257:AF + "\N{LATIN CAPITAL LETTER L WITH CARON}" # windows-1250:BC + "\N{LATIN CAPITAL LETTER L WITH STROKE}" # windows-1250:A3 + "\N{LATIN CAPITAL LETTER O WITH STROKE}" # windows-1257:A8 + "\N{LATIN CAPITAL LETTER R WITH CEDILLA}" # windows-1257:AA + "\N{LATIN CAPITAL LETTER S WITH ACUTE}" # windows-1250:8C + "\N{LATIN CAPITAL LETTER S WITH CARON}" # windows-1252:8A + "\N{LATIN CAPITAL LETTER S WITH CEDILLA}" # windows-1250:AA + "\N{LATIN CAPITAL LETTER T WITH CARON}" # windows-1250:8D + "\N{LATIN CAPITAL LETTER Y WITH DIAERESIS}" # windows-1252:9F + "\N{LATIN CAPITAL LETTER Z WITH ACUTE}" # windows-1250:8F + "\N{LATIN CAPITAL LETTER Z WITH CARON}" # windows-1252:8E + "\N{LATIN CAPITAL LETTER Z WITH DOT ABOVE}" # windows-1250:AF + "\N{LATIN CAPITAL LIGATURE OE}" # windows-1252:8C + "\N{LATIN SMALL LETTER A WITH OGONEK}" # windows-1250:B9 + "\N{LATIN SMALL LETTER AE}" # windows-1257:BF + "\N{LATIN SMALL LETTER F WITH HOOK}" # windows-1252:83 + "\N{LATIN SMALL LETTER L WITH CARON}" # windows-1250:BE + "\N{LATIN SMALL LETTER L WITH STROKE}" # windows-1250:B3 + "\N{LATIN SMALL LETTER O WITH STROKE}" # windows-1257:B8 + "\N{LATIN SMALL LETTER R WITH CEDILLA}" # windows-1257:BA + "\N{LATIN SMALL LETTER S WITH ACUTE}" # windows-1250:9C + "\N{LATIN SMALL LETTER S WITH CARON}" # windows-1252:9A + "\N{LATIN SMALL LETTER S WITH CEDILLA}" # windows-1250:BA + "\N{LATIN SMALL LETTER T WITH CARON}" # windows-1250:9D + "\N{LATIN SMALL LETTER Z WITH ACUTE}" # windows-1250:9F + "\N{LATIN SMALL LETTER Z WITH CARON}" # windows-1252:9E + "\N{LATIN SMALL LETTER Z WITH DOT ABOVE}" # windows-1250:BF + "\N{LATIN SMALL LIGATURE OE}" # windows-1252:9C + "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}" # windows-1252:88 + "\N{CARON}" # windows-1250:A1 + "\N{BREVE}" # windows-1250:A2 + "\N{OGONEK}" # windows-1250:B2 + "\N{SMALL TILDE}" # windows-1252:98 + "\N{DOUBLE ACUTE ACCENT}" # windows-1250:BD + "\N{GREEK TONOS}" # windows-1253:B4 + "\N{GREEK DIALYTIKA TONOS}" # windows-1253:A1 + "\N{GREEK CAPITAL LETTER ALPHA WITH TONOS}" # windows-1253:A2 + "\N{GREEK CAPITAL LETTER EPSILON WITH TONOS}" # windows-1253:B8 + "\N{GREEK CAPITAL LETTER ETA WITH TONOS}" # windows-1253:B9 + "\N{GREEK CAPITAL LETTER IOTA WITH TONOS}" # windows-1253:BA + "\N{GREEK CAPITAL LETTER OMICRON WITH TONOS}" # windows-1253:BC + "\N{GREEK CAPITAL LETTER UPSILON WITH TONOS}" # windows-1253:BE + "\N{GREEK CAPITAL LETTER OMEGA WITH TONOS}" # windows-1253:BF + "\N{CYRILLIC CAPITAL LETTER IO}" # windows-1251:A8 + "\N{CYRILLIC CAPITAL LETTER DJE}" # windows-1251:80 + "\N{CYRILLIC CAPITAL LETTER GJE}" # windows-1251:81 + "\N{CYRILLIC CAPITAL LETTER UKRAINIAN IE}" # windows-1251:AA + "\N{CYRILLIC CAPITAL LETTER DZE}" # windows-1251:BD + "\N{CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I}" # windows-1251:B2 + "\N{CYRILLIC CAPITAL LETTER YI}" # windows-1251:AF + "\N{CYRILLIC CAPITAL LETTER JE}" # windows-1251:A3 + "\N{CYRILLIC CAPITAL LETTER LJE}" # windows-1251:8A + "\N{CYRILLIC CAPITAL LETTER NJE}" # windows-1251:8C + "\N{CYRILLIC CAPITAL LETTER TSHE}" # windows-1251:8E + "\N{CYRILLIC CAPITAL LETTER KJE}" # windows-1251:8D + "\N{CYRILLIC CAPITAL LETTER SHORT U}" # windows-1251:A1 + "\N{CYRILLIC CAPITAL LETTER DZHE}" # windows-1251:8F + "\N{CYRILLIC SMALL LETTER IO}" # windows-1251:B8 + "\N{CYRILLIC SMALL LETTER DJE}" # windows-1251:90 + "\N{CYRILLIC SMALL LETTER GJE}" # windows-1251:83 + "\N{CYRILLIC SMALL LETTER UKRAINIAN IE}" # windows-1251:BA + "\N{CYRILLIC SMALL LETTER DZE}" # windows-1251:BE + "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}" # windows-1251:B3 + "\N{CYRILLIC SMALL LETTER YI}" # windows-1251:BF + "\N{CYRILLIC SMALL LETTER JE}" # windows-1251:BC + "\N{CYRILLIC SMALL LETTER LJE}" # windows-1251:9A + "\N{CYRILLIC SMALL LETTER NJE}" # windows-1251:9C + "\N{CYRILLIC SMALL LETTER TSHE}" # windows-1251:9E + "\N{CYRILLIC SMALL LETTER KJE}" # windows-1251:9D + "\N{CYRILLIC SMALL LETTER SHORT U}" # windows-1251:A2 + "\N{CYRILLIC SMALL LETTER DZHE}" # windows-1251:9F + "\N{CYRILLIC CAPITAL LETTER GHE WITH UPTURN}" # windows-1251:A5 + "\N{CYRILLIC SMALL LETTER GHE WITH UPTURN}" # windows-1251:B4 + "\N{DAGGER}" # windows-1252:86 + "\N{DOUBLE DAGGER}" # windows-1252:87 + "\N{PER MILLE SIGN}" # windows-1252:89 + "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}" # windows-1252:8B + "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}" # windows-1252:9B + "\N{EURO SIGN}" # windows-1252:80 + "\N{NUMERO SIGN}" # windows-1251:B9 + "\N{TRADE MARK SIGN}" # windows-1252:99 + ), +} + +# This regex uses UTF8_CLUES to find sequences of likely mojibake. +# It matches them with + so that several adjacent UTF-8-looking sequences +# get coalesced into one, allowing them to be fixed more efficiently +# and not requiring every individual subsequence to be detected as 'badness'. +# +# We accept spaces in place of "utf8_continuation", because spaces might have +# been intended to be U+A0 NO-BREAK SPACE. +# +# We do a lookbehind to make sure the previous character isn't a +# "utf8_continuation_strict" character, so that we don't fix just a few +# characters in a huge garble and make the situation worse. +# +# Unfortunately, the matches to this regular expression won't show their +# surrounding context, and including context would make the expression much +# less efficient. The 'badness' rules that require context, such as a preceding +# lowercase letter, will prevent some cases of inconsistent UTF-8 from being +# fixed when they don't see it. +UTF8_DETECTOR_RE = re.compile( + """ + (? None: + """ + Run ftfy as a command-line utility. + """ + import argparse + + parser = argparse.ArgumentParser( + description=f"ftfy (fixes text for you), version {__version__}" + ) + parser.add_argument( + "filename", + default="-", + nargs="?", + help="The file whose Unicode is to be fixed. Defaults to -, meaning standard input.", + ) + parser.add_argument( + "-o", + "--output", + type=str, + default="-", + help="The file to output to. Defaults to -, meaning standard output.", + ) + parser.add_argument( + "-g", + "--guess", + action="store_true", + help="Ask ftfy to guess the encoding of your input. This is risky. Overrides -e.", + ) + parser.add_argument( + "-e", + "--encoding", + type=str, + default="utf-8", + help="The encoding of the input. Defaults to UTF-8.", + ) + parser.add_argument( + "-n", + "--normalization", + type=str, + default="NFC", + help='The normalization of Unicode to apply. Defaults to NFC. Can be "none".', + ) + parser.add_argument( + "--preserve-entities", + action="store_true", + help="Leave HTML entities as they are. The default " + "is to decode them, as long as no HTML tags have appeared in the file.", + ) + + args = parser.parse_args() + + encoding = args.encoding + if args.guess: + encoding = None + + if args.filename == "-": + # Get a standard input stream made of bytes, so we can decode it as + # whatever encoding is necessary. + file = sys.stdin.buffer + else: + file = open(args.filename, "rb") + + if args.output == "-": + outfile = sys.stdout + else: + if os.path.realpath(args.output) == os.path.realpath(args.filename): + sys.stderr.write(SAME_FILE_ERROR_TEXT) + sys.exit(1) + outfile = open(args.output, "w", encoding="utf-8") + + normalization = args.normalization + if normalization.lower() == "none": + normalization = None + + unescape_html: Union[str, bool] + if args.preserve_entities: + unescape_html = False + else: + unescape_html = "auto" + + config = TextFixerConfig(unescape_html=unescape_html, normalization=normalization) + + try: + for line in fix_file(file, encoding=encoding, config=config): + try: + outfile.write(line) + except UnicodeEncodeError: + if sys.platform == "win32": + sys.stderr.write(ENCODE_ERROR_TEXT_WINDOWS) + else: + sys.stderr.write(ENCODE_ERROR_TEXT_UNIX) + sys.exit(1) + except UnicodeDecodeError as err: + sys.stderr.write(DECODE_ERROR_TEXT % (encoding, err)) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/lib/python3.10/site-packages/ftfy/fixes.py b/lib/python3.10/site-packages/ftfy/fixes.py new file mode 100644 index 0000000000000000000000000000000000000000..41d3c2f817f0dedfb6887a64a97bb6e5dc0a118f --- /dev/null +++ b/lib/python3.10/site-packages/ftfy/fixes.py @@ -0,0 +1,510 @@ +""" +The `ftfy.fixes` module contains the individual fixes that :func:`ftfy.fix_text` +can perform, and provides the functions that are named in "explanations" +such as the output of :func:`ftfy.fix_and_explain`. + +Two of these functions are particularly useful on their own, as more robust +versions of functions in the Python standard library: + +- :func:`ftfy.fixes.decode_escapes` +- :func:`ftfy.fixes.unescape_html` +""" + +import codecs +import html +import re +import warnings +from re import Match +from typing import Any + +import ftfy +from ftfy.badness import is_bad +from ftfy.chardata import ( + ALTERED_UTF8_RE, + C1_CONTROL_RE, + CONTROL_CHARS, + DOUBLE_QUOTE_RE, + HTML_ENTITIES, + HTML_ENTITY_RE, + LIGATURES, + LOSSY_UTF8_RE, + SINGLE_QUOTE_RE, + UTF8_DETECTOR_RE, + WIDTH_MAP, +) + + +def fix_encoding_and_explain(text: str) -> Any: + """ + Deprecated copy of `ftfy.fix_encoding_and_explain()`. + """ + warnings.warn( + "`fix_encoding_and_explain()` has moved to the main module of ftfy.", + DeprecationWarning, + stacklevel=2, + ) + return ftfy.fix_encoding_and_explain(text) + + +def fix_encoding(text: str) -> str: + """ + Deprecated copy of `ftfy.fix_encoding()`. + """ + warnings.warn( + "`fix_encoding()` has moved to the main module of ftfy.", + DeprecationWarning, + stacklevel=2, + ) + return ftfy.fix_encoding(text) + + +def apply_plan(text: str, plan: list[tuple[str, str]]) -> str: + """ + Deprecated copy of `ftfy.apply_plan()`. + """ + warnings.warn( + "`apply_plan()` has moved to the main module of ftfy.", + DeprecationWarning, + stacklevel=2, + ) + return ftfy.apply_plan(text, plan) + + +def _unescape_fixup(match: Match[str]) -> str: + """ + Replace one matched HTML entity with the character it represents, + if possible. + """ + text = match.group(0) + if text in HTML_ENTITIES: + return HTML_ENTITIES[text] + elif text.startswith("&#"): + unescaped: str = html.unescape(text) + + # If html.unescape only decoded part of the string, that's not what + # we want. The semicolon should be consumed. + if ";" in unescaped: + return text + else: + return unescaped + else: + return text + + +def unescape_html(text: str) -> str: + """ + Decode HTML entities and character references, including some nonstandard + ones written in all-caps. + + Python has a built-in called `html.unescape` that can decode HTML escapes, + including a bunch of messy edge cases such as decoding escapes without + semicolons such as "&". + + If you know you've got HTML-escaped text, applying `html.unescape` is the + right way to convert it to plain text. But in ambiguous situations, that + would create false positives. For example, the informally written text + "this¬ that" should not automatically be decoded as "this¬ that". + + In this function, we decode the escape sequences that appear in the + `html.entities.html5` dictionary, as long as they are the unambiguous ones + that end in semicolons. + + We also decode all-caps versions of Latin letters and common symbols. + If a database contains the name 'P&EACUTE;REZ', we can read that and intuit + that it was supposed to say 'PÉREZ'. This is limited to a smaller set of + entities, because there are many instances where entity names are + case-sensitive in complicated ways. + + >>> unescape_html('<tag>') + '' + + >>> unescape_html('𝒥ohn ℋancock') + '𝒥ohn ℋancock' + + >>> unescape_html('✓') + '✓' + + >>> unescape_html('Pérez') + 'Pérez' + + >>> unescape_html('P&EACUTE;REZ') + 'PÉREZ' + + >>> unescape_html('BUNDESSTRA&SZLIG;E') + 'BUNDESSTRASSE' + + >>> unescape_html('ñ Ñ &NTILDE; &nTILDE;') + 'ñ Ñ Ñ &nTILDE;' + """ + return HTML_ENTITY_RE.sub(_unescape_fixup, text) + + +ANSI_RE = re.compile("\033\\[((?:\\d|;)*)([a-zA-Z])") + + +def remove_terminal_escapes(text: str) -> str: + r""" + Strip out "ANSI" terminal escape sequences, such as those that produce + colored text on Unix. + + >>> print(remove_terminal_escapes( + ... "\033[36;44mI'm blue, da ba dee da ba doo...\033[0m" + ... )) + I'm blue, da ba dee da ba doo... + """ + return ANSI_RE.sub("", text) + + +def uncurl_quotes(text: str) -> str: + r""" + Replace curly quotation marks with straight equivalents. + + >>> print(uncurl_quotes('\u201chere\u2019s a test\u201d')) + "here's a test" + """ + return SINGLE_QUOTE_RE.sub("'", DOUBLE_QUOTE_RE.sub('"', text)) + + +def fix_latin_ligatures(text: str) -> str: + """ + Replace single-character ligatures of Latin letters, such as 'fi', with the + characters that they contain, as in 'fi'. Latin ligatures are usually not + intended in text strings (though they're lovely in *rendered* text). If + you have such a ligature in your string, it is probably a result of a + copy-and-paste glitch. + + We leave ligatures in other scripts alone to be safe. They may be intended, + and removing them may lose information. If you want to take apart nearly + all ligatures, use NFKC normalization. + + >>> print(fix_latin_ligatures("fluffiest")) + fluffiest + """ + return text.translate(LIGATURES) + + +def fix_character_width(text: str) -> str: + """ + The ASCII characters, katakana, and Hangul characters have alternate + "halfwidth" or "fullwidth" forms that help text line up in a grid. + + If you don't need these width properties, you probably want to replace + these characters with their standard form, which is what this function + does. + + Note that this replaces the ideographic space, U+3000, with the ASCII + space, U+20. + + >>> print(fix_character_width("LOUD NOISES")) + LOUD NOISES + >>> print(fix_character_width("Uターン")) # this means "U-turn" + Uターン + """ + return text.translate(WIDTH_MAP) + + +def fix_line_breaks(text: str) -> str: + r""" + Convert all line breaks to Unix style. + + This will convert the following sequences into the standard \\n + line break: + + - CRLF (\\r\\n), used on Windows and in some communication protocols + - CR (\\r), once used on Mac OS Classic, and now kept alive by misguided + software such as Microsoft Office for Mac + - LINE SEPARATOR (\\u2028) and PARAGRAPH SEPARATOR (\\u2029), defined by + Unicode and used to sow confusion and discord + - NEXT LINE (\\x85), a C1 control character that is certainly not what you + meant + + The NEXT LINE character is a bit of an odd case, because it + usually won't show up if `fix_encoding` is also being run. + \\x85 is very common mojibake for \\u2026, HORIZONTAL ELLIPSIS. + + >>> print(fix_line_breaks( + ... "This string is made of two things:\u2029" + ... "1. Unicode\u2028" + ... "2. Spite" + ... )) + This string is made of two things: + 1. Unicode + 2. Spite + + For further testing and examples, let's define a function to make sure + we can see the control characters in their escaped form: + + >>> def eprint(text): + ... print(text.encode('unicode-escape').decode('ascii')) + + >>> eprint(fix_line_breaks("Content-type: text/plain\r\n\r\nHi.")) + Content-type: text/plain\n\nHi. + + >>> eprint(fix_line_breaks("This is how Microsoft \r trolls Mac users")) + This is how Microsoft \n trolls Mac users + + >>> eprint(fix_line_breaks("What is this \x85 I don't even")) + What is this \n I don't even + """ + return ( + text.replace("\r\n", "\n") + .replace("\r", "\n") + .replace("\u2028", "\n") + .replace("\u2029", "\n") + .replace("\u0085", "\n") + ) + + +SURROGATE_RE = re.compile("[\ud800-\udfff]") +SURROGATE_PAIR_RE = re.compile("[\ud800-\udbff][\udc00-\udfff]") + + +def convert_surrogate_pair(match: Match[str]) -> str: + """ + Convert a surrogate pair to the single codepoint it represents. + + This implements the formula described at: + http://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + """ + pair = match.group(0) + codept = 0x10000 + (ord(pair[0]) - 0xD800) * 0x400 + (ord(pair[1]) - 0xDC00) + return chr(codept) + + +def fix_surrogates(text: str) -> str: + """ + Replace 16-bit surrogate codepoints with the characters they represent + (when properly paired), or with \ufffd otherwise. + + >>> high_surrogate = chr(0xd83d) + >>> low_surrogate = chr(0xdca9) + >>> print(fix_surrogates(high_surrogate + low_surrogate)) + 💩 + >>> print(fix_surrogates(low_surrogate + high_surrogate)) + �� + + The above doctest had to be very carefully written, because even putting + the Unicode escapes of the surrogates in the docstring was causing + various tools to fail, which I think just goes to show why this fixer is + necessary. + """ + if SURROGATE_RE.search(text): + text = SURROGATE_PAIR_RE.sub(convert_surrogate_pair, text) + text = SURROGATE_RE.sub("\ufffd", text) + return text + + +def remove_control_chars(text: str) -> str: + """ + Remove various control characters that you probably didn't intend to be in + your text. Many of these characters appear in the table of "Characters not + suitable for use with markup" at + http://www.unicode.org/reports/tr20/tr20-9.html. + + This includes: + + - ASCII control characters, except for the important whitespace characters + (U+00 to U+08, U+0B, U+0E to U+1F, U+7F) + - Deprecated Arabic control characters (U+206A to U+206F) + - Interlinear annotation characters (U+FFF9 to U+FFFB) + - The Object Replacement Character (U+FFFC) + - The byte order mark (U+FEFF) + + However, these similar characters are left alone: + + - Control characters that produce whitespace (U+09, U+0A, U+0C, U+0D, + U+2028, and U+2029) + - C1 control characters (U+80 to U+9F) -- even though they are basically + never used intentionally, they are important clues about what mojibake + has happened + - Control characters that affect glyph rendering, such as joiners and + right-to-left marks (U+200C to U+200F, U+202A to U+202E) + - Musical notation control characters (U+1D173 to U+1D17A) because wow if + you're using those you probably have a good reason + - Tag characters, because they are now used in emoji sequences such as + "Flag of Wales" + """ + return text.translate(CONTROL_CHARS) + + +def remove_bom(text: str) -> str: + r""" + Remove a byte-order mark that was accidentally decoded as if it were part + of the text. + + >>> print(remove_bom(chr(0xfeff) + "Where do you want to go today?")) + Where do you want to go today? + """ + return text.lstrip(chr(0xFEFF)) + + +# Define a regex to match valid escape sequences in Python string literals. +ESCAPE_SEQUENCE_RE = re.compile( + r""" + ( \\U........ # 8-digit hex escapes + | \\u.... # 4-digit hex escapes + | \\x.. # 2-digit hex escapes + | \\[0-7]{1,3} # Octal escapes + | \\N\{[^}]+\} # Unicode characters by name + | \\[\\'"abfnrtv] # Single-character escapes + )""", + re.UNICODE | re.VERBOSE, +) + + +def decode_escapes(text: str) -> str: + r""" + Decode backslashed escape sequences, including \\x, \\u, and \\U character + references, even in the presence of other Unicode. + + This function has to be called specifically. It's not run automatically by + ftfy, because escaped text is not necessarily a mistake, and there is no + way to distinguish when it is. + + This is what Python's "string-escape" and "unicode-escape" codecs were + meant to do, but in contrast, this actually works. It will decode the + string exactly the same way that the Python interpreter decodes its string + literals. + + >>> factoid = '\\u20a1 is the currency symbol for the colón.' + >>> print(factoid[1:]) + u20a1 is the currency symbol for the colón. + >>> print(decode_escapes(factoid)) + ₡ is the currency symbol for the colón. + + Even though Python itself can read string literals with a combination of + escapes and literal Unicode -- you're looking at one right now -- the + "unicode-escape" codec doesn't work on literal Unicode. (See + http://stackoverflow.com/a/24519338/773754 for more details.) + + Instead, this function searches for just the parts of a string that + represent escape sequences, and decodes them, leaving the rest alone. All + valid escape sequences are made of ASCII characters, and this allows + "unicode-escape" to work correctly. + """ + + def decode_match(match: Match[str]) -> str: + "Given a regex match, decode the escape sequence it contains." + return codecs.decode(match.group(0), "unicode-escape") + + return ESCAPE_SEQUENCE_RE.sub(decode_match, text) + + +# This regex implements an exception to restore_byte_a0, so we can decode the +# very common mojibake of (for example) "à la mode" as "à la mode", not "àla +# mode". +# +# If byte C3 appears with a single space after it -- most commonly this shows +# up as " à " appearing as an entire word -- we'll insert \xa0 while keeping +# the space. Without this change, we would decode "à" as the start of the next +# word, such as "àla". It's almost always intended to be a separate word, as in +# "à la", but when mojibake turns this into "Ã\xa0 la", the two kinds of spaces +# get coalesced into "à la". +# +# We make exceptions for the Portuguese words "às", "àquele", "àquela", +# "àquilo" and their plurals -- these are contractions of, for example, "a +# aquele" and are very common. Note that the final letter is important to +# distinguish this case from French "à quel point". +# +# Other instances in Portuguese, such as "àfrica", seem to be typos (intended +# to be "África" with the accent in the other direction). +# +# Unfortunately, "à" is a common letter in Catalan, and mojibake of words that +# contain it will end up with inserted spaces. We can't do the right thing with +# every word. The cost is that the mojibake text "fà cil" will be interpreted as +# "fà cil", not "fàcil". +A_GRAVE_WORD_RE = re.compile(b"\xc3 (?! |quele|quela|quilo|s )") + + +def restore_byte_a0(byts: bytes) -> bytes: + """ + Some mojibake has been additionally altered by a process that said "hmm, + byte A0, that's basically a space!" and replaced it with an ASCII space. + When the A0 is part of a sequence that we intend to decode as UTF-8, + changing byte A0 to 20 would make it fail to decode. + + This process finds sequences that would convincingly decode as UTF-8 if + byte 20 were changed to A0, and puts back the A0. For the purpose of + deciding whether this is a good idea, this step gets a cost of twice + the number of bytes that are changed. + + This is used as a step within `fix_encoding`. + """ + byts = A_GRAVE_WORD_RE.sub(b"\xc3\xa0 ", byts) + + def replacement(match: Match[bytes]) -> bytes: + "The function to apply when this regex matches." + return match.group(0).replace(b"\x20", b"\xa0") + + return ALTERED_UTF8_RE.sub(replacement, byts) + + +def replace_lossy_sequences(byts: bytes) -> bytes: + """ + This function identifies sequences where information has been lost in + a "sloppy" codec, indicated by byte 1A, and if they would otherwise look + like a UTF-8 sequence, it replaces them with the UTF-8 sequence for U+FFFD. + + A further explanation: + + ftfy can now fix text in a few cases that it would previously fix + incompletely, because of the fact that it can't successfully apply the fix + to the entire string. A very common case of this is when characters have + been erroneously decoded as windows-1252, but instead of the "sloppy" + windows-1252 that passes through unassigned bytes, the unassigned bytes get + turned into U+FFFD (�), so we can't tell what they were. + + This most commonly happens with curly quotation marks that appear + ``“ like this â€�``. + + We can do better by building on ftfy's "sloppy codecs" to let them handle + less-sloppy but more-lossy text. When they encounter the character ``�``, + instead of refusing to encode it, they encode it as byte 1A -- an + ASCII control code called SUBSTITUTE that once was meant for about the same + purpose. We can then apply a fixer that looks for UTF-8 sequences where + some continuation bytes have been replaced by byte 1A, and decode the whole + sequence as �; if that doesn't work, it'll just turn the byte back into � + itself. + + As a result, the above text ``“ like this â€�`` will decode as + ``“ like this �``. + + If U+1A was actually in the original string, then the sloppy codecs will + not be used, and this function will not be run, so your weird control + character will be left alone but wacky fixes like this won't be possible. + + This is used as a transcoder within `fix_encoding`. + """ + return LOSSY_UTF8_RE.sub("\ufffd".encode(), byts) + + +def decode_inconsistent_utf8(text: str) -> str: + """ + Sometimes, text from one encoding ends up embedded within text from a + different one. This is common enough that we need to be able to fix it. + + This is used as a transcoder within `fix_encoding`. + """ + + def fix_embedded_mojibake(match: Match[str]) -> str: + substr = match.group(0) + + # Require the match to be shorter, so that this doesn't recurse infinitely + if len(substr) < len(text) and is_bad(substr): + return ftfy.fix_encoding(substr) + else: + return substr + + return UTF8_DETECTOR_RE.sub(fix_embedded_mojibake, text) + + +def _c1_fixer(match: Match[str]) -> str: + return match.group(0).encode("latin-1").decode("sloppy-windows-1252") + + +def fix_c1_controls(text: str) -> str: + """ + If text still contains C1 control characters, treat them as their + Windows-1252 equivalents. This matches what Web browsers do. + """ + return C1_CONTROL_RE.sub(_c1_fixer, text) diff --git a/lib/python3.10/site-packages/ftfy/formatting.py b/lib/python3.10/site-packages/ftfy/formatting.py new file mode 100644 index 0000000000000000000000000000000000000000..18df64b082ddfe26f079578de57a6bb6f5d2df03 --- /dev/null +++ b/lib/python3.10/site-packages/ftfy/formatting.py @@ -0,0 +1,166 @@ +""" +This module provides functions for justifying Unicode text in a monospaced +display such as a terminal. + +We used to have our own implementation here, but now we mostly rely on +the 'wcwidth' library. +""" + +from unicodedata import normalize + +from wcwidth import wcswidth, wcwidth + +from ftfy.fixes import remove_terminal_escapes + + +def character_width(char: str) -> int: + r""" + Determine the width that a character is likely to be displayed as in + a monospaced terminal. The width for a printable character will + always be 0, 1, or 2. + + Nonprintable or control characters will return -1, a convention that comes + from wcwidth. + + >>> character_width('車') + 2 + >>> character_width('A') + 1 + >>> character_width('\N{ZERO WIDTH JOINER}') + 0 + >>> character_width('\n') + -1 + """ + return int(wcwidth(char)) + + +def monospaced_width(text: str) -> int: + r""" + Return the number of character cells that this string is likely to occupy + when displayed in a monospaced, modern, Unicode-aware terminal emulator. + We refer to this as the "display width" of the string. + + This can be useful for formatting text that may contain non-spacing + characters, or CJK characters that take up two character cells. + + Returns -1 if the string contains a non-printable or control character. + + >>> monospaced_width('ちゃぶ台返し') + 12 + >>> len('ちゃぶ台返し') + 6 + >>> monospaced_width('owl\N{SOFT HYPHEN}flavored') + 11 + >>> monospaced_width('example\x80') + -1 + + A more complex example: The Korean word 'ibnida' can be written with 3 + pre-composed characters or 7 jamo. Either way, it *looks* the same and + takes up 6 character cells. + + >>> monospaced_width('입니다') + 6 + >>> monospaced_width('\u110b\u1175\u11b8\u1102\u1175\u1103\u1161') + 6 + + The word "blue" with terminal escapes to make it blue still takes up only + 4 characters, when shown as intended. + >>> monospaced_width('\x1b[34mblue\x1b[m') + 4 + """ + # NFC-normalize the text first, so that we don't need special cases for + # Hangul jamo. + # + # Remove terminal escapes before calculating width, because if they are + # displayed as intended, they will have zero width. + return int(wcswidth(remove_terminal_escapes(normalize("NFC", text)))) + + +def display_ljust(text: str, width: int, fillchar: str = " ") -> str: + """ + Return `text` left-justified in a Unicode string whose display width, + in a monospaced terminal, should be at least `width` character cells. + The rest of the string will be padded with `fillchar`, which must be + a width-1 character. + + "Left" here means toward the beginning of the string, which may actually + appear on the right in an RTL context. This is similar to the use of the + word "left" in "left parenthesis". + + >>> lines = ['Table flip', '(╯°□°)╯︵ ┻━┻', 'ちゃぶ台返し'] + >>> for line in lines: + ... print(display_ljust(line, 20, '▒')) + Table flip▒▒▒▒▒▒▒▒▒▒ + (╯°□°)╯︵ ┻━┻▒▒▒▒▒▒▒ + ちゃぶ台返し▒▒▒▒▒▒▒▒ + + This example, and the similar ones that follow, should come out justified + correctly when viewed in a monospaced terminal. It will probably not look + correct if you're viewing this code or documentation in a Web browser. + """ + if character_width(fillchar) != 1: + raise ValueError("The padding character must have display width 1") + + text_width = monospaced_width(text) + if text_width == -1: + # There's a control character here, so just don't add padding + return text + + padding = max(0, width - text_width) + return text + fillchar * padding + + +def display_rjust(text: str, width: int, fillchar: str = " ") -> str: + """ + Return `text` right-justified in a Unicode string whose display width, + in a monospaced terminal, should be at least `width` character cells. + The rest of the string will be padded with `fillchar`, which must be + a width-1 character. + + "Right" here means toward the end of the string, which may actually be on + the left in an RTL context. This is similar to the use of the word "right" + in "right parenthesis". + + >>> lines = ['Table flip', '(╯°□°)╯︵ ┻━┻', 'ちゃぶ台返し'] + >>> for line in lines: + ... print(display_rjust(line, 20, '▒')) + ▒▒▒▒▒▒▒▒▒▒Table flip + ▒▒▒▒▒▒▒(╯°□°)╯︵ ┻━┻ + ▒▒▒▒▒▒▒▒ちゃぶ台返し + """ + if character_width(fillchar) != 1: + raise ValueError("The padding character must have display width 1") + + text_width = monospaced_width(text) + if text_width == -1: + return text + + padding = max(0, width - text_width) + return fillchar * padding + text + + +def display_center(text: str, width: int, fillchar: str = " ") -> str: + """ + Return `text` centered in a Unicode string whose display width, in a + monospaced terminal, should be at least `width` character cells. The rest + of the string will be padded with `fillchar`, which must be a width-1 + character. + + >>> lines = ['Table flip', '(╯°□°)╯︵ ┻━┻', 'ちゃぶ台返し'] + >>> for line in lines: + ... print(display_center(line, 20, '▒')) + ▒▒▒▒▒Table flip▒▒▒▒▒ + ▒▒▒(╯°□°)╯︵ ┻━┻▒▒▒▒ + ▒▒▒▒ちゃぶ台返し▒▒▒▒ + """ + if character_width(fillchar) != 1: + raise ValueError("The padding character must have display width 1") + + text_width = monospaced_width(text) + if text_width == -1: + return text + + padding = max(0, width - text_width) + left_padding = padding // 2 + right_padding = padding - left_padding + return fillchar * left_padding + text + fillchar * right_padding diff --git a/lib/python3.10/site-packages/ftfy/py.typed b/lib/python3.10/site-packages/ftfy/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/functorch/__init__.py b/lib/python3.10/site-packages/functorch/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0aef38c8a9bb84a9833c4c2c9c34ad528d564b32 --- /dev/null +++ b/lib/python3.10/site-packages/functorch/__init__.py @@ -0,0 +1,39 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. +import torch +from torch._functorch.deprecated import ( + combine_state_for_ensemble, + functionalize, + grad, + grad_and_value, + hessian, + jacfwd, + jacrev, + jvp, + make_functional, + make_functional_with_buffers, + vjp, + vmap, +) + +# utilities. Maybe these should go in their own namespace in the future? +from torch._functorch.make_functional import ( + FunctionalModule, + FunctionalModuleWithBuffers, +) + +# Was never documented +from torch._functorch.python_key import make_fx + + +# Top-level APIs. Please think carefully before adding something to the +# top-level namespace: +# - private helper functions should go into torch._functorch +# - very experimental things should go into functorch.experimental +# - compilation related things should go into functorch.compile + + +__version__ = torch.__version__ diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/INSTALLER b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/METADATA b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..d778719bb8d5516d96257a76dc38a032681fcbc3 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/METADATA @@ -0,0 +1,39 @@ +Metadata-Version: 2.4 +Name: geomloss +Version: 0.2.5 +Summary: Geometric loss functions between point clouds, images and volumes. +Home-page: +Author: Jean Feydy +Author-email: jean.feydy@inria.fr +License: LICENSE.txt +Keywords: kernels optimal transport measure loss geometry +Classifier: Development Status :: 2 - Pre-Alpha +Classifier: Intended Audience :: Developers +Classifier: Topic :: Scientific/Engineering +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3 +Description-Content-Type: text/markdown +License-File: LICENSE.txt +Requires-Dist: numpy +Provides-Extra: full +Requires-Dist: pykeops; extra == "full" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary + +# Geometric loss functions between point clouds, images and volumes + +Please check our [website](https://www.kernel-operations.io/geomloss)! diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/RECORD b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..ef34ea2cf3d68c0208075877da3bb6402db9729e --- /dev/null +++ b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/RECORD @@ -0,0 +1,23 @@ +geomloss-0.2.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +geomloss-0.2.5.dist-info/METADATA,sha256=HaTRyZel8VWg0oTJ76h0yvYGVQr-bJvdYm5QR_ua1O4,1233 +geomloss-0.2.5.dist-info/RECORD,, +geomloss-0.2.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +geomloss-0.2.5.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91 +geomloss-0.2.5.dist-info/licenses/LICENSE.txt,sha256=kwv1BO9Iz-q2ePTDlCsZ5oKYI7Yi0Qtp_dy52j95P9c,1067 +geomloss-0.2.5.dist-info/top_level.txt,sha256=WZzE5k59gArP6ev5q4ntcptddGkmPseENGfnYR9lX30,9 +geomloss/__init__.py,sha256=2_dl5Ise3syYGFwFrZOLlPD5-teXhtqyZubvCCpbC58,244 +geomloss/__pycache__/__init__.cpython-310.pyc,, +geomloss/__pycache__/kernel_samples.cpython-310.pyc,, +geomloss/__pycache__/samples_loss.cpython-310.pyc,, +geomloss/__pycache__/sinkhorn_divergence.cpython-310.pyc,, +geomloss/__pycache__/sinkhorn_images.cpython-310.pyc,, +geomloss/__pycache__/sinkhorn_samples.cpython-310.pyc,, +geomloss/__pycache__/utils.cpython-310.pyc,, +geomloss/__pycache__/wasserstein_barycenter_images.cpython-310.pyc,, +geomloss/kernel_samples.py,sha256=COr6O39sZRSrRXBbad_dC1kGLfGU0oeuYYinGrmeFq0,8228 +geomloss/samples_loss.py,sha256=D0DX5gbZrnQpYccHTh3C9rOmmnY-KBaNnOd-nHSZ8FQ,19849 +geomloss/sinkhorn_divergence.py,sha256=pulQJ3nD4v6rbMzxmYn0MuDiQ5OsK5rv8gzYmvnmxCo,28449 +geomloss/sinkhorn_images.py,sha256=Kg4qp6hQvFnrf6D6jiUoAHyRjgFC_0ojNY2vYGfG5jw,6833 +geomloss/sinkhorn_samples.py,sha256=eHEOTt6lWSGP7T3pP3roZ6gqt5hADFT-pYTrJnwC540,22666 +geomloss/utils.py,sha256=4ZuXP2lwo2pXfQrx7JRrlIIwO1NGWzSSauVcvOwXi9g,8209 +geomloss/wasserstein_barycenter_images.py,sha256=ExmNwPalKwgeY9t2entkIkSn18HDXdL0h9ACjMQF7g4,3713 diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/REQUESTED b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/WHEEL b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..370ec5f1aa3524bfd31a19fd21e907335f48a7f6 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (78.1.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/top_level.txt b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..1ba167bd51505f5e9d114575319b4d1a5f01994e --- /dev/null +++ b/lib/python3.10/site-packages/geomloss-0.2.5.dist-info/top_level.txt @@ -0,0 +1 @@ +geomloss diff --git a/lib/python3.10/site-packages/geomloss/__init__.py b/lib/python3.10/site-packages/geomloss/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cbcad019215c83457379ac32cb8b3183b5f34b5c --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/__init__.py @@ -0,0 +1,9 @@ +import sys, os.path + +__version__ = "0.2.5" + +from .samples_loss import SamplesLoss +from .wasserstein_barycenter_images import ImagesBarycenter +from .sinkhorn_images import sinkhorn_divergence + +__all__ = sorted(["SamplesLoss, ImagesBarycenter"]) diff --git a/lib/python3.10/site-packages/geomloss/kernel_samples.py b/lib/python3.10/site-packages/geomloss/kernel_samples.py new file mode 100644 index 0000000000000000000000000000000000000000..4aaca513082c2f112973e98e7f05499487cd3b62 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/kernel_samples.py @@ -0,0 +1,272 @@ +"""Implements kernel ("gaussian", "laplacian", "energy") norms between sampled measures. + +.. math:: + \\text{Loss}(\\alpha,\\beta) + ~&=~ \\text{Loss}\\big( \sum_{i=1}^N \\alpha_i \,\delta_{x_i} \,,\, \sum_{j=1}^M \\beta_j \,\delta_{y_j} \\big) + ~=~ \\tfrac{1}{2} \|\\alpha-\\beta\|_k^2 \\\\ + &=~ \\tfrac{1}{2} \langle \\alpha-\\beta \,,\, k\star (\\alpha - \\beta) \\rangle \\\\ + &=~ \\tfrac{1}{2} \sum_{i=1}^N \sum_{j=1}^N \\alpha_i \\alpha_j \cdot k(x_i,x_j) + + \\tfrac{1}{2} \sum_{i=1}^M \sum_{j=1}^M \\beta_i \\beta_j \cdot k(y_i,y_j) \\\\ + &-~\sum_{i=1}^N \sum_{j=1}^M \\alpha_i \\beta_j \cdot k(x_i,y_j) + +where: + +.. math:: + k(x,y)~=~\\begin{cases} + \exp( -\|x-y\|^2/2\sigma^2) & \\text{if loss = ``gaussian''} \\\\ + \exp( -\|x-y\|/\sigma) & \\text{if loss = ``laplacian''} \\\\ + -\|x-y\| & \\text{if loss = ``energy''} \\\\ + \\end{cases} +""" + +import numpy as np +import torch + +try: # Import the keops library, www.kernel-operations.io + from pykeops.torch import generic_sum + from pykeops.torch.cluster import ( + grid_cluster, + cluster_ranges_centroids, + sort_clusters, + from_matrix, + swap_axes, + ) + from pykeops.torch import LazyTensor + + keops_available = True +except: + keops_available = False + +from .utils import scal, squared_distances, distances + + +class DoubleGrad(torch.autograd.Function): + @staticmethod + def forward(ctx, input): + return input + + @staticmethod + def backward(ctx, grad_output): + return 2 * grad_output + + +def double_grad(x): + return DoubleGrad.apply(x) + + +# ============================================================================== +# All backends +# ============================================================================== + + +def gaussian_kernel(x, y, blur=0.05, use_keops=False, ranges=None): + C2 = squared_distances(x / blur, y / blur, use_keops=use_keops) + K = (-C2 / 2).exp() + + if use_keops and ranges is not None: + K.ranges = ranges + return K + + +def laplacian_kernel(x, y, blur=0.05, use_keops=False, ranges=None): + C = distances(x / blur, y / blur, use_keops=use_keops) + K = (-C).exp() + + if use_keops and ranges is not None: + K.ranges = ranges + return K + + +def energy_kernel(x, y, blur=None, use_keops=False, ranges=None): + # N.B.: We never truncate the energy distance kernel + return -distances(x, y, use_keops=use_keops) + + +kernel_routines = { + "gaussian": gaussian_kernel, + "laplacian": laplacian_kernel, + "energy": energy_kernel, +} + + +def kernel_loss( + α, + x, + β, + y, + blur=0.05, + kernel=None, + name=None, + potentials=False, + use_keops=False, + ranges_xx=None, + ranges_yy=None, + ranges_xy=None, + **kwargs +): + if kernel is None: + kernel = kernel_routines[name] + + # Center the point clouds just in case, to prevent numeric overflows: + # N.B.: This may break user-provided kernels and comes at a non-negligible + # cost for small problems, so let's disable this by default. + # center = (x.mean(-2, keepdim=True) + y.mean(-2, keepdim=True)) / 2 + # x, y = x - center, y - center + + # (B,N,N) tensor + K_xx = kernel( + double_grad(x), x.detach(), blur=blur, use_keops=use_keops, ranges=ranges_xx + ) + # (B,M,M) tensor + K_yy = kernel( + double_grad(y), y.detach(), blur=blur, use_keops=use_keops, ranges=ranges_yy + ) + # (B,N,M) tensor + K_xy = kernel(x, y, blur=blur, use_keops=use_keops, ranges=ranges_xy) + + # (B,N,N) @ (B,N) = (B,N) + a_x = (K_xx @ α.detach().unsqueeze(-1)).squeeze(-1) + # (B,M,M) @ (B,M) = (B,M) + b_y = (K_yy @ β.detach().unsqueeze(-1)).squeeze(-1) + # (B,N,M) @ (B,M) = (B,N) + b_x = (K_xy @ β.unsqueeze(-1)).squeeze(-1) + + if potentials: + # (B,M,N) @ (B,N) = (B,M) + Kt = K_xy.t() if use_keops else K_xy.transpose(1, 2) + a_y = (Kt @ α.unsqueeze(-1)).squeeze(-1) + return a_x - b_x, b_y - a_y + + else: # Return the Kernel norm. N.B.: we assume that 'kernel' is symmetric: + batch = x.dim() > 2 + return ( + 0.5 * scal(double_grad(α), a_x, batch=batch) + + 0.5 * scal(double_grad(β), b_y, batch=batch) + - scal(α, b_x, batch=batch) + ) + + +# ============================================================================== +# backend == "tensorized" +# ============================================================================== + +from functools import partial + +kernel_tensorized = partial(kernel_loss, use_keops=False) + + +# ============================================================================== +# backend == "online" +# ============================================================================== + +kernel_online = partial(kernel_loss, use_keops=True) + + +# ============================================================================== +# backend == "multiscale" +# ============================================================================== + + +def max_diameter(x, y): + mins = torch.stack((x.min(dim=0)[0], y.min(dim=0)[0])).min(dim=0)[0] + maxs = torch.stack((x.max(dim=0)[0], y.max(dim=0)[0])).max(dim=0)[0] + diameter = (maxs - mins).norm().item() + return diameter + + +def kernel_multiscale( + α, + x, + β, + y, + blur=0.05, + kernel=None, + name=None, + truncate=5, + diameter=None, + cluster_scale=None, + potentials=False, + verbose=False, + **kwargs +): + + if truncate is None or name == "energy": + return kernel_online( + α.unsqueeze(0), + x.unsqueeze(0), + β.unsqueeze(0), + y.unsqueeze(0), + blur=blur, + kernel=kernel, + truncate=truncate, + name=name, + potentials=potentials, + **kwargs + ) + + # Renormalize our point cloud so that blur = 1: + # Center the point clouds just in case, to prevent numeric overflows: + center = (x.mean(-2, keepdim=True) + y.mean(-2, keepdim=True)) / 2 + x, y = x - center, y - center + x_ = x / blur + y_ = y / blur + + # Don't forget to normalize the diameter too! + if cluster_scale is None: + D = x.shape[-1] + if diameter is None: + diameter = max_diameter(x_.view(-1, D), y_.view(-1, D)) + else: + diameter = diameter / blur + cluster_scale = diameter / (np.sqrt(D) * 2000 ** (1 / D)) + + # Put our points in cubic clusters: + cell_diameter = cluster_scale * np.sqrt(x_.shape[-1]) + x_lab = grid_cluster(x_, cluster_scale) + y_lab = grid_cluster(y_, cluster_scale) + + # Compute the ranges and centroids of each cluster: + ranges_x, x_c, α_c = cluster_ranges_centroids(x_, x_lab, weights=α) + ranges_y, y_c, β_c = cluster_ranges_centroids(y_, y_lab, weights=β) + + if verbose: + print( + "{}x{} clusters, computed at scale = {:2.3f}".format( + len(x_c), len(y_c), cluster_scale + ) + ) + + # Sort the clusters, making them contiguous in memory: + (α, x), x_lab = sort_clusters((α, x), x_lab) + (β, y), y_lab = sort_clusters((β, y), y_lab) + + with torch.no_grad(): # Compute our block-sparse reduction ranges: + # Compute pairwise distances between clusters: + C_xx = squared_distances(x_c, x_c) + C_yy = squared_distances(y_c, y_c) + C_xy = squared_distances(x_c, y_c) + + # Compute the boolean masks: + keep_xx = C_xx <= (truncate + cell_diameter) ** 2 + keep_yy = C_yy <= (truncate + cell_diameter) ** 2 + keep_xy = C_xy <= (truncate + cell_diameter) ** 2 + + # Compute the KeOps reduction ranges: + ranges_xx = from_matrix(ranges_x, ranges_x, keep_xx) + ranges_yy = from_matrix(ranges_y, ranges_y, keep_yy) + ranges_xy = from_matrix(ranges_x, ranges_y, keep_xy) + + return kernel_loss( + α, + x, + β, + y, + blur=blur, + kernel=kernel, + name=name, + potentials=potentials, + use_keops=True, + ranges_xx=ranges_xx, + ranges_yy=ranges_yy, + ranges_xy=ranges_xy, + ) diff --git a/lib/python3.10/site-packages/geomloss/samples_loss.py b/lib/python3.10/site-packages/geomloss/samples_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..66d1fc8212cd054465f40dd56adaac1bb9806ce2 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/samples_loss.py @@ -0,0 +1,474 @@ +import torch +from torch.nn import Module +from functools import partial +import warnings + +from .kernel_samples import kernel_tensorized, kernel_online, kernel_multiscale + +from .sinkhorn_samples import sinkhorn_tensorized +from .sinkhorn_samples import sinkhorn_online +from .sinkhorn_samples import sinkhorn_multiscale + +from .kernel_samples import kernel_tensorized as hausdorff_tensorized +from .kernel_samples import kernel_online as hausdorff_online +from .kernel_samples import kernel_multiscale as hausdorff_multiscale + + +routines = { + "sinkhorn": { + "tensorized": sinkhorn_tensorized, + "online": sinkhorn_online, + "multiscale": sinkhorn_multiscale, + }, + "hausdorff": { + "tensorized": hausdorff_tensorized, + "online": hausdorff_online, + "multiscale": hausdorff_multiscale, + }, + "energy": { + "tensorized": partial(kernel_tensorized, name="energy"), + "online": partial(kernel_online, name="energy"), + "multiscale": partial(kernel_multiscale, name="energy"), + }, + "gaussian": { + "tensorized": partial(kernel_tensorized, name="gaussian"), + "online": partial(kernel_online, name="gaussian"), + "multiscale": partial(kernel_multiscale, name="gaussian"), + }, + "laplacian": { + "tensorized": partial(kernel_tensorized, name="laplacian"), + "online": partial(kernel_online, name="laplacian"), + "multiscale": partial(kernel_multiscale, name="laplacian"), + }, +} + + +class SamplesLoss(Module): + """Creates a criterion that computes distances between sampled measures on a vector space. + + Warning: + If **loss** is ``"sinkhorn"`` and **reach** is **None** (balanced Optimal Transport), + the resulting routine will expect measures whose total masses are equal with each other. + + Parameters: + loss (string, default = ``"sinkhorn"``): The loss function to compute. + The supported values are: + + - ``"sinkhorn"``: (Un-biased) Sinkhorn divergence, which interpolates + between Wasserstein (blur=0) and kernel (blur= :math:`+\infty` ) distances. + - ``"hausdorff"``: Weighted Hausdorff distance, which interpolates + between the ICP loss (blur=0) and a kernel distance (blur= :math:`+\infty` ). + - ``"energy"``: Energy Distance MMD, computed using the kernel + :math:`k(x,y) = -\|x-y\|_2`. + - ``"gaussian"``: Gaussian MMD, computed using the kernel + :math:`k(x,y) = \exp \\big( -\|x-y\|_2^2 \,/\, 2\sigma^2)` + of standard deviation :math:`\sigma` = **blur**. + - ``"laplacian"``: Laplacian MMD, computed using the kernel + :math:`k(x,y) = \exp \\big( -\|x-y\|_2 \,/\, \sigma)` + of standard deviation :math:`\sigma` = **blur**. + + p (int, default=2): If **loss** is ``"sinkhorn"`` or ``"hausdorff"``, + specifies the ground cost function between points. + The supported values are: + + - **p** = 1: :math:`~~C(x,y) ~=~ \|x-y\|_2`. + - **p** = 2: :math:`~~C(x,y) ~=~ \\tfrac{1}{2}\|x-y\|_2^2`. + + blur (float, default=.05): The finest level of detail that + should be handled by the loss function - in + order to prevent overfitting on the samples' locations. + + - If **loss** is ``"gaussian"`` or ``"laplacian"``, + it is the standard deviation :math:`\sigma` of the convolution kernel. + - If **loss** is ``"sinkhorn"`` or ``"hausdorff"``, + it is the typical scale :math:`\sigma` associated + to the temperature :math:`\\varepsilon = \sigma^p`. + The default value of .05 is sensible for input + measures that lie in the unit square/cube. + + Note that the *Energy Distance* is scale-equivariant, and won't + be affected by this parameter. + + reach (float, default=None= :math:`+\infty` ): If **loss** is ``"sinkhorn"`` + or ``"hausdorff"``, + specifies the typical scale :math:`\\tau` associated + to the constraint strength :math:`\\rho = \\tau^p`. + + diameter (float, default=None): A rough indication of the maximum + distance between points, which is used to tune the :math:`\\varepsilon`-scaling + descent and provide a default heuristic for clustering **multiscale** schemes. + If **None**, a conservative estimate will be computed on-the-fly. + + scaling (float, default=.5): If **loss** is ``"sinkhorn"``, + specifies the ratio between successive values + of :math:`\sigma=\\varepsilon^{1/p}` in the + :math:`\\varepsilon`-scaling descent. + This parameter allows you to specify the trade-off between + speed (**scaling** < .4) and accuracy (**scaling** > .9). + + truncate (float, default=None= :math:`+\infty`): If **backend** + is ``"multiscale"``, specifies the effective support of + a Gaussian/Laplacian kernel as a multiple of its standard deviation. + If **truncate** is not **None**, kernel truncation + steps will assume that + :math:`\\exp(-x/\sigma)` or + :math:`\\exp(-x^2/2\sigma^2) are zero when + :math:`\|x\| \,>\, \\text{truncate}\cdot \sigma`. + + + cost (function or string, default=None): if **loss** is ``"sinkhorn"`` + or ``"hausdorff"``, specifies the cost function that should + be used instead of :math:`\\tfrac{1}{p}\|x-y\|^p`: + + - If **backend** is ``"tensorized"``, **cost** should be a + python function that takes as input a + (B,N,D) torch Tensor **x**, a (B,M,D) torch Tensor **y** + and returns a batched Cost matrix as a (B,N,M) Tensor. + - Otherwise, if **backend** is ``"online"`` or ``"multiscale"``, + **cost** should be a `KeOps formula `_, + given as a string, with variables ``X`` and ``Y``. + The default values are ``"Norm2(X-Y)"`` (for **p** = 1) and + ``"(SqDist(X,Y) / IntCst(2))"`` (for **p** = 2). + + cluster_scale (float, default=None): If **backend** is ``"multiscale"``, + specifies the coarse scale at which cluster centroids will be computed. + If **None**, a conservative estimate will be computed from + **diameter** and the ambient space's dimension, + making sure that memory overflows won't take place. + + debias (bool, default=True): If **loss** is ``"sinkhorn"``, + specifies if we should compute the **unbiased** + Sinkhorn divergence instead of the classic, + entropy-regularized "SoftAssign" loss. + + potentials (bool, default=False): When this parameter is set to True, + the :mod:`SamplesLoss` layer returns a pair of optimal dual potentials + :math:`F` and :math:`G`, sampled on the input measures, + instead of differentiable scalar value. + These dual vectors :math:`(F(x_i))` and :math:`(G(y_j))` + are encoded as Torch tensors, with the same shape + as the input weights :math:`(\\alpha_i)` and :math:`(\\beta_j)`. + + verbose (bool, default=False): If **backend** is ``"multiscale"``, + specifies whether information on the clustering and + :math:`\\varepsilon`-scaling descent should be displayed + in the standard output. + + backend (string, default = ``"auto"``): The implementation that + will be used in the background; this choice has a major impact + on performance. The supported values are: + + - ``"auto"``: Choose automatically, using a simple + heuristic based on the inputs' shapes. + - ``"tensorized"``: Relies on a full cost/kernel matrix, computed + once and for all and stored on the device memory. + This method is fast, but has a quadratic + memory footprint and does not scale beyond ~5,000 samples per measure. + - ``"online"``: Computes cost/kernel values on-the-fly, leveraging + online map-reduce CUDA routines provided by + the `pykeops `_ library. + - ``"multiscale"``: Fast implementation that scales to millions + of samples in dimension 1-2-3, relying on the block-sparse + reductions provided by the `pykeops `_ library. + + """ + + def __init__( + self, + loss="sinkhorn", + p=2, + blur=0.05, + reach=None, + diameter=None, + scaling=0.5, + truncate=5, + cost=None, + kernel=None, + cluster_scale=None, + debias=True, + potentials=False, + verbose=False, + backend="auto", + ): + + super(SamplesLoss, self).__init__() + self.loss = loss + self.backend = backend + self.p = p + self.blur = blur + self.reach = reach + self.truncate = truncate + self.diameter = diameter + self.scaling = scaling + self.cost = cost + self.kernel = kernel + self.cluster_scale = cluster_scale + self.debias = debias + self.potentials = potentials + self.verbose = verbose + + def forward(self, *args): + """Computes the loss between sampled measures. + + Documentation and examples: Soon! + Until then, please check the tutorials :-)""" + + l_x, α, x, l_y, β, y = self.process_args(*args) + B, N, M, D, l_x, α, l_y, β = self.check_shapes(l_x, α, x, l_y, β, y) + + backend = ( + self.backend + ) # Choose the backend ----------------------------------------- + if l_x is not None or l_y is not None: + if backend in ["auto", "multiscale"]: + backend = "multiscale" + else: + raise ValueError( + 'Explicit cluster labels are only supported with the "auto" and "multiscale" backends.' + ) + + elif backend == "auto": + if M * N <= 5000 ** 2: + backend = ( + "tensorized" # Fast backend, with a quadratic memory footprint + ) + else: + if ( + D <= 3 + and self.loss == "sinkhorn" + and M * N > 10000 ** 2 + and self.p == 2 + ): + backend = "multiscale" # Super scalable algorithm in low dimension + else: + backend = "online" # Play it safe, without kernel truncation + + # Check compatibility between the batchsize and the backend -------------------------- + + if backend in ["multiscale"]: # multiscale routines work on single measures + if B == 1: + α, x, β, y = α.squeeze(0), x.squeeze(0), β.squeeze(0), y.squeeze(0) + elif B > 1: + warnings.warn( + "The 'multiscale' backend do not support batchsize > 1. " + + "Using 'tensorized' instead: beware of memory overflows!" + ) + backend = "tensorized" + + if B == 0 and backend in [ + "tensorized", + "online", + ]: # tensorized and online routines work on batched tensors + α, x, β, y = α.unsqueeze(0), x.unsqueeze(0), β.unsqueeze(0), y.unsqueeze(0) + + # Run -------------------------------------------------------------------------------- + values = routines[self.loss][backend]( + α, + x, + β, + y, + p=self.p, + blur=self.blur, + reach=self.reach, + diameter=self.diameter, + scaling=self.scaling, + truncate=self.truncate, + cost=self.cost, + kernel=self.kernel, + cluster_scale=self.cluster_scale, + debias=self.debias, + potentials=self.potentials, + labels_x=l_x, + labels_y=l_y, + verbose=self.verbose, + ) + + # Make sure that the output has the correct shape ------------------------------------ + if ( + self.potentials + ): # Return some dual potentials (= test functions) sampled on the input measures + F, G = values + return F.view_as(α), G.view_as(β) + + else: # Return a scalar cost value + if backend in ["multiscale"]: # KeOps backends return a single scalar value + if B == 0: + return values # The user expects a scalar value + else: + return values.view( + -1 + ) # The user expects a "batch list" of distances + + else: # "tensorized" backend returns a "batch vector" of values + if B == 0: + return values[0] # The user expects a scalar value + else: + return values # The user expects a "batch vector" of distances + + def process_args(self, *args): + if len(args) == 6: + return args + if len(args) == 4: + α, x, β, y = args + return None, α, x, None, β, y + elif len(args) == 2: + x, y = args + α = self.generate_weights(x) + β = self.generate_weights(y) + return None, α, x, None, β, y + else: + raise ValueError( + "A SamplesLoss accepts two (x, y), four (α, x, β, y) or six (l_x, α, x, l_y, β, y) arguments." + ) + + def generate_weights(self, x): + if x.dim() == 2: # + N = x.shape[0] + return torch.ones(N).type_as(x) / N + elif x.dim() == 3: + B, N, _ = x.shape + return torch.ones(B, N).type_as(x) / N + else: + raise ValueError( + "Input samples 'x' and 'y' should be encoded as (N,D) or (B,N,D) (batch) tensors." + ) + + def check_shapes(self, l_x, α, x, l_y, β, y): + + if α.dim() != β.dim(): + raise ValueError( + "Input weights 'α' and 'β' should have the same number of dimensions." + ) + if x.dim() != y.dim(): + raise ValueError( + "Input samples 'x' and 'y' should have the same number of dimensions." + ) + if x.shape[-1] != y.shape[-1]: + raise ValueError( + "Input samples 'x' and 'y' should have the same last dimension." + ) + + if ( + x.dim() == 2 + ): # No batch -------------------------------------------------------------------- + B = 0 # Batchsize + N, D = x.shape # Number of "i" samples, dimension of the feature space + M, _ = y.shape # Number of "j" samples, dimension of the feature space + + if α.dim() not in [1, 2]: + raise ValueError( + "Without batches, input weights 'α' and 'β' should be encoded as (N,) or (N,1) tensors." + ) + elif α.dim() == 2: + if α.shape[1] > 1: + raise ValueError( + "Without batches, input weights 'α' should be encoded as (N,) or (N,1) tensors." + ) + if β.shape[1] > 1: + raise ValueError( + "Without batches, input weights 'β' should be encoded as (M,) or (M,1) tensors." + ) + α, β = α.view(-1), β.view(-1) + + if l_x is not None: + if l_x.dim() not in [1, 2]: + raise ValueError( + "Without batches, the vector of labels 'l_x' should be encoded as an (N,) or (N,1) tensor." + ) + elif l_x.dim() == 2: + if l_x.shape[1] > 1: + raise ValueError( + "Without batches, the vector of labels 'l_x' should be encoded as (N,) or (N,1) tensors." + ) + l_x = l_x.view(-1) + if len(l_x) != N: + raise ValueError( + "The vector of labels 'l_x' should have the same length as the point cloud 'x'." + ) + + if l_y is not None: + if l_y.dim() not in [1, 2]: + raise ValueError( + "Without batches, the vector of labels 'l_y' should be encoded as an (M,) or (M,1) tensor." + ) + elif l_y.dim() == 2: + if l_y.shape[1] > 1: + raise ValueError( + "Without batches, the vector of labels 'l_y' should be encoded as (M,) or (M,1) tensors." + ) + l_y = l_y.view(-1) + if len(l_y) != M: + raise ValueError( + "The vector of labels 'l_y' should have the same length as the point cloud 'y'." + ) + + N2, M2 = α.shape[0], β.shape[0] + + elif ( + x.dim() == 3 + ): # batch computation --------------------------------------------------------- + ( + B, + N, + D, + ) = x.shape + # Batchsize, number of "i" samples, dimension of the feature space + ( + B2, + M, + _, + ) = y.shape + # Batchsize, number of "j" samples, dimension of the feature space + if B != B2: + raise ValueError("Samples 'x' and 'y' should have the same batchsize.") + + if α.dim() not in [2, 3]: + raise ValueError( + "With batches, input weights 'α' and 'β' should be encoded as (B,N) or (B,N,1) tensors." + ) + elif α.dim() == 3: + if α.shape[2] > 1: + raise ValueError( + "With batches, input weights 'α' should be encoded as (B,N) or (B,N,1) tensors." + ) + if β.shape[2] > 1: + raise ValueError( + "With batches, input weights 'β' should be encoded as (B,M) or (B,M,1) tensors." + ) + α, β = α.squeeze(-1), β.squeeze(-1) + + if l_x is not None: + raise NotImplementedError( + 'The "multiscale" backend has not been implemented with batches.' + ) + if l_y is not None: + raise NotImplementedError( + 'The "multiscale" backend has not been implemented with batches.' + ) + + B2, N2 = α.shape + B3, M2 = β.shape + if B != B2: + raise ValueError( + "Samples 'x' and weights 'α' should have the same batchsize." + ) + if B != B3: + raise ValueError( + "Samples 'y' and weights 'β' should have the same batchsize." + ) + + else: + raise ValueError( + "Input samples 'x' and 'y' should be encoded as (N,D) or (B,N,D) (batch) tensors." + ) + + if N != N2: + raise ValueError( + "Weights 'α' and samples 'x' should have compatible shapes." + ) + if M != M2: + raise ValueError( + "Weights 'β' and samples 'y' should have compatible shapes." + ) + + return B, N, M, D, l_x, α, l_y, β diff --git a/lib/python3.10/site-packages/geomloss/sinkhorn_divergence.py b/lib/python3.10/site-packages/geomloss/sinkhorn_divergence.py new file mode 100644 index 0000000000000000000000000000000000000000..c6deafee69e8221053684ded70603c28ab3041c0 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/sinkhorn_divergence.py @@ -0,0 +1,632 @@ +r"""Implements the "raw" and "de-biased" Sinkhorn divergences between abstract measures. + +.. math:: + \text{S}_{\varepsilon,\rho}(\alpha,\beta) + ~&=~ \text{OT}_{\varepsilon,\rho}(\alpha, \beta) + ~-~\tfrac{1}{2} \text{OT}_{\varepsilon,\rho}(\alpha, \alpha) + ~-~\tfrac{1}{2} \text{OT}_{\varepsilon,\rho}(\beta, \beta) + ~+~ \tfrac{\varepsilon}{2} \| \langle \alpha, 1\rangle - \langle \beta, 1\rangle \|^2 + +where: + +.. math:: + \text{OT}_{\varepsilon,\rho}(\alpha, \beta) + ~&=~ \min_{\pi\geqslant 0} \langle\, \pi\,,\, \text{C} \,\rangle + ~+~\varepsilon \, \text{KL}(\pi,\alpha\otimes\beta) \\ + ~&+~\rho \, \text{KL}(\pi\,\mathbf{1},\alpha) + ~+~\rho \, \text{KL}(\pi^\intercal \,\mathbf{1},\beta ) \\ + &=~ \max_{f,g} -\rho \langle\, \alpha \,,\, e^{-f/\rho} - 1\,\rangle + -\rho \langle\, \beta \,,\, e^{-g/\rho} - 1\,\rangle \\ + &-~ + \epsilon \langle\, \alpha\otimes\beta \,,\, e^{(f\oplus g - \text{C})/\epsilon} - 1\,\rangle, + +with a Kullback-Leibler divergence defined through: + +.. math:: + \text{KL}(\alpha, \beta)~=~ + \langle \, \alpha \,,\, \log \tfrac{\text{d}\alpha}{\text{d}\beta} \,\rangle + ~-~ \langle \, \alpha \,,\, 1 \,\rangle + ~+~ \langle \, \beta \,,\, 1 \,\rangle ~\geqslant~ 0. +""" + +import numpy as np +import torch +from functools import partial + +try: # Import the keops library, www.kernel-operations.io + from pykeops.torch import generic_logsumexp + from pykeops.torch.cluster import ( + grid_cluster, + cluster_ranges_centroids, + sort_clusters, + from_matrix, + ) + + keops_available = True +except: + keops_available = False + +from .utils import scal + + +# ============================================================================== +# Utility functions +# ============================================================================== + + +def dampening(eps, rho): + """Dampening factor for entropy+unbalanced OT with KL penalization of the marginals.""" + return 1 if rho is None else 1 / (1 + eps / rho) + + +def log_weights(a): + """Returns the log of the input, with values clamped to -100k to avoid numerical bugs.""" + a_log = a.log() + a_log[a <= 0] = -100000 + return a_log + + +class UnbalancedWeight(torch.nn.Module): + """Applies the correct scaling to the dual variables in the Sinkhorn divergence formula. + + Remarkably, the exponentiated potentials should be scaled + by "rho + eps/2" in the forward pass and "rho + eps" in the backward. + For an explanation of this surprising "inconsistency" + between the forward and backward formulas, + please refer to Proposition 12 (Dual formulas for the Sinkhorn costs) + in "Sinkhorn divergences for unbalanced optimal transport", + Sejourne et al., https://arxiv.org/abs/1910.12958. + """ + + def __init__(self, eps, rho): + super(UnbalancedWeight, self).__init__() + self.eps, self.rho = eps, rho + + def forward(self, x): + return (self.rho + self.eps / 2) * x + + def backward(self, g): + return (self.rho + self.eps) * g + + +# ============================================================================== +# eps-scaling heuristic +# ============================================================================== + + +def max_diameter(x, y): + """Returns a rough estimation of the diameter of a pair of point clouds. + + This quantity is used as a maximum "starting scale" in the epsilon-scaling + annealing heuristic. + + Args: + x ((N, D) Tensor): First point cloud. + y ((M, D) Tensor): Second point cloud. + + Returns: + float: Upper bound on the largest distance between points `x[i]` and `y[j]`. + """ + mins = torch.stack((x.min(dim=0)[0], y.min(dim=0)[0])).min(dim=0)[0] + maxs = torch.stack((x.max(dim=0)[0], y.max(dim=0)[0])).max(dim=0)[0] + diameter = (maxs - mins).norm().item() + return diameter + + +def epsilon_schedule(p, diameter, blur, scaling): + r"""Creates a list of values for the temperature "epsilon" across Sinkhorn iterations. + + We use an aggressive strategy with an exponential cooling + schedule: starting from a value of :math:`\text{diameter}^p`, + the temperature epsilon is divided + by :math:`\text{scaling}^p` at every iteration until reaching + a minimum value of :math:`\text{blur}^p`. + + Args: + p (integer or float): The exponent of the Euclidean distance + :math:`\|x_i-y_j\|` that defines the cost function + :math:`\text{C}(x_i,y_j) =\tfrac{1}{p} \|x_i-y_j\|^p`. + + diameter (float, positive): Upper bound on the largest distance between + points :math:`x_i` and :math:`y_j`. + + blur (float, positive): Target value for the entropic regularization + (":math:`\varepsilon = \text{blur}^p`"). + + scaling (float, in (0,1)): Ratio between two successive + values of the blur scale. + + Returns: + list of float: list of values for the temperature epsilon. + """ + eps_list = ( + [diameter ** p] + + [ + np.exp(e) + for e in np.arange( + p * np.log(diameter), p * np.log(blur), p * np.log(scaling) + ) + ] + + [blur ** p] + ) + return eps_list + + +def scaling_parameters(x, y, p, blur, reach, diameter, scaling): + r"""Turns high-level arguments into numerical values for the Sinkhorn loop.""" + if diameter is None: + D = x.shape[-1] + diameter = max_diameter(x.view(-1, D), y.view(-1, D)) + + eps = blur ** p + rho = None if reach is None else reach ** p + eps_list = epsilon_schedule(p, diameter, blur, scaling) + return diameter, eps, eps_list, rho + + +# ============================================================================== +# Sinkhorn divergence +# ============================================================================== + + +def sinkhorn_cost( + eps, rho, a, b, f_aa, g_bb, g_ab, f_ba, batch=False, debias=True, potentials=False +): + r"""Returns the required information (cost, etc.) from a set of dual potentials. + + Args: + eps (float): Target (i.e. final) temperature. + rho (float or None (:math:`+\infty`)): Strength of the marginal constraints. + + a ((..., N) Tensor, nonnegative): Weights for the "source" measure on the points :math:`x_i`. + b ((..., M) Tensor, nonnegative): Weights for the "target" measure on the points :math:`y_j`. + f_aa ((..., N) Tensor)): Dual potential for the "a <-> a" problem. + g_bb ((..., M) Tensor)): Dual potential for the "b <-> b" problem. + g_ab ((..., M) Tensor)): Dual potential supported by :math:`y_j` for the "a <-> b" problem. + f_ba ((..., N) Tensor)): Dual potential supported by :math:`x_i` for the "a <-> a" problem. + batch (bool, optional): Are we working in batch mode? Defaults to False. + debias (bool, optional): Are we working with the "debiased" or the "raw" Sinkhorn divergence? + Defaults to True. + potentials (bool, optional): Shall we return the dual vectors instead of the cost value? + Defaults to False. + + Returns: + Tensor or pair of Tensors: if `potentials` is True, we return a pair + of (..., N), (..., M) Tensors that encode the optimal dual vectors, + respectively supported by :math:`x_i` and :math:`y_j`. + Otherwise, we return a (,) or (B,) Tensor of values for the Sinkhorn divergence. + """ + + if potentials: # Just return the dual potentials + if debias: # See Eq. (3.209) in Jean Feydy's PhD thesis. + # N.B.: This formula does not make much sense in the unbalanced mode + # (i.e. if reach is not None). + return f_ba - f_aa, g_ab - g_bb + else: # See Eq. (3.207) in Jean Feydy's PhD thesis. + return f_ba, g_ab + + else: # Actually compute the Sinkhorn divergence + if ( + debias + ): # UNBIASED Sinkhorn divergence, S_eps(a,b) = OT_eps(a,b) - .5*OT_eps(a,a) - .5*OT_eps(b,b) + if rho is None: # Balanced case: + # See Eq. (3.209) in Jean Feydy's PhD thesis. + return scal(a, f_ba - f_aa, batch=batch) + scal( + b, g_ab - g_bb, batch=batch + ) + else: + # Unbalanced case: + # See Proposition 12 (Dual formulas for the Sinkhorn costs) + # in "Sinkhorn divergences for unbalanced optimal transport", + # Sejourne et al., https://arxiv.org/abs/1910.12958. + return scal( + a, + UnbalancedWeight(eps, rho)( + (-f_aa / rho).exp() - (-f_ba / rho).exp() + ), + batch=batch, + ) + scal( + b, + UnbalancedWeight(eps, rho)( + (-g_bb / rho).exp() - (-g_ab / rho).exp() + ), + batch=batch, + ) + + else: # Classic, BIASED entropized Optimal Transport OT_eps(a,b) + if rho is None: # Balanced case: + # See Eq. (3.207) in Jean Feydy's PhD thesis. + return scal(a, f_ba, batch=batch) + scal(b, g_ab, batch=batch) + else: + # Unbalanced case: + # See Proposition 12 (Dual formulas for the Sinkhorn costs) + # in "Sinkhorn divergences for unbalanced optimal transport", + # Sejourne et al., https://arxiv.org/abs/1910.12958. + # N.B.: Even if this quantity is never used in practice, + # we may want to re-check this computation... + return scal( + a, UnbalancedWeight(eps, rho)(1 - (-f_ba / rho).exp()), batch=batch + ) + scal( + b, UnbalancedWeight(eps, rho)(1 - (-g_ab / rho).exp()), batch=batch + ) + + +# ============================================================================== +# Sinkhorn loop +# ============================================================================== + + +def sinkhorn_loop( + softmin, + a_logs, + b_logs, + C_xxs, + C_yys, + C_xys, + C_yxs, + eps_list, + rho, + jumps=[], + kernel_truncation=None, + truncate=5, + cost=None, + extrapolate=None, + debias=True, + last_extrapolation=True, +): + r"""Implements the (possibly multiscale) symmetric Sinkhorn loop, + with the epsilon-scaling (annealing) heuristic. + + This is the main "core" routine of GeomLoss. It is written to + solve optimal transport problems efficiently in all the settings + that are supported by the library: (generalized) point clouds, + images and volumes. + + This algorithm is described in Section 3.3.3 of Jean Feydy's PhD thesis, + "Geometric data analysis, beyond convolutions" (Universite Paris-Saclay, 2020) + (https://www.jeanfeydy.com/geometric_data_analysis.pdf). + Algorithm 3.5 corresponds to the case where `kernel_truncation` is None, + while Algorithm 3.6 describes the full multiscale algorithm. + + Args: + softmin (function): This routine must implement the (soft-)C-transform + between dual vectors, which is the core computation for + Auction- and Sinkhorn-like optimal transport solvers. + If `eps` is a float number, `C_xy` encodes a cost matrix :math:`C(x_i,y_j)` + and `g` encodes a dual potential :math:`g_j` that is supported by the points + :math:`y_j`'s, then `softmin(eps, C_xy, g)` must return a dual potential + `f` for ":math:`f_i`", supported by the :math:`x_i`'s, that is equal to: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \exp + \big[ g_j - C(x_i, y_j) / \varepsilon \big]~. + + For more detail, see e.g. Section 3.3 and Eq. (3.186) in Jean Feydy's PhD thesis. + + a_logs (list of Tensors): List of log-weights :math:`\log(\alpha_i)` + for the first input measure at different resolutions. + + b_logs (list of Tensors): List of log-weights :math:`\log(\beta_i)` + for the second input measure at different resolutions. + + C_xxs (list): List of objects that encode the cost matrices + :math:`C(x_i, x_j)` between the samples of the first input + measure at different scales. + These will be passed to the `softmin` function as second arguments. + + C_yys (list): List of objects that encode the cost matrices + :math:`C(y_i, y_j)` between the samples of the second input + measure at different scales. + These will be passed to the `softmin` function as second arguments. + + C_xys (list): List of objects that encode the cost matrices + :math:`C(x_i, y_j)` between the samples of the first and second input + measures at different scales. + These will be passed to the `softmin` function as second arguments. + + C_yxs (list): List of objects that encode the cost matrices + :math:`C(y_i, x_j)` between the samples of the second and first input + measures at different scales. + These will be passed to the `softmin` function as second arguments. + + eps_list (list of float): List of successive values for the temperature + :math:`\varepsilon`. The number of iterations in the loop + is equal to the length of this list. + + rho (float or None): Strength of the marginal constraints for unbalanced OT. + None stands for :math:`\rho = +\infty`, i.e. balanced OT. + + jumps (list, optional): List of iteration numbers where we "jump" + from a coarse resolution to a finer one by looking + one step further in the lists `a_logs`, `b_logs`, `C_xxs`, etc. + Count starts at iteration 0. + Defaults to [] - single-scale mode without jumps. + + kernel_truncation (function, optional): Implements the kernel truncation trick. + Defaults to None. + + truncate (int, optional): Optional argument for `kernel_truncation`. + Defaults to 5. + + cost (string or function, optional): Optional argument for `kernel_truncation`. + Defaults to None. + + extrapolate (function, optional): Function. + If + `f_ba` is a dual potential that is supported by the :math:`x_i`'s, + `g_ab` is a dual potential that is supported by the :math:`y_j`'s, + `eps` is the current value of the temperature :math:`\varepsilon`, + `damping` is the current value of the damping coefficient for unbalanced OT, + `C_xy` encodes the cost matrix :math:`C(x_i, y_j)` at the current + ("coarse") resolution, + `b_log` denotes the log-weights :math:`\log(\beta_j)` + that are supported by the :math:`y_j`'s at the coarse resolution, + and + `C_xy_fine` encodes the cost matrix :math:`C(x_i, y_j)` at the next + ("fine") resolution, + then + `extrapolate(f_ba, g_ab, eps, damping, C_xy, b_log, C_xy_fine)` + will be used to compute the new values of the dual potential + `f_ba` on the point cloud :math:`x_i` at a finer resolution. + Defaults to None - it is not needed in single-scale mode. + + debias (bool, optional): Should we used the "de-biased" Sinkhorn divergence + :math:`\text{S}_{\varepsilon, \rho}(\al,\be)` instead + of the "raw" entropic OT cost + :math:`\text{OT}_{\varepsilon, \rho}(\al,\be)`? + This slows down the OT solver but guarantees that our approximation + of the Wasserstein distance will be positive and definite + - up to convergence of the Sinkhorn loop. + For a detailed discussion of the influence of this parameter, + see e.g. Fig. 3.21 in Jean Feydy's PhD thesis. + Defaults to True. + + last_extrapolation (bool, optional): Should we perform a last, "full" + Sinkhorn iteration before returning the dual potentials? + This allows us to retrieve correct gradients without having + to backpropagate trough the full Sinkhorn loop. + Defaults to True. + + Returns: + 4-uple of Tensors: The four optimal dual potentials + `(f_aa, g_bb, g_ab, f_ba)` that are respectively + supported by the first, second, second and first input measures + and associated to the "a <-> a", "b <-> b", + "a <-> b" and "a <-> b" optimal transport problems. + """ + + # Number of iterations, specified by our epsilon-schedule + Nits = len(eps_list) + + # The multiscale algorithm may loop over several representations + # of the input measures. + # In this routine, the convention is that "myvars" denotes + # the list of "myvar" across different scales. + if type(a_logs) is not list: + # The "single-scale" use case is simply encoded + # using lists of length 1. + + # Logarithms of the weights: + a_logs, b_logs = [a_logs], [b_logs] + + # Cost "matrices" C(x_i, y_j) and C(y_i, x_j): + C_xys, C_yxs = [C_xys], [C_yxs] # Used for the "a <-> b" problem. + + # Cost "matrices" C(x_i, x_j) and C(y_i, y_j): + if debias: # Only used for the "a <-> a" and "b <-> b" problems. + C_xxs, C_yys = [C_xxs], [C_yys] + + # N.B.: We don't let users backprop through the Sinkhorn iterations + # and branch instead on an explicit formula "at convergence" + # using some "advanced" PyTorch syntax at the end of the loop. + # This acceleration "trick" relies on the "envelope theorem": + # it works very well if users are only interested in the gradient + # of the Sinkhorn loss, but may not produce correct results + # if one attempts to compute order-2 derivatives, + # or differentiate "non-standard" quantities that + # are defined using the optimal dual potentials. + # + # We may wish to alter this behaviour in the future. + # For reference on the question, see Eq. (3.226-227) in + # Jean Feydy's PhD thesis and e.g. + # "Super-efficiency of automatic differentiation for + # functions defined as a minimum", Ablin, Peyré, Moreau (2020) + # https://arxiv.org/pdf/2002.03722.pdf. + torch.autograd.set_grad_enabled(False) + + # Line 1 (in Algorithm 3.6 from Jean Feydy's PhD thesis) --------------------------- + + # We start at the coarsest resolution available: + k = 0 # Scale index + eps = eps_list[k] # First value of the temperature (typically, = diameter**p) + + # Damping factor: equal to 1 for balanced OT, + # < 1 for unbalanced OT with KL penalty on the marginal constraints. + # For reference, see Table 1 in "Sinkhorn divergences for unbalanced + # optimal transport", Sejourne et al., https://arxiv.org/abs/1910.12958. + damping = dampening(eps, rho) + + # Load the measures and cost matrices at the current scale: + a_log, b_log = a_logs[k], b_logs[k] + C_xy, C_yx = C_xys[k], C_yxs[k] # C(x_i, y_j), C(y_i, x_j) + if debias: # Info for the "a <-> a" and "b <-> b" problems + C_xx, C_yy = C_xxs[k], C_yys[k] # C(x_i, x_j), C(y_j, y_j) + + # Line 2 --------------------------------------------------------------------------- + # Start with a decent initialization for the dual vectors: + # N.B.: eps is really large here, so the log-sum-exp behaves as a sum + # and the softmin is basically + # a convolution with the cost function (i.e. the limit for eps=+infty). + # The algorithm was originally written with this convolution + # - but in this implementation, we use "softmin" for the sake of simplicity. + g_ab = damping * softmin(eps, C_yx, a_log) # a -> b + f_ba = damping * softmin(eps, C_xy, b_log) # b -> a + if debias: + f_aa = damping * softmin(eps, C_xx, a_log) # a -> a + g_bb = damping * softmin(eps, C_yy, b_log) # a -> a + + # Lines 4-5: eps-scaling descent --------------------------------------------------- + for i, eps in enumerate(eps_list): # See Fig. 3.25-26 in Jean Feydy's PhD thesis. + + # Line 6: update the damping coefficient --------------------------------------- + damping = dampening(eps, rho) # eps and damping change across iterations + + # Line 7: "coordinate ascent" on the dual problems ----------------------------- + # N.B.: As discussed in Section 3.3.3 of Jean Feydy's PhD thesis, + # we perform "symmetric" instead of "alternate" updates + # of the dual potentials "f" and "g". + # To this end, we first create buffers "ft", "gt" + # (for "f-tilde", "g-tilde") using the standard + # Sinkhorn formulas, and update both dual vectors + # simultaneously. + ft_ba = damping * softmin(eps, C_xy, b_log + g_ab / eps) # b -> a + gt_ab = damping * softmin(eps, C_yx, a_log + f_ba / eps) # a -> b + + # See Fig. 3.21 in Jean Feydy's PhD thesis to see the importance + # of debiasing when the target "blur" or "eps**(1/p)" value is larger + # than the average distance between samples x_i, y_j and their neighbours. + if debias: + ft_aa = damping * softmin(eps, C_xx, a_log + f_aa / eps) # a -> a + gt_bb = damping * softmin(eps, C_yy, b_log + g_bb / eps) # b -> b + + # Symmetrized updates - see Fig. 3.24.b in Jean Feydy's PhD thesis: + f_ba, g_ab = 0.5 * (f_ba + ft_ba), 0.5 * (g_ab + gt_ab) # OT(a,b) wrt. a, b + if debias: + f_aa, g_bb = 0.5 * (f_aa + ft_aa), 0.5 * (g_bb + gt_bb) # OT(a,a), OT(b,b) + + # Line 8: jump from a coarse to a finer scale ---------------------------------- + # In multi-scale mode, we work we increasingly detailed representations + # of the input measures: this type of strategy is known as "multi-scale" + # in computer graphics, "multi-grid" in numerical analysis, + # "coarse-to-fine" in signal processing or "divide and conquer" + # in standard complexity theory (e.g. for the quick-sort algorithm). + # + # In the Sinkhorn loop with epsilon-scaling annealing, our + # representations of the input measures are fine enough to ensure + # that the typical distance between any two samples x_i, y_j is always smaller + # than the current value of "blur = eps**(1/p)". + # As illustrated in Fig. 3.26 of Jean Feydy's PhD thesis, this allows us + # to reach a satisfying level of precision while speeding up the computation + # of the Sinkhorn iterations in the first few steps. + # + # In practice, different multi-scale representations of the input measures + # are generated by the "parent" code of this solver and stored in the + # lists a_logs, b_logs, C_xxs, etc. + # + # The switch between different scales is specified by the list of "jump" indices, + # that is generated in conjunction with the list of temperatures "eps_list". + # + # N.B.: In single-scale mode, jumps = []: the code below is never executed + # and we retrieve "Algorithm 3.5" from Jean Feydy's PhD thesis. + if i in jumps: + + if i == len(eps_list) - 1: # Last iteration: just extrapolate! + + C_xy_fine, C_yx_fine = C_xys[k + 1], C_yxs[k + 1] + if debias: + C_xx_fine, C_yy_fine = C_xxs[k + 1], C_yys[k + 1] + + last_extrapolation = False # No need to re-extrapolate after the loop + torch.autograd.set_grad_enabled(True) + + else: # It's worth investing some time on kernel truncation... + # The lines below implement the Kernel truncation trick, + # described in Eq. (3.222-3.224) in Jean Feydy's PhD thesis and in + # "Stabilized sparse scaling algorithms for entropy regularized transport + # problems", Schmitzer (2016-2019), (https://arxiv.org/pdf/1610.06519.pdf). + # + # A more principled and "controlled" variant is also described in + # "Capacity constrained entropic optimal transport, Sinkhorn saturated + # domain out-summation and vanishing temperature", Benamou and Martinet + # (2020), (https://hal.archives-ouvertes.fr/hal-02563022/). + # + # On point clouds, this code relies on KeOps' block-sparse routines. + # On grids, it is a "dummy" call: we do not perform any "truncation" + # and rely instead on the separability of the Gaussian convolution kernel. + + # Line 9: a <-> b ------------------------------------------------------ + C_xy_fine, C_yx_fine = kernel_truncation( + C_xy, + C_yx, + C_xys[k + 1], + C_yxs[k + 1], + f_ba, + g_ab, + eps, + truncate=truncate, + cost=cost, + ) + + if debias: + # Line 10: a <-> a ------------------------------------------------ + C_xx_fine, _ = kernel_truncation( + C_xx, + C_xx, + C_xxs[k + 1], + C_xxs[k + 1], + f_aa, + f_aa, + eps, + truncate=truncate, + cost=cost, + ) + # Line 11: b <-> b ------------------------------------------------- + C_yy_fine, _ = kernel_truncation( + C_yy, + C_yy, + C_yys[k + 1], + C_yys[k + 1], + g_bb, + g_bb, + eps, + truncate=truncate, + cost=cost, + ) + + # Line 12: extrapolation step ---------------------------------------------- + # We extra/inter-polate the values of the dual potentials from + # the "coarse" to the "fine" resolution. + # + # On point clouds, we use the expressions of the dual potentials + # detailed e.g. in Eqs. (3.194-3.195) of Jean Feydy's PhD thesis. + # On images and volumes, we simply rely on (bi/tri-)linear interpolation. + # + # N.B.: the cross-updates below *must* be done in parallel! + f_ba, g_ab = ( + extrapolate(f_ba, g_ab, eps, damping, C_xy, b_log, C_xy_fine), + extrapolate(g_ab, f_ba, eps, damping, C_yx, a_log, C_yx_fine), + ) + + # Extrapolation for the symmetric problems: + if debias: + f_aa = extrapolate(f_aa, f_aa, eps, damping, C_xx, a_log, C_xx_fine) + g_bb = extrapolate(g_bb, g_bb, eps, damping, C_yy, b_log, C_yy_fine) + + # Line 13: update the measure weights and cost "matrices" ------------------ + k = k + 1 + a_log, b_log = a_logs[k], b_logs[k] + C_xy, C_yx = C_xy_fine, C_yx_fine + if debias: + C_xx, C_yy = C_xx_fine, C_yy_fine + + # As a very last step, we perform a final "Sinkhorn" iteration. + # As detailed above (around "torch.autograd.set_grad_enabled(False)"), + # this allows us to retrieve correct expressions for the gradient + # without having to backprop through the whole Sinkhorn loop. + torch.autograd.set_grad_enabled(True) + + if last_extrapolation: + # The cross-updates should be done in parallel! + f_ba, g_ab = ( + damping * softmin(eps, C_xy, (b_log + g_ab / eps).detach()), + damping * softmin(eps, C_yx, (a_log + f_ba / eps).detach()), + ) + + if debias: + f_aa = damping * softmin(eps, C_xx, (a_log + f_aa / eps).detach()) + g_bb = damping * softmin(eps, C_yy, (b_log + g_bb / eps).detach()) + + if debias: + return f_aa, g_bb, g_ab, f_ba + else: + return None, None, g_ab, f_ba diff --git a/lib/python3.10/site-packages/geomloss/sinkhorn_images.py b/lib/python3.10/site-packages/geomloss/sinkhorn_images.py new file mode 100644 index 0000000000000000000000000000000000000000..812a5bf39d648b37ecb67684db9ad8cb5003f261 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/sinkhorn_images.py @@ -0,0 +1,202 @@ +import torch +from .utils import log_dens, pyramid, upsample, softmin_grid +from .sinkhorn_divergence import epsilon_schedule, scaling_parameters +from .sinkhorn_divergence import sinkhorn_cost, sinkhorn_loop + + +def extrapolate(f_ba, g_ab, eps, damping, C_xy, b_log, C_xy_fine): + return upsample(f_ba) + + +def kernel_truncation( + C_xy, + C_yx, + C_xy_fine, + C_yx_fine, + f_ba, + g_ab, + eps, + truncate=None, + cost=None, + verbose=False, +): + return C_xy_fine, C_yx_fine + + +def sinkhorn_divergence( + a, + b, + p=2, + blur=None, + reach=None, + axes=None, + scaling=0.5, + cost=None, + debias=True, + potentials=False, + verbose=False, + **kwargs, +): + r"""Sinkhorn divergence between measures supported on 1D/2D/3D grids. + + Args: + a ((B, Nx), (B, Nx, Ny) or (B, Nx, Ny, Nz) Tensor): Weights :math:`\alpha_i` + for the first measure, with a batch dimension. + + b ((B, Nx), (B, Nx, Ny) or (B, Nx, Ny, Nz) Tensor): Weights :math:`\beta_j` + for the second measure, with a batch dimension. + + p (int, optional): Exponent of the ground cost function + :math:`C(x_i,y_j)`, which is equal to + :math:`\tfrac{1}{p}\|x_i-y_j\|^p` if it is not provided + explicitly through the `cost` optional argument. + Defaults to 2. + + blur (float or None, optional): Target value for the blurring scale + of the "point spread function" or Gibbs kernel + :math:`K_{i,j} = \exp(-C(x_i,y_j)/\varepsilon) = \exp(-\|x_i-y_j\|^p / p \text{blur}^p). + In the Sinkhorn algorithm, the temperature :math:`\varepsilon` + is computed as :math:`\text{blur}^p`. + Defaults to None: we pick the smallest pixel size across + the Nx, Ny and Nz dimensions (if applicable). + + axes (tuple of pairs of floats or None (= [0, 1)^(1/2/3)), optional): + Dimensions of the image domain, specified through a 1/2/3-uple + of [vmin, vmax] bounds. + For instance, if the batched 2D images correspond to sampled + measures on [-10, 10) x [-3, 5), you may use "axes = ([-10, 10], [-3, 5])". + The (implicit) pixel coordinates are computed using a "torch.linspace(...)" + across each dimension: along any given axis, the spacing between two pixels + is equal to "(vmax - vmin) / npixels". + + Defaults to None: we assume that the signal / image / volume + is sampled on the unit interval [0, 1) / square [0, 1)^2 / cube [0, 1)^3. + + scaling (float in (0, 1), optional): Ratio between two successive + values of the blur radius in the epsilon-scaling annealing descent. + Defaults to 0.5. + + cost (function or None, optional): ... + Defaults to None: we use a Euclidean cost + :math:`C(x_i,y_j) = \tfrac{1}{p}\|x_i-y_j\|^p`. + + debias (bool, optional): Should we used the "de-biased" Sinkhorn divergence + :math:`\text{S}_{\varepsilon, \rho}(\al,\be)` instead + of the "raw" entropic OT cost + :math:`\text{OT}_{\varepsilon, \rho}(\al,\be)`? + This slows down the OT solver but guarantees that our approximation + of the Wasserstein distance will be positive and definite + - up to convergence of the Sinkhorn loop. + For a detailed discussion of the influence of this parameter, + see e.g. Fig. 3.21 in Jean Feydy's PhD thesis. + Defaults to True. + + potentials (bool, optional): Should we return the optimal dual potentials + instead of the cost value? + Defaults to False. + + Returns: + (B,) Tensor or pair of (B, Nx, ...), (B, Nx, ...) Tensors: If `potentials` is True, + we return a pair of (B, Nx, ...), (B, Nx, ...) Tensors that encode the optimal + dual vectors, respectively supported by :math:`x_i` and :math:`y_j`. + Otherwise, we return a (B,) Tensor of values for the Sinkhorn divergence. + """ + + if blur is None: + blur = 1 / a.shape[-1] + + # Pre-compute a multiscale decomposition (=Binary/Quad/OcTree) + # of the input measures, stored as logarithms + a_s, b_s = pyramid(a)[1:], pyramid(b)[1:] + a_logs = list(map(log_dens, a_s)) + b_logs = list(map(log_dens, b_s)) + + # By default, our cost function :math:`C(x_i,y_j)` is a halved, + # squared Euclidean distance (p=2) or a simple Euclidean distance (p=1): + depth = len(a_logs) + if cost is None: + C_s = [p] * depth # Dummy "cost matrices" + else: + raise NotImplementedError() + + # Diameter of the configuration: + diameter = 1 + # Target temperature epsilon: + eps = blur ** p + # Strength of the marginal constraints: + rho = None if reach is None else reach ** p + + # Schedule for the multiscale descent, with ε-scaling: + """ + sigma = diameter + for n in range(depth): + for _ in range(scaling_N): # Number of steps per scale + eps_list.append(sigma ** p) + + # Decrease the kernel radius, making sure that + # the radius sigma is divided by two at every scale until we reach + # the target value, "blur": + scale = max(sigma * (2 ** (-1 / scaling_N)), blur) + + jumps = [scaling_N * (i + 1) - 1 for i in range(depth - 1)] + """ + if scaling < 0.5: + raise ValueError( + f"Scaling value of {scaling} is too small: please use a number in [0.5, 1)." + ) + + diameter, eps, eps_list, rho = scaling_parameters( + None, None, p, blur, reach, diameter, scaling + ) + + # List of pixel widths: + pyramid_scales = [diameter / a.shape[-1] for a in a_s] + if verbose: + print("Pyramid scales:", pyramid_scales) + + current_scale = pyramid_scales.pop(0) + jumps = [] + for i, eps in enumerate(eps_list[1:]): + if current_scale ** p > eps: + jumps.append(i + 1) + current_scale = pyramid_scales.pop(0) + + if verbose: + print("Temperatures: ", eps_list) + print("Jumps: ", jumps) + + assert ( + len(jumps) == len(a_s) - 1 + ), "There's a bug in the multicale pre-processing..." + + # Use an optimal transport solver to retrieve the dual potentials: + f_aa, g_bb, g_ab, f_ba = sinkhorn_loop( + softmin_grid, + a_logs, + b_logs, + C_s, + C_s, + C_s, + C_s, + eps_list, + rho, + jumps=jumps, + kernel_truncation=kernel_truncation, + extrapolate=extrapolate, + debias=debias, + ) + + # Optimal transport cost: + return sinkhorn_cost( + eps, + rho, + a, + b, + f_aa, + g_bb, + g_ab, + f_ba, + batch=True, + debias=debias, + potentials=potentials, + ) diff --git a/lib/python3.10/site-packages/geomloss/sinkhorn_samples.py b/lib/python3.10/site-packages/geomloss/sinkhorn_samples.py new file mode 100644 index 0000000000000000000000000000000000000000..d0b0197a2faa8598ab24b89f1aec3c3510587192 --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/sinkhorn_samples.py @@ -0,0 +1,684 @@ +"""Implements the (debiased) Sinkhorn divergence between sampled measures.""" + +import numpy as np +import torch +from functools import partial + +try: # Import the keops library, www.kernel-operations.io + from pykeops.torch import generic_logsumexp + from pykeops.torch.cluster import grid_cluster, cluster_ranges_centroids + from pykeops.torch.cluster import sort_clusters, from_matrix, swap_axes + from pykeops.torch import LazyTensor, Vi, Vj, Pm + + keops_available = True +except: + keops_available = False + +from .utils import scal, squared_distances, distances + +from .sinkhorn_divergence import epsilon_schedule, scaling_parameters +from .sinkhorn_divergence import dampening, log_weights, sinkhorn_cost, sinkhorn_loop + + +# ============================================================================== +# backend == "tensorized" +# ============================================================================== + +cost_routines = { + 1: (lambda x, y: distances(x, y)), + 2: (lambda x, y: squared_distances(x, y) / 2), +} + + +def softmin_tensorized(eps, C_xy, h_y): + r"""Soft-C-transform, implemented using dense torch Tensors. + + This routine implements the (soft-)C-transform + between dual vectors, which is the core computation for + Auction- and Sinkhorn-like optimal transport solvers. + + If `eps` is a float number, `C_xy` is a (batched) cost matrix :math:`C(x_i,y_j)` + and `h_y` encodes a dual potential :math:`h_j` that is supported by the points + :math:`y_j`'s, then `softmin_tensorized(eps, C_xy, h_y)` returns a dual potential + `f` for ":math:`f_i`", supported by the :math:`x_i`'s, that is equal to: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \exp + \big[ h_j - C(x_i, y_j) / \varepsilon \big]~. + + For more detail, see e.g. Section 3.3 and Eq. (3.186) in Jean Feydy's PhD thesis. + + Args: + eps (float, positive): Temperature :math:`\varepsilon` for the Gibbs kernel + :math:`K_{i,j} = \exp(-C(x_i, y_j) / \varepsilon)`. + + C_xy ((B, N, M) Tensor): Cost matrix :math:`C(x_i,y_j)`, with a batch dimension. + + h_y ((B, M) Tensor): Vector of logarithmic "dual" values, with a batch dimension. + Most often, this vector will be computed as `h_y = b_log + g_j / eps`, + where `b_log` is a vector of log-weights :math:`\log(\beta_j)` + for the :math:`y_j`'s and :math:`g_j` is a dual vector + in the Sinkhorn algorithm, so that: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \beta_j + \exp \tfrac{1}{\varepsilon} \big[ g_j - C(x_i, y_j) \big]~. + + Returns: + (B, N) Tensor: Dual potential `f` of values :math:`f_i`, supported + by the points :math:`x_i`. + """ + B = C_xy.shape[0] + return -eps * (h_y.view(B, 1, -1) - C_xy / eps).logsumexp(2).view(B, -1) + + +def sinkhorn_tensorized( + a, + x, + b, + y, + p=2, + blur=0.05, + reach=None, + diameter=None, + scaling=0.5, + cost=None, + debias=True, + potentials=False, + **kwargs, +): + r"""Vanilla PyTorch implementation of the Sinkhorn divergence. + + Args: + a ((B, N) Tensor): Weights :math:`\alpha_i` for the first measure, + with a batch dimension. + + x ((B, N, D) Tensor): Sampling locations :math:`x_i` for the first measure, + with a batch dimension. + + b ((B, M) Tensor): Weights :math:`\beta_j` for the second measure, + with a batch dimension. + + y ((B, M, D) Tensor): Sampling locations :math:`y_j` for the second measure, + with a batch dimension. + + p (int, optional): Exponent of the ground cost function + :math:`C(x_i,y_j)`, which is equal to + :math:`\tfrac{1}{p}\|x_i-y_j\|^p` if it is not provided + explicitly through the `cost` optional argument. + Defaults to 2. + + blur (float, optional): Target value for the blurring scale + of the Gibbs kernel + :math:`K_{i,j} = \exp(-C(x_i,y_j)/\varepsilon) = \exp(-\|x_i-y_j\|^p / p \text{blur}^p). + In the Sinkhorn algorithm, the temperature :math:`\varepsilon` + is computed as :math:`\text{blur}^p`. + Defaults to 0.05. + + reach (float or None (= +infty), optional): Typical scale for the + maximum displacement between any two points :math:`x_i` and :math:`y_j` + in the optimal transport model. + In the unbalanced Sinkhorn divergence, + the strength :math:`\rho` of the soft marginal constraints + is computed as :math:`\rho = \text{reach}^p`. + Defaults to None. + + diameter (float or None, optional): Upper bound on the value + of the distance :math:`\|x_i-y_j\|` between any two samples. + This will be used as a first value of the `blur` radius + in the epsilon-scaling annealing descent. + Defaults to None: an upper bound will be estimated on the fly. + + scaling (float in (0, 1), optional): Ratio between two successive + values of the blur radius in the epsilon-scaling annealing descent. + Defaults to 0.5. + + cost (function, optional): Cost function :math:`C(x_i,y_j)`. + It should take as input two point clouds `x` and `y` + with a batch dimension, encoded as `(B, N, D)`, `(B, M, D)` + torch Tensors and return a `(B, N, M)` torch Tensor. + Defaults to None: we use a Euclidean cost + :math:`C(x_i,y_j) = \tfrac{1}{p}\|x_i-y_j\|^p`. + + debias (bool, optional): Should we used the "de-biased" Sinkhorn divergence + :math:`\text{S}_{\varepsilon, \rho}(\al,\be)` instead + of the "raw" entropic OT cost + :math:`\text{OT}_{\varepsilon, \rho}(\al,\be)`? + This slows down the OT solver but guarantees that our approximation + of the Wasserstein distance will be positive and definite + - up to convergence of the Sinkhorn loop. + For a detailed discussion of the influence of this parameter, + see e.g. Fig. 3.21 in Jean Feydy's PhD thesis. + Defaults to True. + + potentials (bool, optional): Should we return the optimal dual potentials + instead of the cost value? + Defaults to False. + + Returns: + (B,) Tensor or pair of (B, N), (B, M) Tensors: if `potentials` is True, + we return a pair of (B, N), (B, M) Tensors that encode the optimal dual vectors, + respectively supported by :math:`x_i` and :math:`y_j`. + Otherwise, we return a (B,) Tensor of values for the Sinkhorn divergence. + """ + + # Retrieve the batch size B, the numbers of samples N, M + # and the size of the ambient space D: + B, N, D = x.shape + _, M, _ = y.shape + + # By default, our cost function :math:`C(x_i,y_j)` is a halved, + # squared Euclidean distance (p=2) or a simple Euclidean distance (p=1): + if cost is None: + cost = cost_routines[p] + + # Compute the relevant cost matrices C(x_i, y_j), C(y_j, x_i), etc. + # Note that we "detach" the gradients of the "right-hand sides": + # this is coherent with the way we compute our gradients + # in the `sinkhorn_loop(...)` routine, in the `sinkhorn_divergence.py` file. + # Please refer to the comments in this file for more details. + C_xy = cost(x, y.detach()) # (B,N,M) torch Tensor + C_yx = cost(y, x.detach()) # (B,M,N) torch Tensor + + # N.B.: The "auto-correlation" matrices C(x_i, x_j) and C(y_i, y_j) + # are only used by the "debiased" Sinkhorn algorithm. + C_xx = cost(x, x.detach()) if debias else None # (B,N,N) torch Tensor + C_yy = cost(y, y.detach()) if debias else None # (B,M,M) torch Tensor + + # Compute the relevant values of the diameter of the configuration, + # target temperature epsilon, temperature schedule across itereations + # and strength of the marginal constraints: + diameter, eps, eps_list, rho = scaling_parameters( + x, y, p, blur, reach, diameter, scaling + ) + + # Use an optimal transport solver to retrieve the dual potentials: + f_aa, g_bb, g_ab, f_ba = sinkhorn_loop( + softmin_tensorized, + log_weights(a), + log_weights(b), + C_xx, + C_yy, + C_xy, + C_yx, + eps_list, + rho, + debias=debias, + ) + + # Optimal transport cost: + return sinkhorn_cost( + eps, + rho, + a, + b, + f_aa, + g_bb, + g_ab, + f_ba, + batch=True, + debias=debias, + potentials=potentials, + ) + + +# ============================================================================== +# backend == "online" +# ============================================================================== + + +def softmin_online_lazytensor(eps, C_xy, h_y, p=2): + r"""Soft-C-transform, implemented using symbolic KeOps LazyTensors. + + This routine implements the (soft-)C-transform + between dual vectors, which is the core computation for + Auction- and Sinkhorn-like optimal transport solvers. + + If `eps` is a float number, `C_xy = (x, y)` is a pair of (batched) + point clouds, encoded as (B, N, D) and (B, M, D) Tensors + and `h_y` encodes a dual potential :math:`h_j` that is supported by the points + :math:`y_j`'s, then `softmin_tensorized(eps, C_xy, h_y)` returns a dual potential + `f` for ":math:`f_i`", supported by the :math:`x_i`'s, that is equal to: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \exp + \big[ h_j - \|x_i - y_j\|^p / p \varepsilon \big]~. + + For more detail, see e.g. Section 3.3 and Eq. (3.186) in Jean Feydy's PhD thesis. + + Args: + eps (float, positive): Temperature :math:`\varepsilon` for the Gibbs kernel + :math:`K_{i,j} = \exp(- \|x_i - y_j\|^p / p \varepsilon)`. + + C_xy (pair of (B, N, D), (B, M, D) Tensors): Point clouds :math:`x_i` + and :math:`y_j`, with a batch dimension. + + h_y ((B, M) Tensor): Vector of logarithmic "dual" values, with a batch dimension. + Most often, this vector will be computed as `h_y = b_log + g_j / eps`, + where `b_log` is a vector of log-weights :math:`\log(\beta_j)` + for the :math:`y_j`'s and :math:`g_j` is a dual vector + in the Sinkhorn algorithm, so that: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \beta_j + \exp \tfrac{1}{\varepsilon} \big[ g_j - \|x_i - y_j\|^p / p \big]~. + + Returns: + (B, N) Tensor: Dual potential `f` of values :math:`f_i`, supported + by the points :math:`x_i`. + """ + x, y = C_xy # Retrieve our point clouds + B = x.shape[0] # Batch dimension + + # Encoding as batched KeOps LazyTensors: + x_i = LazyTensor(x[:, :, None, :]) # (B, N, 1, D) + y_j = LazyTensor(y[:, None, :, :]) # (B, 1, M, D) + h_j = LazyTensor(h_y[:, None, :, None]) # (B, 1, M, 1) + + # Cost matrix: + if p == 2: # Halved, squared Euclidean distance + C_ij = ((x_i - y_j) ** 2).sum(-1) / 2 # (B, N, M, 1) + + elif p == 1: # Simple Euclidean distance + C_ij = ((x_i - y_j) ** 2).sum(-1).sqrt() # (B, N, M, 1) + + else: + raise NotImplementedError() + + # KeOps log-sum-exp reduction over the "M" dimension: + smin = (h_j - C_ij * torch.Tensor([1 / eps]).type_as(x)).logsumexp(2).view(B, -1) + + return -eps * smin + + +def lse_lazytensor(p, D, batchdims=(1,)): + """This implementation is currently disabled.""" + + x_i = Vi(0, D) + y_j = Vj(1, D) + f_j = Vj(2, 1) + epsinv = Pm(3, 1) + + x_i.batchdims = batchdims + y_j.batchdims = batchdims + f_j.batchdims = batchdims + epsinv.batchdims = batchdims + + if p == 2: + D_ij = ((x_i - y_j) ** 2).sum(-1) / 2 + elif p == 1: + D_ij = ((x_i - y_j) ** 2).sum(-1).sqrt() + + smin = (f_j - epsinv * D_ij).logsumexp(2) + return smin + + +# Low-level KeOps formulas for the ground cost: +cost_formulas = { + 1: "Norm2(X-Y)", + 2: "(SqDist(X,Y) / IntCst(2))", +} + + +def lse_genred(cost, D, dtype="float32"): + """Legacy "Genred" implementation, with low-level KeOps formulas.""" + + log_conv = generic_logsumexp( + "( B - (P * " + cost + " ) )", + "A = Vi(1)", + f"X = Vi({D})", + f"Y = Vj({D})", + "B = Vj(1)", + "P = Pm(1)", + # dtype=dtype, + ) + return log_conv + + +def softmin_online(eps, C_xy, h_y, log_conv=None): + x, y = C_xy + # KeOps is pretty picky on the input shapes... + batch = x.dim() > 2 + B = x.shape[0] + h = h_y.view(B, -1, 1) if batch else h_y.view(-1, 1) + + out = -eps * log_conv(x, y, h, torch.Tensor([1 / eps]).type_as(x)) + + return out.view(B, -1) if batch else out.view(1, -1) + + +def sinkhorn_online( + a, + x, + b, + y, + p=2, + blur=0.05, + reach=None, + diameter=None, + scaling=0.5, + cost=None, + debias=True, + potentials=False, + **kwargs, +): + + B, N, D = x.shape + B, M, _ = y.shape + + if cost is None and B > 1: + if True: + # raise ValueError("Not expected in this benchmark!") + softmin = partial(softmin_online_lazytensor, p=p) + else: + my_lse = lse_lazytensor(p, D, batchdims=(B,)) + softmin = partial(softmin_online, log_conv=my_lse) + + else: + if B > 1: + raise ValueError( + "Custom cost functions are not yet supported with batches." "" + ) + + x = x.squeeze(0) # (1, N, D) -> (N, D) + y = y.squeeze(0) # (1, M, D) -> (M, D) + + if cost is None: + cost = cost_formulas[p] + + my_lse = lse_genred(cost, D, dtype=str(x.dtype)[6:]) + softmin = partial(softmin_online, log_conv=my_lse) + + # The "cost matrices" are implicitly encoded in the point clouds, + # and re-computed on-the-fly: + C_xx, C_yy = ((x, x.detach()), (y, y.detach())) if debias else (None, None) + C_xy, C_yx = ((x, y.detach()), (y, x.detach())) + + diameter, eps, eps_list, rho = scaling_parameters( + x, y, p, blur, reach, diameter, scaling + ) + + f_aa, g_bb, g_ab, f_ba = sinkhorn_loop( + softmin, + log_weights(a), + log_weights(b), + C_xx, + C_yy, + C_xy, + C_yx, + eps_list, + rho, + debias=debias, + ) + + return sinkhorn_cost( + eps, + rho, + a, + b, + f_aa, + g_bb, + g_ab, + f_ba, + batch=True, + debias=debias, + potentials=potentials, + ) + + +# ============================================================================== +# backend == "multiscale" +# ============================================================================== + + +def keops_lse(cost, D, dtype="float32"): + log_conv = generic_logsumexp( + "( B - (P * " + cost + " ) )", + "A = Vi(1)", + "X = Vi({})".format(D), + "Y = Vj({})".format(D), + "B = Vj(1)", + "P = Pm(1)", + # dtype=dtype, + ) + return log_conv + + +def softmin_multiscale(eps, C_xy, f_y, log_conv=None): + x, y, ranges_x, ranges_y, ranges_xy = C_xy + # KeOps is pretty picky on the input shapes... + return -eps * log_conv( + x, y, f_y.view(-1, 1), torch.Tensor([1 / eps]).type_as(x), ranges=ranges_xy + ).view(-1) + + +def clusterize(a, x, scale=None, labels=None): + """ + Performs a simple 'voxelgrid' clustering on the input measure, + putting points into cubic bins of size 'scale' = σ_c. + The weights are summed, and the centroid position is that of the bin's center of mass. + Most importantly, the "fine" lists of weights and points are *sorted* + so that clusters are *contiguous in memory*: this allows us to perform + kernel truncation efficiently on the GPU. + + If + [a_c, a], [x_c, x], [x_ranges] = clusterize(a, x, σ_c), + then + a_c[k], x_c[k] correspond to + a[x_ranges[k,0]:x_ranges[k,1]], x[x_ranges[k,0]:x_ranges[k,1],:] + """ + perm = None # did we sort the point cloud at some point? Here's the permutation. + + if ( + labels is None and scale is None + ): # No clustering, single-scale Sinkhorn on the way... + return [a], [x], [] + + else: # As of today, only two-scale Sinkhorn is implemented: + # Compute simple (voxel-like) class labels: + x_lab = grid_cluster(x, scale) if labels is None else labels + # Compute centroids and weights: + ranges_x, x_c, a_c = cluster_ranges_centroids(x, x_lab, weights=a) + # Make clusters contiguous in memory: + x_labels, perm = torch.sort(x_lab.view(-1)) + a, x = a[perm], x[perm] + + # N.B.: the lines above were return to replace a call to + # 'sort_clusters' which does not return the permutation, + # an information that is needed to de-permute the dual potentials + # if they are required by the user. + # (a, x), x_labels = sort_clusters( (a,x), x_lab) + + return [a_c, a], [x_c, x], [ranges_x], perm + + +def kernel_truncation( + C_xy, C_yx, C_xy_, C_yx_, f_ba, g_ab, eps, truncate=None, cost=None, verbose=False +): + """Prunes out useless parts of the (block-sparse) cost matrices for finer scales. + + This is where our approximation takes place. + To be mathematically rigorous, we should make several coarse-to-fine passes, + making sure that we're not forgetting anyone. A good reference here is + Bernhard Schmitzer's work: "Stabilized Sparse Scaling Algorithms for + Entropy Regularized Transport Problems, (2016)". + """ + if truncate is None: + return C_xy_, C_yx_ + else: + x, yd, ranges_x, ranges_y, _ = C_xy + y, xd, _, _, _ = C_yx + x_, yd_, ranges_x_, ranges_y_, _ = C_xy_ + y_, xd_, _, _, _ = C_yx_ + + with torch.no_grad(): + C = cost(x, y) + keep = f_ba.view(-1, 1) + g_ab.view(1, -1) > C - truncate * eps + ranges_xy_ = from_matrix(ranges_x, ranges_y, keep) + if verbose: + ks, Cs = keep.sum(), C.shape[0] * C.shape[1] + print( + "Keep {}/{} = {:2.1f}% of the coarse cost matrix.".format( + ks, Cs, 100 * float(ks) / Cs + ) + ) + + return (x_, yd_, ranges_x_, ranges_y_, ranges_xy_), ( + y_, + xd_, + ranges_y_, + ranges_x_, + swap_axes(ranges_xy_), + ) + + +def extrapolate_samples(f_ba, g_ab, eps, damping, C_xy, b_log, C_xy_, softmin=None): + yd = C_xy[1] # Source points (coarse) + x_ = C_xy_[0] # Target points (fine) + + C = ( + x_, + yd, + None, + None, + None, + ) # "Rectangular" cost matrix, don't bother with ranges + return damping * softmin(eps, C, (b_log + g_ab / eps).detach()) + + +def sinkhorn_multiscale( + a, + x, + b, + y, + p=2, + blur=0.05, + reach=None, + diameter=None, + scaling=0.5, + truncate=5, + cost=None, + cluster_scale=None, + debias=True, + potentials=False, + labels_x=None, + labels_y=None, + verbose=False, + **kwargs, +): + + N, D = x.shape + M, _ = y.shape + + if cost is None: + cost = cost_formulas[p], cost_routines[p] + cost_formula, cost_routine = cost[0], cost[1] + + softmin = partial( + softmin_multiscale, log_conv=keops_lse(cost_formula, D, dtype=str(x.dtype)[6:]) + ) + extrapolate = partial(extrapolate_samples, softmin=softmin) + + diameter, eps, eps_list, rho = scaling_parameters( + x, y, p, blur, reach, diameter, scaling + ) + + # Clusterize and sort our point clouds: + if cluster_scale is None: + cluster_scale = diameter / (np.sqrt(D) * 2000 ** (1 / D)) + [a_c, a], [x_c, x], [ranges_x], perm_x = clusterize( + a, x, scale=cluster_scale, labels=labels_x + ) + [b_c, b], [y_c, y], [ranges_y], perm_y = clusterize( + b, y, scale=cluster_scale, labels=labels_y + ) + + jumps = [len(eps_list) - 1] + for i, eps in enumerate(eps_list[2:]): + if cluster_scale ** p > eps: + jumps = [i + 1] + break + + if verbose: + print( + "{}x{} clusters, computed at scale = {:2.3f}".format( + len(x_c), len(y_c), cluster_scale + ) + ) + print( + "Successive scales : ", + ", ".join(["{:.3f}".format(x ** (1 / p)) for x in eps_list]), + ) + if jumps[0] >= len(eps_list) - 1: + print("Extrapolate from coarse to fine after the last iteration.") + else: + print( + "Jump from coarse to fine between indices {} (σ={:2.3f}) and {} (σ={:2.3f}).".format( + jumps[0], + eps_list[jumps[0]] ** (1 / p), + jumps[0] + 1, + eps_list[jumps[0] + 1] ** (1 / p), + ) + ) + + # The input measures are stored at two levels: coarse and fine + a_logs = [log_weights(a_c), log_weights(a)] + b_logs = [log_weights(b_c), log_weights(b)] + + # We do the same [ coarse, fine ] decomposition for "cost matrices", + # which are implicitely encoded as point clouds + # + integer summation ranges, and re-computed on-the-fly: + C_xxs = ( + [ + (x_c, x_c.detach(), ranges_x, ranges_x, None), + (x, x.detach(), None, None, None), + ] + if debias + else None + ) + C_yys = ( + [ + (y_c, y_c.detach(), ranges_y, ranges_y, None), + (y, y.detach(), None, None, None), + ] + if debias + else None + ) + C_xys = [ + (x_c, y_c.detach(), ranges_x, ranges_y, None), + (x, y.detach(), None, None, None), + ] + C_yxs = [ + (y_c, x_c.detach(), ranges_y, ranges_x, None), + (y, x.detach(), None, None, None), + ] + + f_aa, g_bb, g_ab, f_ba = sinkhorn_loop( + softmin, + a_logs, + b_logs, + C_xxs, + C_yys, + C_xys, + C_yxs, + eps_list, + rho, + jumps=jumps, + cost=cost_routine, + kernel_truncation=partial(kernel_truncation, verbose=verbose), + truncate=truncate, + extrapolate=extrapolate, + debias=debias, + ) + + cost = sinkhorn_cost( + eps, rho, a, b, f_aa, g_bb, g_ab, f_ba, debias=debias, potentials=potentials + ) + + if potentials: # we should de-sort the vectors of potential values + F_x, G_y = cost + f_x, g_y = F_x.clone(), G_y.clone() + f_x[perm_x], g_y[perm_y] = F_x, G_y + return f_x, g_y + else: + return cost diff --git a/lib/python3.10/site-packages/geomloss/utils.py b/lib/python3.10/site-packages/geomloss/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..667c03c95678af20a6229d712a386d9a41770efe --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/utils.py @@ -0,0 +1,281 @@ +import numpy as np +import torch +from torch.nn.functional import conv1d, avg_pool2d, avg_pool3d, interpolate + + +try: # Import the keops library, www.kernel-operations.io + from pykeops.torch import LazyTensor + + keops_available = True +except: + keops_available = False + + +def scal(a, f, batch=False): + if batch: + B = a.shape[0] + return (a.reshape(B, -1) * f.reshape(B, -1)).sum(1) + else: + return torch.dot(a.reshape(-1), f.reshape(-1)) + + +####################################### +# On point clouds +####################################### + + +def squared_distances(x, y, use_keops=False): + + if use_keops and keops_available: + if x.dim() == 2: + x_i = LazyTensor(x[:, None, :]) # (N,1,D) + y_j = LazyTensor(y[None, :, :]) # (1,M,D) + elif x.dim() == 3: # Batch computation + x_i = LazyTensor(x[:, :, None, :]) # (B,N,1,D) + y_j = LazyTensor(y[:, None, :, :]) # (B,1,M,D) + else: + print("x.shape : ", x.shape) + raise ValueError("Incorrect number of dimensions") + + return ((x_i - y_j) ** 2).sum(-1) + + else: + if x.dim() == 2: + D_xx = (x * x).sum(-1).unsqueeze(1) # (N,1) + D_xy = torch.matmul(x, y.permute(1, 0)) # (N,D) @ (D,M) = (N,M) + D_yy = (y * y).sum(-1).unsqueeze(0) # (1,M) + elif x.dim() == 3: # Batch computation + D_xx = (x * x).sum(-1).unsqueeze(2) # (B,N,1) + D_xy = torch.matmul(x, y.permute(0, 2, 1)) # (B,N,D) @ (B,D,M) = (B,N,M) + D_yy = (y * y).sum(-1).unsqueeze(1) # (B,1,M) + else: + print("x.shape : ", x.shape) + raise ValueError("Incorrect number of dimensions") + + return D_xx - 2 * D_xy + D_yy + + +def distances(x, y, use_keops=False): + if use_keops: + return squared_distances(x, y, use_keops=use_keops).sqrt() + + else: + return torch.sqrt(torch.clamp_min(squared_distances(x, y), 1e-8)) + + +####################################### +# On grids +####################################### + + +BATCH, CHANNEL, HEIGHT, WIDTH, DEPTH = 0, 1, 2, 3, 4 + + +def dimension(I): + """Returns 2 if we are working with 2D images and 3 for volumes.""" + return I.dim() - 2 + + +subsample = { + 2: (lambda x: 4 * avg_pool2d(x, 2)), + 3: (lambda x: 8 * avg_pool3d(x, 2)), +} + +upsample_mode = { + 2: "bilinear", + 3: "trilinear", +} + + +def pyramid(I): + D = dimension(I) + I_s = [I] + + for i in range(int(np.log2(I.shape[HEIGHT]))): + I = subsample[D](I) + I_s.append(I) + + I_s.reverse() + return I_s + + +def upsample(I): + D = dimension(I) + return interpolate(I, scale_factor=2, mode=upsample_mode[D], align_corners=False) + + +def log_dens(α): + α_log = α.log() + α_log[α <= 0] = -10000.0 + return α_log + + +######################## +# "Hard" C-transform: +# + + +def C_transform(G, tau=1, p=2): + """ + Computes the forward C-transform of an array G of shape: + - (Batch, Nx) in 1D + - (Batch, Nx, Ny) in 2D + - (Batch, Nx, Ny, Nz) in 3D + + i.e. + F(x_i) <- max_j [G(x_j) - C(x_i, x_j)] + + with: + C(x,y) = |x-y|^p / (p * tau) + + In this first demo, we assume that: + - We are working with square images: Nx = Ny = Nz = N. + - p = 1 or 2 (Manhattan or Euclidean distance). + - Pixels have unit length in all dimensions. + """ + D = G.ndim - 1 # D = 1, 2 or 3 + B, N = G.shape[0], G.shape[1] + + x = torch.arange(N).type_as(G) # [0, ..., N-1], on the same device as G. + if p == 1: + x = x / tau + if p == 2: + x = x / np.sqrt(2 * tau) + else: + raise NotImplementedError() + + if not keops_available: + raise ImportError("This routine depends on the pykeops library.") + + def lines(g): + g = g.contiguous() # Make sure that g is not "transposed" implicitely, + # but stored as a contiguous array of numbers. + + g_j = LazyTensor(g.view(-1, 1, N, 1)) + x_i = LazyTensor(x.view(1, N, 1, 1)) + x_j = LazyTensor(x.view(1, 1, N, 1)) + + if p == 1: + Cg_ij = g_j - (x_i - x_j).abs() # (B * N, N, N, 1) + elif p == 2: + Cg_ij = g_j - (x_i - x_j) ** 2 # (B * N, N, N, 1) + + f_i = Cg_ij.max(dim=2) # (B * N, N, 1) + + if D == 1: + return f_i.view(B, N) + elif D == 2: + return f_i.view(B, N, N) + elif D == 3: + return f_i.view(B, N, N, N) + + if D == 1: + G = lines(G) + + if D == 2: + G = lines(G) # Act on lines + G = lines(G.permute([0, 2, 1])).permute([0, 2, 1]) # Act on columns + + elif D == 3: + G = lines(G) # Act on dim 4 + G = lines(G.permute([0, 1, 3, 2])).permute([0, 1, 3, 2]) # Act on dim 3 + G = lines(G.permute([0, 3, 2, 1])).permute([0, 3, 2, 1]) # Act on dim 2 + + return G + + +######################## +# "Soft" C-transform: +# + + +def softmin_grid(eps, C_xy, h_y): + r"""Soft-C-transform, implemented using seperable KeOps operations. + + This routine implements the (soft-)C-transform + between dual vectors, which is the core computation for + Auction- and Sinkhorn-like optimal transport solvers. + + If `eps` is a float number, `C_xy` is a tuple of axes dimensions + and `h_y` encodes a dual potential :math:`h_j` that is supported by the 1D/2D/3D grid + points :math:`y_j`'s, then `softmin_tensorized(eps, C_xy, h_y)` returns a dual potential + `f` for ":math:`f_i`", supported by the :math:`x_i`'s, that is equal to: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \exp + \big[ h_j - C(x_i, y_j) / \varepsilon \big]~. + + For more detail, see e.g. Section 3.3 and Eq. (3.186) in Jean Feydy's PhD thesis. + + Args: + eps (float, positive): Temperature :math:`\varepsilon` for the Gibbs kernel + :math:`K_{i,j} = \exp(-C(x_i, y_j) / \varepsilon)`. + + C_xy (): Encodes the implicit cost matrix :math:`C(x_i,y_j)`. + + h_y ((B, Nx), (B, Nx, Ny) or (B, Nx, Ny, Nz) Tensor): + Grid of logarithmic "dual" values, with a batch dimension. + Most often, this image will be computed as `h_y = b_log + g_j / eps`, + where `b_log` is an array of log-weights :math:`\log(\beta_j)` + for the :math:`y_j`'s and :math:`g_j` is a dual variable + in the Sinkhorn algorithm, so that: + + .. math:: + f_i \gets - \varepsilon \log \sum_{j=1}^{\text{M}} \beta_j + \exp \tfrac{1}{\varepsilon} \big[ g_j - C(x_i, y_j) \big]~. + + Returns: + (B, Nx), (B, Nx, Ny) or (B, Nx, Ny, Nz) Tensor: Dual potential `f` of values + :math:`f_i`, supported by the points :math:`x_i`. + """ + D = dimension(h_y) + B, K, N = h_y.shape[BATCH], h_y.shape[CHANNEL], h_y.shape[WIDTH] + + if not keops_available: + raise ImportError("This routine depends on the pykeops library.") + + x = torch.arange(N).type_as(h_y) / N + p = C_xy + if p == 1: + x = x / eps + elif p == 2: + x = x / np.sqrt(2 * eps) + else: + raise NotImplementedError() + + def softmin(a_log): + a_log = a_log.contiguous() + # print(a_log.shape) + a_log_j = LazyTensor(a_log.view(-1, 1, N, 1)) + x_i = LazyTensor(x.view(1, N, 1, 1)) + x_j = LazyTensor(x.view(1, 1, N, 1)) + + if p == 1: + kA_log_ij = a_log_j - (x_i - x_j).abs() # (B * N, N, N, 1) + elif p == 2: + kA_log_ij = a_log_j - (x_i - x_j) ** 2 # (B * N, N, N, 1) + + # kA_log_ij = (x_i - x_j)**2 - g_j + + # print(kA_log_ij) + kA_log = kA_log_ij.logsumexp(dim=2) # (B * N, N, 1) + + if D == 2: + return kA_log.view(B, K, N, N) + elif D == 3: + return kA_log.view(B, K, N, N, N) + + if D == 2: + h_y = softmin(h_y) # Act on lines + h_y = softmin(h_y.permute([0, 1, 3, 2])).permute([0, 1, 3, 2]) # Act on columns + + elif D == 3: + h_y = softmin(h_y) # Act on dim 4 + h_y = softmin(h_y.permute([0, 1, 2, 4, 3])).permute( + [0, 1, 2, 4, 3] + ) # Act on dim 3 + h_y = softmin(h_y.permute([0, 1, 4, 3, 2])).permute( + [0, 1, 4, 3, 2] + ) # Act on dim 2 + + return -eps * h_y diff --git a/lib/python3.10/site-packages/geomloss/wasserstein_barycenter_images.py b/lib/python3.10/site-packages/geomloss/wasserstein_barycenter_images.py new file mode 100644 index 0000000000000000000000000000000000000000..f45fd2c96447de1323e3aa4c17912bf4fb3c022f --- /dev/null +++ b/lib/python3.10/site-packages/geomloss/wasserstein_barycenter_images.py @@ -0,0 +1,96 @@ +import torch +from .utils import log_dens, pyramid, upsample +from .utils import softmin_grid as softmin + + +def barycenter_iteration(f_k, g_k, d_log, eps, p, ak_log, w_k): + + # Sinkhorn "pseudo-step" - from the measures to the barycenter: + ft_k = softmin(eps, p, ak_log + g_k / eps) / eps # (B,K,n,n) + # Update the barycenter: + # (B,1,n,n) = (B,1,n,n) - (B,K,n,n) @ (B,K,1,1) + bar_log = d_log - (ft_k * w_k[:, :, None, None]).sum(1, keepdim=True) + + # Symmetric Sinkhorn updates: + # From the measures to the barycenter: + ft_k = softmin(eps, p, ak_log + g_k / eps) # (B,K,n,n) + # From the barycenter to the measures: + gt_k = softmin(eps, p, bar_log + f_k / eps) # (B,K,n,n) + f_k = (f_k + ft_k) / 2 + g_k = (g_k + gt_k) / 2 + + # Sinkhorn "pseudo-step" - from the measures to the barycenter: + ft_k = softmin(eps, p, ak_log + g_k / eps) / eps + # Update the barycenter: + # (B,1,n,n) = (B,1,n,n) - (B,K,n,n) @ (B,K,1,1) + bar_log = d_log - (ft_k * w_k[:, :, None, None]).sum(1, keepdim=True) + + # Update the de-biasing measure: + # (B,1,n,n) = (B,1,n,n) + (B,1,n,n) + (B,1,n,n) + d_log = 0.5 * (d_log + bar_log + softmin(eps, p, d_log) / eps) + + return f_k, g_k, d_log, bar_log + + +def ImagesBarycenter( + measures, weights, blur=0, p=2, scaling_N=10, backward_iterations=5 +): + + a_k = measures # Densities, (B,K,N,N) + w_k = weights # Barycentric weights, (B,K) + + # Default precision settings: blur = pixel size. + if blur == 0: + blur = 1 / measures.shape[-1] + + with torch.set_grad_enabled(backward_iterations == 0): + + # Initialize the barycenter as a pointwise linear combination: + bar = (a_k * w_k[:, :, None, None]).sum(1) # (B,K,N,N) @ (B,K,1,1) -> (B,N,N) + + # Pre-compute a multiscale decomposition (=QuadTree) + # of the input measures, stored as logarithms + ak_s = pyramid(a_k)[1:] # We remove the 1x1 image, keep the 2x2, 4x4... + ak_log_s = list(map(log_dens, ak_s)) # The code below relies on log-sum-exps + + # Initialize the blur scale at 1, i.e. the full image length: + sigma = 1 # sigma = blur scale + eps = sigma ** p # eps = temperature + + # Initialize the dual variables + f_k, g_k = softmin(eps, p, ak_log_s[0]), softmin(eps, p, ak_log_s[0]) + + # Logarithm of the debiasing term: + d_log = torch.ones_like(ak_log_s[0]).sum(dim=1, keepdim=True) # (B,1,2,2) + d_log = d_log - d_log.logsumexp( + [2, 3], keepdim=True + ) # Normalize each 2x2 image + + # Multiscale descent, with eps-scaling: + # We iterate over sub-sampled images of shape nxn = 2x2, 4x4, ..., NxN + for n, ak_log in enumerate(ak_log_s): + for _ in range(scaling_N): # Number of steps per scale + # Update the temperature: + eps = sigma ** p + + f_k, g_k, d_log, bar_log = barycenter_iteration( + f_k, g_k, d_log, eps, p, ak_log, w_k + ) + + # Decrease the kernel radius, making sure that + # sigma is divided by two at every scale until we reach + # the target value, "blur": + sigma = max(sigma * (2 ** (-1 / scaling_N)), blur) + + if n + 1 < len(ak_s): # Re-fine the maps, if needed + f_k = upsample(f_k) + g_k = upsample(g_k) + d_log = upsample(d_log) + + if (measures.requires_grad or weights.requires_grad) and backward_iterations > 0: + for _ in range(backward_iterations): + f_k, g_k, d_log, bar_log = barycenter_iteration( + f_k, g_k, d_log, eps, p, ak_log, w_k + ) + + return bar_log.exp() diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..94a9ed024d3859793618152ea559a168bbcbb5e2 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING.LESSER b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING.LESSER new file mode 100644 index 0000000000000000000000000000000000000000..65c5ca88a67c30becee01c5a8816d964b03862f9 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/INSTALLER b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/METADATA b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..66753dcaaf34b63cbf609e61af86c3f28ea848ff --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/METADATA @@ -0,0 +1,74 @@ +Metadata-Version: 2.2 +Name: gmpy2 +Version: 2.2.1 +Summary: gmpy2 interface to GMP, MPFR, and MPC for Python 3.7+ +Author: Case Van Horsen +Maintainer-email: Case Van Horsen +License: LGPL-3.0+ +Project-URL: Homepage, https://github.com/aleaxit/gmpy +Keywords: gmp,mpfr,mpc,multiple-precision,arbitrary-precision,precision,bignum +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) +Classifier: Natural Language :: English +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: C +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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 :: Implementation :: CPython +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: COPYING +License-File: COPYING.LESSER +Provides-Extra: docs +Requires-Dist: sphinx>=4; extra == "docs" +Requires-Dist: sphinx-rtd-theme>=1; extra == "docs" +Provides-Extra: tests +Requires-Dist: pytest; extra == "tests" +Requires-Dist: hypothesis; extra == "tests" +Requires-Dist: cython; extra == "tests" +Requires-Dist: mpmath; extra == "tests" +Requires-Dist: setuptools; extra == "tests" + +gmpy2 is an optimized, C-coded Python extension module that supports fast +multiple-precision arithmetic. gmpy2 is based on the original gmpy module. +gmpy2 adds support for correctly rounded multiple-precision real arithmetic +(using the MPFR library) and complex arithmetic (using the MPC library). + +Version 2.2 +----------- + +gmpy2 2.2.1 +----------- + +* Bug fix: use C int instead of C char for some internal code. Issue +* Bug fix: add xmpz.bit_count method. + +gmpy2 2.2.0 +----------- + +gmpy2 2.2.0 is now available with support for Python 3.7 to 3.13. + +* Support for thread-safe contexts and context methods has been improved. +* Interoperability with Cython extensions has been updated. +* Extensive improvements have been made to the build and testing processes. +* Many bug fixes. +* Extensive documentation cleanup. + +Availability +------------ + +gmpy2 is available at https://pypi.python.org/pypi/gmpy2/ + +Documentation is available at https://gmpy2.readthedocs.io/en/latest/ diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/RECORD b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..35b33e094fca32c6baa82a6b952c61e1f5a558ac --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/RECORD @@ -0,0 +1,15 @@ +gmpy2-2.2.1.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147 +gmpy2-2.2.1.dist-info/COPYING.LESSER,sha256=2n6rt7r999OuXp8iOqW9we7ORaxWncIbOwN1ILRGR2g,7651 +gmpy2-2.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +gmpy2-2.2.1.dist-info/METADATA,sha256=vGl-fRfZCGCfffmdynNIH6UZeDvak3qB2scGOUQABe0,2838 +gmpy2-2.2.1.dist-info/RECORD,, +gmpy2-2.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +gmpy2-2.2.1.dist-info/WHEEL,sha256=aJ4RvyGkNe3spInijG_tdeo2KZhSBTVwmh5LQ523pbk,104 +gmpy2-2.2.1.dist-info/direct_url.json,sha256=uYJGU3xJQ4Tvt3uK4I9GNznpcxzj30IV86Rj4YYvHWM,65 +gmpy2-2.2.1.dist-info/top_level.txt,sha256=Sm6GYEILtBPk_gjz0sngTjx5Jmn4ebtxebdkQVOJgRQ,6 +gmpy2/__init__.pxd,sha256=3p7VoE0x5rWxOB14O8pCEOClR6A33VQ-1mywaNeOflM,27 +gmpy2/__init__.py,sha256=PU8hoOnW0yyTXj0570viOpp9DqVjROu7C43KT1ZR6KI,412 +gmpy2/__pycache__/__init__.cpython-310.pyc,, +gmpy2/gmpy2.cpython-310-x86_64-linux-gnu.so,sha256=I485MuLLIKSsfD2Ajwsnn5InBUYMofknaWg9ht0AWAM,671224 +gmpy2/gmpy2.h,sha256=wV-kCcH0m7f_HESBRZxywDeaC7NaYkkd68E8hnNMVKU,18741 +gmpy2/gmpy2.pxd,sha256=82qHB21P0I5k8RvJFfzvtmo-FySLjAn2cPB3AjleuOI,4681 diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/REQUESTED b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/WHEEL b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..94856657663aef513dc1fcecdf40a37eb27b1551 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: false +Tag: cp310-cp310-linux_x86_64 + diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/direct_url.json b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..e33754448dc839d62c6c723c0abff4dd34ffa62d --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///croot/gmpy2_1738085463648/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/top_level.txt b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..1777652f24915a4fe702f86bb5d763e10588c47e --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2-2.2.1.dist-info/top_level.txt @@ -0,0 +1 @@ +gmpy2 diff --git a/lib/python3.10/site-packages/gmpy2/__init__.pxd b/lib/python3.10/site-packages/gmpy2/__init__.pxd new file mode 100644 index 0000000000000000000000000000000000000000..3fcc1186a1d0b003e20d4f972184934abd053606 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2/__init__.pxd @@ -0,0 +1 @@ +from gmpy2.gmpy2 cimport * diff --git a/lib/python3.10/site-packages/gmpy2/__init__.py b/lib/python3.10/site-packages/gmpy2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e07a3b7c885c1c7b9357549c890eda13a21db771 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2/__init__.py @@ -0,0 +1,10 @@ +from .gmpy2 import * +from .gmpy2 import __version__ +# Internal variables/functions are not imported by * above. +# These are used by some python level functions and are needed +# at the top level. +# Use try...except to for static builds were _C_API is not available. +try: + from .gmpy2 import _C_API, _mpmath_normalize, _mpmath_create +except ImportError: + from .gmpy2 import _mpmath_normalize, _mpmath_create diff --git a/lib/python3.10/site-packages/gmpy2/gmpy2.h b/lib/python3.10/site-packages/gmpy2/gmpy2.h new file mode 100644 index 0000000000000000000000000000000000000000..03a8443d6e2be99fc1c0e604cbd2005f75ab5a25 --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2/gmpy2.h @@ -0,0 +1,541 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * gmpy2.h * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Python interface to the GMP, MPFR, and MPC multiple precision * + * libraries. * + * * + * Copyright 2000 - 2009 Alex Martelli * + * * + * Copyright 2008 - 2024 Case Van Horsen * + * * + * This file is part of GMPY2. * + * * + * GMPY2 is free software: you can redistribute it and/or modify it under * + * the terms of the GNU Lesser General Public License as published by the * + * Free Software Foundation, either version 3 of the License, or (at your * + * option) any later version. * + * * + * GMPY2 is distributed in the hope that it will be useful, but WITHOUT * + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * + * License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with GMPY2; if not, see * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +/* + gmpy C API extension header file. + Part of Python's gmpy module since version 0.4 + + Created by Pearu Peterson , November 2000. + Edited by A. Martelli , December 2000. + Edited by Case Van Horsen , 2009, 2010, 2011. + + Version 1.02, February 2007. + Version 1.03, June 2008 + Version 1.04, June 2008 (no changes) + Version 1.05, February 2009 (support MPIR) + Version 1.20, January 2010 (remove obsolete MS hacks) casevh + Version 2.00, April 2010 (change to gmpy2) casevh + October 2010 (added Py_hash_t) casevh + December 2010 (added mpfr, mpc) casevh + January 2011 (add Pygmpy_context) casevh + April 2011 (split into multiple files) casevh + Version 2.10 August 2014 (reflect major rewrite during 2013/2014) casevh + */ + +#ifndef Py_GMPYMODULE_H +#define Py_GMPYMODULE_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Structure of gmpy2.h + * + * Revised 17-APR-2017 casevh + * + * 1. Checks for specific Python versions. + * 2. Include headers for GMP/MPIR, MPFR, and MPC. + * 3. Define public C-API. + * 1. Define gmpy2 types. + * 2. Define the public API. + * + */ + +/* Check for minimum Python version requirements. */ + +#if PY_VERSION_HEX < 0x03070000 +# error "GMPY2 requires Python 3.7 or later." +#endif + +/* Include headers for GMP, MPFR, and MPC. */ + +#include +#include +#include + +/* Check MPFR and MPC versions. */ + +/* gmpy2 supports the two most recent major versions of MPFR and MPC. + * + * As of gmpy2 2.2.0, the code assumes the MPFR version is at least 4.1.0 + * and new features included in 4.2.0 are guarded by #ifdef MPFR_420. + * For MPC, the minumum version is assumed to 1.2.1 and new features + * included in 1.3.0 are guarded by #ifdef MPC_130. + * + */ + +#if (!defined(MPFR_VERSION) || (MPFR_VERSION < MPFR_VERSION_NUM(4,1,0))) +# error "GMPY2 requires MPFR 4.1.0 or later." +#endif + +#if (defined(MPFR_VERSION) && (MPFR_VERSION >= MPFR_VERSION_NUM(4,2,0))) +# define MPFR_420 +#endif + +#if (!defined(MPC_VERSION) || (MPC_VERSION < MPC_VERSION_NUM(1,2,1))) +# error "GMPY2 requires MPC 1.2.1 or later." +#endif + +#if (defined(MPC_VERSION) && (MPC_VERSION == MPC_VERSION_NUM(1,3,0))) +# define MPC_130 +#endif + +/* GMPY2 Public API */ + +/* Types + * MPZ_Object + * XMPZ_Object (mutable version of MPZ_Object) + * MPQ_Object + * XMPQ_Object (mutable version of MPQ_Object) + * MPFR_Object + * XMPFR_Object (mutable version of MPFR_Object) + * MPC_Object + * XMPC_Object (mutable version of MPC_Object) + * CTXT_Object + * RandomState_Object + */ + +typedef struct { + PyObject_HEAD + mpz_t z; + Py_hash_t hash_cache; +} MPZ_Object; + +typedef struct { + PyObject_HEAD + mpz_t z; +} XMPZ_Object; + +typedef struct { + PyObject_HEAD + mpq_t q; + Py_hash_t hash_cache; +} MPQ_Object; + +typedef struct { + PyObject_HEAD + mpfr_t f; + Py_hash_t hash_cache; + int rc; +} MPFR_Object; + +typedef struct { + PyObject_HEAD + mpc_t c; + Py_hash_t hash_cache; + int rc; +} MPC_Object; + +typedef struct { + PyObject_HEAD + gmp_randstate_t state; +} RandomState_Object; + +typedef struct { + mpfr_prec_t mpfr_prec; /* current precision in bits, for MPFR */ + mpfr_rnd_t mpfr_round; /* current rounding mode for float (MPFR) */ + mpfr_exp_t emax; /* maximum exponent */ + mpfr_exp_t emin; /* minimum exponent */ + int subnormalize; /* if 1, subnormalization is performed */ + int underflow; /* did an underflow occur? */ + int overflow; /* did an overflow occur? */ + int inexact; /* was the result inexact? */ + int invalid; /* invalid operation (i.e. NaN)? */ + int erange; /* did a range error occur? */ + int divzero; /* divided by zero? */ + int traps; /* if 0, do not trap any exceptions */ + /* if not 0, then raise traps per bits above */ + mpfr_prec_t real_prec; /* current precision in bits, for Re(MPC) */ + mpfr_prec_t imag_prec; /* current precision in bits, for Im(MPC) */ + mpfr_rnd_t real_round; /* current rounding mode for Re(MPC) */ + mpfr_rnd_t imag_round; /* current rounding mode for Im(MPC) */ + int allow_complex; /* if 1, allow mpfr functions to return an mpc */ + int rational_division; /* if 1, mpz/mpz returns an mpq result */ + int allow_release_gil; /* if 1, allow mpz functions to release the GIL */ +} gmpy_context; + +typedef struct { + PyObject_HEAD + gmpy_context ctx; + PyObject *token; +} CTXT_Object; + +#define MPZ(obj) (((MPZ_Object*)(obj))->z) +#define MPQ(obj) (((MPQ_Object*)(obj))->q) +#define MPFR(obj) (((MPFR_Object*)(obj))->f) +#define MPC(obj) (((MPC_Object*)(obj))->c) + +/* Start of the C-API definitions */ + +#define MPZ_Type_NUM 0 +#define XMPZ_Type_NUM 1 +#define MPQ_Type_NUM 2 +#define XMPQ_Type_NUM 3 +#define MPFR_Type_NUM 4 +#define XMPFR_Type_NUM 5 +#define MPC_Type_NUM 6 +#define XMPC_Type_NUM 7 +#define CTXT_Type_NUM 8 +#define RandomState_Type_NUM 10 + +/* The following functions are found in gmpy2_cache. */ + +#define GMPy_MPZ_New_NUM 11 +#define GMPy_MPZ_New_RETURN MPZ_Object * +#define GMPy_MPZ_New_PROTO (CTXT_Object *context) + +#define GMPy_MPZ_NewInit_NUM 12 +#define GMPy_MPZ_NewInit_RETURN PyObject * +#define GMPy_MPZ_NewInit_PROTO (PyTypeObject *type, PyObject *args, PyObject *keywds) + +#define GMPy_MPZ_Dealloc_NUM 13 +#define GMPy_MPZ_Dealloc_RETURN void +#define GMPy_MPZ_Dealloc_PROTO (MPZ_Object *self) + +/* The following function is found in gmpy2_convert_gmp. */ + +#define GMPy_MPZ_ConvertArg_NUM 14 +#define GMPy_MPZ_ConvertArg_RETURN int +#define GMPy_MPZ_ConvertArg_PROTO (PyObject *arg, PyObject **ptr) + +/* The following functions are found in gmpy2_cache. */ + +#define GMPy_XMPZ_New_NUM 15 +#define GMPy_XMPZ_New_RETURN XMPZ_Object * +#define GMPy_XMPZ_New_PROTO (CTXT_Object *context) + +#define GMPy_XMPZ_NewInit_NUM 16 +#define GMPy_XMPZ_NewInit_RETURN PyObject * +#define GMPy_XMPZ_NewInit_PROTO (PyTypeObject *type, PyObject *args, PyObject *keywds) + +#define GMPy_XMPZ_Dealloc_NUM 17 +#define GMPy_XMPZ_Dealloc_RETURN void +#define GMPy_XMPZ_Dealloc_PROTO (XMPZ_Object *self) + +/* The following functions are found in gmpy2_cache. */ + +#define GMPy_MPQ_New_NUM 18 +#define GMPy_MPQ_New_RETURN MPQ_Object * +#define GMPy_MPQ_New_PROTO (CTXT_Object *context) + +#define GMPy_MPQ_NewInit_NUM 19 +#define GMPy_MPQ_NewInit_RETURN PyObject * +#define GMPy_MPQ_NewInit_PROTO (PyTypeObject *type, PyObject *args, PyObject *keywds) + +#define GMPy_MPQ_Dealloc_NUM 20 +#define GMPy_MPQ_Dealloc_RETURN void +#define GMPy_MPQ_Dealloc_PROTO (MPQ_Object *self) + +/* The following function is found in gmpy2_convert_gmp. */ + +#define GMPy_MPQ_ConvertArg_NUM 21 +#define GMPy_MPQ_ConvertArg_RETURN int +#define GMPy_MPQ_ConvertArg_PROTO (PyObject *arg, PyObject **ptr) + +/* The following functions are found in gmpy2_cache. */ + +#define GMPy_MPFR_New_NUM 22 +#define GMPy_MPFR_New_RETURN MPFR_Object * +#define GMPy_MPFR_New_PROTO (mpfr_prec_t bits, CTXT_Object *context) + +#define GMPy_MPFR_NewInit_NUM 23 +#define GMPy_MPFR_NewInit_RETURN PyObject * +#define GMPy_MPFR_NewInit_PROTO (PyTypeObject *type, PyObject *args, PyObject *keywds) + +#define GMPy_MPFR_Dealloc_NUM 24 +#define GMPy_MPFR_Dealloc_RETURN void +#define GMPy_MPFR_Dealloc_PROTO (MPFR_Object *self) + +/* The following function is found in gmpy2_convert_gmp. */ + +#define GMPy_MPFR_ConvertArg_NUM 25 +#define GMPy_MPFR_ConvertArg_RETURN int +#define GMPy_MPFR_ConvertArg_PROTO (PyObject *arg, PyObject **ptr) + +/* The following functions are found in gmpy2_cache. */ + +#define GMPy_MPC_New_NUM 26 +#define GMPy_MPC_New_RETURN MPC_Object * +#define GMPy_MPC_New_PROTO (mpfr_prec_t rprec, mpfr_prec_t iprec, CTXT_Object *context) + +#define GMPy_MPC_NewInit_NUM 27 +#define GMPy_MPC_NewInit_RETURN PyObject * +#define GMPy_MPC_NewInit_PROTO (PyTypeObject *type, PyObject *args, PyObject *keywds) + +#define GMPy_MPC_Dealloc_NUM 28 +#define GMPy_MPC_Dealloc_RETURN void +#define GMPy_MPC_Dealloc_PROTO (MPC_Object *self) + +/* The following function is found in gmpy2_convert_gmp. */ + +#define GMPy_MPC_ConvertArg_NUM 29 +#define GMPy_MPC_ConvertArg_RETURN int +#define GMPy_MPC_ConvertArg_PROTO (PyObject *arg, PyObject **ptr) + +/* Total number of C-API pointers. */ + +#define GMPy_API_pointers 30 + +/* End of C-API definitions. */ + +#ifdef GMPY2_MODULE + +#define PyString_1Char(obj) (PyUnicode_READY(obj) ? (Py_UCS4)0 : PyUnicode_READ_CHAR(obj, 0)) +#define PyStrOrUnicode_Check(op) (PyBytes_Check(op) || PyUnicode_Check(op)) + +#ifndef ABS +# define ABS(a) (((a) < 0) ? -(a) : (a)) +#endif + +#if defined(MS_WIN32) && defined(_MSC_VER) + /* so one won't need to link explicitly to gmp.lib...: */ +# pragma comment(lib,"gmp.lib") +# define USE_ALLOCA 1 +# define inline __inline +#endif + +#ifdef __GNUC__ +# define USE_ALLOCA 1 +#endif + +#ifndef alloca +# ifdef __GNUC__ +# define alloca __builtin_alloca +# else +# ifdef _MSC_VER +# include +# define alloca _alloca +# else +# if HAVE_ALLOCA_H +# include +# else + char *alloca (); +# endif +# endif +# endif +#endif + +#define ALLOC_THRESHOLD 8192 + +#define INDEX_ERROR(msg) PyErr_SetString(PyExc_IndexError, msg) +#define TYPE_ERROR(msg) PyErr_SetString(PyExc_TypeError, msg) +#define VALUE_ERROR(msg) PyErr_SetString(PyExc_ValueError, msg) +#define ZERO_ERROR(msg) PyErr_SetString(PyExc_ZeroDivisionError, msg) +#define SYSTEM_ERROR(msg) PyErr_SetString(PyExc_SystemError, msg) +#define OVERFLOW_ERROR(msg) PyErr_SetString(PyExc_OverflowError, msg) +#define RUNTIME_ERROR(msg) PyErr_SetString(PyExc_RuntimeError, msg) + +#define GMPY_DEFAULT -1 + +/* To prevent excessive memory usage, we don't want to save very large + * numbers in the cache. The default value specified in the options + * structure is 128 words (512 bytes on 32-bit platforms, 1024 bytes on + * 64-bit platforms). + */ +#define MAX_CACHE_LIMBS 16384 + +/* The maximum number of objects that can be saved in a cache is specified + * here. The default value is 100.*/ +#define MAX_CACHE 1000 + +#ifdef USE_ALLOCA +# define TEMP_ALLOC(B, S) \ + if(S < ALLOC_THRESHOLD) { \ + B = alloca(S); \ + } else { \ + if(!(B = malloc(S))) { \ + PyErr_NoMemory(); \ + return NULL; \ + } \ + } +# define TEMP_FREE(B, S) if(S >= ALLOC_THRESHOLD) free(B) +#else +# define TEMP_ALLOC(B, S) \ + if(!(B = malloc(S))) { \ + PyErr_NoMemory(); \ + return NULL; \ + } +# define TEMP_FREE(B, S) free(B) +#endif + +#ifndef Py_SIZE +# define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size) +#endif + +#ifndef Py_TYPE +# define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) +#endif + +/* Import a collection of general purpose macros. */ + +#include "gmpy2_macros.h" + +/* Import the files that complete the definition of the types defined above. */ + +#include "gmpy2_mpz.h" +#include "gmpy2_xmpz.h" +#include "gmpy2_mpq.h" +#include "gmpy2_mpfr.h" +#include "gmpy2_mpc.h" +#include "gmpy2_context.h" +#include "gmpy2_random.h" + +/* Import the header files that provide the various functions. */ + +/* Support object caching, creation, and deletion. */ + +#include "gmpy2_cache.h" + +/* Suport for miscellaneous functions (ie. version, license, etc.). */ + +#include "gmpy2_misc.h" + +/* Support conversion to/from binary format. */ + +#include "gmpy2_binary.h" + +/* Support for mpz/xmpz specific functions. */ + +#include "gmpy2_convert.h" +#include "gmpy2_convert_utils.h" +#include "gmpy2_convert_gmp.h" +#include "gmpy2_convert_mpfr.h" +#include "gmpy2_convert_mpc.h" + +#include "gmpy2_mpz_divmod.h" +#include "gmpy2_mpz_divmod2exp.h" +#include "gmpy2_mpz_pack.h" +#include "gmpy2_mpz_bitops.h" +#include "gmpy2_mpz_misc.h" + +#include "gmpy2_xmpz_inplace.h" +#include "gmpy2_xmpz_misc.h" +#include "gmpy2_xmpz_limbs.h" + +/* Support for mpq specific functions. */ + +#include "gmpy2_mpq_misc.h" + +/* Support for mpfr specific functions. */ + +#include "gmpy2_mpfr_misc.h" + +/* Support for mpc specific functions. */ + +#include "gmpy2_mpc_misc.h" + +/* Support Lucas sequences. */ + +#include "gmpy_mpz_lucas.h" + +/* Support probable-prime tests. */ + +#include "gmpy_mpz_prp.h" + +/* Support higher-level Python methods and functions; generally not + * specific to a single type. + */ + +#include "gmpy2_abs.h" +#include "gmpy2_add.h" +#include "gmpy2_divmod.h" +#include "gmpy2_floordiv.h" +#include "gmpy2_minus.h" +#include "gmpy2_mod.h" +#include "gmpy2_mul.h" +#include "gmpy2_plus.h" +#include "gmpy2_pow.h" +#include "gmpy2_sub.h" +#include "gmpy2_truediv.h" +#include "gmpy2_math.h" +#include "gmpy2_const.h" +#include "gmpy2_square.h" +#include "gmpy2_format.h" +#include "gmpy2_hash.h" +#include "gmpy2_fused.h" +#include "gmpy2_muldiv_2exp.h" +#include "gmpy2_predicate.h" +#include "gmpy2_sign.h" +#include "gmpy2_richcompare.h" +#include "gmpy2_cmp.h" + +#ifdef VECTOR +# include "gmpy2_vector.h" +#endif /* defined(VECTOR) */ + +#else /* defined(GMPY2_MODULE) */ + +/* This section is used for other C-coded modules that use gmpy2's API. */ + +static void **GMPy_C_API; + +#define MPZ_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[MPZ_Type_NUM]) +#define XMPZ_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[XMPZ_Type_NUM]) +#define MPQ_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[MPQ_Type_NUM]) +#define XMPQ_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[XMPQ_Type_NUM]) +#define MPFR_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[MPFR_Type_NUM]) +#define XMPFR_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[XMPFR_Type_NUM]) +#define MPC_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[MPC_Type_NUM]) +#define XMPC_Check(op) ((op)->ob_type == (PyTypeObject*)GMPy_C_API[XMPC_Type_NUM]) + +#define GMPy_MPZ_New (*(GMPy_MPZ_New_RETURN (*)GMPy_MPZ_New_PROTO) GMPy_C_API[GMPy_MPZ_New_NUM]) +#define GMPy_MPZ_NewInit (*(GMPy_MPZ_NewInit_RETURN (*)GMPy_MPZ_NewInit_PROTO) GMPy_C_API[GMPy_MPZ_NewInit_NUM]) +#define GMPy_MPZ_Dealloc (*(GMPy_MPZ_Dealloc_RETURN (*)GMPy_MPZ_Dealloc_PROTO) GMPy_C_API[GMPy_MPZ_Dealloc_NUM]) +#define GMPy_MPZ_ConvertArg (*(GMPy_MPZ_ConvertArg_RETURN (*)GMPy_MPZ_ConvertArg_PROTO) GMPy_C_API[GMPy_MPZ_ConvertArg_NUM]) + +#define GMPy_XMPZ_New (*(GMPy_XMPZ_New_RETURN (*)GMPy_XMPZ_New_PROTO) GMPy_C_API[GMPy_XMPZ_New_NUM]) +#define GMPy_XMPZ_NewInit (*(GMPy_XMPZ_NewInit_RETURN (*)GMPy_XMPZ_NewInit_PROTO) GMPy_C_API[GMPy_XMPZ_NewInit_NUM]) +#define GMPy_XMPZ_Dealloc (*(GMPy_XMPZ_Dealloc_RETURN (*)GMPy_XMPZ_Dealloc_PROTO) GMPy_C_API[GMPy_XMPZ_Dealloc_NUM]) + +#define GMPy_MPQ_New (*(GMPy_MPQ_New_RETURN (*)GMPy_MPQ_New_PROTO) GMPy_C_API[GMPy_MPQ_New_NUM]) +#define GMPy_MPQ_NewInit (*(GMPy_MPQ_NewInit_RETURN (*)GMPy_MPQ_NewInit_PROTO) GMPy_C_API[GMPy_MPQ_NewInit_NUM]) +#define GMPy_MPQ_Dealloc (*(GMPy_MPQ_Dealloc_RETURN (*)GMPy_MPQ_Dealloc_PROTO) GMPy_C_API[GMPy_MPQ_Dealloc_NUM]) +#define GMPy_MPQ_ConvertArg (*(GMPy_MPQ_ConvertArg_RETURN (*)GMPy_MPQ_ConvertArg_PROTO) GMPy_C_API[GMPy_MPQ_ConvertArg_NUM]) + +#define GMPy_MPFR_New (*(GMPy_MPFR_New_RETURN (*)GMPy_MPFR_New_PROTO) GMPy_C_API[GMPy_MPFR_New_NUM]) +#define GMPy_MPFR_NewInit (*(GMPy_MPFR_NewInit_RETURN (*)GMPy_MPFR_NewInit_PROTO) GMPy_C_API[GMPy_MPFR_NewInit_NUM]) +#define GMPy_MPFR_Dealloc (*(GMPy_MPFR_Dealloc_RETURN (*)GMPy_MPFR_Dealloc_PROTO) GMPy_C_API[GMPy_MPFR_Dealloc_NUM]) +#define GMPy_MPFR_ConvertArg (*(GMPy_MPFR_ConvertArg_RETURN (*)GMPy_MPFR_ConvertArg_PROTO) GMPy_C_API[GMPy_MPFR_ConvertArg_NUM]) + +#define GMPy_MPC_New (*(GMPy_MPC_New_RETURN (*)GMPy_MPC_New_PROTO) GMPy_C_API[GMPy_MPC_New_NUM]) +#define GMPy_MPC_NewInit (*(GMPy_MPC_NewInit_RETURN (*)GMPy_MPC_NewInit_PROTO) GMPy_C_API[GMPy_MPC_NewInit_NUM]) +#define GMPy_MPC_Dealloc (*(GMPy_MPC_Dealloc_RETURN (*)GMPy_MPC_Dealloc_PROTO) GMPy_C_API[GMPy_MPC_Dealloc_NUM]) +#define GMPy_MPC_ConvertArg (*(GMPy_MPC_ConvertArg_RETURN (*)GMPy_MPC_ConvertArg_PROTO) GMPy_C_API[GMPy_MPC_ConvertArg_NUM]) + +static int +import_gmpy2(void) +{ + GMPy_C_API = (void **)PyCapsule_Import("gmpy2._C_API", 0); + return (GMPy_C_API != NULL) ? 0 : -1; +} + +#endif /* defined(GMPY2_MODULE) */ + +#ifdef __cplusplus +} +#endif /* defined(__cplusplus */ +#endif /* !defined(Py_GMPYMODULE_H */ diff --git a/lib/python3.10/site-packages/gmpy2/gmpy2.pxd b/lib/python3.10/site-packages/gmpy2/gmpy2.pxd new file mode 100644 index 0000000000000000000000000000000000000000..8afd5d4b4058e89fd47e9be2ef92a3a81289d43c --- /dev/null +++ b/lib/python3.10/site-packages/gmpy2/gmpy2.pxd @@ -0,0 +1,170 @@ +cdef extern from "gmp.h": + # gmp integers + ctypedef long mp_limb_t + + ctypedef struct __mpz_struct: + int _mp_alloc + int _mp_size + mp_limb_t* _mp_d + + ctypedef __mpz_struct mpz_t[1] + ctypedef __mpz_struct *mpz_ptr + ctypedef const __mpz_struct *mpz_srcptr + + # gmp rationals + ctypedef struct __mpq_struct: + __mpz_struct _mp_num + __mpz_struct _mp_den + + ctypedef __mpq_struct mpq_t[1] + ctypedef __mpq_struct *mpq_ptr + ctypedef const __mpq_struct *mpq_srcptr + + void mpz_set(mpz_t rop, mpz_t op) + void mpq_set(mpq_ptr rop, mpq_srcptr op) + void mpq_set_num(mpq_t rational, mpz_t numerator) + void mpq_set_den(mpq_t rational, mpz_t denominator) + + +cdef extern from "mpfr.h": + # mpfr reals + ctypedef int mpfr_sign_t + ctypedef long mpfr_prec_t + ctypedef long mpfr_exp_t + + ctypedef struct __mpfr_struct: + mpfr_prec_t _mpfr_prec + mpfr_sign_t _mpfr_sign + mpfr_exp_t _mpfr_exp + mp_limb_t* _mpfr_d + + ctypedef __mpfr_struct mpfr_t[1] + ctypedef __mpfr_struct *mpfr_ptr + ctypedef const __mpfr_struct *mpfr_srcptr + + ctypedef enum mpfr_rnd_t: + MPFR_RNDN + MPFR_RNDZ + MPFR_RNDU + MPFR_RNDD + MPFR_RNDA + MPFR_RNDF + MPFR_RNDNA + + mpfr_prec_t mpfr_get_prec(mpfr_t x) + int mpfr_set(mpfr_t rop, mpfr_t op, mpfr_rnd_t rnd) + + +cdef extern from "mpc.h": + # mpc complexes + ctypedef struct __mpc_struct: + mpfr_t re + mpfr_t im + + ctypedef __mpc_struct mpc_t[1]; + ctypedef __mpc_struct *mpc_ptr; + ctypedef const __mpc_struct *mpc_srcptr; + ctypedef enum mpc_rnd_t: + MPC_RNDNN + MPC_RNDNZ + MPC_RNDNU + MPC_RNDND + MPC_RNDZN + MPC_RNDZZ + MPC_RNDZU + MPC_RNDZD + MPC_RNDUN + MPC_RNDUZ + MPC_RNDUU + MPC_RNDUD + MPC_RNDDN + MPC_RNDDZ + MPC_RNDDU + MPC_RNDDD + + mpfr_prec_t mpc_get_prec(mpc_srcptr x) + void mpc_get_prec2(mpfr_prec_t *pr, mpfr_prec_t *pi, mpc_srcptr x) + int mpc_set(mpc_ptr rop, mpc_srcptr op, mpc_rnd_t rnd) + int mpc_set_fr_fr(mpc_ptr rop, mpfr_srcptr rp, mpfr_srcptr ip, mpc_rnd_t rnd) + + +cdef extern from "gmpy2.h": + # Initialize the C-API + # This must be called before any other functions, but not to access + # the types. + cdef int import_gmpy2() except -1 + + # Object types + ctypedef class gmpy2.mpz [object MPZ_Object]: + cdef mpz_t z + ctypedef class gmpy2.mpq [object MPQ_Object]: + cdef mpq_t q + ctypedef class gmpy2.mpfr [object MPFR_Object]: + cdef mpfr_t f + cdef int rc + ctypedef class gmpy2.mpc [object MPC_Object]: + cdef mpc_t c + cdef int rc + + # Object creation + cdef mpz GMPy_MPZ_New(void *) + cdef mpq GMPy_MPQ_New(void *) + cdef mpfr GMPy_MPFR_New(mpfr_prec_t prec, void *) + cdef mpc GMPy_MPC_New(mpfr_prec_t rprec, mpfr_prec_t iprec, void *) + + # C field access + cdef mpz_t MPZ(mpz) + cdef mpq_t MPQ(mpq) + cdef mpfr_t MPFR(mpfr) + cdef mpc_t MPC(mpc) + + # Type check + cdef bint MPZ_Check(object) + cdef bint MPQ_Check(object) + cdef bint MPFR_Check(object) + cdef bint MPC_Check(object) + + +# Build a gmpy2 mpz from a gmp mpz +cdef inline mpz GMPy_MPZ_From_mpz(mpz_srcptr z): + cdef mpz res = GMPy_MPZ_New(NULL) + mpz_set(res.z, z) + return res + +# Build a gmpy2 mpq from a gmp mpq +cdef inline mpq GMPy_MPQ_From_mpq(mpq_srcptr q): + cdef mpq res = GMPy_MPQ_New(NULL) + mpq_set(res.q, q) + return res + +# Build a gmpy2 mpq from gmp mpz numerator and denominator +cdef inline mpq GMPy_MPQ_From_mpz(mpz_srcptr num, mpz_srcptr den): + cdef mpq res = GMPy_MPQ_New(NULL) + mpq_set_num(res.q, num) + mpq_set_den(res.q, den) + return res + +# Build a gmpy2 mpfr from a mpfr +cdef inline mpfr GMPy_MPFR_From_mpfr(mpfr_srcptr x): + cdef mpfr res = GMPy_MPFR_New(mpfr_get_prec(x), NULL) + mpfr_set(res.f, x, MPFR_RNDN) + return res + +# Build a gmpy2 mpc from a mpc +cdef inline mpc GMPy_MPC_From_mpc(mpc_srcptr c): + cdef mpfr_prec_t pr + cdef mpfr_prec_t pi + mpc_get_prec2(&pr, &pi, c) + cdef mpc res = GMPy_MPC_New(pr, pi, NULL) + mpc_set(res.c, c, MPC_RNDNN) + return res + +# Build a gmpy2 mpc from a real part mpfr and an imaginary part mpfr +cdef inline mpc GMPy_MPC_From_mpfr(mpfr_srcptr re, mpfr_srcptr im): + cdef mpc res = GMPy_MPC_New(mpfr_get_prec(re), mpfr_get_prec(im), NULL) + # We intentionally use MPFR funtions instead of MPC functions here + # in order not to add an unneeded dependency on MPC. It's probably + # faster too this way. + mpfr_set(res.c.re, re, MPFR_RNDN) + mpfr_set(res.c.im, im, MPFR_RNDN) + return res diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/INSTALLER b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/LICENSE b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..67589cbb8600ecbd6589f3374ccb724320c82617 --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright 2011 Sybren A. Stüvel + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/METADATA b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..76959d84154d83f823a4b66b2a21f52e48ec0a6d --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/METADATA @@ -0,0 +1,140 @@ +Metadata-Version: 2.3 +Name: rsa +Version: 4.9.1 +Summary: Pure-Python RSA implementation +License: Apache-2.0 +Author: Sybren A. Stüvel +Author-email: sybren@stuvel.eu +Requires-Python: >=3.6,<4 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +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 :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Security :: Cryptography +Requires-Dist: pyasn1 (>=0.1.3) +Project-URL: Homepage, https://stuvel.eu/rsa +Project-URL: Repository, https://github.com/sybrenstuvel/python-rsa +Description-Content-Type: text/markdown + +# Python-RSA has been archived + +Hi folks, + +I'm Sybren, one of the original authors and the maintainer of this project. +Unfortunately I don't have the time and brain space left to properly maintain +Python-RSA. As you can see from the lack of activity on the open issues, and the +lack of commits, that has been the case for a while now. + +As Python-RSA is included as a dependency in quite a few high-profile projects, +I don't feel comfortable handing over the project to someone else. It's just too +big of a risk. + +Thanks for having used this little library for so long, and in so many projects. +I truely didn't expect that when I started working on it. Also big thanks to all +the people helping out and improving the project. + +There are improvements that haven't made it into a new release. As I said, I +don't have the time and the brain space to really investigate and oversee the +security impact of all those changes. It's not a decision I've made lightly. + +So that's it. If you want to keep the project alive, please fork it. Give it the +love it deserves, investigate those yet-unreleased improvements, and have a +project that's then already better than how I left this one. + +Cheers, +Sybren + + +--------------------------------------------- + +# Pure Python RSA implementation + +[![PyPI](https://img.shields.io/pypi/v/rsa.svg)](https://pypi.org/project/rsa/) +[![Build Status](https://travis-ci.org/sybrenstuvel/python-rsa.svg?branch=master)](https://travis-ci.org/sybrenstuvel/python-rsa) +[![Coverage Status](https://coveralls.io/repos/github/sybrenstuvel/python-rsa/badge.svg?branch=master)](https://coveralls.io/github/sybrenstuvel/python-rsa?branch=master) +[![Code Climate](https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability)](https://codeclimate.com/github/codeclimate/codeclimate/maintainability) + +[Python-RSA](https://stuvel.eu/rsa) is a pure-Python RSA implementation. It supports +encryption and decryption, signing and verifying signatures, and key +generation according to PKCS#1 version 1.5. It can be used as a Python +library as well as on the commandline. The code was mostly written by +Sybren A. Stüvel. + +Documentation can be found at the [Python-RSA homepage](https://stuvel.eu/rsa). For all changes, check [the changelog](https://github.com/sybrenstuvel/python-rsa/blob/master/CHANGELOG.md). + +Download and install using: + + pip install rsa + +or download it from the [Python Package Index](https://pypi.org/project/rsa/). + +The source code is maintained at [GitHub](https://github.com/sybrenstuvel/python-rsa/) and is +licensed under the [Apache License, version 2.0](https://www.apache.org/licenses/LICENSE-2.0) + +## Security + +Because of how Python internally stores numbers, it is very hard (if not impossible) to make a pure-Python program secure against timing attacks. This library is no exception, so use it with care. See https://securitypitfalls.wordpress.com/2018/08/03/constant-time-compare-in-python/ for more info. + +## Setup of Development Environment + +``` +python3 -m venv .venv +. ./.venv/bin/activate +pip install poetry +poetry install +``` + +## Publishing a New Release + +Since this project is considered critical on the Python Package Index, +two-factor authentication is required. For uploading packages to PyPi, an API +key is required; username+password will not work. + +First, generate an API token at https://pypi.org/manage/account/token/. Then, +use this token when publishing instead of your username and password. + +As username, use `__token__`. +As password, use the token itself, including the `pypi-` prefix. + +See https://pypi.org/help/#apitoken for help using API tokens to publish. This +is what I have in `~/.pypirc`: + +``` +[distutils] +index-servers = + rsa + +# Use `twine upload -r rsa` to upload with this token. +[rsa] + repository = https://upload.pypi.org/legacy/ + username = __token__ + password = pypi-token +``` + +``` +. ./.venv/bin/activate +pip install twine + +poetry build +twine check dist/rsa-4.9.1.tar.gz dist/rsa-4.9.1-*.whl +twine upload -r rsa dist/rsa-4.9.1.tar.gz dist/rsa-4.9.1-*.whl +``` + +The `pip install twine` is necessary as Python-RSA requires Python >= 3.6, and +Twine requires at least version 3.7. This means Poetry refuses to add it as +dependency. + diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/RECORD b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..bc75beeffe2613e464bd8e895d7d16cbd32f83fc --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/RECORD @@ -0,0 +1,41 @@ +../../../bin/pyrsa-decrypt,sha256=OEuNPfI2nWyqrd74SanMg34HBTf0faoOEPJ7ebiMyrE,267 +../../../bin/pyrsa-encrypt,sha256=GcpmUQtJrJAaHg5h3K_erVwfcIFhld1uLBtVmKd67Tk,267 +../../../bin/pyrsa-keygen,sha256=lHEUXEgx49v6Ij__mdQhXeTiN60BMi-TiGqOY5ce_20,265 +../../../bin/pyrsa-priv2pub,sha256=txZ9iPqw6JwqISCRYtbOjKeVHaiwHNBppytPYSMHcuM,288 +../../../bin/pyrsa-sign,sha256=E97muQWyb5dZ9JGNr8vyKs-c8CZ-2-wk23W070FKtVc,261 +../../../bin/pyrsa-verify,sha256=z2WuXcZ84xv9VfBAFFfYRzff3stT_dzo4rV4qPAID0o,265 +rsa-4.9.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +rsa-4.9.1.dist-info/LICENSE,sha256=Bz8ot9OJyP509gfhfCf4HqpazmntxDqITyP0G0HFxyY,577 +rsa-4.9.1.dist-info/METADATA,sha256=dlNfSHIPYbcprUNaXLdI_h220MlG7Wa5kzbjPjqpd4k,5590 +rsa-4.9.1.dist-info/RECORD,, +rsa-4.9.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88 +rsa-4.9.1.dist-info/entry_points.txt,sha256=p0nVsezmPSjm5x4GDMD4a9Sshc9ukdfw1kkmOmpaAu0,201 +rsa/__init__.py,sha256=F_HYsjAm4fNMky56VN6BraayH4wmWQuhuduc_iJdG1Y,1607 +rsa/__pycache__/__init__.cpython-310.pyc,, +rsa/__pycache__/asn1.cpython-310.pyc,, +rsa/__pycache__/cli.cpython-310.pyc,, +rsa/__pycache__/common.cpython-310.pyc,, +rsa/__pycache__/core.cpython-310.pyc,, +rsa/__pycache__/key.cpython-310.pyc,, +rsa/__pycache__/parallel.cpython-310.pyc,, +rsa/__pycache__/pem.cpython-310.pyc,, +rsa/__pycache__/pkcs1.cpython-310.pyc,, +rsa/__pycache__/pkcs1_v2.cpython-310.pyc,, +rsa/__pycache__/prime.cpython-310.pyc,, +rsa/__pycache__/randnum.cpython-310.pyc,, +rsa/__pycache__/transform.cpython-310.pyc,, +rsa/__pycache__/util.cpython-310.pyc,, +rsa/asn1.py,sha256=fZPoHdVV-8ERZacGM6Wa2pW2l3F31HghGfqT9qIfs9Y,1740 +rsa/cli.py,sha256=K4tCTgNaY1h8H9c9UVRiSgeyvHsyi6Mxkgj8m55bnvI,9862 +rsa/common.py,sha256=w0UuV5HNvkFC4sBMjMDO8JTUJCoXvsLzcoJrAuqzLpA,4679 +rsa/core.py,sha256=yAj0Lg2G0HNxsa6xHMI-RF-OcIlE7GHzoBgWO7_2z5g,1661 +rsa/key.py,sha256=MgSlCEeWnEROLrr_FDCSqvC-_CbWbdjlkcO4kgh4sjw,27427 +rsa/parallel.py,sha256=lp8ln5nEw5mWbBr9yoegvHEcawbR96GVkgCKjwPHYbk,2309 +rsa/pem.py,sha256=7eZt4U9im0hLuCMQOAMaPHi2FexxQ-a7FVXyzbJS_HM,3989 +rsa/pkcs1.py,sha256=iRUFVeMf_5QwZHP_CudyMaDbD-RhzDKwjgoisMOnsbE,16205 +rsa/pkcs1_v2.py,sha256=pY22h-EJHV7jaeeNjqjlen0CbMgl-UP7d9CsQceHpek,3449 +rsa/prime.py,sha256=OFpVIF3JjXzeMWdYeGEnXt1Fy3cYnHQystcltgpfoR0,5106 +rsa/py.typed,sha256=bzd2a8c8TpHiUMxJz1pUd7d9LKFo71w1NxpG1fR73JA,63 +rsa/randnum.py,sha256=23l2gOUY9Vv9f67md_16tANrbBDpUP7dW1EuDdEklUs,2657 +rsa/transform.py,sha256=n44DPrO1CZLgKXYtJkTkvhwyFTcIt1hrbD_jM0KRBu0,2200 +rsa/util.py,sha256=DC27D6LR5E9pOPvxMwlTA1Y46Irx30Yh5gW3tCyp43E,2993 diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/WHEEL b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..5b4f9fcb0f00b8a9bd531ba1586141e1dcfd2786 --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 2.1.2 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/rsa-4.9.1.dist-info/entry_points.txt b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..bf058e3ebda1b5bda76eefdf3e7c188ae20bb911 --- /dev/null +++ b/lib/python3.10/site-packages/rsa-4.9.1.dist-info/entry_points.txt @@ -0,0 +1,8 @@ +[console_scripts] +pyrsa-decrypt=rsa.cli:decrypt +pyrsa-encrypt=rsa.cli:encrypt +pyrsa-keygen=rsa.cli:keygen +pyrsa-priv2pub=rsa.util:private_to_public +pyrsa-sign=rsa.cli:sign +pyrsa-verify=rsa.cli:verify + diff --git a/lib/python3.10/site-packages/rsa/__init__.py b/lib/python3.10/site-packages/rsa/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e7689072b2932cd57dd95a501cedca34cce39b0b --- /dev/null +++ b/lib/python3.10/site-packages/rsa/__init__.py @@ -0,0 +1,60 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""RSA module + +Module for calculating large primes, and RSA encryption, decryption, signing +and verification. Includes generating public and private keys. + +WARNING: this implementation does not use compression of the cleartext input to +prevent repetitions, or other common security improvements. Use with care. + +""" + +from rsa.key import newkeys, PrivateKey, PublicKey +from rsa.pkcs1 import ( + encrypt, + decrypt, + sign, + verify, + DecryptionError, + VerificationError, + find_signature_hash, + sign_hash, + compute_hash, +) + +__author__ = "Sybren Stuvel, Barry Mead and Yesudeep Mangalapilly" +__date__ = "2025-04-16" +__version__ = "4.9.1" + +# Do doctest if we're run directly +if __name__ == "__main__": + import doctest + + doctest.testmod() + +__all__ = [ + "newkeys", + "encrypt", + "decrypt", + "sign", + "verify", + "PublicKey", + "PrivateKey", + "DecryptionError", + "VerificationError", + "find_signature_hash", + "compute_hash", + "sign_hash", +] diff --git a/lib/python3.10/site-packages/rsa/asn1.py b/lib/python3.10/site-packages/rsa/asn1.py new file mode 100644 index 0000000000000000000000000000000000000000..4cc4dd35de5e346154c5c179eca716f3ffdbb315 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/asn1.py @@ -0,0 +1,52 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ASN.1 definitions. + +Not all ASN.1-handling code use these definitions, but when it does, they should be here. +""" + +from pyasn1.type import univ, namedtype, tag + + +class PubKeyHeader(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("oid", univ.ObjectIdentifier()), + namedtype.NamedType("parameters", univ.Null()), + ) + + +class OpenSSLPubKey(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("header", PubKeyHeader()), + # This little hack (the implicit tag) allows us to get a Bit String as Octet String + namedtype.NamedType( + "key", + univ.OctetString().subtype(implicitTag=tag.Tag(tagClass=0, tagFormat=0, tagId=3)), + ), + ) + + +class AsnPubKey(univ.Sequence): + """ASN.1 contents of DER encoded public key: + + RSAPublicKey ::= SEQUENCE { + modulus INTEGER, -- n + publicExponent INTEGER, -- e + """ + + componentType = namedtype.NamedTypes( + namedtype.NamedType("modulus", univ.Integer()), + namedtype.NamedType("publicExponent", univ.Integer()), + ) diff --git a/lib/python3.10/site-packages/rsa/cli.py b/lib/python3.10/site-packages/rsa/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..4db3f0b5e03dcfb6c138a90246b4bcc19e7ea727 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/cli.py @@ -0,0 +1,321 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Commandline scripts. + +These scripts are called by the executables defined in setup.py. +""" + +import abc +import sys +import typing +import optparse + +import rsa +import rsa.key +import rsa.pkcs1 + +HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys()) +Indexable = typing.Union[typing.Tuple, typing.List[str]] + + +def keygen() -> None: + """Key generator.""" + + # Parse the CLI options + parser = optparse.OptionParser( + usage="usage: %prog [options] keysize", + description='Generates a new RSA key pair of "keysize" bits.', + ) + + parser.add_option( + "--pubout", + type="string", + help="Output filename for the public key. The public key is " + "not saved if this option is not present. You can use " + "pyrsa-priv2pub to create the public key file later.", + ) + + parser.add_option( + "-o", + "--out", + type="string", + help="Output filename for the private key. The key is " + "written to stdout if this option is not present.", + ) + + parser.add_option( + "--form", + help="key format of the private and public keys - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv[1:]) + + if len(cli_args) != 1: + parser.print_help() + raise SystemExit(1) + + try: + keysize = int(cli_args[0]) + except ValueError as ex: + parser.print_help() + print("Not a valid number: %s" % cli_args[0], file=sys.stderr) + raise SystemExit(1) from ex + + print("Generating %i-bit key" % keysize, file=sys.stderr) + (pub_key, priv_key) = rsa.newkeys(keysize) + + # Save public key + if cli.pubout: + print("Writing public key to %s" % cli.pubout, file=sys.stderr) + data = pub_key.save_pkcs1(format=cli.form) + with open(cli.pubout, "wb") as outfile: + outfile.write(data) + + # Save private key + data = priv_key.save_pkcs1(format=cli.form) + + if cli.out: + print("Writing private key to %s" % cli.out, file=sys.stderr) + with open(cli.out, "wb") as outfile: + outfile.write(data) + else: + print("Writing private key to stdout", file=sys.stderr) + sys.stdout.buffer.write(data) + + +class CryptoOperation(metaclass=abc.ABCMeta): + """CLI callable that operates with input, output, and a key.""" + + keyname = "public" # or 'private' + usage = "usage: %%prog [options] %(keyname)s_key" + description = "" + operation = "decrypt" + operation_past = "decrypted" + operation_progressive = "decrypting" + input_help = "Name of the file to %(operation)s. Reads from stdin if " "not specified." + output_help = ( + "Name of the file to write the %(operation_past)s file " + "to. Written to stdout if this option is not present." + ) + expected_cli_args = 1 + has_output = True + + key_class = rsa.PublicKey # type: typing.Type[rsa.key.AbstractKey] + + def __init__(self) -> None: + self.usage = self.usage % self.__class__.__dict__ + self.input_help = self.input_help % self.__class__.__dict__ + self.output_help = self.output_help % self.__class__.__dict__ + + @abc.abstractmethod + def perform_operation( + self, indata: bytes, key: rsa.key.AbstractKey, cli_args: Indexable + ) -> typing.Any: + """Performs the program's operation. + + Implement in a subclass. + + :returns: the data to write to the output. + """ + + def __call__(self) -> None: + """Runs the program.""" + + (cli, cli_args) = self.parse_cli() + + key = self.read_key(cli_args[0], cli.keyform) + + indata = self.read_infile(cli.input) + + print(self.operation_progressive.title(), file=sys.stderr) + outdata = self.perform_operation(indata, key, cli_args) + + if self.has_output: + self.write_outfile(outdata, cli.output) + + def parse_cli(self) -> typing.Tuple[optparse.Values, typing.List[str]]: + """Parse the CLI options + + :returns: (cli_opts, cli_args) + """ + + parser = optparse.OptionParser(usage=self.usage, description=self.description) + + parser.add_option("-i", "--input", type="string", help=self.input_help) + + if self.has_output: + parser.add_option("-o", "--output", type="string", help=self.output_help) + + parser.add_option( + "--keyform", + help="Key format of the %s key - default PEM" % self.keyname, + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv[1:]) + + if len(cli_args) != self.expected_cli_args: + parser.print_help() + raise SystemExit(1) + + return cli, cli_args + + def read_key(self, filename: str, keyform: str) -> rsa.key.AbstractKey: + """Reads a public or private key.""" + + print("Reading %s key from %s" % (self.keyname, filename), file=sys.stderr) + with open(filename, "rb") as keyfile: + keydata = keyfile.read() + + return self.key_class.load_pkcs1(keydata, keyform) + + def read_infile(self, inname: str) -> bytes: + """Read the input file""" + + if inname: + print("Reading input from %s" % inname, file=sys.stderr) + with open(inname, "rb") as infile: + return infile.read() + + print("Reading input from stdin", file=sys.stderr) + return sys.stdin.buffer.read() + + def write_outfile(self, outdata: bytes, outname: str) -> None: + """Write the output file""" + + if outname: + print("Writing output to %s" % outname, file=sys.stderr) + with open(outname, "wb") as outfile: + outfile.write(outdata) + else: + print("Writing output to stdout", file=sys.stderr) + sys.stdout.buffer.write(outdata) + + +class EncryptOperation(CryptoOperation): + """Encrypts a file.""" + + keyname = "public" + description = ( + "Encrypts a file. The file must be shorter than the key " "length in order to be encrypted." + ) + operation = "encrypt" + operation_past = "encrypted" + operation_progressive = "encrypting" + + def perform_operation( + self, indata: bytes, pub_key: rsa.key.AbstractKey, cli_args: Indexable = () + ) -> bytes: + """Encrypts files.""" + assert isinstance(pub_key, rsa.key.PublicKey) + return rsa.encrypt(indata, pub_key) + + +class DecryptOperation(CryptoOperation): + """Decrypts a file.""" + + keyname = "private" + description = ( + "Decrypts a file. The original file must be shorter than " + "the key length in order to have been encrypted." + ) + operation = "decrypt" + operation_past = "decrypted" + operation_progressive = "decrypting" + key_class = rsa.PrivateKey + + def perform_operation( + self, indata: bytes, priv_key: rsa.key.AbstractKey, cli_args: Indexable = () + ) -> bytes: + """Decrypts files.""" + assert isinstance(priv_key, rsa.key.PrivateKey) + return rsa.decrypt(indata, priv_key) + + +class SignOperation(CryptoOperation): + """Signs a file.""" + + keyname = "private" + usage = "usage: %%prog [options] private_key hash_method" + description = ( + "Signs a file, outputs the signature. Choose the hash " + "method from %s" % ", ".join(HASH_METHODS) + ) + operation = "sign" + operation_past = "signature" + operation_progressive = "Signing" + key_class = rsa.PrivateKey + expected_cli_args = 2 + + output_help = ( + "Name of the file to write the signature to. Written " + "to stdout if this option is not present." + ) + + def perform_operation( + self, indata: bytes, priv_key: rsa.key.AbstractKey, cli_args: Indexable + ) -> bytes: + """Signs files.""" + assert isinstance(priv_key, rsa.key.PrivateKey) + + hash_method = cli_args[1] + if hash_method not in HASH_METHODS: + raise SystemExit("Invalid hash method, choose one of %s" % ", ".join(HASH_METHODS)) + + return rsa.sign(indata, priv_key, hash_method) + + +class VerifyOperation(CryptoOperation): + """Verify a signature.""" + + keyname = "public" + usage = "usage: %%prog [options] public_key signature_file" + description = ( + "Verifies a signature, exits with status 0 upon success, " + "prints an error message and exits with status 1 upon error." + ) + operation = "verify" + operation_past = "verified" + operation_progressive = "Verifying" + key_class = rsa.PublicKey + expected_cli_args = 2 + has_output = False + + def perform_operation( + self, indata: bytes, pub_key: rsa.key.AbstractKey, cli_args: Indexable + ) -> None: + """Verifies files.""" + assert isinstance(pub_key, rsa.key.PublicKey) + + signature_file = cli_args[1] + + with open(signature_file, "rb") as sigfile: + signature = sigfile.read() + + try: + rsa.verify(indata, signature, pub_key) + except rsa.VerificationError as ex: + raise SystemExit("Verification failed.") from ex + + print("Verification OK", file=sys.stderr) + + +encrypt = EncryptOperation() +decrypt = DecryptOperation() +sign = SignOperation() +verify = VerifyOperation() diff --git a/lib/python3.10/site-packages/rsa/common.py b/lib/python3.10/site-packages/rsa/common.py new file mode 100644 index 0000000000000000000000000000000000000000..ca732e58190391ddfd33d02142ce599f5bce76df --- /dev/null +++ b/lib/python3.10/site-packages/rsa/common.py @@ -0,0 +1,184 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common functionality shared by several modules.""" + +import typing + + +class NotRelativePrimeError(ValueError): + def __init__(self, a: int, b: int, d: int, msg: str = "") -> None: + super().__init__(msg or "%d and %d are not relatively prime, divider=%i" % (a, b, d)) + self.a = a + self.b = b + self.d = d + + +def bit_size(num: int) -> int: + """ + Number of bits needed to represent a integer excluding any prefix + 0 bits. + + Usage:: + + >>> bit_size(1023) + 10 + >>> bit_size(1024) + 11 + >>> bit_size(1025) + 11 + + :param num: + Integer value. If num is 0, returns 0. Only the absolute value of the + number is considered. Therefore, signed integers will be abs(num) + before the number's bit length is determined. + :returns: + Returns the number of bits in the integer. + """ + + try: + return num.bit_length() + except AttributeError as ex: + raise TypeError("bit_size(num) only supports integers, not %r" % type(num)) from ex + + +def byte_size(number: int) -> int: + """ + Returns the number of bytes required to hold a specific long number. + + The number of bytes is rounded up. + + Usage:: + + >>> byte_size(1 << 1023) + 128 + >>> byte_size((1 << 1024) - 1) + 128 + >>> byte_size(1 << 1024) + 129 + + :param number: + An unsigned integer + :returns: + The number of bytes required to hold a specific long number. + """ + if number == 0: + return 1 + return ceil_div(bit_size(number), 8) + + +def ceil_div(num: int, div: int) -> int: + """ + Returns the ceiling function of a division between `num` and `div`. + + Usage:: + + >>> ceil_div(100, 7) + 15 + >>> ceil_div(100, 10) + 10 + >>> ceil_div(1, 4) + 1 + + :param num: Division's numerator, a number + :param div: Division's divisor, a number + + :return: Rounded up result of the division between the parameters. + """ + quanta, mod = divmod(num, div) + if mod: + quanta += 1 + return quanta + + +def extended_gcd(a: int, b: int) -> typing.Tuple[int, int, int]: + """Returns a tuple (r, i, j) such that r = gcd(a, b) = ia + jb""" + # r = gcd(a,b) i = multiplicitive inverse of a mod b + # or j = multiplicitive inverse of b mod a + # Neg return values for i or j are made positive mod b or a respectively + # Iterateive Version is faster and uses much less stack space + x = 0 + y = 1 + lx = 1 + ly = 0 + oa = a # Remember original a/b to remove + ob = b # negative values from return results + while b != 0: + q = a // b + (a, b) = (b, a % b) + (x, lx) = ((lx - (q * x)), x) + (y, ly) = ((ly - (q * y)), y) + if lx < 0: + lx += ob # If neg wrap modulo original b + if ly < 0: + ly += oa # If neg wrap modulo original a + return a, lx, ly # Return only positive values + + +def inverse(x: int, n: int) -> int: + """Returns the inverse of x % n under multiplication, a.k.a x^-1 (mod n) + + >>> inverse(7, 4) + 3 + >>> (inverse(143, 4) * 143) % 4 + 1 + """ + + (divider, inv, _) = extended_gcd(x, n) + + if divider != 1: + raise NotRelativePrimeError(x, n, divider) + + return inv + + +def crt(a_values: typing.Iterable[int], modulo_values: typing.Iterable[int]) -> int: + """Chinese Remainder Theorem. + + Calculates x such that x = a[i] (mod m[i]) for each i. + + :param a_values: the a-values of the above equation + :param modulo_values: the m-values of the above equation + :returns: x such that x = a[i] (mod m[i]) for each i + + + >>> crt([2, 3], [3, 5]) + 8 + + >>> crt([2, 3, 2], [3, 5, 7]) + 23 + + >>> crt([2, 3, 0], [7, 11, 15]) + 135 + """ + + m = 1 + x = 0 + + for modulo in modulo_values: + m *= modulo + + for (m_i, a_i) in zip(modulo_values, a_values): + M_i = m // m_i + inv = inverse(M_i, m_i) + + x = (x + a_i * M_i * inv) % m + + return x + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/lib/python3.10/site-packages/rsa/core.py b/lib/python3.10/site-packages/rsa/core.py new file mode 100644 index 0000000000000000000000000000000000000000..84ed3f883f70a30829f32966051e4a9d22fbeb0a --- /dev/null +++ b/lib/python3.10/site-packages/rsa/core.py @@ -0,0 +1,53 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core mathematical operations. + +This is the actual core RSA implementation, which is only defined +mathematically on integers. +""" + + +def assert_int(var: int, name: str) -> None: + if isinstance(var, int): + return + + raise TypeError("%s should be an integer, not %s" % (name, var.__class__)) + + +def encrypt_int(message: int, ekey: int, n: int) -> int: + """Encrypts a message using encryption key 'ekey', working modulo n""" + + assert_int(message, "message") + assert_int(ekey, "ekey") + assert_int(n, "n") + + if message < 0: + raise ValueError("Only non-negative numbers are supported") + + if message > n: + raise OverflowError("The message %i is too long for n=%i" % (message, n)) + + return pow(message, ekey, n) + + +def decrypt_int(cyphertext: int, dkey: int, n: int) -> int: + """Decrypts a cypher text using the decryption key 'dkey', working modulo n""" + + assert_int(cyphertext, "cyphertext") + assert_int(dkey, "dkey") + assert_int(n, "n") + + message = pow(cyphertext, dkey, n) + return message diff --git a/lib/python3.10/site-packages/rsa/key.py b/lib/python3.10/site-packages/rsa/key.py new file mode 100644 index 0000000000000000000000000000000000000000..f80064430869d8fb937ec703582fe363d4289b77 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/key.py @@ -0,0 +1,858 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RSA key generation code. + +Create new keys with the newkeys() function. It will give you a PublicKey and a +PrivateKey object. + +Loading and saving keys requires the pyasn1 module. This module is imported as +late as possible, such that other functionality will remain working in absence +of pyasn1. + +.. note:: + + Storing public and private keys via the `pickle` module is possible. + However, it is insecure to load a key from an untrusted source. + The pickle module is not secure against erroneous or maliciously + constructed data. Never unpickle data received from an untrusted + or unauthenticated source. + +""" + +import threading +import typing +import warnings + +import rsa.prime +import rsa.pem +import rsa.common +import rsa.randnum +import rsa.core + + +DEFAULT_EXPONENT = 65537 + + +T = typing.TypeVar("T", bound="AbstractKey") + + +class AbstractKey: + """Abstract superclass for private and public keys.""" + + __slots__ = ("n", "e", "blindfac", "blindfac_inverse", "mutex") + + def __init__(self, n: int, e: int) -> None: + self.n = n + self.e = e + + # These will be computed properly on the first call to blind(). + self.blindfac = self.blindfac_inverse = -1 + + # Used to protect updates to the blinding factor in multi-threaded + # environments. + self.mutex = threading.Lock() + + @classmethod + def _load_pkcs1_pem(cls: typing.Type[T], keyfile: bytes) -> T: + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a PEM-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + @classmethod + def _load_pkcs1_der(cls: typing.Type[T], keyfile: bytes) -> T: + """Loads a key in PKCS#1 PEM format, implement in a subclass. + + :param keyfile: contents of a DER-encoded file that contains + the public key. + :type keyfile: bytes + + :return: the loaded key + :rtype: AbstractKey + """ + + def _save_pkcs1_pem(self) -> bytes: + """Saves the key in PKCS#1 PEM format, implement in a subclass. + + :returns: the PEM-encoded key. + :rtype: bytes + """ + + def _save_pkcs1_der(self) -> bytes: + """Saves the key in PKCS#1 DER format, implement in a subclass. + + :returns: the DER-encoded key. + :rtype: bytes + """ + + @classmethod + def load_pkcs1(cls: typing.Type[T], keyfile: bytes, format: str = "PEM") -> T: + """Loads a key in PKCS#1 DER or PEM format. + + :param keyfile: contents of a DER- or PEM-encoded file that contains + the key. + :type keyfile: bytes + :param format: the format of the file to load; 'PEM' or 'DER' + :type format: str + + :return: the loaded key + :rtype: AbstractKey + """ + + methods = { + "PEM": cls._load_pkcs1_pem, + "DER": cls._load_pkcs1_der, + } + + method = cls._assert_format_exists(format, methods) + return method(keyfile) + + @staticmethod + def _assert_format_exists( + file_format: str, methods: typing.Mapping[str, typing.Callable] + ) -> typing.Callable: + """Checks whether the given file format exists in 'methods'.""" + + try: + return methods[file_format] + except KeyError as ex: + formats = ", ".join(sorted(methods.keys())) + raise ValueError( + "Unsupported format: %r, try one of %s" % (file_format, formats) + ) from ex + + def save_pkcs1(self, format: str = "PEM") -> bytes: + """Saves the key in PKCS#1 DER or PEM format. + + :param format: the format to save; 'PEM' or 'DER' + :type format: str + :returns: the DER- or PEM-encoded key. + :rtype: bytes + """ + + methods = { + "PEM": self._save_pkcs1_pem, + "DER": self._save_pkcs1_der, + } + + method = self._assert_format_exists(format, methods) + return method() + + def blind(self, message: int) -> typing.Tuple[int, int]: + """Performs blinding on the message. + + :param message: the message, as integer, to blind. + :param r: the random number to blind with. + :return: tuple (the blinded message, the inverse of the used blinding factor) + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + """ + blindfac, blindfac_inverse = self._update_blinding_factor() + blinded = (message * pow(blindfac, self.e, self.n)) % self.n + return blinded, blindfac_inverse + + def unblind(self, blinded: int, blindfac_inverse: int) -> int: + """Performs blinding on the message using random number 'blindfac_inverse'. + + :param blinded: the blinded message, as integer, to unblind. + :param blindfac: the factor to unblind with. + :return: the original message. + + The blinding is such that message = unblind(decrypt(blind(encrypt(message))). + + See https://en.wikipedia.org/wiki/Blinding_%28cryptography%29 + """ + return (blindfac_inverse * blinded) % self.n + + def _initial_blinding_factor(self) -> int: + for _ in range(1000): + blind_r = rsa.randnum.randint(self.n - 1) + if rsa.prime.are_relatively_prime(self.n, blind_r): + return blind_r + raise RuntimeError("unable to find blinding factor") + + def _update_blinding_factor(self) -> typing.Tuple[int, int]: + """Update blinding factors. + + Computing a blinding factor is expensive, so instead this function + does this once, then updates the blinding factor as per section 9 + of 'A Timing Attack against RSA with the Chinese Remainder Theorem' + by Werner Schindler. + See https://tls.mbed.org/public/WSchindler-RSA_Timing_Attack.pdf + + :return: the new blinding factor and its inverse. + """ + + with self.mutex: + if self.blindfac < 0: + # Compute initial blinding factor, which is rather slow to do. + self.blindfac = self._initial_blinding_factor() + self.blindfac_inverse = rsa.common.inverse(self.blindfac, self.n) + else: + # Reuse previous blinding factor. + self.blindfac = pow(self.blindfac, 2, self.n) + self.blindfac_inverse = pow(self.blindfac_inverse, 2, self.n) + + return self.blindfac, self.blindfac_inverse + + +class PublicKey(AbstractKey): + """Represents a public RSA key. + + This key is also known as the 'encryption key'. It contains the 'n' and 'e' + values. + + Supports attributes as well as dictionary-like access. Attribute access is + faster, though. + + >>> PublicKey(5, 3) + PublicKey(5, 3) + + >>> key = PublicKey(5, 3) + >>> key.n + 5 + >>> key['n'] + 5 + >>> key.e + 3 + >>> key['e'] + 3 + + """ + + __slots__ = () + + def __getitem__(self, key: str) -> int: + return getattr(self, key) + + def __repr__(self) -> str: + return "PublicKey(%i, %i)" % (self.n, self.e) + + def __getstate__(self) -> typing.Tuple[int, int]: + """Returns the key as tuple for pickling.""" + return self.n, self.e + + def __setstate__(self, state: typing.Tuple[int, int]) -> None: + """Sets the key from tuple.""" + self.n, self.e = state + AbstractKey.__init__(self, self.n, self.e) + + def __eq__(self, other: typing.Any) -> bool: + if other is None: + return False + + if not isinstance(other, PublicKey): + return False + + return self.n == other.n and self.e == other.e + + def __ne__(self, other: typing.Any) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((self.n, self.e)) + + @classmethod + def _load_pkcs1_der(cls, keyfile: bytes) -> "PublicKey": + """Loads a key in PKCS#1 DER format. + + :param keyfile: contents of a DER-encoded file that contains the public + key. + :return: a PublicKey object + + First let's construct a DER encoded key: + + >>> import base64 + >>> b64der = 'MAwCBQCNGmYtAgMBAAE=' + >>> der = base64.standard_b64decode(b64der) + + This loads the file: + + >>> PublicKey._load_pkcs1_der(der) + PublicKey(2367317549, 65537) + + """ + + from pyasn1.codec.der import decoder + from rsa.asn1 import AsnPubKey + + (priv, _) = decoder.decode(keyfile, asn1Spec=AsnPubKey()) + return cls(n=int(priv["modulus"]), e=int(priv["publicExponent"])) + + def _save_pkcs1_der(self) -> bytes: + """Saves the public key in PKCS#1 DER format. + + :returns: the DER-encoded public key. + :rtype: bytes + """ + + from pyasn1.codec.der import encoder + from rsa.asn1 import AsnPubKey + + # Create the ASN object + asn_key = AsnPubKey() + asn_key.setComponentByName("modulus", self.n) + asn_key.setComponentByName("publicExponent", self.e) + + return encoder.encode(asn_key) + + @classmethod + def _load_pkcs1_pem(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1 PEM-encoded public key file. + + The contents of the file before the "-----BEGIN RSA PUBLIC KEY-----" and + after the "-----END RSA PUBLIC KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the public + key. + :return: a PublicKey object + """ + + der = rsa.pem.load_pem(keyfile, "RSA PUBLIC KEY") + return cls._load_pkcs1_der(der) + + def _save_pkcs1_pem(self) -> bytes: + """Saves a PKCS#1 PEM-encoded public key file. + + :return: contents of a PEM-encoded file that contains the public key. + :rtype: bytes + """ + + der = self._save_pkcs1_der() + return rsa.pem.save_pem(der, "RSA PUBLIC KEY") + + @classmethod + def load_pkcs1_openssl_pem(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1.5 PEM-encoded public key file from OpenSSL. + + These files can be recognised in that they start with BEGIN PUBLIC KEY + rather than BEGIN RSA PUBLIC KEY. + + The contents of the file before the "-----BEGIN PUBLIC KEY-----" and + after the "-----END PUBLIC KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the public + key, from OpenSSL. + :type keyfile: bytes + :return: a PublicKey object + """ + + der = rsa.pem.load_pem(keyfile, "PUBLIC KEY") + return cls.load_pkcs1_openssl_der(der) + + @classmethod + def load_pkcs1_openssl_der(cls, keyfile: bytes) -> "PublicKey": + """Loads a PKCS#1 DER-encoded public key file from OpenSSL. + + :param keyfile: contents of a DER-encoded file that contains the public + key, from OpenSSL. + :return: a PublicKey object + """ + + from rsa.asn1 import OpenSSLPubKey + from pyasn1.codec.der import decoder + from pyasn1.type import univ + + (keyinfo, _) = decoder.decode(keyfile, asn1Spec=OpenSSLPubKey()) + + if keyinfo["header"]["oid"] != univ.ObjectIdentifier("1.2.840.113549.1.1.1"): + raise TypeError("This is not a DER-encoded OpenSSL-compatible public key") + + return cls._load_pkcs1_der(keyinfo["key"][1:]) + + +class PrivateKey(AbstractKey): + """Represents a private RSA key. + + This key is also known as the 'decryption key'. It contains the 'n', 'e', + 'd', 'p', 'q' and other values. + + Supports attributes as well as dictionary-like access. Attribute access is + faster, though. + + >>> PrivateKey(3247, 65537, 833, 191, 17) + PrivateKey(3247, 65537, 833, 191, 17) + + exp1, exp2 and coef will be calculated: + + >>> pk = PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + >>> pk.exp1 + 55063 + >>> pk.exp2 + 10095 + >>> pk.coef + 50797 + + """ + + __slots__ = ("d", "p", "q", "exp1", "exp2", "coef") + + def __init__(self, n: int, e: int, d: int, p: int, q: int) -> None: + AbstractKey.__init__(self, n, e) + self.d = d + self.p = p + self.q = q + + # Calculate exponents and coefficient. + self.exp1 = int(d % (p - 1)) + self.exp2 = int(d % (q - 1)) + self.coef = rsa.common.inverse(q, p) + + def __getitem__(self, key: str) -> int: + return getattr(self, key) + + def __repr__(self) -> str: + return "PrivateKey(%i, %i, %i, %i, %i)" % ( + self.n, + self.e, + self.d, + self.p, + self.q, + ) + + def __getstate__(self) -> typing.Tuple[int, int, int, int, int, int, int, int]: + """Returns the key as tuple for pickling.""" + return self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef + + def __setstate__(self, state: typing.Tuple[int, int, int, int, int, int, int, int]) -> None: + """Sets the key from tuple.""" + self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef = state + AbstractKey.__init__(self, self.n, self.e) + + def __eq__(self, other: typing.Any) -> bool: + if other is None: + return False + + if not isinstance(other, PrivateKey): + return False + + return ( + self.n == other.n + and self.e == other.e + and self.d == other.d + and self.p == other.p + and self.q == other.q + and self.exp1 == other.exp1 + and self.exp2 == other.exp2 + and self.coef == other.coef + ) + + def __ne__(self, other: typing.Any) -> bool: + return not (self == other) + + def __hash__(self) -> int: + return hash((self.n, self.e, self.d, self.p, self.q, self.exp1, self.exp2, self.coef)) + + def blinded_decrypt(self, encrypted: int) -> int: + """Decrypts the message using blinding to prevent side-channel attacks. + + :param encrypted: the encrypted message + :type encrypted: int + + :returns: the decrypted message + :rtype: int + """ + + # Blinding and un-blinding should be using the same factor + blinded, blindfac_inverse = self.blind(encrypted) + + # Instead of using the core functionality, use the Chinese Remainder + # Theorem and be 2-4x faster. This the same as: + # + # decrypted = rsa.core.decrypt_int(blinded, self.d, self.n) + s1 = pow(blinded, self.exp1, self.p) + s2 = pow(blinded, self.exp2, self.q) + h = ((s1 - s2) * self.coef) % self.p + decrypted = s2 + self.q * h + + return self.unblind(decrypted, blindfac_inverse) + + def blinded_encrypt(self, message: int) -> int: + """Encrypts the message using blinding to prevent side-channel attacks. + + :param message: the message to encrypt + :type message: int + + :returns: the encrypted message + :rtype: int + """ + + blinded, blindfac_inverse = self.blind(message) + encrypted = rsa.core.encrypt_int(blinded, self.d, self.n) + return self.unblind(encrypted, blindfac_inverse) + + @classmethod + def _load_pkcs1_der(cls, keyfile: bytes) -> "PrivateKey": + """Loads a key in PKCS#1 DER format. + + :param keyfile: contents of a DER-encoded file that contains the private + key. + :type keyfile: bytes + :return: a PrivateKey object + + First let's construct a DER encoded key: + + >>> import base64 + >>> b64der = 'MC4CAQACBQDeKYlRAgMBAAECBQDHn4npAgMA/icCAwDfxwIDANcXAgInbwIDAMZt' + >>> der = base64.standard_b64decode(b64der) + + This loads the file: + + >>> PrivateKey._load_pkcs1_der(der) + PrivateKey(3727264081, 65537, 3349121513, 65063, 57287) + + """ + + from pyasn1.codec.der import decoder + + (priv, _) = decoder.decode(keyfile) + + # ASN.1 contents of DER encoded private key: + # + # RSAPrivateKey ::= SEQUENCE { + # version Version, + # modulus INTEGER, -- n + # publicExponent INTEGER, -- e + # privateExponent INTEGER, -- d + # prime1 INTEGER, -- p + # prime2 INTEGER, -- q + # exponent1 INTEGER, -- d mod (p-1) + # exponent2 INTEGER, -- d mod (q-1) + # coefficient INTEGER, -- (inverse of q) mod p + # otherPrimeInfos OtherPrimeInfos OPTIONAL + # } + + if priv[0] != 0: + raise ValueError("Unable to read this file, version %s != 0" % priv[0]) + + as_ints = map(int, priv[1:6]) + key = cls(*as_ints) + + exp1, exp2, coef = map(int, priv[6:9]) + + if (key.exp1, key.exp2, key.coef) != (exp1, exp2, coef): + warnings.warn( + "You have provided a malformed keyfile. Either the exponents " + "or the coefficient are incorrect. Using the correct values " + "instead.", + UserWarning, + ) + + return key + + def _save_pkcs1_der(self) -> bytes: + """Saves the private key in PKCS#1 DER format. + + :returns: the DER-encoded private key. + :rtype: bytes + """ + + from pyasn1.type import univ, namedtype + from pyasn1.codec.der import encoder + + class AsnPrivKey(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType("version", univ.Integer()), + namedtype.NamedType("modulus", univ.Integer()), + namedtype.NamedType("publicExponent", univ.Integer()), + namedtype.NamedType("privateExponent", univ.Integer()), + namedtype.NamedType("prime1", univ.Integer()), + namedtype.NamedType("prime2", univ.Integer()), + namedtype.NamedType("exponent1", univ.Integer()), + namedtype.NamedType("exponent2", univ.Integer()), + namedtype.NamedType("coefficient", univ.Integer()), + ) + + # Create the ASN object + asn_key = AsnPrivKey() + asn_key.setComponentByName("version", 0) + asn_key.setComponentByName("modulus", self.n) + asn_key.setComponentByName("publicExponent", self.e) + asn_key.setComponentByName("privateExponent", self.d) + asn_key.setComponentByName("prime1", self.p) + asn_key.setComponentByName("prime2", self.q) + asn_key.setComponentByName("exponent1", self.exp1) + asn_key.setComponentByName("exponent2", self.exp2) + asn_key.setComponentByName("coefficient", self.coef) + + return encoder.encode(asn_key) + + @classmethod + def _load_pkcs1_pem(cls, keyfile: bytes) -> "PrivateKey": + """Loads a PKCS#1 PEM-encoded private key file. + + The contents of the file before the "-----BEGIN RSA PRIVATE KEY-----" and + after the "-----END RSA PRIVATE KEY-----" lines is ignored. + + :param keyfile: contents of a PEM-encoded file that contains the private + key. + :type keyfile: bytes + :return: a PrivateKey object + """ + + der = rsa.pem.load_pem(keyfile, b"RSA PRIVATE KEY") + return cls._load_pkcs1_der(der) + + def _save_pkcs1_pem(self) -> bytes: + """Saves a PKCS#1 PEM-encoded private key file. + + :return: contents of a PEM-encoded file that contains the private key. + :rtype: bytes + """ + + der = self._save_pkcs1_der() + return rsa.pem.save_pem(der, b"RSA PRIVATE KEY") + + +def find_p_q( + nbits: int, + getprime_func: typing.Callable[[int], int] = rsa.prime.getprime, + accurate: bool = True, +) -> typing.Tuple[int, int]: + """Returns a tuple of two different primes of nbits bits each. + + The resulting p * q has exactly 2 * nbits bits, and the returned p and q + will not be equal. + + :param nbits: the number of bits in each of p and q. + :param getprime_func: the getprime function, defaults to + :py:func:`rsa.prime.getprime`. + + *Introduced in Python-RSA 3.1* + + :param accurate: whether to enable accurate mode or not. + :returns: (p, q), where p > q + + >>> (p, q) = find_p_q(128) + >>> from rsa import common + >>> common.bit_size(p * q) + 256 + + When not in accurate mode, the number of bits can be slightly less + + >>> (p, q) = find_p_q(128, accurate=False) + >>> from rsa import common + >>> common.bit_size(p * q) <= 256 + True + >>> common.bit_size(p * q) > 240 + True + + """ + + total_bits = nbits * 2 + + # Make sure that p and q aren't too close or the factoring programs can + # factor n. + shift = nbits // 16 + pbits = nbits + shift + qbits = nbits - shift + + # Choose the two initial primes + p = getprime_func(pbits) + q = getprime_func(qbits) + + def is_acceptable(p: int, q: int) -> bool: + """Returns True iff p and q are acceptable: + + - p and q differ + - (p * q) has the right nr of bits (when accurate=True) + """ + + if p == q: + return False + + if not accurate: + return True + + # Make sure we have just the right amount of bits + found_size = rsa.common.bit_size(p * q) + return total_bits == found_size + + # Keep choosing other primes until they match our requirements. + change_p = False + while not is_acceptable(p, q): + # Change p on one iteration and q on the other + if change_p: + p = getprime_func(pbits) + else: + q = getprime_func(qbits) + + change_p = not change_p + + # We want p > q as described on + # http://www.di-mgt.com.au/rsa_alg.html#crt + return max(p, q), min(p, q) + + +def calculate_keys_custom_exponent(p: int, q: int, exponent: int) -> typing.Tuple[int, int]: + """Calculates an encryption and a decryption key given p, q and an exponent, + and returns them as a tuple (e, d) + + :param p: the first large prime + :param q: the second large prime + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + + """ + + phi_n = (p - 1) * (q - 1) + + try: + d = rsa.common.inverse(exponent, phi_n) + except rsa.common.NotRelativePrimeError as ex: + raise rsa.common.NotRelativePrimeError( + exponent, + phi_n, + ex.d, + msg="e (%d) and phi_n (%d) are not relatively prime (divider=%i)" + % (exponent, phi_n, ex.d), + ) from ex + + if (exponent * d) % phi_n != 1: + raise ValueError( + "e (%d) and d (%d) are not mult. inv. modulo " "phi_n (%d)" % (exponent, d, phi_n) + ) + + return exponent, d + + +def calculate_keys(p: int, q: int) -> typing.Tuple[int, int]: + """Calculates an encryption and a decryption key given p and q, and + returns them as a tuple (e, d) + + :param p: the first large prime + :param q: the second large prime + + :return: tuple (e, d) with the encryption and decryption exponents. + """ + + return calculate_keys_custom_exponent(p, q, DEFAULT_EXPONENT) + + +def gen_keys( + nbits: int, + getprime_func: typing.Callable[[int], int], + accurate: bool = True, + exponent: int = DEFAULT_EXPONENT, +) -> typing.Tuple[int, int, int, int]: + """Generate RSA keys of nbits bits. Returns (p, q, e, d). + + Note: this can take a long time, depending on the key size. + + :param nbits: the total number of bits in ``p`` and ``q``. Both ``p`` and + ``q`` will use ``nbits/2`` bits. + :param getprime_func: either :py:func:`rsa.prime.getprime` or a function + with similar signature. + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + """ + + # Regenerate p and q values, until calculate_keys doesn't raise a + # ValueError. + while True: + (p, q) = find_p_q(nbits // 2, getprime_func, accurate) + try: + (e, d) = calculate_keys_custom_exponent(p, q, exponent=exponent) + break + except ValueError: + pass + + return p, q, e, d + + +def newkeys( + nbits: int, + accurate: bool = True, + poolsize: int = 1, + exponent: int = DEFAULT_EXPONENT, +) -> typing.Tuple[PublicKey, PrivateKey]: + """Generates public and private keys, and returns them as (pub, priv). + + The public key is also known as the 'encryption key', and is a + :py:class:`rsa.PublicKey` object. The private key is also known as the + 'decryption key' and is a :py:class:`rsa.PrivateKey` object. + + :param nbits: the number of bits required to store ``n = p*q``. + :param accurate: when True, ``n`` will have exactly the number of bits you + asked for. However, this makes key generation much slower. When False, + `n`` may have slightly less bits. + :param poolsize: the number of processes to use to generate the prime + numbers. If set to a number > 1, a parallel algorithm will be used. + This requires Python 2.6 or newer. + :param exponent: the exponent for the key; only change this if you know + what you're doing, as the exponent influences how difficult your + private key can be cracked. A very common choice for e is 65537. + :type exponent: int + + :returns: a tuple (:py:class:`rsa.PublicKey`, :py:class:`rsa.PrivateKey`) + + The ``poolsize`` parameter was added in *Python-RSA 3.1* and requires + Python 2.6 or newer. + + """ + + if nbits < 16: + raise ValueError("Key too small") + + if poolsize < 1: + raise ValueError("Pool size (%i) should be >= 1" % poolsize) + + # Determine which getprime function to use + if poolsize > 1: + from rsa import parallel + + def getprime_func(nbits: int) -> int: + return parallel.getprime(nbits, poolsize=poolsize) + + else: + getprime_func = rsa.prime.getprime + + # Generate the key components + (p, q, e, d) = gen_keys(nbits, getprime_func, accurate=accurate, exponent=exponent) + + # Create the key objects + n = p * q + + return (PublicKey(n, e), PrivateKey(n, e, d, p, q)) + + +__all__ = ["PublicKey", "PrivateKey", "newkeys"] + +if __name__ == "__main__": + import doctest + + try: + for count in range(100): + (failures, tests) = doctest.testmod() + if failures: + break + + if (count % 10 == 0 and count) or count == 1: + print("%i times" % count) + except KeyboardInterrupt: + print("Aborted") + else: + print("Doctests done") diff --git a/lib/python3.10/site-packages/rsa/parallel.py b/lib/python3.10/site-packages/rsa/parallel.py new file mode 100644 index 0000000000000000000000000000000000000000..5020edbc760b826d2a2debec03f3f78b492e143f --- /dev/null +++ b/lib/python3.10/site-packages/rsa/parallel.py @@ -0,0 +1,96 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for parallel computation on multiple cores. + +Introduced in Python-RSA 3.1. + +.. note:: + + Requires Python 2.6 or newer. + +""" + +import multiprocessing as mp +from multiprocessing.connection import Connection + +import rsa.prime +import rsa.randnum + + +def _find_prime(nbits: int, pipe: Connection) -> None: + while True: + integer = rsa.randnum.read_random_odd_int(nbits) + + # Test for primeness + if rsa.prime.is_prime(integer): + pipe.send(integer) + return + + +def getprime(nbits: int, poolsize: int) -> int: + """Returns a prime number that can be stored in 'nbits' bits. + + Works in multiple threads at the same time. + + >>> p = getprime(128, 3) + >>> rsa.prime.is_prime(p-1) + False + >>> rsa.prime.is_prime(p) + True + >>> rsa.prime.is_prime(p+1) + False + + >>> from rsa import common + >>> common.bit_size(p) == 128 + True + + """ + + (pipe_recv, pipe_send) = mp.Pipe(duplex=False) + + # Create processes + try: + procs = [mp.Process(target=_find_prime, args=(nbits, pipe_send)) for _ in range(poolsize)] + # Start processes + for p in procs: + p.start() + + result = pipe_recv.recv() + finally: + pipe_recv.close() + pipe_send.close() + + # Terminate processes + for p in procs: + p.terminate() + + return result + + +__all__ = ["getprime"] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(100): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 10 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/lib/python3.10/site-packages/rsa/pem.py b/lib/python3.10/site-packages/rsa/pem.py new file mode 100644 index 0000000000000000000000000000000000000000..5d26e6ed097055bd56e8619358996529e0a59cd8 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/pem.py @@ -0,0 +1,134 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions that load and write PEM-encoded files.""" + +import base64 +import typing + +# Should either be ASCII strings or bytes. +FlexiText = typing.Union[str, bytes] + + +def _markers(pem_marker: FlexiText) -> typing.Tuple[bytes, bytes]: + """ + Returns the start and end PEM markers, as bytes. + """ + + if not isinstance(pem_marker, bytes): + pem_marker = pem_marker.encode("ascii") + + return ( + b"-----BEGIN " + pem_marker + b"-----", + b"-----END " + pem_marker + b"-----", + ) + + +def _pem_lines(contents: bytes, pem_start: bytes, pem_end: bytes) -> typing.Iterator[bytes]: + """Generator over PEM lines between pem_start and pem_end.""" + + in_pem_part = False + seen_pem_start = False + + for line in contents.splitlines(): + line = line.strip() + + # Skip empty lines + if not line: + continue + + # Handle start marker + if line == pem_start: + if in_pem_part: + raise ValueError('Seen start marker "%r" twice' % pem_start) + + in_pem_part = True + seen_pem_start = True + continue + + # Skip stuff before first marker + if not in_pem_part: + continue + + # Handle end marker + if in_pem_part and line == pem_end: + in_pem_part = False + break + + # Load fields + if b":" in line: + continue + + yield line + + # Do some sanity checks + if not seen_pem_start: + raise ValueError('No PEM start marker "%r" found' % pem_start) + + if in_pem_part: + raise ValueError('No PEM end marker "%r" found' % pem_end) + + +def load_pem(contents: FlexiText, pem_marker: FlexiText) -> bytes: + """Loads a PEM file. + + :param contents: the contents of the file to interpret + :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' + when your file has '-----BEGIN RSA PRIVATE KEY-----' and + '-----END RSA PRIVATE KEY-----' markers. + + :return: the base64-decoded content between the start and end markers. + + @raise ValueError: when the content is invalid, for example when the start + marker cannot be found. + + """ + + # We want bytes, not text. If it's text, it can be converted to ASCII bytes. + if not isinstance(contents, bytes): + contents = contents.encode("ascii") + + (pem_start, pem_end) = _markers(pem_marker) + pem_lines = [line for line in _pem_lines(contents, pem_start, pem_end)] + + # Base64-decode the contents + pem = b"".join(pem_lines) + return base64.standard_b64decode(pem) + + +def save_pem(contents: bytes, pem_marker: FlexiText) -> bytes: + """Saves a PEM file. + + :param contents: the contents to encode in PEM format + :param pem_marker: the marker of the PEM content, such as 'RSA PRIVATE KEY' + when your file has '-----BEGIN RSA PRIVATE KEY-----' and + '-----END RSA PRIVATE KEY-----' markers. + + :return: the base64-encoded content between the start and end markers, as bytes. + + """ + + (pem_start, pem_end) = _markers(pem_marker) + + b64 = base64.standard_b64encode(contents).replace(b"\n", b"") + pem_lines = [pem_start] + + for block_start in range(0, len(b64), 64): + block = b64[block_start : block_start + 64] + pem_lines.append(block) + + pem_lines.append(pem_end) + pem_lines.append(b"") + + return b"\n".join(pem_lines) diff --git a/lib/python3.10/site-packages/rsa/pkcs1.py b/lib/python3.10/site-packages/rsa/pkcs1.py new file mode 100644 index 0000000000000000000000000000000000000000..ec6998e537a26161f70dfb2267747e45e7c9d9ea --- /dev/null +++ b/lib/python3.10/site-packages/rsa/pkcs1.py @@ -0,0 +1,485 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for PKCS#1 version 1.5 encryption and signing + +This module implements certain functionality from PKCS#1 version 1.5. For a +very clear example, read http://www.di-mgt.com.au/rsa_alg.html#pkcs1schemes + +At least 8 bytes of random padding is used when encrypting a message. This makes +these methods much more secure than the ones in the ``rsa`` module. + +WARNING: this module leaks information when decryption fails. The exceptions +that are raised contain the Python traceback information, which can be used to +deduce where in the process the failure occurred. DO NOT PASS SUCH INFORMATION +to your users. +""" + +import hashlib +import os +import sys +import typing +from hmac import compare_digest + +from . import common, transform, core, key + +if typing.TYPE_CHECKING: + HashType = hashlib._Hash +else: + HashType = typing.Any + +# ASN.1 codes that describe the hash algorithm used. +HASH_ASN1 = { + "MD5": b"\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10", + "SHA-1": b"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", + "SHA-224": b"\x30\x2d\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x04\x05\x00\x04\x1c", + "SHA-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20", + "SHA-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30", + "SHA-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40", +} + +HASH_METHODS: typing.Dict[str, typing.Callable[[], HashType]] = { + "MD5": hashlib.md5, + "SHA-1": hashlib.sha1, + "SHA-224": hashlib.sha224, + "SHA-256": hashlib.sha256, + "SHA-384": hashlib.sha384, + "SHA-512": hashlib.sha512, +} +"""Hash methods supported by this library.""" + + +if sys.version_info >= (3, 6): + # Python 3.6 introduced SHA3 support. + HASH_ASN1.update( + { + "SHA3-256": b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x08\x05\x00\x04\x20", + "SHA3-384": b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x09\x05\x00\x04\x30", + "SHA3-512": b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x0a\x05\x00\x04\x40", + } + ) + + HASH_METHODS.update( + { + "SHA3-256": hashlib.sha3_256, + "SHA3-384": hashlib.sha3_384, + "SHA3-512": hashlib.sha3_512, + } + ) + + +class CryptoError(Exception): + """Base class for all exceptions in this module.""" + + +class DecryptionError(CryptoError): + """Raised when decryption fails.""" + + +class VerificationError(CryptoError): + """Raised when verification fails.""" + + +def _pad_for_encryption(message: bytes, target_length: int) -> bytes: + r"""Pads the message for encryption, returning the padded message. + + :return: 00 02 RANDOM_DATA 00 MESSAGE + + >>> block = _pad_for_encryption(b'hello', 16) + >>> len(block) + 16 + >>> block[0:2] + b'\x00\x02' + >>> block[-6:] + b'\x00hello' + + """ + + max_msglength = target_length - 11 + msglength = len(message) + + if msglength > max_msglength: + raise OverflowError( + "%i bytes needed for message, but there is only" + " space for %i" % (msglength, max_msglength) + ) + + # Get random padding + padding = b"" + padding_length = target_length - msglength - 3 + + # We remove 0-bytes, so we'll end up with less padding than we've asked for, + # so keep adding data until we're at the correct length. + while len(padding) < padding_length: + needed_bytes = padding_length - len(padding) + + # Always read at least 8 bytes more than we need, and trim off the rest + # after removing the 0-bytes. This increases the chance of getting + # enough bytes, especially when needed_bytes is small + new_padding = os.urandom(needed_bytes + 5) + new_padding = new_padding.replace(b"\x00", b"") + padding = padding + new_padding[:needed_bytes] + + assert len(padding) == padding_length + + return b"".join([b"\x00\x02", padding, b"\x00", message]) + + +def _pad_for_signing(message: bytes, target_length: int) -> bytes: + r"""Pads the message for signing, returning the padded message. + + The padding is always a repetition of FF bytes. + + :return: 00 01 PADDING 00 MESSAGE + + >>> block = _pad_for_signing(b'hello', 16) + >>> len(block) + 16 + >>> block[0:2] + b'\x00\x01' + >>> block[-6:] + b'\x00hello' + >>> block[2:-6] + b'\xff\xff\xff\xff\xff\xff\xff\xff' + + """ + + max_msglength = target_length - 11 + msglength = len(message) + + if msglength > max_msglength: + raise OverflowError( + "%i bytes needed for message, but there is only" + " space for %i" % (msglength, max_msglength) + ) + + padding_length = target_length - msglength - 3 + + return b"".join([b"\x00\x01", padding_length * b"\xff", b"\x00", message]) + + +def encrypt(message: bytes, pub_key: key.PublicKey) -> bytes: + """Encrypts the given message using PKCS#1 v1.5 + + :param message: the message to encrypt. Must be a byte string no longer than + ``k-11`` bytes, where ``k`` is the number of bytes needed to encode + the ``n`` component of the public key. + :param pub_key: the :py:class:`rsa.PublicKey` to encrypt with. + :raise OverflowError: when the message is too large to fit in the padded + block. + + >>> from rsa import key, common + >>> (pub_key, priv_key) = key.newkeys(256) + >>> message = b'hello' + >>> crypto = encrypt(message, pub_key) + + The crypto text should be just as long as the public key 'n' component: + + >>> len(crypto) == common.byte_size(pub_key.n) + True + + """ + + keylength = common.byte_size(pub_key.n) + padded = _pad_for_encryption(message, keylength) + + payload = transform.bytes2int(padded) + encrypted = core.encrypt_int(payload, pub_key.e, pub_key.n) + block = transform.int2bytes(encrypted, keylength) + + return block + + +def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes: + r"""Decrypts the given message using PKCS#1 v1.5 + + The decryption is considered 'failed' when the resulting cleartext doesn't + start with the bytes 00 02, or when the 00 byte between the padding and + the message cannot be found. + + :param crypto: the crypto text as returned by :py:func:`rsa.encrypt` + :param priv_key: the :py:class:`rsa.PrivateKey` to decrypt with. + :raise DecryptionError: when the decryption fails. No details are given as + to why the code thinks the decryption fails, as this would leak + information about the private key. + + + >>> import rsa + >>> (pub_key, priv_key) = rsa.newkeys(256) + + It works with strings: + + >>> crypto = encrypt(b'hello', pub_key) + >>> decrypt(crypto, priv_key) + b'hello' + + And with binary data: + + >>> crypto = encrypt(b'\x00\x00\x00\x00\x01', pub_key) + >>> decrypt(crypto, priv_key) + b'\x00\x00\x00\x00\x01' + + Altering the encrypted information will *likely* cause a + :py:class:`rsa.pkcs1.DecryptionError`. If you want to be *sure*, use + :py:func:`rsa.sign`. + + + .. warning:: + + Never display the stack trace of a + :py:class:`rsa.pkcs1.DecryptionError` exception. It shows where in the + code the exception occurred, and thus leaks information about the key. + It's only a tiny bit of information, but every bit makes cracking the + keys easier. + + >>> crypto = encrypt(b'hello', pub_key) + >>> crypto = crypto[0:5] + b'X' + crypto[6:] # change a byte + >>> decrypt(crypto, priv_key) + Traceback (most recent call last): + ... + rsa.pkcs1.DecryptionError: Decryption failed + + """ + + blocksize = common.byte_size(priv_key.n) + encrypted = transform.bytes2int(crypto) + decrypted = priv_key.blinded_decrypt(encrypted) + cleartext = transform.int2bytes(decrypted, blocksize) + + # Detect leading zeroes in the crypto. These are not reflected in the + # encrypted value (as leading zeroes do not influence the value of an + # integer). This fixes CVE-2020-13757. + if len(crypto) > blocksize: + # This is operating on public information, so doesn't need to be constant-time. + raise DecryptionError("Decryption failed") + + # If we can't find the cleartext marker, decryption failed. + cleartext_marker_bad = not compare_digest(cleartext[:2], b"\x00\x02") + + # Find the 00 separator between the padding and the message + sep_idx = cleartext.find(b"\x00", 2) + + # sep_idx indicates the position of the `\x00` separator that separates the + # padding from the actual message. The padding should be at least 8 bytes + # long (see https://tools.ietf.org/html/rfc8017#section-7.2.2 step 3), which + # means the separator should be at least at index 10 (because of the + # `\x00\x02` marker that precedes it). + sep_idx_bad = sep_idx < 10 + + anything_bad = cleartext_marker_bad | sep_idx_bad + if anything_bad: + raise DecryptionError("Decryption failed") + + return cleartext[sep_idx + 1 :] + + +def sign_hash(hash_value: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs a precomputed hash with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param hash_value: A precomputed hash to sign (ignores message). + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + # Get the ASN1 code for this hash method + if hash_method not in HASH_ASN1: + raise ValueError("Invalid hash method: %s" % hash_method) + asn1code = HASH_ASN1[hash_method] + + # Encrypt the hash with the private key + cleartext = asn1code + hash_value + keylength = common.byte_size(priv_key.n) + padded = _pad_for_signing(cleartext, keylength) + + payload = transform.bytes2int(padded) + encrypted = priv_key.blinded_encrypt(payload) + block = transform.int2bytes(encrypted, keylength) + + return block + + +def sign(message: bytes, priv_key: key.PrivateKey, hash_method: str) -> bytes: + """Signs the message with the private key. + + Hashes the message, then signs the hash with the given key. This is known + as a "detached signature", because the message itself isn't altered. + + :param message: the message to sign. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param priv_key: the :py:class:`rsa.PrivateKey` to sign with + :param hash_method: the hash method used on the message. Use 'MD5', 'SHA-1', + 'SHA-224', SHA-256', 'SHA-384' or 'SHA-512'. + :return: a message signature block. + :raise OverflowError: if the private key is too small to contain the + requested hash. + + """ + + msg_hash = compute_hash(message, hash_method) + return sign_hash(msg_hash, priv_key, hash_method) + + +def verify(message: bytes, signature: bytes, pub_key: key.PublicKey) -> str: + """Verifies that the signature matches the message. + + The hash method is detected automatically from the signature. + + :param message: the signed message. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param signature: the signature block, as created with :py:func:`rsa.sign`. + :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. + :raise VerificationError: when the signature doesn't match the message. + :returns: the name of the used hash. + + """ + + keylength = common.byte_size(pub_key.n) + encrypted = transform.bytes2int(signature) + decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) + clearsig = transform.int2bytes(decrypted, keylength) + + # Get the hash method + method_name = _find_method_hash(clearsig) + message_hash = compute_hash(message, method_name) + + # Reconstruct the expected padded hash + cleartext = HASH_ASN1[method_name] + message_hash + expected = _pad_for_signing(cleartext, keylength) + + if len(signature) != keylength: + raise VerificationError("Verification failed") + + # Compare with the signed one + if expected != clearsig: + raise VerificationError("Verification failed") + + return method_name + + +def find_signature_hash(signature: bytes, pub_key: key.PublicKey) -> str: + """Returns the hash name detected from the signature. + + If you also want to verify the message, use :py:func:`rsa.verify()` instead. + It also returns the name of the used hash. + + :param signature: the signature block, as created with :py:func:`rsa.sign`. + :param pub_key: the :py:class:`rsa.PublicKey` of the person signing the message. + :returns: the name of the used hash. + """ + + keylength = common.byte_size(pub_key.n) + encrypted = transform.bytes2int(signature) + decrypted = core.decrypt_int(encrypted, pub_key.e, pub_key.n) + clearsig = transform.int2bytes(decrypted, keylength) + + return _find_method_hash(clearsig) + + +def yield_fixedblocks(infile: typing.BinaryIO, blocksize: int) -> typing.Iterator[bytes]: + """Generator, yields each block of ``blocksize`` bytes in the input file. + + :param infile: file to read and separate in blocks. + :param blocksize: block size in bytes. + :returns: a generator that yields the contents of each block + """ + + while True: + block = infile.read(blocksize) + + read_bytes = len(block) + if read_bytes == 0: + break + + yield block + + if read_bytes < blocksize: + break + + +def compute_hash(message: typing.Union[bytes, typing.BinaryIO], method_name: str) -> bytes: + """Returns the message digest. + + :param message: the signed message. Can be an 8-bit string or a file-like + object. If ``message`` has a ``read()`` method, it is assumed to be a + file-like object. + :param method_name: the hash method, must be a key of + :py:const:`rsa.pkcs1.HASH_METHODS`. + + """ + + if method_name not in HASH_METHODS: + raise ValueError("Invalid hash method: %s" % method_name) + + method = HASH_METHODS[method_name] + hasher = method() + + if isinstance(message, bytes): + hasher.update(message) + else: + assert hasattr(message, "read") and hasattr(message.read, "__call__") + # read as 1K blocks + for block in yield_fixedblocks(message, 1024): + hasher.update(block) + + return hasher.digest() + + +def _find_method_hash(clearsig: bytes) -> str: + """Finds the hash method. + + :param clearsig: full padded ASN1 and hash. + :return: the used hash method. + :raise VerificationFailed: when the hash method cannot be found + """ + + for (hashname, asn1code) in HASH_ASN1.items(): + if asn1code in clearsig: + return hashname + + raise VerificationError("Verification failed") + + +__all__ = [ + "encrypt", + "decrypt", + "sign", + "verify", + "DecryptionError", + "VerificationError", + "CryptoError", +] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/lib/python3.10/site-packages/rsa/pkcs1_v2.py b/lib/python3.10/site-packages/rsa/pkcs1_v2.py new file mode 100644 index 0000000000000000000000000000000000000000..d68b9077215d0d69e181ccda47d6969cd4b245a3 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/pkcs1_v2.py @@ -0,0 +1,100 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for PKCS#1 version 2 encryption and signing + +This module implements certain functionality from PKCS#1 version 2. Main +documentation is RFC 2437: https://tools.ietf.org/html/rfc2437 +""" + +from rsa import ( + common, + pkcs1, + transform, +) + + +def mgf1(seed: bytes, length: int, hasher: str = "SHA-1") -> bytes: + """ + MGF1 is a Mask Generation Function based on a hash function. + + A mask generation function takes an octet string of variable length and a + desired output length as input, and outputs an octet string of the desired + length. The plaintext-awareness of RSAES-OAEP relies on the random nature of + the output of the mask generation function, which in turn relies on the + random nature of the underlying hash. + + :param bytes seed: seed from which mask is generated, an octet string + :param int length: intended length in octets of the mask, at most 2^32(hLen) + :param str hasher: hash function (hLen denotes the length in octets of the hash + function output) + + :return: mask, an octet string of length `length` + :rtype: bytes + + :raise OverflowError: when `length` is too large for the specified `hasher` + :raise ValueError: when specified `hasher` is invalid + """ + + try: + hash_length = pkcs1.HASH_METHODS[hasher]().digest_size + except KeyError as ex: + raise ValueError( + "Invalid `hasher` specified. Please select one of: {hash_list}".format( + hash_list=", ".join(sorted(pkcs1.HASH_METHODS.keys())) + ) + ) from ex + + # If l > 2^32(hLen), output "mask too long" and stop. + if length > (2 ** 32 * hash_length): + raise OverflowError( + "Desired length should be at most 2**32 times the hasher's output " + "length ({hash_length} for {hasher} function)".format( + hash_length=hash_length, + hasher=hasher, + ) + ) + + # Looping `counter` from 0 to ceil(l / hLen)-1, build `output` based on the + # hashes formed by (`seed` + C), being `C` an octet string of length 4 + # generated by converting `counter` with the primitive I2OSP + output = b"".join( + pkcs1.compute_hash( + seed + transform.int2bytes(counter, fill_size=4), + method_name=hasher, + ) + for counter in range(common.ceil_div(length, hash_length) + 1) + ) + + # Output the leading `length` octets of `output` as the octet string mask. + return output[:length] + + +__all__ = [ + "mgf1", +] + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/lib/python3.10/site-packages/rsa/prime.py b/lib/python3.10/site-packages/rsa/prime.py new file mode 100644 index 0000000000000000000000000000000000000000..ec486bcc051595a4a0119d8e2b2edb24137555e4 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/prime.py @@ -0,0 +1,198 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Numerical functions related to primes. + +Implementation based on the book Algorithm Design by Michael T. Goodrich and +Roberto Tamassia, 2002. +""" + +import rsa.common +import rsa.randnum + +__all__ = ["getprime", "are_relatively_prime"] + + +def gcd(p: int, q: int) -> int: + """Returns the greatest common divisor of p and q + + >>> gcd(48, 180) + 12 + """ + + while q != 0: + (p, q) = (q, p % q) + return p + + +def get_primality_testing_rounds(number: int) -> int: + """Returns minimum number of rounds for Miller-Rabing primality testing, + based on number bitsize. + + According to NIST FIPS 186-4, Appendix C, Table C.3, minimum number of + rounds of M-R testing, using an error probability of 2 ** (-100), for + different p, q bitsizes are: + * p, q bitsize: 512; rounds: 7 + * p, q bitsize: 1024; rounds: 4 + * p, q bitsize: 1536; rounds: 3 + See: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf + """ + + # Calculate number bitsize. + bitsize = rsa.common.bit_size(number) + # Set number of rounds. + if bitsize >= 1536: + return 3 + if bitsize >= 1024: + return 4 + if bitsize >= 512: + return 7 + # For smaller bitsizes, set arbitrary number of rounds. + return 10 + + +def miller_rabin_primality_testing(n: int, k: int) -> bool: + """Calculates whether n is composite (which is always correct) or prime + (which theoretically is incorrect with error probability 4**-k), by + applying Miller-Rabin primality testing. + + For reference and implementation example, see: + https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test + + :param n: Integer to be tested for primality. + :type n: int + :param k: Number of rounds (witnesses) of Miller-Rabin testing. + :type k: int + :return: False if the number is composite, True if it's probably prime. + :rtype: bool + """ + + # prevent potential infinite loop when d = 0 + if n < 2: + return False + + # Decompose (n - 1) to write it as (2 ** r) * d + # While d is even, divide it by 2 and increase the exponent. + d = n - 1 + r = 0 + + while not (d & 1): + r += 1 + d >>= 1 + + # Test k witnesses. + for _ in range(k): + # Generate random integer a, where 2 <= a <= (n - 2) + a = rsa.randnum.randint(n - 3) + 1 + + x = pow(a, d, n) + if x == 1 or x == n - 1: + continue + + for _ in range(r - 1): + x = pow(x, 2, n) + if x == 1: + # n is composite. + return False + if x == n - 1: + # Exit inner loop and continue with next witness. + break + else: + # If loop doesn't break, n is composite. + return False + + return True + + +def is_prime(number: int) -> bool: + """Returns True if the number is prime, and False otherwise. + + >>> is_prime(2) + True + >>> is_prime(42) + False + >>> is_prime(41) + True + """ + + # Check for small numbers. + if number < 10: + return number in {2, 3, 5, 7} + + # Check for even numbers. + if not (number & 1): + return False + + # Calculate minimum number of rounds. + k = get_primality_testing_rounds(number) + + # Run primality testing with (minimum + 1) rounds. + return miller_rabin_primality_testing(number, k + 1) + + +def getprime(nbits: int) -> int: + """Returns a prime number that can be stored in 'nbits' bits. + + >>> p = getprime(128) + >>> is_prime(p-1) + False + >>> is_prime(p) + True + >>> is_prime(p+1) + False + + >>> from rsa import common + >>> common.bit_size(p) == 128 + True + """ + + assert nbits > 3 # the loop will hang on too small numbers + + while True: + integer = rsa.randnum.read_random_odd_int(nbits) + + # Test for primeness + if is_prime(integer): + return integer + + # Retry if not prime + + +def are_relatively_prime(a: int, b: int) -> bool: + """Returns True if a and b are relatively prime, and False if they + are not. + + >>> are_relatively_prime(2, 3) + True + >>> are_relatively_prime(2, 4) + False + """ + + d = gcd(a, b) + return d == 1 + + +if __name__ == "__main__": + print("Running doctests 1000x or until failure") + import doctest + + for count in range(1000): + (failures, tests) = doctest.testmod() + if failures: + break + + if count % 100 == 0 and count: + print("%i times" % count) + + print("Doctests done") diff --git a/lib/python3.10/site-packages/rsa/py.typed b/lib/python3.10/site-packages/rsa/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..6c27071a3915dec7d5f6c67b50e7b00442c9d3d9 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The rsa package uses inline types. diff --git a/lib/python3.10/site-packages/rsa/randnum.py b/lib/python3.10/site-packages/rsa/randnum.py new file mode 100644 index 0000000000000000000000000000000000000000..c65facddc70f9d4da9d12c94ccd8e334b03c954f --- /dev/null +++ b/lib/python3.10/site-packages/rsa/randnum.py @@ -0,0 +1,95 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions for generating random numbers.""" + +# Source inspired by code by Yesudeep Mangalapilly + +import os +import struct + +from rsa import common, transform + + +def read_random_bits(nbits: int) -> bytes: + """Reads 'nbits' random bits. + + If nbits isn't a whole number of bytes, an extra byte will be appended with + only the lower bits set. + """ + + nbytes, rbits = divmod(nbits, 8) + + # Get the random bytes + randomdata = os.urandom(nbytes) + + # Add the remaining random bits + if rbits > 0: + randomvalue = ord(os.urandom(1)) + randomvalue >>= 8 - rbits + randomdata = struct.pack("B", randomvalue) + randomdata + + return randomdata + + +def read_random_int(nbits: int) -> int: + """Reads a random integer of approximately nbits bits.""" + + randomdata = read_random_bits(nbits) + value = transform.bytes2int(randomdata) + + # Ensure that the number is large enough to just fill out the required + # number of bits. + value |= 1 << (nbits - 1) + + return value + + +def read_random_odd_int(nbits: int) -> int: + """Reads a random odd integer of approximately nbits bits. + + >>> read_random_odd_int(512) & 1 + 1 + """ + + value = read_random_int(nbits) + + # Make sure it's odd + return value | 1 + + +def randint(maxvalue: int) -> int: + """Returns a random integer x with 1 <= x <= maxvalue + + May take a very long time in specific situations. If maxvalue needs N bits + to store, the closer maxvalue is to (2 ** N) - 1, the faster this function + is. + """ + + bit_size = common.bit_size(maxvalue) + + tries = 0 + while True: + value = read_random_int(bit_size) + if value <= maxvalue: + break + + if tries % 10 == 0 and tries: + # After a lot of tries to get the right number of bits but still + # smaller than maxvalue, decrease the number of bits by 1. That'll + # dramatically increase the chances to get a large enough number. + bit_size -= 1 + tries += 1 + + return value diff --git a/lib/python3.10/site-packages/rsa/transform.py b/lib/python3.10/site-packages/rsa/transform.py new file mode 100644 index 0000000000000000000000000000000000000000..c609b65f3cdb8877efd7cb071974de87a794acc3 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/transform.py @@ -0,0 +1,72 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Data transformation functions. + +From bytes to a number, number to bytes, etc. +""" + +import math + + +def bytes2int(raw_bytes: bytes) -> int: + r"""Converts a list of bytes or an 8-bit string to an integer. + + When using unicode strings, encode it to some encoding like UTF8 first. + + >>> (((128 * 256) + 64) * 256) + 15 + 8405007 + >>> bytes2int(b'\x80@\x0f') + 8405007 + + """ + return int.from_bytes(raw_bytes, "big", signed=False) + + +def int2bytes(number: int, fill_size: int = 0) -> bytes: + """ + Convert an unsigned integer to bytes (big-endian):: + + Does not preserve leading zeros if you don't specify a fill size. + + :param number: + Integer value + :param fill_size: + If the optional fill size is given the length of the resulting + byte string is expected to be the fill size and will be padded + with prefix zero bytes to satisfy that length. + :returns: + Raw bytes (base-256 representation). + :raises: + ``OverflowError`` when fill_size is given and the number takes up more + bytes than fit into the block. This requires the ``overflow`` + argument to this function to be set to ``False`` otherwise, no + error will be raised. + """ + + if number < 0: + raise ValueError("Number must be an unsigned integer: %d" % number) + + bytes_required = max(1, math.ceil(number.bit_length() / 8)) + + if fill_size > 0: + return number.to_bytes(fill_size, "big") + + return number.to_bytes(bytes_required, "big") + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/lib/python3.10/site-packages/rsa/util.py b/lib/python3.10/site-packages/rsa/util.py new file mode 100644 index 0000000000000000000000000000000000000000..087caf8df5ecdafbbfc0b7dcf5dc0f447fbc7946 --- /dev/null +++ b/lib/python3.10/site-packages/rsa/util.py @@ -0,0 +1,97 @@ +# Copyright 2011 Sybren A. Stüvel +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions.""" + +import sys +from optparse import OptionParser + +import rsa.key + + +def private_to_public() -> None: + """Reads a private key and outputs the corresponding public key.""" + + # Parse the CLI options + parser = OptionParser( + usage="usage: %prog [options]", + description="Reads a private key and outputs the " + "corresponding public key. Both private and public keys use " + "the format described in PKCS#1 v1.5", + ) + + parser.add_option( + "-i", + "--input", + dest="infilename", + type="string", + help="Input filename. Reads from stdin if not specified", + ) + parser.add_option( + "-o", + "--output", + dest="outfilename", + type="string", + help="Output filename. Writes to stdout of not specified", + ) + + parser.add_option( + "--inform", + dest="inform", + help="key format of input - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + parser.add_option( + "--outform", + dest="outform", + help="key format of output - default PEM", + choices=("PEM", "DER"), + default="PEM", + ) + + (cli, cli_args) = parser.parse_args(sys.argv) + + # Read the input data + if cli.infilename: + print( + "Reading private key from %s in %s format" % (cli.infilename, cli.inform), + file=sys.stderr, + ) + with open(cli.infilename, "rb") as infile: + in_data = infile.read() + else: + print("Reading private key from stdin in %s format" % cli.inform, file=sys.stderr) + in_data = sys.stdin.read().encode("ascii") + + assert type(in_data) == bytes, type(in_data) + + # Take the public fields and create a public key + priv_key = rsa.key.PrivateKey.load_pkcs1(in_data, cli.inform) + pub_key = rsa.key.PublicKey(priv_key.n, priv_key.e) + + # Save to the output file + out_data = pub_key.save_pkcs1(cli.outform) + + if cli.outfilename: + print( + "Writing public key to %s in %s format" % (cli.outfilename, cli.outform), + file=sys.stderr, + ) + with open(cli.outfilename, "wb") as outfile: + outfile.write(out_data) + else: + print("Writing public key to stdout in %s format" % cli.outform, file=sys.stderr) + sys.stdout.write(out_data.decode("ascii")) diff --git a/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/INSTALLER b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/LICENSE.txt b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..710ea3c93015813ea401bebbfd85073a2c6ecdfc --- /dev/null +++ b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/LICENSE.txt @@ -0,0 +1,152 @@ +Files: * +Copyright: 2009-2022 the scikit-image team +License: BSD-3-Clause + +Files: doc/source/themes/scikit-image/layout.html +Copyright: 2007-2010 the Sphinx team +License: BSD-3-Clause + +Files: skimage/feature/_canny.py + skimage/filters/edges.py + skimage/filters/_rank_order.py + skimage/morphology/_skeletonize.py + skimage/morphology/tests/test_watershed.py + skimage/morphology/watershed.py + skimage/segmentation/heap_general.pxi + skimage/segmentation/heap_watershed.pxi + skimage/segmentation/_watershed.py + skimage/segmentation/_watershed_cy.pyx +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2003-2005 Peter J. Verveer +License: BSD-3-Clause + +Files: skimage/filters/thresholding.py + skimage/graph/_mcp.pyx + skimage/graph/heap.pyx +Copyright: 2009-2015 Board of Regents of the University of + Wisconsin-Madison, Broad Institute of MIT and Harvard, + and Max Planck Institute of Molecular Cell Biology and + Genetics + 2009 Zachary Pincus + 2009 Almar Klein +License: BSD-2-Clause + +File: skimage/morphology/grayreconstruct.py + skimage/morphology/tests/test_reconstruction.py +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky +License: BSD-3-Clause + +File: skimage/morphology/_grayreconstruct.pyx +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2022 Gregory Lee (added a 64-bit integer variant for large images) +License: BSD-3-Clause + +File: skimage/segmentation/_expand_labels.py +Copyright: 2020 Broad Institute + 2020 CellProfiler team +License: BSD-3-Clause + +File: skimage/exposure/_adapthist.py +Copyright: 1994 Karel Zuiderveld +License: BSD-3-Clause + +Function: skimage/morphology/_skeletonize_various_cy.pyx:_skeletonize_loop +Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky +License: BSD-3-Clause + +Function: skimage/_shared/version_requirements.py:_check_version +Copyright: 2013 The IPython Development Team +License: BSD-3-Clause + +Function: skimage/_shared/version_requirements.py:is_installed +Copyright: 2009-2011 Pierre Raybaut +License: MIT + +File: skimage/feature/_fisher_vector.py +Copyright: 2014 2014 Dan Oneata +License: MIT + +File: skimage/_vendored/numpy_lookfor.py +Copyright: 2005-2023, NumPy Developers +License: BSD-3-Clause + +File: skimage/transform/_thin_plate_splines.py +Copyright: 2007 Zachary Pincus +License: BSD-3-Clause + +License: BSD-2-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. +. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License: MIT + +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/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/METADATA b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..e7dc42d9da9effd818907d56349eee460ea976ef --- /dev/null +++ b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/METADATA @@ -0,0 +1,280 @@ +Metadata-Version: 2.1 +Name: scikit-image +Version: 0.25.2 +Summary: Image processing in Python +Maintainer-Email: scikit-image developers +License: Files: * + Copyright: 2009-2022 the scikit-image team + License: BSD-3-Clause + + Files: doc/source/themes/scikit-image/layout.html + Copyright: 2007-2010 the Sphinx team + License: BSD-3-Clause + + Files: skimage/feature/_canny.py + skimage/filters/edges.py + skimage/filters/_rank_order.py + skimage/morphology/_skeletonize.py + skimage/morphology/tests/test_watershed.py + skimage/morphology/watershed.py + skimage/segmentation/heap_general.pxi + skimage/segmentation/heap_watershed.pxi + skimage/segmentation/_watershed.py + skimage/segmentation/_watershed_cy.pyx + Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2003-2005 Peter J. Verveer + License: BSD-3-Clause + + Files: skimage/filters/thresholding.py + skimage/graph/_mcp.pyx + skimage/graph/heap.pyx + Copyright: 2009-2015 Board of Regents of the University of + Wisconsin-Madison, Broad Institute of MIT and Harvard, + and Max Planck Institute of Molecular Cell Biology and + Genetics + 2009 Zachary Pincus + 2009 Almar Klein + License: BSD-2-Clause + + File: skimage/morphology/grayreconstruct.py + skimage/morphology/tests/test_reconstruction.py + Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + License: BSD-3-Clause + + File: skimage/morphology/_grayreconstruct.pyx + Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + 2022 Gregory Lee (added a 64-bit integer variant for large images) + License: BSD-3-Clause + + File: skimage/segmentation/_expand_labels.py + Copyright: 2020 Broad Institute + 2020 CellProfiler team + License: BSD-3-Clause + + File: skimage/exposure/_adapthist.py + Copyright: 1994 Karel Zuiderveld + License: BSD-3-Clause + + Function: skimage/morphology/_skeletonize_various_cy.pyx:_skeletonize_loop + Copyright: 2003-2009 Massachusetts Institute of Technology + 2009-2011 Broad Institute + 2003 Lee Kamentsky + License: BSD-3-Clause + + Function: skimage/_shared/version_requirements.py:_check_version + Copyright: 2013 The IPython Development Team + License: BSD-3-Clause + + Function: skimage/_shared/version_requirements.py:is_installed + Copyright: 2009-2011 Pierre Raybaut + License: MIT + + File: skimage/feature/_fisher_vector.py + Copyright: 2014 2014 Dan Oneata + License: MIT + + File: skimage/_vendored/numpy_lookfor.py + Copyright: 2005-2023, NumPy Developers + License: BSD-3-Clause + + File: skimage/transform/_thin_plate_splines.py + Copyright: 2007 Zachary Pincus + License: BSD-3-Clause + + License: BSD-2-Clause + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + License: BSD-3-Clause + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE HOLDERS OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + License: MIT + + 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. + +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: C +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 :: Only +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Scientific/Engineering +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +Project-URL: homepage, https://scikit-image.org +Project-URL: documentation, https://scikit-image.org/docs/stable +Project-URL: source, https://github.com/scikit-image/scikit-image +Project-URL: download, https://pypi.org/project/scikit-image/#files +Project-URL: tracker, https://github.com/scikit-image/scikit-image/issues +Requires-Python: >=3.10 +Requires-Dist: numpy>=1.24 +Requires-Dist: scipy>=1.11.4 +Requires-Dist: networkx>=3.0 +Requires-Dist: pillow>=10.1 +Requires-Dist: imageio!=2.35.0,>=2.33 +Requires-Dist: tifffile>=2022.8.12 +Requires-Dist: packaging>=21 +Requires-Dist: lazy-loader>=0.4 +Provides-Extra: build +Requires-Dist: meson-python>=0.16; extra == "build" +Requires-Dist: ninja>=1.11.1.1; extra == "build" +Requires-Dist: Cython>=3.0.8; extra == "build" +Requires-Dist: pythran>=0.16; extra == "build" +Requires-Dist: numpy>=2.0; extra == "build" +Requires-Dist: spin==0.13; extra == "build" +Requires-Dist: build>=1.2.1; extra == "build" +Provides-Extra: data +Requires-Dist: pooch>=1.6.0; extra == "data" +Provides-Extra: developer +Requires-Dist: pre-commit; extra == "developer" +Requires-Dist: ipython; extra == "developer" +Requires-Dist: tomli; python_version < "3.11" and extra == "developer" +Provides-Extra: docs +Requires-Dist: sphinx>=8.0; extra == "docs" +Requires-Dist: sphinx-gallery[parallel]>=0.18; extra == "docs" +Requires-Dist: numpydoc>=1.7; extra == "docs" +Requires-Dist: sphinx-copybutton; extra == "docs" +Requires-Dist: matplotlib>=3.7; extra == "docs" +Requires-Dist: dask[array]>=2023.2.0; extra == "docs" +Requires-Dist: pandas>=2.0; extra == "docs" +Requires-Dist: seaborn>=0.11; extra == "docs" +Requires-Dist: pooch>=1.6; extra == "docs" +Requires-Dist: tifffile>=2022.8.12; extra == "docs" +Requires-Dist: myst-parser; extra == "docs" +Requires-Dist: intersphinx-registry>=0.2411.14; extra == "docs" +Requires-Dist: ipywidgets; extra == "docs" +Requires-Dist: ipykernel; extra == "docs" +Requires-Dist: plotly>=5.20; extra == "docs" +Requires-Dist: kaleido==0.2.1; extra == "docs" +Requires-Dist: scikit-learn>=1.2; extra == "docs" +Requires-Dist: sphinx_design>=0.5; extra == "docs" +Requires-Dist: pydata-sphinx-theme>=0.16; extra == "docs" +Requires-Dist: PyWavelets>=1.6; extra == "docs" +Requires-Dist: pytest-doctestplus; extra == "docs" +Provides-Extra: optional +Requires-Dist: SimpleITK; extra == "optional" +Requires-Dist: astropy>=5.0; extra == "optional" +Requires-Dist: cloudpickle>=1.1.1; extra == "optional" +Requires-Dist: dask[array]>=2023.2.0; extra == "optional" +Requires-Dist: matplotlib>=3.7; extra == "optional" +Requires-Dist: pooch>=1.6.0; extra == "optional" +Requires-Dist: pyamg>=5.2; extra == "optional" +Requires-Dist: PyWavelets>=1.6; extra == "optional" +Requires-Dist: scikit-learn>=1.2; extra == "optional" +Provides-Extra: test +Requires-Dist: asv; extra == "test" +Requires-Dist: numpydoc>=1.7; extra == "test" +Requires-Dist: pooch>=1.6.0; extra == "test" +Requires-Dist: pytest>=8; extra == "test" +Requires-Dist: pytest-cov>=2.11.0; extra == "test" +Requires-Dist: pytest-localserver; extra == "test" +Requires-Dist: pytest-faulthandler; extra == "test" +Requires-Dist: pytest-doctestplus; extra == "test" +Description-Content-Type: text/markdown + +# scikit-image: Image processing in Python + +[![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftags%2Fscikit-image.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tags/scikit-image) +[![Stackoverflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)](https://stackoverflow.com/questions/tagged/scikit-image) +[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://skimage.zulipchat.com) + +- **Website (including documentation):** [https://scikit-image.org/](https://scikit-image.org) +- **Documentation:** [https://scikit-image.org/docs/stable/](https://scikit-image.org/docs/stable/) +- **User forum:** [https://forum.image.sc/tag/scikit-image](https://forum.image.sc/tag/scikit-image) +- **Developer forum:** [https://discuss.scientific-python.org/c/contributor/skimage](https://discuss.scientific-python.org/c/contributor/skimage) +- **Source:** [https://github.com/scikit-image/scikit-image](https://github.com/scikit-image/scikit-image) + +## Installation + +- **pip:** `pip install scikit-image` +- **conda:** `conda install -c conda-forge scikit-image` + +Also see [installing `scikit-image`](https://github.com/scikit-image/scikit-image/blob/main/INSTALL.rst). + +## License + +See [LICENSE.txt](https://github.com/scikit-image/scikit-image/blob/main/LICENSE.txt). + +## Citation + +If you find this project useful, please cite: + +> Stéfan van der Walt, Johannes L. Schönberger, Juan Nunez-Iglesias, +> François Boulogne, Joshua D. Warner, Neil Yager, Emmanuelle +> Gouillart, Tony Yu, and the scikit-image contributors. +> _scikit-image: Image processing in Python_. PeerJ 2:e453 (2014) +> https://doi.org/10.7717/peerj.453 diff --git a/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/RECORD b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..36ddf115d3aef6a9e4285150b557db1ce6b5bdda --- /dev/null +++ b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/RECORD @@ -0,0 +1,830 @@ +scikit_image-0.25.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +scikit_image-0.25.2.dist-info/LICENSE.txt,sha256=YR0yB1BNy0qAjfIJ0ufJtPLon-aQ-Ec1wNVIFglmtSc,6435 +scikit_image-0.25.2.dist-info/METADATA,sha256=78C5N2fDpfI5xcj4NdqaE63c-rlTOmlxFzo20Gh9uTQ,14355 +scikit_image-0.25.2.dist-info/RECORD,, +scikit_image-0.25.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scikit_image-0.25.2.dist-info/WHEEL,sha256=sZM_NeUMz2G4fDenMf11eikcCxcLaQWiYRmjwQBavQs,137 +skimage/__init__.py,sha256=0xoAHLxU20QDimJE--uurfxEDzWk1ySZ11yV9x0ODd4,4107 +skimage/__init__.pyi,sha256=-q2FUJzv9v_H7mN2vSJuwDSZedVPCpVDUQQb4-sz5i0,848 +skimage/__pycache__/__init__.cpython-310.pyc,, +skimage/__pycache__/conftest.cpython-310.pyc,, +skimage/_shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/_shared/__pycache__/__init__.cpython-310.pyc,, +skimage/_shared/__pycache__/_dependency_checks.cpython-310.pyc,, +skimage/_shared/__pycache__/_geometry.cpython-310.pyc,, +skimage/_shared/__pycache__/_tempfile.cpython-310.pyc,, +skimage/_shared/__pycache__/_warnings.cpython-310.pyc,, +skimage/_shared/__pycache__/compat.cpython-310.pyc,, +skimage/_shared/__pycache__/coord.cpython-310.pyc,, +skimage/_shared/__pycache__/dtype.cpython-310.pyc,, +skimage/_shared/__pycache__/filters.cpython-310.pyc,, +skimage/_shared/__pycache__/tester.cpython-310.pyc,, +skimage/_shared/__pycache__/testing.cpython-310.pyc,, +skimage/_shared/__pycache__/utils.cpython-310.pyc,, +skimage/_shared/__pycache__/version_requirements.cpython-310.pyc,, +skimage/_shared/_dependency_checks.py,sha256=6ZaHZf0d65U5cGMtSEegDUYQ_gJL1wSkC-w9zZkcA18,211 +skimage/_shared/_geometry.py,sha256=vagNwhLEcKvM0k19agDXvhnZDjwjTL9au5O-CSuDCQY,1343 +skimage/_shared/_tempfile.py,sha256=lWfbjc6DHgndl9D8L4Gflq6fPVfVp-GSOwLgc9mdmpo,763 +skimage/_shared/_warnings.py,sha256=daVYJAaNlKvzrYNfoG6uRkz7IQos6zTkqZjnVflsfFE,5225 +skimage/_shared/compat.py,sha256=GwI-eClJ6GpQgURalFbNfkH4P0az0441TRRwwPUxqus,976 +skimage/_shared/coord.py,sha256=YgUPW4TYe1j3cYTGnlGYqnbRAtroPWzsyten1A3-gIw,4329 +skimage/_shared/dtype.py,sha256=-6l3DaRdHqFRB9EnvMU5xqHxwN7c519Svv47qy1R-Sk,2397 +skimage/_shared/fast_exp.cpython-310-x86_64-linux-gnu.so,sha256=xHFlarmwJGAi96I6307jAYBcmQRgO5vsgeYEyPUOEsU,83280 +skimage/_shared/fast_exp.h,sha256=HtIZ7X68IvcNN47SM5E1SxBgvVYziJKvzvawzO94vi8,1246 +skimage/_shared/filters.py,sha256=yQZFHt97P8XyQuZ5qEkVxBT1KRSXoWBbBQf2NimST0I,4877 +skimage/_shared/geometry.cpython-310-x86_64-linux-gnu.so,sha256=Lj1qc0-lePEIZ4E-rbMMzSLMmtC8mqgCMo6lvQgKqCg,198288 +skimage/_shared/interpolation.cpython-310-x86_64-linux-gnu.so,sha256=9O1-hPJtqPljEkGf29cUa_dJj5o5h-YmEJNIJCMpzUI,59120 +skimage/_shared/tester.py,sha256=bA-Foz30kjF7LLpnUsOcllI5HS38f7e7hogP0uxG0vo,3589 +skimage/_shared/testing.py,sha256=EUPc8YyzJJdOuw4HQWt7cKT_CVK9FFvffXEyC9xyFhs,9443 +skimage/_shared/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/_shared/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_coord.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_dtype.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_fast_exp.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_geometry.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_interpolation.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_safe_as_int.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_testing.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_utils.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_version_requirements.cpython-310.pyc,, +skimage/_shared/tests/__pycache__/test_warnings.cpython-310.pyc,, +skimage/_shared/tests/test_coord.py,sha256=XjSxfX5yHcaLc9YwTj994kkPX823piDUC-z5LyCw9nc,3056 +skimage/_shared/tests/test_dtype.py,sha256=wwqc5o2vwjTsxkjUrmYDrN-8nhM-WscaQaQe6K0aWB8,369 +skimage/_shared/tests/test_fast_exp.py,sha256=w0Vmeds1ufT_bh8Y8jewLzB1X4luoOD82c_H2k3GMuk,490 +skimage/_shared/tests/test_geometry.py,sha256=3CXvdgORLkvXZZm8xSNr9PLqG-prRADMWJTPWxRXDFs,2120 +skimage/_shared/tests/test_interpolation.py,sha256=OxsQEISFgYSb_neW9hCMvT2R30fYIFAqK7CnLTY3WSI,1137 +skimage/_shared/tests/test_safe_as_int.py,sha256=uNtuC58dXNXzcbErawF7ModKJZiVcaK8vUnrv4ZFkSs,1424 +skimage/_shared/tests/test_testing.py,sha256=hmbDxV-qArgB4vfO0Os_E9Weh3OPzNrhTOEYAn7iOS8,4204 +skimage/_shared/tests/test_utils.py,sha256=zIfCaeZTBJmm-RdwRZM6aqmmBwgC5Oy6LbLejhc801I,15616 +skimage/_shared/tests/test_version_requirements.py,sha256=o2My9JA0fbIik6bXv-hWevyuQrWRWdbFWCr23ee99tE,1075 +skimage/_shared/tests/test_warnings.py,sha256=O-IVek3zadm58cuq_P-F59DX_6ETBHjiASAZ3sEB_8o,1250 +skimage/_shared/transform.cpython-310-x86_64-linux-gnu.so,sha256=S-bh6N3sKubC1tY-fvVZ263XuzFN0Yl5eGIm-y7RAek,198440 +skimage/_shared/utils.py,sha256=aAWaxbetX2TeV-vVAH9sSv0q-aWMBF-2hrBYXzFCwvU,29655 +skimage/_shared/version_requirements.py,sha256=ogsM5Qn6Zb6AAU_f6M4N7r3Vo2-NLUxT1RG0VSqZK2w,4347 +skimage/_vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/_vendored/__pycache__/__init__.cpython-310.pyc,, +skimage/_vendored/__pycache__/numpy_lookfor.cpython-310.pyc,, +skimage/_vendored/numpy_lookfor.py,sha256=jX7qOi9QMR3oPRFiMTA15X6hHNyjvHNqHbw9w-s2H0k,9669 +skimage/color/__init__.py,sha256=QDtz_aFnz27WW9iaRg8rXNS9_8EQpNKFLB8Zzi7m-qw,130 +skimage/color/__init__.pyi,sha256=03z6CYNjJon-6DHGJF1xHPRr0ZhCcXOzkn1owqORxXY,2382 +skimage/color/__pycache__/__init__.cpython-310.pyc,, +skimage/color/__pycache__/adapt_rgb.cpython-310.pyc,, +skimage/color/__pycache__/colorconv.cpython-310.pyc,, +skimage/color/__pycache__/colorlabel.cpython-310.pyc,, +skimage/color/__pycache__/delta_e.cpython-310.pyc,, +skimage/color/__pycache__/rgb_colors.cpython-310.pyc,, +skimage/color/adapt_rgb.py,sha256=eWZhGvWQ3nucRZkrDP6sgnF6Ka0rL6BB7jxeXL4pcp8,2489 +skimage/color/colorconv.py,sha256=BJJb9MJP36qa3YZinXS6VfHH9Xj8RFUqa4sBrpHjcqY,67505 +skimage/color/colorlabel.py,sha256=v0VVZjMBf5_Dzq38w9txqJSzwjAQfA84ZL1_fnVE8JA,10506 +skimage/color/delta_e.py,sha256=YwGF-yexqlZs6es1bFyuHEKa9wKuBeye74uLe6IF1JA,12706 +skimage/color/rgb_colors.py,sha256=Nj-fsrXpuCtLFIjKMgzpvb6HkrpZiuvgDc5imTMGs9w,4493 +skimage/color/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/color/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/color/tests/__pycache__/test_adapt_rgb.cpython-310.pyc,, +skimage/color/tests/__pycache__/test_colorconv.cpython-310.pyc,, +skimage/color/tests/__pycache__/test_colorlabel.cpython-310.pyc,, +skimage/color/tests/__pycache__/test_delta_e.cpython-310.pyc,, +skimage/color/tests/test_adapt_rgb.py,sha256=iXgHO2eYi3DTtou4Ddg1tquK2vVL-5DcWlJets14fyA,2761 +skimage/color/tests/test_colorconv.py,sha256=ATNAUWNyTpzm_31HpXZeZwTEwTTYte2XsfZXh2oafxg,37267 +skimage/color/tests/test_colorlabel.py,sha256=l1CUo4d0GNLXr-fcv4sedV9BokdNJKLwsmhYtf_JmDY,10906 +skimage/color/tests/test_delta_e.py,sha256=-ggGG1FtoN1ShcWsLIaAEOTZ2eCwnOm5NS4OpA9QN0o,8087 +skimage/conftest.py,sha256=KG_ignoqlB5vlbHIdtsVdobltaJRE83LhcG-9EjCKUI,310 +skimage/data/README.txt,sha256=Wra5WN0KyiF-Pp3FNWCMJDKkdIKRCdEGTwPSSUYACyc,280 +skimage/data/__init__.py,sha256=8Y4bk3tEAfBWnB-xKf7pXopkkP-AczTaXFxAQewZxS0,388 +skimage/data/__init__.pyi,sha256=varbukgcATJEhBB6ICnSPDI7SgNyIu1d8WQYZgXHJIE,1411 +skimage/data/__pycache__/__init__.cpython-310.pyc,, +skimage/data/__pycache__/_binary_blobs.cpython-310.pyc,, +skimage/data/__pycache__/_fetchers.cpython-310.pyc,, +skimage/data/__pycache__/_registry.cpython-310.pyc,, +skimage/data/_binary_blobs.py,sha256=hkOjiAH4pbAqLUEeY3_d8b1mz8paqSKKncBIMHVaRz8,2193 +skimage/data/_fetchers.py,sha256=f0KeqiKE4Y33sXCxn2dUiDR_-Q3-4usQH7ztLAcvS94,38807 +skimage/data/_registry.py,sha256=EClzXun1o6hXIZyLb4DRgdmw10RhDyJOAo-rr_2L9po,15860 +skimage/data/astronaut.png,sha256=iEMc2WU8zVOXQbVV-wpGthVYswHUEQQStbwotePqbLU,791555 +skimage/data/brick.png,sha256=eWbK8yT2uoQxGNmPegd0bSL2o0NDCt0CM-yl9uqqj88,106634 +skimage/data/camera.png,sha256=sHk9Kt2g-mromcA5iUgr_5pC09VpD8fjZI8nldcwwjo,139512 +skimage/data/cell.png,sha256=jSOn-4H3zId80J8zA1f8f1lWUTBuhOFyUvbgobP2FRU,74183 +skimage/data/chelsea.png,sha256=WWqh58uHXrefQ34xA4HSazOKgcLaI0OXBKc8RlHoxLs,240512 +skimage/data/chessboard_GRAY.png,sha256=PlGHB3RRWvTQfYIL2IJzZMcIOb-bVzx0bkhQleiT35A,418 +skimage/data/chessboard_RGB.png,sha256=GsAe_y1OUPTtpVot3s3CimV2YjpY16fvhFE8XMGaAzE,1127 +skimage/data/clock_motion.png,sha256=8Ckiayi2QugBE9hmIumyFe4Geglm_q9eYGBKHgVzOVU,58784 +skimage/data/coffee.png,sha256=zAL4yhiLFnx3WnEBtddn0ecXks92LDPW-hWkWZtajec,466706 +skimage/data/coins.png,sha256=-Ndz_Jz6b02OWULcNNCgeI_K7SpP77vtCu9TmNfvTLo,75825 +skimage/data/color.png,sha256=fS35k94rT6KnjgTl34BQ9JqcURqnXlmrO9VqycmK734,85584 +skimage/data/grass.png,sha256=trYCJCaziTbEOkrAljXNeK8HTpD0L_qCJ6yLdFLTn4k,217893 +skimage/data/gravel.png,sha256=xIYVtFG_HmBvvXLAqp-MwPBoq3ER732Tu5sPJYZEDBI,194247 +skimage/data/horse.png,sha256=x_tgeJ_jlMSF-EIpHqOyHlDRQPOdbctfuZF8wXgiVFU,16633 +skimage/data/hubble_deep_field.jpg,sha256=OhnF3YqSepM0uxIpptY3EbHAx2f7J-IobnyEo-LC9fQ,527940 +skimage/data/ihc.png,sha256=-N0ao4fd0fSditE7UJIbI3346bJiYG0lh3Boew75PO8,477916 +skimage/data/lbpcascade_frontalface_opencv.xml,sha256=Awl3iaPcuw5A0gue-CU328O2cLan8iaNc1Rw8i4AOpE,51858 +skimage/data/lfw_subset.npy,sha256=lWDsL17frAGXP2OoqZ0ABT_s0R4hh34YA4--UA-Ohyw,1000080 +skimage/data/logo.png,sha256=8sV_6K8Inwi1ulI9lVc8JuYpBKxZZ_TIhRsn0DNpAWg,179723 +skimage/data/microaneurysms.png,sha256=oeG-WapEf4zggvf6gJmXqzaaKxN8tsQgKrxkfHzPZFY,4950 +skimage/data/moon.png,sha256=eHOWGdEffrnBZbtdLv1Hcs7lV4EuyEdTLbsdku9x9Xc,50177 +skimage/data/motorcycle_disp.npz,sha256=LknIzr_z-iA1mgzGiAyC4cA7uxBtqBoXchgoG8LxE9c,1146173 +skimage/data/motorcycle_left.png,sha256=2xjpxBV2F0A8NTemujVd_q_pp-q7a5uUyzP2Ul3UkXk,644701 +skimage/data/motorcycle_right.png,sha256=X8kTrocOQqS2YjFLyQTReGvK2OLwubZ9uloilAY1d5c,640373 +skimage/data/multipage.tif,sha256=TaCtDT30gHqYRyR9G15WW1DUZIH2Q6-1w3wUgCx4Ew8,940 +skimage/data/multipage_rgb.tif,sha256=HSO4RP043ODi0G8wQygXzbheUgcNj1RgorpYrr80oN4,5278 +skimage/data/no_time_for_that_tiny.gif,sha256=IKvpS6nkXxjeQWxfvvjR9XpJlgC-QPmiAPriRgEO784,4438 +skimage/data/page.png,sha256=NBpvCmFVdmKwJzSptuVuwzqRWyxBiGuXUJ3t8qQ7R6M,47679 +skimage/data/phantom.png,sha256=VS_2mBZ6pALM6xeYETBgeiKKCgqnxRkpnqpNXzAbo2w,3386 +skimage/data/retina.jpg,sha256=OKB_NvJ_CV6Biup7ltNCAsBRdtMCU8ZnM_LgA3np4OY,269564 +skimage/data/rocket.jpg,sha256=wt0N58U4340RHkeWGbEpRk0CadCuX9GMqR0zp_3-qVw,112525 +skimage/data/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/data/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/data/tests/__pycache__/test_data.cpython-310.pyc,, +skimage/data/tests/test_data.py,sha256=WiBVkVqcOG5Uw31jjxXYxxGpv5u9gv5SUy2SV-q4Hjc,5344 +skimage/data/text.png,sha256=vYSqOm48mIeFDUXWBslrLllDP771AzhXC2PDGeZo5tE,42704 +skimage/draw/__init__.py,sha256=TjMI6gosMA2CafWXFmuObiMxykg9yCGlUAkc_j-A0UA,161 +skimage/draw/__init__.pyi,sha256=dzGdH8dVkLPbpDxkcA9v7wQdAEbxHyhq1MqnWPWYLcM,963 +skimage/draw/__pycache__/__init__.cpython-310.pyc,, +skimage/draw/__pycache__/_polygon2mask.cpython-310.pyc,, +skimage/draw/__pycache__/_random_shapes.cpython-310.pyc,, +skimage/draw/__pycache__/draw.cpython-310.pyc,, +skimage/draw/__pycache__/draw3d.cpython-310.pyc,, +skimage/draw/__pycache__/draw_nd.cpython-310.pyc,, +skimage/draw/_draw.cpython-310-x86_64-linux-gnu.so,sha256=13kjoN5KBavka3KTXAcbIMrEVg30yz6NyXbdBCeo4qE,410080 +skimage/draw/_polygon2mask.py,sha256=Y1dShd48WRbxpRqvPEvURPPxwFKaR0g-RY11_epKoyU,2472 +skimage/draw/_random_shapes.py,sha256=tkB2pDoNYxoW_9VP2qUtnKeplNewLSpS6_x9J1kuBdI,16010 +skimage/draw/draw.py,sha256=-Nyt6p05jtPhYqz5z3Awy0HPRytLsGqqMnvHax-svkk,33672 +skimage/draw/draw3d.py,sha256=eMmpG3VNigylno7M11-pRY3BKlKSMPVChY1O5q5sV8o,3287 +skimage/draw/draw_nd.py,sha256=RKG1GHjsTyXN8J2nWJGVDeoqG3srqPuqmTReZsJst_4,3692 +skimage/draw/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/draw/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/draw/tests/__pycache__/test_draw.cpython-310.pyc,, +skimage/draw/tests/__pycache__/test_draw3d.cpython-310.pyc,, +skimage/draw/tests/__pycache__/test_draw_nd.cpython-310.pyc,, +skimage/draw/tests/__pycache__/test_polygon2mask.cpython-310.pyc,, +skimage/draw/tests/__pycache__/test_random_shapes.cpython-310.pyc,, +skimage/draw/tests/test_draw.py,sha256=An0-U8Pt-lN2QT-j99mQvJBZrOYsjLn_-gt07IcJscI,41136 +skimage/draw/tests/test_draw3d.py,sha256=jKSpKmoYbkp9AYE40QWK8t_6sfmXraDooZBXe9NWtKM,6559 +skimage/draw/tests/test_draw_nd.py,sha256=W9-C2gRov5aVmXhbKxdTep5X69bie0XpfuYu-u-KL44,485 +skimage/draw/tests/test_polygon2mask.py,sha256=jvWREquUe7C1Ufjhc5yhe6qd4v15M1cLI0176lujjFs,329 +skimage/draw/tests/test_random_shapes.py,sha256=PCgaS5KkUj1Gyf7JDYIiAjYjtLWNTLPyVHvbQSG3dWY,5481 +skimage/exposure/__init__.py,sha256=8MMNgHdza2XatOj7Zf9h5xpl3O2gHeMn8N1gpIHKMJo,169 +skimage/exposure/__init__.pyi,sha256=HKwoewNTdeXl1yHaCJqErXZbxT4tLpBtfjYz53PnPQk,682 +skimage/exposure/__pycache__/__init__.cpython-310.pyc,, +skimage/exposure/__pycache__/_adapthist.cpython-310.pyc,, +skimage/exposure/__pycache__/exposure.cpython-310.pyc,, +skimage/exposure/__pycache__/histogram_matching.cpython-310.pyc,, +skimage/exposure/_adapthist.py,sha256=uxRzK1pMT0Gbp1dFS2W-22I8en155yqHDXwE8NodWsQ,10971 +skimage/exposure/exposure.py,sha256=YJ23YDwc3b6Q5CfpMX6k3HduMI7wNIyZ-2IacrAQzgY,27785 +skimage/exposure/histogram_matching.py,sha256=Q7_OwedrtvDcPO4KTRfqq-gxD6SQXqm2NfhPmUZhZA0,3194 +skimage/exposure/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/exposure/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/exposure/tests/__pycache__/test_exposure.cpython-310.pyc,, +skimage/exposure/tests/__pycache__/test_histogram_matching.cpython-310.pyc,, +skimage/exposure/tests/test_exposure.py,sha256=JZwxWAhem1fHzgQ6QubKSZ8MzIhKv6PPEEYdq_M3UzU,37090 +skimage/exposure/tests/test_histogram_matching.py,sha256=f1BByEh05wfR3v2VrxMcrjdLkevnmNuhmouLJPKeLsM,5157 +skimage/feature/__init__.py,sha256=paLASf66MmyJ8A1Ha8HE0WOn1GDnSioXhHIvA49gJ4g,178 +skimage/feature/__init__.pyi,sha256=weDA8amsNEfeKiXUnHbRiuDCOKXt-saPei6aTkdhIw8,2148 +skimage/feature/__pycache__/__init__.cpython-310.pyc,, +skimage/feature/__pycache__/_basic_features.cpython-310.pyc,, +skimage/feature/__pycache__/_canny.cpython-310.pyc,, +skimage/feature/__pycache__/_daisy.cpython-310.pyc,, +skimage/feature/__pycache__/_fisher_vector.cpython-310.pyc,, +skimage/feature/__pycache__/_hog.cpython-310.pyc,, +skimage/feature/__pycache__/_orb_descriptor_positions.cpython-310.pyc,, +skimage/feature/__pycache__/blob.cpython-310.pyc,, +skimage/feature/__pycache__/brief.cpython-310.pyc,, +skimage/feature/__pycache__/censure.cpython-310.pyc,, +skimage/feature/__pycache__/corner.cpython-310.pyc,, +skimage/feature/__pycache__/haar.cpython-310.pyc,, +skimage/feature/__pycache__/match.cpython-310.pyc,, +skimage/feature/__pycache__/orb.cpython-310.pyc,, +skimage/feature/__pycache__/peak.cpython-310.pyc,, +skimage/feature/__pycache__/sift.cpython-310.pyc,, +skimage/feature/__pycache__/template.cpython-310.pyc,, +skimage/feature/__pycache__/texture.cpython-310.pyc,, +skimage/feature/__pycache__/util.cpython-310.pyc,, +skimage/feature/_basic_features.py,sha256=3WBA3AMZks0u4MeVDV1Ry1Hms_NLweiqva6RzxIiHEY,6778 +skimage/feature/_canny.py,sha256=g4aAemdO553jRF04M7oSXWQDvaE9b_29eewQWbIX9GM,9466 +skimage/feature/_canny_cy.cpython-310-x86_64-linux-gnu.so,sha256=u64T5PLynCPtknkS2DcziN0-JOIMYGu1jdvxNL3P84k,281520 +skimage/feature/_cascade.cpython-310-x86_64-linux-gnu.so,sha256=lSQapFI5gW1pAs8nvMESAIXtwr2YZNH7WTWdjdQABm0,363672 +skimage/feature/_daisy.py,sha256=BC42Hao8q2P5MHE0syRThFRfmBa5JeGNzDG1g3Ee9MA,10064 +skimage/feature/_fisher_vector.py,sha256=QzY3m_kA6pJVowDSnNQzwnB1Bou0YTz8EIureYN6BL4,10511 +skimage/feature/_haar.cpython-310-x86_64-linux-gnu.so,sha256=MBSHdEKpcTXa5pyC1R5UejneVsWe07eZ5yQyZJ56GfQ,571056 +skimage/feature/_hessian_det_appx.cpython-310-x86_64-linux-gnu.so,sha256=YxZKm2ZRnkxNp37kmFioU_EklqzI13vYwmPLJTnnK0s,85664 +skimage/feature/_hog.py,sha256=APuWjYzXNJId9FKAz3PXpn-g4pJfw2KDOO-rp99fqBI,13208 +skimage/feature/_hoghistogram.cpython-310-x86_64-linux-gnu.so,sha256=27Phpm9AwrJYjkL3_brAf6QkSiNrfqFaYvpPQuTNTcY,298968 +skimage/feature/_orb_descriptor_positions.py,sha256=zM1l5vjzmuH9QhABX2ZkgS19VoIb_7k08OzYL467anM,450 +skimage/feature/_sift.cpython-310-x86_64-linux-gnu.so,sha256=Uh5BMfFVsVfe24BecNzLo4bvX_lcVuHNrk0i3a8uPpQ,358520 +skimage/feature/_texture.cpython-310-x86_64-linux-gnu.so,sha256=xjUMGsRkGS34we-AF7I7hKznmD-XjckvnElcJinAaok,398936 +skimage/feature/blob.py,sha256=z2Zc-fJBXzsb7r1TpgGU4y7wRA4f6sTbY3n4uaynDAU,27885 +skimage/feature/brief.py,sha256=sOAu-qx44Np32s0lzzAzkIt6CRpFf8h4RKdvivLYG1Y,8115 +skimage/feature/brief_cy.cpython-310-x86_64-linux-gnu.so,sha256=chyMoQt1Fr4nkqUgNcWR3CS7pp3JBKVPkP2MeC6IF7Y,236344 +skimage/feature/censure.py,sha256=TCW-RMHVpyFf51qBhrwMjRlQ5OfJIapHkRDgA5IPgzM,12003 +skimage/feature/censure_cy.cpython-310-x86_64-linux-gnu.so,sha256=XVYeimq3GT2auuWCjZWUUlApnuFs7d7xhs9Oi7kU8OA,235688 +skimage/feature/corner.py,sha256=uKXtC13ILrZzIRBKPwh03sJ090ozqrKnMql1fXr5scE,45569 +skimage/feature/corner_cy.cpython-310-x86_64-linux-gnu.so,sha256=eM5Y4UyAIZSokGKrZc_MszkaSUq_XXqkGpgxnIVVdok,377448 +skimage/feature/haar.py,sha256=sGtUnmD8-KGGvsK-6risY-lTH-CYL4tBrbFTGFjhPRA,12921 +skimage/feature/match.py,sha256=WayyFreFmSp02NEayvsjQt4HYwUoJPftHvTj9jVZBDs,4023 +skimage/feature/orb.py,sha256=Bi41N8GYP6DtYlu5vjagmD0HTy_GqdEy5mdCwj2fCq8,13148 +skimage/feature/orb_cy.cpython-310-x86_64-linux-gnu.so,sha256=0IxGc9lEW2YuIYwySPwhWzUtYUr3PKC0BzNisyFIcNE,282768 +skimage/feature/orb_descriptor_positions.txt,sha256=5lNSwCXegQMV0huO4FszyWGoPy1291cKt7Vpvddg8BE,2840 +skimage/feature/peak.py,sha256=hb-0LUKtcVDz7saNV3je11VqNtOGz3lT4yaw0JdnzcM,14478 +skimage/feature/sift.py,sha256=Rh7oRFE0XaA6w1emFjAxcal1Kd49AVkaXYvOi9Ktwkw,29407 +skimage/feature/template.py,sha256=P8w0Wxx4voJQxMMbWGPIhled4NO9VSulT3Mf5qxEvfQ,6571 +skimage/feature/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/feature/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_basic_features.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_blob.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_brief.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_canny.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_cascade.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_censure.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_corner.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_daisy.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_fisher_vector.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_haar.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_hog.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_match.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_orb.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_peak.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_sift.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_template.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_texture.cpython-310.pyc,, +skimage/feature/tests/__pycache__/test_util.cpython-310.pyc,, +skimage/feature/tests/test_basic_features.py,sha256=CpevV4VpfphjsQVIpfD8W6iPh56SejDhr-UpoDXTbO0,2092 +skimage/feature/tests/test_blob.py,sha256=Eo-UNr1nJnPPZWjwzB6SSkHtAJ0MWamNTY-EeDegj2k,16199 +skimage/feature/tests/test_brief.py,sha256=RBnlDa-J-XwaALha7ImGnIbVR_iraIyoG_JWeBWRqbw,3202 +skimage/feature/tests/test_canny.py,sha256=oHWtVrwzEQadumXnBBdkS_zgvHDOgSOP7Q_u_zpUqvM,6388 +skimage/feature/tests/test_cascade.py,sha256=uZnmMTtZUYeTRG-oJMLyKJDuS9o9ByGHmA0cPHpRdUs,514 +skimage/feature/tests/test_censure.py,sha256=66Xit4dKcxnEmowgzy5ryc0Zdbii_fhUSISag_Ou5kE,3161 +skimage/feature/tests/test_corner.py,sha256=8t1OjndiLrcMgYcFFK2a2ee5_yWWcUzeaS4gsNOKSTY,24187 +skimage/feature/tests/test_daisy.py,sha256=XYgJOwW-6tLaEcHAhUwo_2QV_BbG2H1yNAH2U2Dusus,3393 +skimage/feature/tests/test_fisher_vector.py,sha256=pmA0mq2Q-b8KltbfD5Fpqfpnp5Ks5n0VTLRC4wpTP58,5730 +skimage/feature/tests/test_haar.py,sha256=tdewpi8wvEnEpkq8aKkFODuP9_g_Nk1qKXq7nkKsEnI,6659 +skimage/feature/tests/test_hog.py,sha256=ch433R-JcroWz18LMi3BAJ8-b6HxZOoUoVBI4peCXu0,11720 +skimage/feature/tests/test_match.py,sha256=0iVIAbz_JZhq8PVh787zJti_E5IeaGSZ33peA_Qljx4,8065 +skimage/feature/tests/test_orb.py,sha256=0cO1P-ctZDBggWXpY0ZvPr9l3aERUs8h0g5f868fyCQ,6267 +skimage/feature/tests/test_peak.py,sha256=BhSx6M4q8VNfh_RGFtjmg5R7AL_B8BQw8uxDy-rsICY,21988 +skimage/feature/tests/test_sift.py,sha256=C1B2qOD3wrjlAuKoMnirXQhyPgCGpxCo3DDWwl6fQQo,5753 +skimage/feature/tests/test_template.py,sha256=cSKzOc-o6X2ojyYqLe3I1LDSLcaa-EagVpfLt204VIY,6134 +skimage/feature/tests/test_texture.py,sha256=s392qhy72ab_Z4rnP7GWZ8wGTOqK3ponL93ac-7LA3E,13596 +skimage/feature/tests/test_util.py,sha256=fY1-kQb9g-HxCXNn4f14_q9_zumrWPDT1ZjTMz7e4JE,6164 +skimage/feature/texture.py,sha256=pOt1rEzszGSlvXbA4L30NgEl7r-Paw86ZjTVV9aZmWo,20544 +skimage/feature/util.py,sha256=v-9dcDXtQWLO6fuH_jhr-QBdIzCk7TJbBJUI2iAdYbA,7049 +skimage/filters/__init__.py,sha256=1ehkljJM7R29gbYqXS75AZ6fNba7FMttFo67IFJSpD4,165 +skimage/filters/__init__.pyi,sha256=KonQ6JGwzrRlNcoG5wOUmU7ujdNQkW2G5A7Ub52DGq8,2158 +skimage/filters/__pycache__/__init__.cpython-310.pyc,, +skimage/filters/__pycache__/_fft_based.cpython-310.pyc,, +skimage/filters/__pycache__/_gabor.cpython-310.pyc,, +skimage/filters/__pycache__/_gaussian.cpython-310.pyc,, +skimage/filters/__pycache__/_median.cpython-310.pyc,, +skimage/filters/__pycache__/_rank_order.cpython-310.pyc,, +skimage/filters/__pycache__/_sparse.cpython-310.pyc,, +skimage/filters/__pycache__/_unsharp_mask.cpython-310.pyc,, +skimage/filters/__pycache__/_window.cpython-310.pyc,, +skimage/filters/__pycache__/edges.cpython-310.pyc,, +skimage/filters/__pycache__/lpi_filter.cpython-310.pyc,, +skimage/filters/__pycache__/ridges.cpython-310.pyc,, +skimage/filters/__pycache__/thresholding.cpython-310.pyc,, +skimage/filters/_fft_based.py,sha256=LUwb9JlvarVhLlSP5nPacsI6C5doVlJWufTh95QqOOg,6709 +skimage/filters/_gabor.py,sha256=Ag--tND24VcErppMMsTMUsIRhyCxLDUv79lT8Jyg-As,7720 +skimage/filters/_gaussian.py,sha256=fyHfkvJuEptkhm9USYrZMZ6dytWK39V2HqXYi7lxDvU,6037 +skimage/filters/_median.py,sha256=4pp1HTIXijKWnt29j8rRTdqjXeRadYDPEGkOGQznjuQ,2964 +skimage/filters/_multiotsu.cpython-310-x86_64-linux-gnu.so,sha256=0ZUr4ntWoLttatdF86QFsOIOyE1Y2HdJZ2-cHZlWjsA,267568 +skimage/filters/_rank_order.py,sha256=NUbi5sPtKTW-Xm6VZcwn0F5pV45yfaYDeHMEJtGy4nk,2057 +skimage/filters/_sparse.py,sha256=jXiCqNqR5gUHFnhHoUVPsN16ufbU5CLEAQU_XexrI5g,4654 +skimage/filters/_unsharp_mask.py,sha256=EoJ5JHrndcgrliDsef_WcQM6O3HGnswRUUW3tLBlUYs,5511 +skimage/filters/_window.py,sha256=xO_yx7DSEXTBIWwPhXlsKNirmfH6Koc7k5Us_6A1wfY,4351 +skimage/filters/edges.py,sha256=_z6jvZNt5_4DHc-tvhzgXv7ixQtNUvf0oZ5dmfzfV3M,25639 +skimage/filters/lpi_filter.py,sha256=Z6SWW7EAM2Hj3sVOzQtUs9R_d7ZHwqIYAqNAdqpSOY8,7937 +skimage/filters/rank/__init__.py,sha256=SFt54WGOb_evjktH87sKTcKk3i-4C2mqNN3c6nv46fw,1548 +skimage/filters/rank/__pycache__/__init__.cpython-310.pyc,, +skimage/filters/rank/__pycache__/_percentile.cpython-310.pyc,, +skimage/filters/rank/__pycache__/bilateral.cpython-310.pyc,, +skimage/filters/rank/__pycache__/generic.cpython-310.pyc,, +skimage/filters/rank/_percentile.py,sha256=wmJYo8LeNrHgSYDMs-Lro8ErowHgGlbOjzxdcC8Dx-c,13895 +skimage/filters/rank/bilateral.py,sha256=HpJnwjxRWsdpPRlZMBG0pZchLXrr0RJbVz-KFobsU0s,7822 +skimage/filters/rank/bilateral_cy.cpython-310-x86_64-linux-gnu.so,sha256=tHq2UKYq4IVOGnGnOtZVPTGC8BNUmQJnXzN0tAemfoI,577496 +skimage/filters/rank/core_cy.cpython-310-x86_64-linux-gnu.so,sha256=OpUtnsfF6cVWS4_R4HfkOjaMlMepUeT5S91cGqs-rk8,608488 +skimage/filters/rank/core_cy_3d.cpython-310-x86_64-linux-gnu.so,sha256=868ck09xIvb-sp_QzS2QTd01ih4uen54cojmP4Dhb1Y,362952 +skimage/filters/rank/generic.py,sha256=6Sep8UCofRH_MiwHicSY42YX-qsoctYr4gf7sR9eae4,55984 +skimage/filters/rank/generic_cy.cpython-310-x86_64-linux-gnu.so,sha256=GRE0fNo589e1BgzFvnE9PRxnpAYuIwV3dDj02suyklc,3972808 +skimage/filters/rank/percentile_cy.cpython-310-x86_64-linux-gnu.so,sha256=btX4dNs6vl2wem-BcKKyAzKrc7GOR_0sT1R2uh_IXXo,1247280 +skimage/filters/rank/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/filters/rank/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/filters/rank/tests/__pycache__/test_rank.cpython-310.pyc,, +skimage/filters/rank/tests/test_rank.py,sha256=Juf040oJMF53n1ieo1i_-Dppvd-LV_mZkZvEd7KmdD0,38992 +skimage/filters/ridges.py,sha256=vH-NFuH4M5VzbNUjW4fgs3-gOgaOyJeGhRi8lWXOC_M,14247 +skimage/filters/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/filters/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_correlate.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_edges.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_fft_based.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_gabor.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_gaussian.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_lpi_filter.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_median.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_ridges.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_thresholding.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_unsharp_mask.cpython-310.pyc,, +skimage/filters/tests/__pycache__/test_window.cpython-310.pyc,, +skimage/filters/tests/test_correlate.py,sha256=kd80vN3t94fOaZLvD9kl_iql3slFelLZ1A4wzzL7T_M,1995 +skimage/filters/tests/test_edges.py,sha256=txSSnwoR2-XbesvX97oYTtp4vk6_SRLkJbbvv7XIqeg,21462 +skimage/filters/tests/test_fft_based.py,sha256=OhqNpO1-1n5HotznDIoBMfc79e_8ua_RqBOuJrDTLh4,14556 +skimage/filters/tests/test_gabor.py,sha256=BMfvYzHi1ZCmpXsVMcs12Rfawzz2pA_AG-YyuThkReU,3689 +skimage/filters/tests/test_gaussian.py,sha256=82x-GcssO88CrGDjsiafSoDQOSFJ461JXquhqnmplk8,5454 +skimage/filters/tests/test_lpi_filter.py,sha256=joUHee4ZFTvUuGs6SZKYw_WZVEAYCeMghSb7i27R7ts,3097 +skimage/filters/tests/test_median.py,sha256=T9JoGWrDMAHwAZZ-_uFvonx9Fj4Q14aAjz4GTnHLjeQ,2113 +skimage/filters/tests/test_ridges.py,sha256=T8QK0BJv8W282q0H-dA8LsYXtaRszFmYDpqYMDUZoe8,9538 +skimage/filters/tests/test_thresholding.py,sha256=87P58TmJihPOdCgostVVWullDGBdHvawLmcez5rhhsI,26407 +skimage/filters/tests/test_unsharp_mask.py,sha256=hv6ZVRGiH93Z5f1SxsBgNG83IRRtjCavuSnd2PIAq_o,5056 +skimage/filters/tests/test_window.py,sha256=6_HHYrhGAIhh4QHE4XwSAMrido4gLiwIZRc_OfYKgEE,1624 +skimage/filters/thresholding.py,sha256=0NuTX4VQbA2vRDN8VqAdIN4HlTkB-IthBZ1dsSoaZf0,47905 +skimage/future/__init__.py,sha256=YX1ggMLn08L_Cy-UzcKlEJd1JbFjGAtWGGht705VXZE,506 +skimage/future/__init__.pyi,sha256=q-RbSvWEFevrbvLiFJ44XiFL6jPKvSV-NqwVQ0FZMAk,493 +skimage/future/__pycache__/__init__.cpython-310.pyc,, +skimage/future/__pycache__/manual_segmentation.cpython-310.pyc,, +skimage/future/__pycache__/trainable_segmentation.cpython-310.pyc,, +skimage/future/manual_segmentation.py,sha256=EQ-atSKTONFrnVjuGAia5XZdoKQ-Z_PtoHRGsDJY4r4,7610 +skimage/future/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/future/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/future/tests/__pycache__/test_trainable_segmentation.cpython-310.pyc,, +skimage/future/tests/test_trainable_segmentation.py,sha256=Wr2uvOtG4LUf1mzrRtD5HphzSkSpTDJOZhIE_d9FTqY,4223 +skimage/future/trainable_segmentation.py,sha256=ziB_HovaKyLENPGUaCPABXoIxBh7wkOW0894qTWWFIQ,5604 +skimage/graph/__init__.py,sha256=fXO5xY04KgnleV0BeITsL522zKGo4LaC7Bi6V4rh-c4,338 +skimage/graph/__init__.pyi,sha256=-fh5gug7Wi0FFVl0BDxTla37aXRDOsNfmHtHC_r7HJk,782 +skimage/graph/__pycache__/__init__.cpython-310.pyc,, +skimage/graph/__pycache__/_graph.cpython-310.pyc,, +skimage/graph/__pycache__/_graph_cut.cpython-310.pyc,, +skimage/graph/__pycache__/_graph_merge.cpython-310.pyc,, +skimage/graph/__pycache__/_ncut.cpython-310.pyc,, +skimage/graph/__pycache__/_rag.cpython-310.pyc,, +skimage/graph/__pycache__/mcp.cpython-310.pyc,, +skimage/graph/__pycache__/spath.cpython-310.pyc,, +skimage/graph/_graph.py,sha256=cyfQ1zvBVb2_5zW_c00yiYY4rOEzkwFBVWjor0a0uCA,9223 +skimage/graph/_graph_cut.py,sha256=WItPbPIQVpUkQLRszjtAXJ2ZAT-YwHAC9vLKO9hxmso,10148 +skimage/graph/_graph_merge.py,sha256=lrgunWZGrplz42xIDIz3IJoR4AfhzNLc_MdD5O-3tkQ,4304 +skimage/graph/_mcp.cpython-310-x86_64-linux-gnu.so,sha256=j-eE7qwpBmdzsMtOr4-ghUYlrBoxf1SGbOKMfCDYEug,545712 +skimage/graph/_ncut.py,sha256=EmchgBgVFKaveAKMElDIcOGFOVGlrQWqb_bt-2lcy8M,1825 +skimage/graph/_ncut_cy.cpython-310-x86_64-linux-gnu.so,sha256=P2aGOEzYONN1174JW8Pb4W_5-otxPizWDA1u9rQfzXg,299592 +skimage/graph/_rag.py,sha256=0b0iGwAtnVwzmKX69x1JRxviXgtWDdn18UDkvUq_NdY,20626 +skimage/graph/_spath.cpython-310-x86_64-linux-gnu.so,sha256=2h-quZfZIHNgfm_wvC4rEFUp6U4U6DYslZCAMh8wlX0,260320 +skimage/graph/heap.cpython-310-x86_64-linux-gnu.so,sha256=Fz_6pyEGEiI2Xa7EsDiWpqazO2bi9G7ypk9lc5f6z1k,138360 +skimage/graph/mcp.py,sha256=1gRa1f2QhCiup4WFaaCr9YcnNHZVc8H1DoIA0oS-vVM,3182 +skimage/graph/spath.py,sha256=uav6ZRcaw3O93NyLiq-EVQouZUtxK16VIYUYoWGuJNM,3399 +skimage/graph/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/graph/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_anisotropy.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_connect.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_flexible.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_heap.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_mcp.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_pixel_graph.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_rag.cpython-310.pyc,, +skimage/graph/tests/__pycache__/test_spath.cpython-310.pyc,, +skimage/graph/tests/test_anisotropy.py,sha256=_PGkxQt2is3f1bYmAzWPZJ-KKmAh-_9OYXkQ89-ceTY,3518 +skimage/graph/tests/test_connect.py,sha256=XU3UMqDRacTy-dMulRNwEFBixyvYmxbK-RtfhFc8HzY,2367 +skimage/graph/tests/test_flexible.py,sha256=TgkxK1QpeHAZu6BoULWoIY0ir83_vG9VZFplLjhYz5s,1560 +skimage/graph/tests/test_heap.py,sha256=PHdBGa90S3k0VuEAZhUOi7-zSoDSeduQdtQUCaD8GIY,1105 +skimage/graph/tests/test_mcp.py,sha256=hB5fPpB73Rj2c9YPtukyA0Hxeg424yuIOMkmVfeY3BM,5402 +skimage/graph/tests/test_pixel_graph.py,sha256=Ein8JyF1gPYEY7P-u2mgQgOknKFsae5f-FTctg37kLw,3745 +skimage/graph/tests/test_rag.py,sha256=VEdfa47xlwForkv0q1vfFiW-4nfGB9wjR46wVz9sSdc,7857 +skimage/graph/tests/test_spath.py,sha256=keQxRt7EyZyEMO41d1UcAdT5ZVHvnp5HJ83l9RQYXVg,826 +skimage/io/__init__.py,sha256=wHwMf7uBa0rcB4BcF76XIWhbCeLYW-CyCz6_4UnDUeE,1009 +skimage/io/__pycache__/__init__.cpython-310.pyc,, +skimage/io/__pycache__/_image_stack.cpython-310.pyc,, +skimage/io/__pycache__/_io.cpython-310.pyc,, +skimage/io/__pycache__/collection.cpython-310.pyc,, +skimage/io/__pycache__/manage_plugins.cpython-310.pyc,, +skimage/io/__pycache__/sift.cpython-310.pyc,, +skimage/io/__pycache__/util.cpython-310.pyc,, +skimage/io/_image_stack.py,sha256=A75v07PK6yd-5Qi60JfoaKG2esjJI5bMtlou8dNjUSc,570 +skimage/io/_io.py,sha256=j1PqWx7_rpaw-FQ_TGmtosGg9mdCuOKd-qD5VZtZqBA,8986 +skimage/io/_plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/io/_plugins/__pycache__/__init__.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/fits_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/gdal_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/imageio_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/imread_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/matplotlib_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/pil_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/simpleitk_plugin.cpython-310.pyc,, +skimage/io/_plugins/__pycache__/tifffile_plugin.cpython-310.pyc,, +skimage/io/_plugins/fits_plugin.ini,sha256=60y6Ey9etFQTf3oRxr4hAgptiBxkmNG_PpJaP5LdhR8,88 +skimage/io/_plugins/fits_plugin.py,sha256=hE0QRLBX6AcTtodB9VVopw5O7U62ektN251opWSu6MQ,4407 +skimage/io/_plugins/gdal_plugin.ini,sha256=Yx6iK4NqTllzpc726aXZXOlktuhm8IR28i4hfB7Qkn8,89 +skimage/io/_plugins/gdal_plugin.py,sha256=oNQjajA4GUxb3swAQMf19X_XJhyY3pRdaFde145K4J4,349 +skimage/io/_plugins/imageio_plugin.ini,sha256=Gi4D65yDOFzg7gRCwXYGxlJoHsvlFSEwyt1wJkedqWc,88 +skimage/io/_plugins/imageio_plugin.py,sha256=d9MQzuDOWadJlfDhNptaNQJEI8sPsXWgzmwJuDxldLk,330 +skimage/io/_plugins/imread_plugin.ini,sha256=yNvmnjjif85MJpwIfSheQ0IMQ86rPZuuTDjuaZZH6Pk,86 +skimage/io/_plugins/imread_plugin.py,sha256=dM_Z_GsqUIbZQatLUPKJEiOC6nYQP7k3nscDyTLjJvs,956 +skimage/io/_plugins/matplotlib_plugin.ini,sha256=re_KwCXXwedUEqJ749d7q6YqPDde5_RxH72M7y3PYY0,123 +skimage/io/_plugins/matplotlib_plugin.py,sha256=QOIyQmgZGQ7NBZf4mZ5SWHC8P86y64bmAROy_4DYhKk,6467 +skimage/io/_plugins/pil_plugin.ini,sha256=ZSFIMpRtr2-tUPvK_cG0xLOrPSWgw1ObRWVe8TMIJF0,91 +skimage/io/_plugins/pil_plugin.py,sha256=28qO6oO8ZUbac8wFnMw3E1Lv6KMUiNqf5lwsj9XgDU0,7843 +skimage/io/_plugins/simpleitk_plugin.ini,sha256=Zu1mh1RaPIu6vw2cSPev_T2or0DTMYZeLxyc1u7Hcig,92 +skimage/io/_plugins/simpleitk_plugin.py,sha256=N2Ja-_0TpfrixNl0iEWJydrIrHgOYlzuxHkF10KrYOE,531 +skimage/io/_plugins/tifffile_plugin.ini,sha256=vQpbXXPSQN242crgASKbsxqn1aDUfHwftslt9vvuSkQ,110 +skimage/io/_plugins/tifffile_plugin.py,sha256=mAT73YEKXXefD2DtMqZyUioqljO4FmKRi7D5b-gHeps,2071 +skimage/io/collection.py,sha256=GoXoj5LNTEIiFVA44ZIVmOug4M2eS_p2yF3q_RfoDNg,15956 +skimage/io/manage_plugins.py,sha256=F2oA6_YfA-ztjgKSi5e0t7PViLaZkNRLkUqwvRAcGv8,12309 +skimage/io/sift.py,sha256=mfUV7SJhTqmxpwHXwyPMYhQT_1d6y5JloNfHgH40qnc,2561 +skimage/io/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/io/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_collection.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_fits.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_imageio.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_imread.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_io.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_mpl_imshow.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_multi_image.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_pil.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_plugin.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_sift.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_simpleitk.cpython-310.pyc,, +skimage/io/tests/__pycache__/test_tifffile.cpython-310.pyc,, +skimage/io/tests/test_collection.py,sha256=oyDQBMq7uD4Dt6PYkr8q_GGskWh8I_vy3vRlorJrWSc,5357 +skimage/io/tests/test_fits.py,sha256=sn5VaWkM1dr94F-WqbfIUooAW0wet7wCa43w5PZ4s1Q,884 +skimage/io/tests/test_imageio.py,sha256=e2IgcZL3PWKZK3A-Guj849PjVUhC4sPz9V_hKDWD27Y,2724 +skimage/io/tests/test_imread.py,sha256=id3_fw7zwgWODWpHvzrCoidMfzyTtPB_8JcTLbljUfk,1946 +skimage/io/tests/test_io.py,sha256=rygpx2BDJ1SygelrrnEvv-fU0cFHcRLLQnlM22an134,5361 +skimage/io/tests/test_mpl_imshow.py,sha256=Wb0QXVNaN21KF6vpp6vDVhWMY0EzBKz4sichtVMSRbY,3741 +skimage/io/tests/test_multi_image.py,sha256=3jx3EAfU4_nSw4grgq-TCBeQ_CaGa74iQJHk6d3RytY,2527 +skimage/io/tests/test_pil.py,sha256=XbW6HHktiKmAwDfs3Zw9E4XsMx0RcYwpctHM6Vy5o74,9217 +skimage/io/tests/test_plugin.py,sha256=XEmvZxdIrlHALTcoOC8Uoj0srQwQuGK0mAWVCI4NNaA,3014 +skimage/io/tests/test_sift.py,sha256=RE4W1QRzqBKaLSdhElhDO806EvcDcFwidR8RxYH2mic,3336 +skimage/io/tests/test_simpleitk.py,sha256=THFUDW98M--DNcyMUenGobvw2IAxk4NikJa2Dbm_Iq4,2557 +skimage/io/tests/test_tifffile.py,sha256=IF8wH16H9iqSFL55hforf9FNWtkbnqYGn72hWmqJbCY,2665 +skimage/io/util.py,sha256=7gQdn6BD2HX-R5yAI6A8GCjG3HMWzUFjzjPJD1Qa3HE,1283 +skimage/measure/__init__.py,sha256=QV04vcq3E_ez4cLSw_3xjbS1ptDAzmVTeJrZ3IpdA4I,174 +skimage/measure/__init__.pyi,sha256=MFpgokeHZgVTQ2NRJ5JvIZLfFY6PoWt4zuWe4sO646M,1858 +skimage/measure/__pycache__/__init__.cpython-310.pyc,, +skimage/measure/__pycache__/_blur_effect.cpython-310.pyc,, +skimage/measure/__pycache__/_colocalization.cpython-310.pyc,, +skimage/measure/__pycache__/_find_contours.cpython-310.pyc,, +skimage/measure/__pycache__/_label.cpython-310.pyc,, +skimage/measure/__pycache__/_marching_cubes_lewiner.cpython-310.pyc,, +skimage/measure/__pycache__/_marching_cubes_lewiner_luts.cpython-310.pyc,, +skimage/measure/__pycache__/_moments.cpython-310.pyc,, +skimage/measure/__pycache__/_moments_analytical.cpython-310.pyc,, +skimage/measure/__pycache__/_polygon.cpython-310.pyc,, +skimage/measure/__pycache__/_regionprops.cpython-310.pyc,, +skimage/measure/__pycache__/_regionprops_utils.cpython-310.pyc,, +skimage/measure/__pycache__/block.cpython-310.pyc,, +skimage/measure/__pycache__/entropy.cpython-310.pyc,, +skimage/measure/__pycache__/fit.cpython-310.pyc,, +skimage/measure/__pycache__/pnpoly.cpython-310.pyc,, +skimage/measure/__pycache__/profile.cpython-310.pyc,, +skimage/measure/_blur_effect.py,sha256=mhgvLVugOfQo3iI_ZkU2_zcRVVmHEh28MxEhfVjc8oA,3324 +skimage/measure/_ccomp.cpython-310-x86_64-linux-gnu.so,sha256=hoWTcU8mbSm6BVvISDS8OFpSZWoFXEek7h71IrP6DbE,132936 +skimage/measure/_colocalization.py,sha256=PUSk-oqQ6nVfXn0uK9feLfiBvdwEqufqQH0yohX0yCo,12233 +skimage/measure/_find_contours.py,sha256=9XR2aSiN9vl0WpaA0s19HzP0cbiKxd1zvg7Qaht-I6M,9585 +skimage/measure/_find_contours_cy.cpython-310-x86_64-linux-gnu.so,sha256=DJOrOOiThBMyXUzxmhD428as6GpA1YbRXbZFH1UXA-o,244488 +skimage/measure/_label.py,sha256=WoRzje_lZLblB0BSpGsb4rHifWg9ZO9u19Fudq_HmPU,3960 +skimage/measure/_marching_cubes_lewiner.py,sha256=qX3b_vAdibqb34AkLqL1zK8sW9mwf5fJ3E3aRENNMMk,12856 +skimage/measure/_marching_cubes_lewiner_cy.cpython-310-x86_64-linux-gnu.so,sha256=lU2gJmLldRdzn0J6zscYrvbxPZ6X-MziYApn5xAxD3A,385072 +skimage/measure/_marching_cubes_lewiner_luts.py,sha256=puYUThcl9gATqqmerwTqAdpqooOeMoR0oV6J_gvP5wE,27802 +skimage/measure/_moments.py,sha256=85KWsIKWuYDMifNeEADXsHvZt8khf13O2GpkbLwHcl8,17848 +skimage/measure/_moments_analytical.py,sha256=1OzL8mhKfqxfKT6Q4t4WwpFyM_1cgKlquMM7jr5LJ-Q,6563 +skimage/measure/_moments_cy.cpython-310-x86_64-linux-gnu.so,sha256=i7eEkIDejxMRd80x1s2z5QnzcXOUwZebA-yrOvoPrd8,277144 +skimage/measure/_pnpoly.cpython-310-x86_64-linux-gnu.so,sha256=DQjvC9ySQ-gaL71SNE0_vQGRpsXgg-lD26pic2Opzb4,259736 +skimage/measure/_polygon.py,sha256=JqJ9iVRdFY9rVChXKXmVY5slHtOvBCjIyGpWyEkoqr4,5268 +skimage/measure/_regionprops.py,sha256=F7peBsKw3pjyjvLuuUDKOfK08j2Ci-wI_k7agh39sxk,51730 +skimage/measure/_regionprops_utils.py,sha256=CC4bxkhCTIZH7c7QBWLEu4HafFZBo_MsBn17L2eMOyQ,15989 +skimage/measure/block.py,sha256=b4dhXeeaiBerPmFpNjtm52KvwRmIByPI1U-Y0S5gws4,3261 +skimage/measure/entropy.py,sha256=aNypYauao_uvywjL1DewtbCDB01jrwX6BnUIAo_CbhM,1144 +skimage/measure/fit.py,sha256=a5FqsqSuGNKPxdFTjgVyAhpIykjHMmXHT2dFIPMrSHk,32458 +skimage/measure/pnpoly.py,sha256=DGgYjnvOANCvZOb_oOgzHTiDi62vWD03AOFQhuctJXo,2023 +skimage/measure/profile.py,sha256=-r9sI9iSsu57zaFKtGc0rE5-x1W5ZgP_XleR_bAS0d0,6781 +skimage/measure/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/measure/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_block.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_blur_effect.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_ccomp.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_colocalization.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_entropy.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_find_contours.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_fit.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_label.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_marching_cubes.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_moments.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_pnpoly.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_polygon.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_profile.cpython-310.pyc,, +skimage/measure/tests/__pycache__/test_regionprops.cpython-310.pyc,, +skimage/measure/tests/test_block.py,sha256=lOMhCz-a-zEPLJvK6fGiGVrBFGvFGKucb3fuf9W2yN0,4022 +skimage/measure/tests/test_blur_effect.py,sha256=-nmf0zUEbtTeToaZaVwxggT4W8VEZTHoboQSkQ5j6fE,2502 +skimage/measure/tests/test_ccomp.py,sha256=pHQ8jGMj7WlShxyaunDb3kwd2DopZxKq-MmL_2mmCoA,8033 +skimage/measure/tests/test_colocalization.py,sha256=Hr1D1pu1jU7PuBb_Lp-Wwp37athNjIUHzycnPBJe74E,4672 +skimage/measure/tests/test_entropy.py,sha256=mMUvbULp-qBSbEJ73k25xqG-D8dLymExO4I5WGrJsJI,400 +skimage/measure/tests/test_find_contours.py,sha256=gexiCwV1eWsQy4boYOe3pUHGlJEbD0rRalHtrMBUZSc,5276 +skimage/measure/tests/test_fit.py,sha256=WoySIrX_A6XU0opKgLe3d5W6mmsrQD8eNC9ww7yukEo,22093 +skimage/measure/tests/test_label.py,sha256=6tTyVeNbgjhxxnwnyN0VhXKLuFTk21QWCvg1C-5Aa34,1783 +skimage/measure/tests/test_marching_cubes.py,sha256=A4M4I8bnXm5bxVi6ChEvckje0AiN8m0CTWcGB15_fpg,7055 +skimage/measure/tests/test_moments.py,sha256=kDxKemYhwnYajGEFa4EP2VALEOdUri9Z8lcvOR1RWQU,11768 +skimage/measure/tests/test_pnpoly.py,sha256=fLKSNA2r7ABNqS6vpY_3D8AVJb0wAEnkWhAhLy_XMrI,1239 +skimage/measure/tests/test_polygon.py,sha256=jor2cmpYG4XXfEu-jlURTmPUEho5YRF8i_NDJNid57s,2295 +skimage/measure/tests/test_profile.py,sha256=OHBxbElqoVXz4HoeSrKXapDgLpoIoHriSt0KHVI-NIY,7822 +skimage/measure/tests/test_regionprops.py,sha256=lC6_gd5iaWUiywXuAi4ZKa3SA5rtE7aUIXCKT5JFJqk,53066 +skimage/metrics/__init__.py,sha256=TWewBmmkeJ32pddMk6TfIIGK8-kWALIor7GMHx5gMJg,180 +skimage/metrics/__init__.pyi,sha256=1t3MLs46xdqirO2XUiVV__auPvFJY0rAM-S1PDVoHKQ,886 +skimage/metrics/__pycache__/__init__.cpython-310.pyc,, +skimage/metrics/__pycache__/_adapted_rand_error.cpython-310.pyc,, +skimage/metrics/__pycache__/_contingency_table.cpython-310.pyc,, +skimage/metrics/__pycache__/_structural_similarity.cpython-310.pyc,, +skimage/metrics/__pycache__/_variation_of_information.cpython-310.pyc,, +skimage/metrics/__pycache__/set_metrics.cpython-310.pyc,, +skimage/metrics/__pycache__/simple_metrics.cpython-310.pyc,, +skimage/metrics/_adapted_rand_error.py,sha256=zbCyRSiOGz3QCzYVLdnCDpHtvh2ZoHyCEKKQe-uHJkA,3571 +skimage/metrics/_contingency_table.py,sha256=4zT07BasPCvZ-wxTPoykVAKckUyGSdUyK28Mxa29v2I,1734 +skimage/metrics/_structural_similarity.py,sha256=l4HTTumNP0vDABKt06R0Dq-7ZpUtupxqWz64e9C0sKA,10422 +skimage/metrics/_variation_of_information.py,sha256=Kf02JWuAOTT6N5Y9vksiUHFSuSezrHv7KwWpc8ZVLag,4254 +skimage/metrics/set_metrics.py,sha256=VNcOP2w9oNrrOS0qjPkVuI8IKTG1yYliMWGlzqbVtEk,4896 +skimage/metrics/simple_metrics.py,sha256=RTpf2sSCOpLG6q7AtYMIdPHkYqchhqdEGXh4dB8Yt7Q,8247 +skimage/metrics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/metrics/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/metrics/tests/__pycache__/test_segmentation_metrics.cpython-310.pyc,, +skimage/metrics/tests/__pycache__/test_set_metrics.cpython-310.pyc,, +skimage/metrics/tests/__pycache__/test_simple_metrics.cpython-310.pyc,, +skimage/metrics/tests/__pycache__/test_structural_similarity.cpython-310.pyc,, +skimage/metrics/tests/test_segmentation_metrics.py,sha256=mIjMWvOLIrJ38U7DFrEK2m0OAFF7uDZ5LWIKxvyZpUE,2593 +skimage/metrics/tests/test_set_metrics.py,sha256=o42FKIdel_BcnmBUm1cWZ1nRj1u8Ik3EmJ1p8TEcg5g,6930 +skimage/metrics/tests/test_simple_metrics.py,sha256=i3tXEQ4G9aa_V3quNvXWNKBzRauIgSiPVqpm6WW4RyY,4830 +skimage/metrics/tests/test_structural_similarity.py,sha256=8dVJmhsqhKzc4TSy1hEOCj6Dz7ot8tqbkpS6BBigR8Q,9746 +skimage/morphology/__init__.py,sha256=zJ3EJavfwOhIrQhF5DqXvMC8qM3JtpAU2baJneuc1vM,2117 +skimage/morphology/__pycache__/__init__.cpython-310.pyc,, +skimage/morphology/__pycache__/_flood_fill.cpython-310.pyc,, +skimage/morphology/__pycache__/_skeletonize.cpython-310.pyc,, +skimage/morphology/__pycache__/_util.cpython-310.pyc,, +skimage/morphology/__pycache__/binary.cpython-310.pyc,, +skimage/morphology/__pycache__/convex_hull.cpython-310.pyc,, +skimage/morphology/__pycache__/extrema.cpython-310.pyc,, +skimage/morphology/__pycache__/footprints.cpython-310.pyc,, +skimage/morphology/__pycache__/gray.cpython-310.pyc,, +skimage/morphology/__pycache__/grayreconstruct.cpython-310.pyc,, +skimage/morphology/__pycache__/isotropic.cpython-310.pyc,, +skimage/morphology/__pycache__/max_tree.cpython-310.pyc,, +skimage/morphology/__pycache__/misc.cpython-310.pyc,, +skimage/morphology/_convex_hull.cpython-310-x86_64-linux-gnu.so,sha256=vBcaGOdxr2d55zwH0djpnBsrmvKXQVzIuArnta1yqX8,236312 +skimage/morphology/_extrema_cy.cpython-310-x86_64-linux-gnu.so,sha256=J7Y_T64UXkV6FaSG7jnx6ljsFDCphOqOCzNoYROMwR4,338728 +skimage/morphology/_flood_fill.py,sha256=349jaVevFjmsQHM6ZRRR3Z-Oi0-gelWj6WP5MILXqwk,10733 +skimage/morphology/_flood_fill_cy.cpython-310-x86_64-linux-gnu.so,sha256=i05fmwwEVgGhkkc-W5rmfPWjDPeYK-WDj9A4H9Psr7s,410264 +skimage/morphology/_grayreconstruct.cpython-310-x86_64-linux-gnu.so,sha256=EevjoySaPknyjVFHyWopaOB-kPtSYpPppubV62bGn6s,294480 +skimage/morphology/_max_tree.cpython-310-x86_64-linux-gnu.so,sha256=PsrSrdoyLCo2lhWmMI2qj8nhCySwkHr5Ub6J5SXUvCQ,924168 +skimage/morphology/_misc_cy.cpython-310-x86_64-linux-gnu.so,sha256=mVYBmgk4drCtey6aLrBhnnlWDmuWBufSc7jmRzRQqI8,341440 +skimage/morphology/_skeletonize.py,sha256=nDm0COssRlhL8tlV3Z7mZWz1koRycB4-8G9kOT6fYqc,23432 +skimage/morphology/_skeletonize_lee_cy.cpython-310-x86_64-linux-gnu.so,sha256=Yv9Qaj-8QtKPGNVFBzodh_ImZkoh_EAD59mHbBdva1E,254424 +skimage/morphology/_skeletonize_various_cy.cpython-310-x86_64-linux-gnu.so,sha256=jtF5gMyfo-qMfpvq2m4Rv6cTUBeO7RLDTX8I8TMN5AU,259584 +skimage/morphology/_util.py,sha256=TBba9j3tF3srL-q54cNYTRaYy3_8rPuD73v1r1i63vc,12019 +skimage/morphology/ball_decompositions.npy,sha256=T561HzYf19fSLTQt7Hu5gXiiGqnJRFB_JYmLjKIT5U0,431 +skimage/morphology/binary.py,sha256=4Py-51eyBa64w1c_WInfWRNyZiknHQKmtC-y--EAo8I,12112 +skimage/morphology/convex_hull.py,sha256=5jLe9JbKXW9OBIuklxqYEm_ns-2usVuEx0wTZVmRnJA,8419 +skimage/morphology/disk_decompositions.npy,sha256=dpc557PYxwYZk2gs30WgwEiUCVL7eOMbFy2DD3545-4,881 +skimage/morphology/extrema.py,sha256=9LS6UvpGSmGW2gzxPpVtEU00beb_mh8C4Y1_ua47mlE,20816 +skimage/morphology/footprints.py,sha256=Q_XBpszl9pJ2nKMTp0sMM7d3ogf4PUuKMDusMPXCrsE,42736 +skimage/morphology/gray.py,sha256=aCBVlVr8mbFOhkUc2v05JadxY3UTFo65FLeQoKc8sgI,25928 +skimage/morphology/grayreconstruct.py,sha256=-HwSfTlqe29Yp-IQmxhNQdfbJtgCHMoixj3fynFXEU0,9350 +skimage/morphology/isotropic.py,sha256=XRVAX2ha5tw_XLQWd5HiQUGtcZ8MvChzaQPYDzXcinQ,7879 +skimage/morphology/max_tree.py,sha256=7ZhC3CU-L4_GTq4yd_mwLPWC1xw7-uT_lGi5wS86hnQ,26983 +skimage/morphology/misc.py,sha256=-iqoEXLgnhfGm0uXFCnT1B2zX28UtdBLtycbmUZ3sOE,16681 +skimage/morphology/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/morphology/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_binary.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_convex_hull.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_extrema.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_flood_fill.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_footprints.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_gray.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_isotropic.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_max_tree.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_misc.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_reconstruction.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_skeletonize.cpython-310.pyc,, +skimage/morphology/tests/__pycache__/test_util.cpython-310.pyc,, +skimage/morphology/tests/test_binary.py,sha256=QqlWSIas2c3fORXHSkp6B_puCXYp-5hSu_wfE2ycAsE,12011 +skimage/morphology/tests/test_convex_hull.py,sha256=VJTgBb1BgQucfdm79vVwxasw_1K6ez4iDNgOADB80W8,8860 +skimage/morphology/tests/test_extrema.py,sha256=xMZ1GPgOZELD3yGX_Bx9CYOrjjGux5A8pKlNF1VgZqg,26904 +skimage/morphology/tests/test_flood_fill.py,sha256=YQrhCQWlDE9nnu_G280p7t3bsUvOLD2UEmKx9uuoLhQ,9418 +skimage/morphology/tests/test_footprints.py,sha256=egkde7pWzwp_NK_0lxAjQNeNC_akQ5NxuuEdoEcJT94,11775 +skimage/morphology/tests/test_gray.py,sha256=u9Xq9AlQbRWQ16wn4FoDuYyVd7JKP-kMc1HvgneM9CM,16340 +skimage/morphology/tests/test_isotropic.py,sha256=U-04r2oLsEJ-mh1nlSpIlYAxD0Cg4BqWZ9xS-KSZ9N8,2845 +skimage/morphology/tests/test_max_tree.py,sha256=72PZorjk-37dJ_vm7jxD92pH1EnU-82IVCJOlIyjsME,20611 +skimage/morphology/tests/test_misc.py,sha256=mcqCySpSnLakmn_gKFYa7jdv57CV6mjnkdjY2i5AF_s,18008 +skimage/morphology/tests/test_reconstruction.py,sha256=FVzLHWnKvsxWE73lLEmnwQIIUNSkfuQ-NXXoaeLtN3c,5868 +skimage/morphology/tests/test_skeletonize.py,sha256=fOa1G4ARj22l_06DvLUji3SPcuzpU8KMBZe15xt19mE,13680 +skimage/morphology/tests/test_util.py,sha256=Vi6MJVYtIaWtFdDrxeK1e-hXv7Ff8WKeAfmzeK5HHTs,5660 +skimage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/registration/__init__.py,sha256=WGYVol8x2WxB28mx35mdliJgJ_c0JHE7baSQ4Kxqtqo,184 +skimage/registration/__init__.pyi,sha256=FDJiD5iCOBzWws3vx3ON2PhpoujSvUhtcCjdArGrM74,366 +skimage/registration/__pycache__/__init__.cpython-310.pyc,, +skimage/registration/__pycache__/_masked_phase_cross_correlation.cpython-310.pyc,, +skimage/registration/__pycache__/_optical_flow.cpython-310.pyc,, +skimage/registration/__pycache__/_optical_flow_utils.cpython-310.pyc,, +skimage/registration/__pycache__/_phase_cross_correlation.cpython-310.pyc,, +skimage/registration/_masked_phase_cross_correlation.py,sha256=BigH4m9EEQL0p-ZrZ9YTirNuJ9u0jbYi5YmOZKGH_30,12412 +skimage/registration/_optical_flow.py,sha256=dVDHwS5GHlT_ir1_OjGieNz56RCpvC6v1pJxzLgG9U4,14547 +skimage/registration/_optical_flow_utils.py,sha256=aWgyZhaeXedlmzgr614oShB_ITs__sgAdnMHpVlL4P8,3680 +skimage/registration/_phase_cross_correlation.py,sha256=Lg0gsrIujU2fIytSnHm64P55XlYWpVuXgklOVPKWQME,17867 +skimage/registration/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/registration/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/registration/tests/__pycache__/test_ilk.cpython-310.pyc,, +skimage/registration/tests/__pycache__/test_masked_phase_cross_correlation.cpython-310.pyc,, +skimage/registration/tests/__pycache__/test_phase_cross_correlation.cpython-310.pyc,, +skimage/registration/tests/__pycache__/test_tvl1.cpython-310.pyc,, +skimage/registration/tests/test_ilk.py,sha256=xeAUy-zHmcyXulomUGujIoB8NZ8NdM6zzfGliYcIM_M,3096 +skimage/registration/tests/test_masked_phase_cross_correlation.py,sha256=-QQxqNJhL3P9OqlO6Msk_vsCezXIlirMlA_TWw3ltLc,9824 +skimage/registration/tests/test_phase_cross_correlation.py,sha256=CB6_eqOa_j_GuZBZPAlHACD3kTyGBprlVZm6WpLIHJ4,8377 +skimage/registration/tests/test_tvl1.py,sha256=gIIgiMW7mGkoiWr9LjEz-c3n5F7h2IW64aiqMjUKdhI,3564 +skimage/restoration/__init__.py,sha256=DgJTx8pODATVzrTsgIy0RpwL4ojH-dl6ym7w312JF-4,178 +skimage/restoration/__init__.pyi,sha256=mw9Cug3REvQ1R0NveVQXbSkT-JRo6uqSp0gx4eyEG8Y,1067 +skimage/restoration/__pycache__/__init__.cpython-310.pyc,, +skimage/restoration/__pycache__/_cycle_spin.cpython-310.pyc,, +skimage/restoration/__pycache__/_denoise.cpython-310.pyc,, +skimage/restoration/__pycache__/_rolling_ball.cpython-310.pyc,, +skimage/restoration/__pycache__/deconvolution.cpython-310.pyc,, +skimage/restoration/__pycache__/inpaint.cpython-310.pyc,, +skimage/restoration/__pycache__/j_invariant.cpython-310.pyc,, +skimage/restoration/__pycache__/non_local_means.cpython-310.pyc,, +skimage/restoration/__pycache__/uft.cpython-310.pyc,, +skimage/restoration/__pycache__/unwrap.cpython-310.pyc,, +skimage/restoration/_cycle_spin.py,sha256=9VZkZGE9IOEnMEs3WcMeijeKvzqa61NYAsp40mr8Htk,5885 +skimage/restoration/_denoise.py,sha256=_vOvvb4SZyll-WcGUDNyLMSLkbPLQIiWBns7W8i6J2s,41403 +skimage/restoration/_denoise_cy.cpython-310-x86_64-linux-gnu.so,sha256=pfMm5UpX_d71mkFTrEaETKMojUn2Co9ZCNm09zyAnao,371536 +skimage/restoration/_inpaint.cpython-310-x86_64-linux-gnu.so,sha256=Gj1FJobB7iFytFvsqF0RLT9uD2SiqH1akUWCl7zye5A,281040 +skimage/restoration/_nl_means_denoising.cpython-310-x86_64-linux-gnu.so,sha256=lpBayS0Z-RPP243GSWc5d8LsN8AHVFbg79fhXLj2DJw,635280 +skimage/restoration/_rolling_ball.py,sha256=rnjEy1G1IXN1uh70bpYLDwou2pIqoG7WHtKmrT_z0Co,6851 +skimage/restoration/_rolling_ball_cy.cpython-310-x86_64-linux-gnu.so,sha256=dOKQFXOAZiA2u1a6YfOrJrmGNvwrbNA6bn9qfcPwaKI,327960 +skimage/restoration/_unwrap_1d.cpython-310-x86_64-linux-gnu.so,sha256=z0w4izLBnmwMXzABaa7CsthytewC7L7NDRNPOY93yAs,227056 +skimage/restoration/_unwrap_2d.cpython-310-x86_64-linux-gnu.so,sha256=HIOSnzT7jDJ3sIREGULGIAZHPf0QuORw3PmtLwGQZxc,244520 +skimage/restoration/_unwrap_3d.cpython-310-x86_64-linux-gnu.so,sha256=nBPpllAKG4zA4zmhUWr6QuIgHOzV1Kbjq0FlzI6mk7c,261104 +skimage/restoration/deconvolution.py,sha256=F3RX4x-78NYUt0b_6o4sqFXejR-U1vLziefWLhKESNY,16032 +skimage/restoration/inpaint.py,sha256=87oZAt6ixrbH7KfdUTOokfyUyhcnLYrr8CIpLR9jW_I,12657 +skimage/restoration/j_invariant.py,sha256=7e_m9oFc2gFlY_9eM5vRJ_6KLMzrqFmkdodC-0JOmC4,12402 +skimage/restoration/non_local_means.py,sha256=ftcQMALybovCQn-aJCMCsdmUtZh0BYx5dT9wAVIZFLI,7718 +skimage/restoration/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/restoration/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_denoise.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_inpaint.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_j_invariant.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_restoration.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_rolling_ball.cpython-310.pyc,, +skimage/restoration/tests/__pycache__/test_unwrap.cpython-310.pyc,, +skimage/restoration/tests/test_denoise.py,sha256=l97RnNfxN9xfe0ymygYpcnJ1zpFPeoxeVPweKf1rmT4,40375 +skimage/restoration/tests/test_inpaint.py,sha256=TdZRykL8xCuu5d1kRT7gtXCT8N-zeswu4mn35jNY7GU,6853 +skimage/restoration/tests/test_j_invariant.py,sha256=rRlL8t4SIHsczzyk1VB9GFlL_LFfo8HyuJnKckVqyTM,3273 +skimage/restoration/tests/test_restoration.py,sha256=CMd-epKF5WkD4fVg9vevnrhU__OHZBeqkqd-d8F_rpw,6451 +skimage/restoration/tests/test_rolling_ball.py,sha256=hIGTGLrlk4LHidMUxjkIxRBsBw7lLscA8jGAR447MfM,3071 +skimage/restoration/tests/test_unwrap.py,sha256=6J0xxzISR5CvTZm6di22AlaylLbDf0z63bVZCIg7zXY,8347 +skimage/restoration/uft.py,sha256=gTCw6pnKeRO0R7DqMTrOkV3H1r00qHQKoht_k0WnExs,12613 +skimage/restoration/unwrap.py,sha256=BxeM0cWdKpQSMCEUR2uzlI5kbTryFtk8tkLBV0zXDn0,4830 +skimage/segmentation/__init__.py,sha256=nUFS40ztKnGtESHJLFMPRN5xA1igZ00SknBK-_lxKp8,1253 +skimage/segmentation/__pycache__/__init__.cpython-310.pyc,, +skimage/segmentation/__pycache__/_chan_vese.cpython-310.pyc,, +skimage/segmentation/__pycache__/_clear_border.cpython-310.pyc,, +skimage/segmentation/__pycache__/_expand_labels.cpython-310.pyc,, +skimage/segmentation/__pycache__/_felzenszwalb.cpython-310.pyc,, +skimage/segmentation/__pycache__/_join.cpython-310.pyc,, +skimage/segmentation/__pycache__/_quickshift.cpython-310.pyc,, +skimage/segmentation/__pycache__/_watershed.cpython-310.pyc,, +skimage/segmentation/__pycache__/active_contour_model.cpython-310.pyc,, +skimage/segmentation/__pycache__/boundaries.cpython-310.pyc,, +skimage/segmentation/__pycache__/morphsnakes.cpython-310.pyc,, +skimage/segmentation/__pycache__/random_walker_segmentation.cpython-310.pyc,, +skimage/segmentation/__pycache__/slic_superpixels.cpython-310.pyc,, +skimage/segmentation/_chan_vese.py,sha256=HHgn7gjE8dOqdAt6QM8zyyYZt1SE_EQDtp73c9Gd09I,13774 +skimage/segmentation/_clear_border.py,sha256=d9JzobF-EdVq6rQh3IhcSNPDW9FJCWDyNUHSOu9fIgE,3989 +skimage/segmentation/_expand_labels.py,sha256=Q8NYU2LYQO5tQ2rXKhg0xpKu-RMwI6WwA_LxrskYlUg,4201 +skimage/segmentation/_felzenszwalb.py,sha256=wl6nKzjrWW_fJw-sFeLDq5fhGVtJlZEBD3W4yu8DbFs,2486 +skimage/segmentation/_felzenszwalb_cy.cpython-310-x86_64-linux-gnu.so,sha256=Fy1cNUAQXMhRZZMnw25R2V3MiPcEpTybhte9elS50vA,182264 +skimage/segmentation/_join.py,sha256=jR2hcXvzWBj1cVb4ipTNhKlSf0iv8W1g4lC32ARc2L4,7134 +skimage/segmentation/_quickshift.py,sha256=jPSH6-X45DTnxEA_17EMo0W2fzEoVpc3G2hY_nzlzzs,3469 +skimage/segmentation/_quickshift_cy.cpython-310-x86_64-linux-gnu.so,sha256=grODum8T3gGAXabKISnlAvVMakCDYjs9nEa7mBzkjmg,326120 +skimage/segmentation/_slic.cpython-310-x86_64-linux-gnu.so,sha256=uX2F8qCudvgf_-AF7FneC2LvifjgrXhbyt6e68HRFpY,340408 +skimage/segmentation/_watershed.py,sha256=LEqyylS6eJ9sAVgdQAwqM7gKDhF-DFyVJUWmdQPuNUw,9813 +skimage/segmentation/_watershed_cy.cpython-310-x86_64-linux-gnu.so,sha256=ZIT5rb6C7ApB6k17jQ-pxj-QckEzhT4fiNDVatwxLdY,342240 +skimage/segmentation/active_contour_model.py,sha256=MV8bhOv5vTaLwYAvW1uzafYtZTbEHPkC24kKHEjQXNY,7839 +skimage/segmentation/boundaries.py,sha256=-9u_NtiBPAnQBYPSNQY6ywkR2ELNlGKNSC1cVoWBgTE,10016 +skimage/segmentation/morphsnakes.py,sha256=cc7Qm7qzkYUjw4q-2hI1p5yjTrdGfXaHXg71sT0gktY,14882 +skimage/segmentation/random_walker_segmentation.py,sha256=eT1p12CM98AVr4HbvWyF9dvNUBI5nN74BdkpLZvi04g,21712 +skimage/segmentation/slic_superpixels.py,sha256=FLukNVkXZIEaXepIzzFJjLoG0jv2xr1lZqHtaOfGXaU,16353 +skimage/segmentation/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/segmentation/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_active_contour_model.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_boundaries.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_chan_vese.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_clear_border.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_expand_labels.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_felzenszwalb.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_join.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_morphsnakes.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_quickshift.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_random_walker.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_slic.cpython-310.pyc,, +skimage/segmentation/tests/__pycache__/test_watershed.cpython-310.pyc,, +skimage/segmentation/tests/test_active_contour_model.py,sha256=HEtlByzNaoFJ4zm-Te6bW6vCsTr4W0cseTM0LM9QgnM,5878 +skimage/segmentation/tests/test_boundaries.py,sha256=d7MUg8exUxgWuKLqyV9I_6F0nJ11sPZEJIX1cd4jFOE,5212 +skimage/segmentation/tests/test_chan_vese.py,sha256=7uf4IzKfGMzmENIGtHEhXqn2e5-x5MbHmCoi-IlIyxI,3369 +skimage/segmentation/tests/test_clear_border.py,sha256=aLtS_uRV8Rih3OdZT1TlmZpcO8vPvrqEJkegTaYTo4c,5501 +skimage/segmentation/tests/test_expand_labels.py,sha256=OYRVdLrvrilXfykHrOua8fTmFzSL8IgtJdMpsKSUCHg,6620 +skimage/segmentation/tests/test_felzenszwalb.py,sha256=Ffy_yzqAYypIim_GnqHWGFgIdkQAn042GhWrw87iJ2U,2853 +skimage/segmentation/tests/test_join.py,sha256=5InTQU1SCd489FJLwL-3jEaLfgpzRVNYR0YaU2pJOxo,7198 +skimage/segmentation/tests/test_morphsnakes.py,sha256=U2PfLgu1yK0OYQmOH2c9cwZGVMUZPnxbUTp1HmrhLss,4605 +skimage/segmentation/tests/test_quickshift.py,sha256=Id75BasgHc5trx6Q6PZDc2T5iELEeqD8mLkwgacLKjY,2388 +skimage/segmentation/tests/test_random_walker.py,sha256=eU6OLkRKpfonhzhofMxYHwa061R3xwFfLUCHtKdyceA,21603 +skimage/segmentation/tests/test_slic.py,sha256=4qRp3R4bLhkLcpuLH0Yx4JWKDchCw9B84FUAXmP2wSY,18490 +skimage/segmentation/tests/test_watershed.py,sha256=h4FAhNaHtSjAX6QULJ5qqtW9rrPcNL8tZWYCtkFXjTk,33360 +skimage/transform/__init__.py,sha256=SjgjAu0K4Tya74fEiJSd5yITbVoH7pe4DGmDcgk4Vrk,1493 +skimage/transform/__init__.pyi,sha256=uDxd4AETK9S_MLmnSXoTTAVNUXCy2idNbiWtW4IavPE,1981 +skimage/transform/__pycache__/__init__.cpython-310.pyc,, +skimage/transform/__pycache__/_geometric.cpython-310.pyc,, +skimage/transform/__pycache__/_thin_plate_splines.cpython-310.pyc,, +skimage/transform/__pycache__/_warps.cpython-310.pyc,, +skimage/transform/__pycache__/finite_radon_transform.cpython-310.pyc,, +skimage/transform/__pycache__/hough_transform.cpython-310.pyc,, +skimage/transform/__pycache__/integral.cpython-310.pyc,, +skimage/transform/__pycache__/pyramids.cpython-310.pyc,, +skimage/transform/__pycache__/radon_transform.cpython-310.pyc,, +skimage/transform/_geometric.py,sha256=JvJfZewlvFj1xtyv4tNOZ78Ja_aaqS8PJs-XrT6AuC8,58708 +skimage/transform/_hough_transform.cpython-310-x86_64-linux-gnu.so,sha256=x3vYdt_6DRfEeZUjBv9GsSvcyUlWT8xalFLLCUJg87I,354160 +skimage/transform/_radon_transform.cpython-310-x86_64-linux-gnu.so,sha256=W2X_HN_tjdK8GjCmO2K4MC6tYfmC8WIU5_i0jgcBE9k,294088 +skimage/transform/_thin_plate_splines.py,sha256=WZxyhkzasSY0v9tCPM8pa6eP36M1l-zbrztIT7rXTLQ,5801 +skimage/transform/_warps.py,sha256=0SOXcrkW-C8zzJC8_GSpJfDrGDWSRDODTFz234WaDh4,48511 +skimage/transform/_warps_cy.cpython-310-x86_64-linux-gnu.so,sha256=1x-LTaVyGTjCz3ogBTK6fxpX9FRq9xqelg3Ri3gtbk4,309000 +skimage/transform/finite_radon_transform.py,sha256=Jln0B-406LJcnpSknQui7xD-igI0-SzO57hGmCkBccw,3179 +skimage/transform/hough_transform.py,sha256=z5bPOHpGFjsatTomi9_LHddR_wYjuPZVQ_UKd37S-ZY,15867 +skimage/transform/integral.py,sha256=7Rh9I7C0fbuEV7Z9RR-aoZ45I3vzIsgakvq1oYApJ9Y,5096 +skimage/transform/pyramids.py,sha256=s6eEuuaVfT7gm2U7oPY-IfKradesoVwTKcjFoDkEemU,13357 +skimage/transform/radon_transform.py,sha256=y03ACiOrIETso2bUT9Gp4cbkMzDED0F7reL99I45rK8,20738 +skimage/transform/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/transform/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_finite_radon_transform.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_geometric.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_hough_transform.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_integral.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_pyramids.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_radon_transform.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_thin_plate_splines.cpython-310.pyc,, +skimage/transform/tests/__pycache__/test_warps.cpython-310.pyc,, +skimage/transform/tests/test_finite_radon_transform.py,sha256=Ms2EwaWZ6irxVaUU-zovgL_3tF-YiFA-r8v3UKV78Ak,316 +skimage/transform/tests/test_geometric.py,sha256=juR9L4y9I5Xo7cF2lB_-NrocTolT07pbipNIcuHDj-4,34477 +skimage/transform/tests/test_hough_transform.py,sha256=bEuKtpe3jEofqDVdbOJ8k6m8rrxIzZtfCi32thyn4Jk,19060 +skimage/transform/tests/test_integral.py,sha256=4yL_gCYjzk5OtI7unTmBNbEu4k1KLRHiOrXx2rS42Os,2337 +skimage/transform/tests/test_pyramids.py,sha256=YLylJjJkJyvE9QeAyAiPL-6w6LdN1KefFdS81waamlI,8047 +skimage/transform/tests/test_radon_transform.py,sha256=gmo0Goii6SgriMUtf0Rw4PeV1ZHw0CV42Fb8UPi945c,18625 +skimage/transform/tests/test_thin_plate_splines.py,sha256=UfkAFYDuLL7kBUIQ5n_S992fxqMVMtUFthzb0LEy6Cg,2742 +skimage/transform/tests/test_warps.py,sha256=hoddh3wEAG0GLCDNHjFyXPwfLNxDp6pRR_aU3J5RUK0,32993 +skimage/util/__init__.py,sha256=R-wuBwNEj640SbbdyXBP7UAQiWavvHAIQR5mw-UQTFM,1327 +skimage/util/__pycache__/__init__.cpython-310.pyc,, +skimage/util/__pycache__/_invert.cpython-310.pyc,, +skimage/util/__pycache__/_label.cpython-310.pyc,, +skimage/util/__pycache__/_map_array.cpython-310.pyc,, +skimage/util/__pycache__/_montage.cpython-310.pyc,, +skimage/util/__pycache__/_regular_grid.cpython-310.pyc,, +skimage/util/__pycache__/_slice_along_axes.cpython-310.pyc,, +skimage/util/__pycache__/apply_parallel.cpython-310.pyc,, +skimage/util/__pycache__/arraycrop.cpython-310.pyc,, +skimage/util/__pycache__/compare.cpython-310.pyc,, +skimage/util/__pycache__/dtype.cpython-310.pyc,, +skimage/util/__pycache__/lookfor.cpython-310.pyc,, +skimage/util/__pycache__/noise.cpython-310.pyc,, +skimage/util/__pycache__/shape.cpython-310.pyc,, +skimage/util/__pycache__/unique.cpython-310.pyc,, +skimage/util/_invert.py,sha256=Yb-ML5OWLDxkrZ1KezI0SUUToJoiWmMDNiQ2bhMGYbw,2560 +skimage/util/_label.py,sha256=IIZB-YW5o8kfWw6gWnaiOlKpN9QA2YaTF1k1_cvl7lg,1568 +skimage/util/_map_array.py,sha256=KyrZTTtoNjcbnxKP5AwScKYVAdAw1wEMsexW7y-qFk4,6695 +skimage/util/_montage.py,sha256=zDz0BAOyBIgVU080c2L_qLqWzLM8_BKxDkp5e5i_9LE,4779 +skimage/util/_regular_grid.py,sha256=ZPzONVbwy_jK_QWlTqNUW2Wxi9yrvuJblTohugaHVYE,3889 +skimage/util/_remap.cpython-310-x86_64-linux-gnu.so,sha256=Y9M_NK8SiSMXvesz6ERE-elnoWpqHJnQSRBTvqzbAVg,863048 +skimage/util/_slice_along_axes.py,sha256=GnVxeqAjbDULUc27edh4dBHXFRvsLc0zWkdII7K-G6w,2577 +skimage/util/apply_parallel.py,sha256=RmxuSK7XzT3I09W7HNpmxTeFWB6mNNXl4wlMsvZwrM0,7649 +skimage/util/arraycrop.py,sha256=VY355ZEFWG0LDjdKM4Nr1EeRnp5dkfIVhhJ7fK-r-6w,2486 +skimage/util/compare.py,sha256=99-dgt1POAOxXGe25O3WrU0mVEnf8VR2YTDEp8Sot48,4507 +skimage/util/dtype.py,sha256=lo4Q6NjGv8B5VT-JRhIX7kIOAZYlVvl8_3XdtO55c6s,17681 +skimage/util/lookfor.py,sha256=dVNQdXVwNjifvr0U7hl8lWnIkA-yx4aUQZhayBdkawE,790 +skimage/util/noise.py,sha256=US93udCAXXM9Hyrua8mMfd3fnV91BL50kIv9qJ4LD08,8563 +skimage/util/shape.py,sha256=_JqxvcCfpGSCi27jzpbmENBGBrkME6PtWPgBOOZch2s,7828 +skimage/util/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skimage/util/tests/__pycache__/__init__.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_apply_parallel.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_arraycrop.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_compare.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_dtype.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_invert.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_labels.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_lookfor.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_map_array.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_montage.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_random_noise.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_regular_grid.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_shape.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_slice_along_axes.cpython-310.pyc,, +skimage/util/tests/__pycache__/test_unique_rows.cpython-310.pyc,, +skimage/util/tests/test_apply_parallel.py,sha256=JJktiQVZthEx31ez46tgh31b9B8XBGXoB8fBVrRbISE,5028 +skimage/util/tests/test_arraycrop.py,sha256=74HQ6L3Fr-M7SbzsSysQX5C5WJi_rkCYaT1CEPt2TUY,1848 +skimage/util/tests/test_compare.py,sha256=6vw-pKwGQmoeksMI7xaXEDb6t0-fl9oEU-04n8Z2elY,3890 +skimage/util/tests/test_dtype.py,sha256=1WgIQDe2AJ6mJSa2Jc51dHdBgLjGal8_cxyyr_7meTM,6538 +skimage/util/tests/test_invert.py,sha256=zyGo_pKyh4lkGBYMoH2NUybt8bS8mrLCKORFhfldegg,2415 +skimage/util/tests/test_labels.py,sha256=4anR0FT19CwbCUeSws27o3OB3GqBgF94FsfGvHnZQv4,1802 +skimage/util/tests/test_lookfor.py,sha256=mWlprwlDVZ5wZCF5-vpqNCG3JszKzW1pbuFi_u7f-I0,305 +skimage/util/tests/test_map_array.py,sha256=_sZd5Y0cOSH4vrffi0ytQv61LChB64pSrtc8yzD3FUA,2900 +skimage/util/tests/test_montage.py,sha256=NtQ6oOPXBnP1IedY6PhcAJvJLH0DE11ifXU02CZCsO0,5670 +skimage/util/tests/test_random_noise.py,sha256=M-AfwvDPTGx5knXRRg8Fgj76EML8dPmnkKDXFhWjFZU,7670 +skimage/util/tests/test_regular_grid.py,sha256=mBlLa6recFg17Re44x25nts1ZLF9GKFs-skHc_yxPdc,980 +skimage/util/tests/test_shape.py,sha256=5xNtsof9rRzi_-GfHNeyuHvhCyZE-Q3HIlZ2Y-tOqUs,4549 +skimage/util/tests/test_slice_along_axes.py,sha256=3JwGWfOXd9tU1pRtVJQhB1PxFxS5p5FUDomRt9AgGeY,1681 +skimage/util/tests/test_unique_rows.py,sha256=YZvcX1tHXRGyg7x9w71v7dJCi4CuS_ZZkZtkfjM2H3k,1099 +skimage/util/unique.py,sha256=ctiKRbgA7Zra5-yA58gKup0fksPuPSHYcx_O_HU711E,1516 diff --git a/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/REQUESTED b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/WHEEL b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..4e4c38ae320920b8f083b87f408214cdecd350d2 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_image-0.25.2.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: meson +Root-Is-Purelib: false +Tag: cp310-cp310-manylinux_2_17_x86_64 +Tag: cp310-cp310-manylinux2014_x86_64 + diff --git a/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/INSTALLER b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/METADATA b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..2ec4ebf9b1c8691818e8d2b451a21e25c0dd80a3 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/METADATA @@ -0,0 +1,307 @@ +Metadata-Version: 2.4 +Name: scikit-learn +Version: 1.7.1 +Summary: A set of python modules for machine learning and data mining +Maintainer-Email: scikit-learn developers +License-Expression: BSD-3-Clause +License-File: COPYING +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development +Classifier: Topic :: Scientific/Engineering +Classifier: Development Status :: 5 - Production/Stable +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +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 :: Implementation :: CPython +Project-URL: homepage, https://scikit-learn.org +Project-URL: source, https://github.com/scikit-learn/scikit-learn +Project-URL: download, https://pypi.org/project/scikit-learn/#files +Project-URL: tracker, https://github.com/scikit-learn/scikit-learn/issues +Project-URL: release notes, https://scikit-learn.org/stable/whats_new +Requires-Python: >=3.10 +Requires-Dist: numpy>=1.22.0 +Requires-Dist: scipy>=1.8.0 +Requires-Dist: joblib>=1.2.0 +Requires-Dist: threadpoolctl>=3.1.0 +Provides-Extra: build +Requires-Dist: numpy>=1.22.0; extra == "build" +Requires-Dist: scipy>=1.8.0; extra == "build" +Requires-Dist: cython>=3.0.10; extra == "build" +Requires-Dist: meson-python>=0.17.1; extra == "build" +Provides-Extra: install +Requires-Dist: numpy>=1.22.0; extra == "install" +Requires-Dist: scipy>=1.8.0; extra == "install" +Requires-Dist: joblib>=1.2.0; extra == "install" +Requires-Dist: threadpoolctl>=3.1.0; extra == "install" +Provides-Extra: benchmark +Requires-Dist: matplotlib>=3.5.0; extra == "benchmark" +Requires-Dist: pandas>=1.4.0; extra == "benchmark" +Requires-Dist: memory_profiler>=0.57.0; extra == "benchmark" +Provides-Extra: docs +Requires-Dist: matplotlib>=3.5.0; extra == "docs" +Requires-Dist: scikit-image>=0.19.0; extra == "docs" +Requires-Dist: pandas>=1.4.0; extra == "docs" +Requires-Dist: seaborn>=0.9.0; extra == "docs" +Requires-Dist: memory_profiler>=0.57.0; extra == "docs" +Requires-Dist: sphinx>=7.3.7; extra == "docs" +Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs" +Requires-Dist: sphinx-gallery>=0.17.1; extra == "docs" +Requires-Dist: numpydoc>=1.2.0; extra == "docs" +Requires-Dist: Pillow>=8.4.0; extra == "docs" +Requires-Dist: pooch>=1.6.0; extra == "docs" +Requires-Dist: sphinx-prompt>=1.4.0; extra == "docs" +Requires-Dist: sphinxext-opengraph>=0.9.1; extra == "docs" +Requires-Dist: plotly>=5.14.0; extra == "docs" +Requires-Dist: polars>=0.20.30; extra == "docs" +Requires-Dist: sphinx-design>=0.5.0; extra == "docs" +Requires-Dist: sphinx-design>=0.6.0; extra == "docs" +Requires-Dist: sphinxcontrib-sass>=0.3.4; extra == "docs" +Requires-Dist: pydata-sphinx-theme>=0.15.3; extra == "docs" +Requires-Dist: sphinx-remove-toctrees>=1.0.0.post1; extra == "docs" +Requires-Dist: towncrier>=24.8.0; extra == "docs" +Provides-Extra: examples +Requires-Dist: matplotlib>=3.5.0; extra == "examples" +Requires-Dist: scikit-image>=0.19.0; extra == "examples" +Requires-Dist: pandas>=1.4.0; extra == "examples" +Requires-Dist: seaborn>=0.9.0; extra == "examples" +Requires-Dist: pooch>=1.6.0; extra == "examples" +Requires-Dist: plotly>=5.14.0; extra == "examples" +Provides-Extra: tests +Requires-Dist: matplotlib>=3.5.0; extra == "tests" +Requires-Dist: scikit-image>=0.19.0; extra == "tests" +Requires-Dist: pandas>=1.4.0; extra == "tests" +Requires-Dist: pytest>=7.1.2; extra == "tests" +Requires-Dist: pytest-cov>=2.9.0; extra == "tests" +Requires-Dist: ruff>=0.11.7; extra == "tests" +Requires-Dist: mypy>=1.15; extra == "tests" +Requires-Dist: pyamg>=4.2.1; extra == "tests" +Requires-Dist: polars>=0.20.30; extra == "tests" +Requires-Dist: pyarrow>=12.0.0; extra == "tests" +Requires-Dist: numpydoc>=1.2.0; extra == "tests" +Requires-Dist: pooch>=1.6.0; extra == "tests" +Provides-Extra: maintenance +Requires-Dist: conda-lock==3.0.1; extra == "maintenance" +Description-Content-Type: text/x-rst + +.. -*- mode: rst -*- + +|Azure| |Codecov| |CircleCI| |Nightly wheels| |Ruff| |PythonVersion| |PyPi| |DOI| |Benchmark| + +.. |Azure| image:: https://dev.azure.com/scikit-learn/scikit-learn/_apis/build/status/scikit-learn.scikit-learn?branchName=main + :target: https://dev.azure.com/scikit-learn/scikit-learn/_build/latest?definitionId=1&branchName=main + +.. |CircleCI| image:: https://circleci.com/gh/scikit-learn/scikit-learn/tree/main.svg?style=shield + :target: https://circleci.com/gh/scikit-learn/scikit-learn + +.. |Codecov| image:: https://codecov.io/gh/scikit-learn/scikit-learn/branch/main/graph/badge.svg?token=Pk8G9gg3y9 + :target: https://codecov.io/gh/scikit-learn/scikit-learn + +.. |Nightly wheels| image:: https://github.com/scikit-learn/scikit-learn/actions/workflows/wheels.yml/badge.svg?event=schedule + :target: https://github.com/scikit-learn/scikit-learn/actions?query=workflow%3A%22Wheel+builder%22+event%3Aschedule + +.. |Ruff| image:: https://img.shields.io/badge/code%20style-ruff-000000.svg + :target: https://github.com/astral-sh/ruff + +.. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/scikit-learn.svg + :target: https://pypi.org/project/scikit-learn/ + +.. |PyPi| image:: https://img.shields.io/pypi/v/scikit-learn + :target: https://pypi.org/project/scikit-learn + +.. |DOI| image:: https://zenodo.org/badge/21369/scikit-learn/scikit-learn.svg + :target: https://zenodo.org/badge/latestdoi/21369/scikit-learn/scikit-learn + +.. |Benchmark| image:: https://img.shields.io/badge/Benchmarked%20by-asv-blue + :target: https://scikit-learn.org/scikit-learn-benchmarks + +.. |PythonMinVersion| replace:: 3.10 +.. |NumPyMinVersion| replace:: 1.22.0 +.. |SciPyMinVersion| replace:: 1.8.0 +.. |JoblibMinVersion| replace:: 1.2.0 +.. |ThreadpoolctlMinVersion| replace:: 3.1.0 +.. |MatplotlibMinVersion| replace:: 3.5.0 +.. |Scikit-ImageMinVersion| replace:: 0.19.0 +.. |PandasMinVersion| replace:: 1.4.0 +.. |SeabornMinVersion| replace:: 0.9.0 +.. |PytestMinVersion| replace:: 7.1.2 +.. |PlotlyMinVersion| replace:: 5.14.0 + +.. image:: https://raw.githubusercontent.com/scikit-learn/scikit-learn/main/doc/logos/scikit-learn-logo.png + :target: https://scikit-learn.org/ + +**scikit-learn** is a Python module for machine learning built on top of +SciPy and is distributed under the 3-Clause BSD license. + +The project was started in 2007 by David Cournapeau as a Google Summer +of Code project, and since then many volunteers have contributed. See +the `About us `__ page +for a list of core contributors. + +It is currently maintained by a team of volunteers. + +Website: https://scikit-learn.org + +Installation +------------ + +Dependencies +~~~~~~~~~~~~ + +scikit-learn requires: + +- Python (>= |PythonMinVersion|) +- NumPy (>= |NumPyMinVersion|) +- SciPy (>= |SciPyMinVersion|) +- joblib (>= |JoblibMinVersion|) +- threadpoolctl (>= |ThreadpoolctlMinVersion|) + +======= + +Scikit-learn plotting capabilities (i.e., functions start with ``plot_`` and +classes end with ``Display``) require Matplotlib (>= |MatplotlibMinVersion|). +For running the examples Matplotlib >= |MatplotlibMinVersion| is required. +A few examples require scikit-image >= |Scikit-ImageMinVersion|, a few examples +require pandas >= |PandasMinVersion|, some examples require seaborn >= +|SeabornMinVersion| and plotly >= |PlotlyMinVersion|. + +User installation +~~~~~~~~~~~~~~~~~ + +If you already have a working installation of NumPy and SciPy, +the easiest way to install scikit-learn is using ``pip``:: + + pip install -U scikit-learn + +or ``conda``:: + + conda install -c conda-forge scikit-learn + +The documentation includes more detailed `installation instructions `_. + + +Changelog +--------- + +See the `changelog `__ +for a history of notable changes to scikit-learn. + +Development +----------- + +We welcome new contributors of all experience levels. The scikit-learn +community goals are to be helpful, welcoming, and effective. The +`Development Guide `_ +has detailed information about contributing code, documentation, tests, and +more. We've included some basic information in this README. + +Important links +~~~~~~~~~~~~~~~ + +- Official source code repo: https://github.com/scikit-learn/scikit-learn +- Download releases: https://pypi.org/project/scikit-learn/ +- Issue tracker: https://github.com/scikit-learn/scikit-learn/issues + +Source code +~~~~~~~~~~~ + +You can check the latest sources with the command:: + + git clone https://github.com/scikit-learn/scikit-learn.git + +Contributing +~~~~~~~~~~~~ + +To learn more about making a contribution to scikit-learn, please see our +`Contributing guide +`_. + +Testing +~~~~~~~ + +After installation, you can launch the test suite from outside the source +directory (you will need to have ``pytest`` >= |PyTestMinVersion| installed):: + + pytest sklearn + +See the web page https://scikit-learn.org/dev/developers/contributing.html#testing-and-improving-test-coverage +for more information. + + Random number generation can be controlled during testing by setting + the ``SKLEARN_SEED`` environment variable. + +Submitting a Pull Request +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before opening a Pull Request, have a look at the +full Contributing page to make sure your code complies +with our guidelines: https://scikit-learn.org/stable/developers/index.html + +Project History +--------------- + +The project was started in 2007 by David Cournapeau as a Google Summer +of Code project, and since then many volunteers have contributed. See +the `About us `__ page +for a list of core contributors. + +The project is currently maintained by a team of volunteers. + +**Note**: `scikit-learn` was previously referred to as `scikits.learn`. + +Help and Support +---------------- + +Documentation +~~~~~~~~~~~~~ + +- HTML documentation (stable release): https://scikit-learn.org +- HTML documentation (development version): https://scikit-learn.org/dev/ +- FAQ: https://scikit-learn.org/stable/faq.html + +Communication +~~~~~~~~~~~~~ + +Main Channels +^^^^^^^^^^^^^ + +- **Website**: https://scikit-learn.org +- **Blog**: https://blog.scikit-learn.org +- **Mailing list**: https://mail.python.org/mailman/listinfo/scikit-learn + +Developer & Support +^^^^^^^^^^^^^^^^^^^^^^ + +- **GitHub Discussions**: https://github.com/scikit-learn/scikit-learn/discussions +- **Stack Overflow**: https://stackoverflow.com/questions/tagged/scikit-learn +- **Discord**: https://discord.gg/h9qyrK8Jc8 + +Social Media Platforms +^^^^^^^^^^^^^^^^^^^^^^ + +- **LinkedIn**: https://www.linkedin.com/company/scikit-learn +- **YouTube**: https://www.youtube.com/channel/UCJosFjYm0ZYVUARxuOZqnnw/playlists +- **Facebook**: https://www.facebook.com/scikitlearnofficial/ +- **Instagram**: https://www.instagram.com/scikitlearnofficial/ +- **TikTok**: https://www.tiktok.com/@scikit.learn +- **Bluesky**: https://bsky.app/profile/scikit-learn.org +- **Mastodon**: https://mastodon.social/@sklearn@fosstodon.org + +Resources +^^^^^^^^^ + +- **Calendar**: https://blog.scikit-learn.org/calendar/ +- **Logos & Branding**: https://github.com/scikit-learn/scikit-learn/tree/main/doc/logos + +Citation +~~~~~~~~ + +If you use scikit-learn in a scientific publication, we would appreciate citations: https://scikit-learn.org/stable/about.html#citing-scikit-learn diff --git a/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/RECORD b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..b49f9af802359f4bbbfead4d0032a695fd5c4850 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/RECORD @@ -0,0 +1,1579 @@ +scikit_learn-1.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +scikit_learn-1.7.1.dist-info/METADATA,sha256=kfnaI2JCT7RdHZv6hQO0n1zumVQ20SXfR3bZbfYuAsI,11784 +scikit_learn-1.7.1.dist-info/RECORD,, +scikit_learn-1.7.1.dist-info/WHEEL,sha256=sZM_NeUMz2G4fDenMf11eikcCxcLaQWiYRmjwQBavQs,137 +scikit_learn-1.7.1.dist-info/licenses/COPYING,sha256=_ebNwKhsZahFrxcIb5ZPejjZNEZ7fzYgOJSvMOzudkA,5078 +scikit_learn.libs/libgomp-a34b3233.so.1.0.0,sha256=On6uznIxkRvi-7Gz58tMtcLg-E4MK7c3OUcrWh_uyME,168193 +sklearn/__check_build/__init__.py,sha256=qOmiYYd8XWCN-knP2AdJLoNrN7E-Jn48vx1iZpYRugY,1843 +sklearn/__check_build/__pycache__/__init__.cpython-310.pyc,, +sklearn/__check_build/_check_build.cpython-310-x86_64-linux-gnu.so,sha256=kfI088e2Nl18zwEEzXS2NMkqtuYUlLMNTTNLdEk-x-w,41064 +sklearn/__check_build/_check_build.pyx,sha256=8uo0MEvoqggJXyJug6X1iOtrHEjEuRHEy8XK9EEEsVE,30 +sklearn/__check_build/meson.build,sha256=kYUehV7zeGx_ckXUuJZoUHqzFr_QjTkEQFpzUdCmUeM,135 +sklearn/__init__.py,sha256=UZUfg2wF4tfGx3xt9YjRpuuqkV9kYevn2dT9XHP-Fl8,4640 +sklearn/__pycache__/__init__.cpython-310.pyc,, +sklearn/__pycache__/_built_with_meson.cpython-310.pyc,, +sklearn/__pycache__/_config.cpython-310.pyc,, +sklearn/__pycache__/_distributor_init.cpython-310.pyc,, +sklearn/__pycache__/_min_dependencies.cpython-310.pyc,, +sklearn/__pycache__/base.cpython-310.pyc,, +sklearn/__pycache__/calibration.cpython-310.pyc,, +sklearn/__pycache__/conftest.cpython-310.pyc,, +sklearn/__pycache__/discriminant_analysis.cpython-310.pyc,, +sklearn/__pycache__/dummy.cpython-310.pyc,, +sklearn/__pycache__/exceptions.cpython-310.pyc,, +sklearn/__pycache__/isotonic.cpython-310.pyc,, +sklearn/__pycache__/kernel_approximation.cpython-310.pyc,, +sklearn/__pycache__/kernel_ridge.cpython-310.pyc,, +sklearn/__pycache__/multiclass.cpython-310.pyc,, +sklearn/__pycache__/multioutput.cpython-310.pyc,, +sklearn/__pycache__/naive_bayes.cpython-310.pyc,, +sklearn/__pycache__/pipeline.cpython-310.pyc,, +sklearn/__pycache__/random_projection.cpython-310.pyc,, +sklearn/_build_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/_build_utils/__pycache__/__init__.cpython-310.pyc,, +sklearn/_build_utils/__pycache__/tempita.cpython-310.pyc,, +sklearn/_build_utils/__pycache__/version.cpython-310.pyc,, +sklearn/_build_utils/tempita.py,sha256=D-5VlYirbKymB12g0lRet-BHq40YXbViGh51Ngr_yi8,1684 +sklearn/_build_utils/version.py,sha256=MXulZf33cp8otqGocAwKzSBIM6MUerYtE8fxqZsAfJA,448 +sklearn/_built_with_meson.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/_config.py,sha256=YzP6N9DtWSRDbPnw2xF3JKGdvAwkl5XDSXl-QqHETZs,14970 +sklearn/_cyutility.cpython-310-x86_64-linux-gnu.so,sha256=3_I0Pwiuiztt5jgoB2F8FoYMWo2nIYuDx56ILd5aH8Y,199832 +sklearn/_distributor_init.py,sha256=HJ3OJ8FgzN3a-dNHePdJd_rdMK7_GYsnqU_fe3VuipE,424 +sklearn/_isotonic.cpython-310-x86_64-linux-gnu.so,sha256=hKAAflOsBEAVut-c1ncNxX-iQuLfdppUyVSqNraOM7A,179664 +sklearn/_isotonic.pyx,sha256=L1JOQOTp5SoiZjzZKTczOtGuxilh_0bAluCk8Q9YM3Y,3733 +sklearn/_loss/__init__.py,sha256=ZGxLoo-OlLqcwI4Za5lYA31dcTayjaZzO54BjuymyBQ,687 +sklearn/_loss/__pycache__/__init__.cpython-310.pyc,, +sklearn/_loss/__pycache__/link.cpython-310.pyc,, +sklearn/_loss/__pycache__/loss.cpython-310.pyc,, +sklearn/_loss/_loss.cpython-310-x86_64-linux-gnu.so,sha256=avorXtWDH9rJ4M4kzyMPBVFoYFexDHWG5bWR1iLzWrY,2813465 +sklearn/_loss/_loss.pxd,sha256=8LvWX3YNUuv3E5KQtl2o68mEqzu3tFFGjk8Qn-9lnk0,4577 +sklearn/_loss/_loss.pyx.tp,sha256=PKBBX3n6ASIt5IuZYp8F4fWee5dK3RTNVyMrJKgfErA,53677 +sklearn/_loss/link.py,sha256=1-PzVdqnGp7eE1Q7UoILBLHwM9TRaYwN1P-jfXa7xp8,8126 +sklearn/_loss/loss.py,sha256=_39Z0lvdVL_hs8Llt_PjdmyoKwtzR-4cvIKFv9v1h1g,41317 +sklearn/_loss/meson.build,sha256=jltTivAK8aZP29ZOeyF6HuUihkj_qVV1UxhIm8Ux7uE,654 +sklearn/_loss/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/_loss/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/_loss/tests/__pycache__/test_link.cpython-310.pyc,, +sklearn/_loss/tests/__pycache__/test_loss.cpython-310.pyc,, +sklearn/_loss/tests/test_link.py,sha256=XMHMLjmPA1RHLrAhexoMzVQRlokcCT9LMhajVVnneS0,3954 +sklearn/_loss/tests/test_loss.py,sha256=hSgF_G5R2cv1P3lrdYloiwdDYpimT-AwfU1jhcZ6FcQ,49712 +sklearn/_min_dependencies.py,sha256=Qvl6tAcvEqabDE6qyuOfRpNxA_tEFOBUTmTwyzHDH-k,2800 +sklearn/base.py,sha256=oq05xf9kkdBMNXOOlChg3vDoDWgArrFpDP1ov0a-jZE,47777 +sklearn/calibration.py,sha256=Tg-mX0eMYow7li9GGBH8UDfYvrniuNMbiiFD-P4ottU,51595 +sklearn/cluster/__init__.py,sha256=DPe0qNOVhLx5mSDInkJBNgxd-22nhkKLjBlRV-6fWYg,1476 +sklearn/cluster/__pycache__/__init__.cpython-310.pyc,, +sklearn/cluster/__pycache__/_affinity_propagation.cpython-310.pyc,, +sklearn/cluster/__pycache__/_agglomerative.cpython-310.pyc,, +sklearn/cluster/__pycache__/_bicluster.cpython-310.pyc,, +sklearn/cluster/__pycache__/_birch.cpython-310.pyc,, +sklearn/cluster/__pycache__/_bisect_k_means.cpython-310.pyc,, +sklearn/cluster/__pycache__/_dbscan.cpython-310.pyc,, +sklearn/cluster/__pycache__/_feature_agglomeration.cpython-310.pyc,, +sklearn/cluster/__pycache__/_kmeans.cpython-310.pyc,, +sklearn/cluster/__pycache__/_mean_shift.cpython-310.pyc,, +sklearn/cluster/__pycache__/_optics.cpython-310.pyc,, +sklearn/cluster/__pycache__/_spectral.cpython-310.pyc,, +sklearn/cluster/_affinity_propagation.py,sha256=axsTYyWEvxM8Drc7hO9BDWKWwGgFi_wXqmByIgF21AU,20706 +sklearn/cluster/_agglomerative.py,sha256=RipbZwQoduArZZTJQmCfFnZr2JheIbB0NkWd4ruUTKM,49368 +sklearn/cluster/_bicluster.py,sha256=89i_H3m0wrBHvDw11qM2yKQ_dFZqa9-mKGpBIEHLU_w,21975 +sklearn/cluster/_birch.py,sha256=10YX8EiSfbXlnJnpRuTvBpb793HoR5wcLagtpFkRkTM,26834 +sklearn/cluster/_bisect_k_means.py,sha256=Z0WCdf03rpS3m1XkEjyIfBC_r6ch8cNtAJcA632nfzw,19359 +sklearn/cluster/_dbscan.py,sha256=25tD7FhfLbgcDUkU_jxP78lXqjCsqEx65vtH6WCjWMg,18529 +sklearn/cluster/_dbscan_inner.cpython-310-x86_64-linux-gnu.so,sha256=XNQf8Q0-uvA73W2u_ma1kqqWRtsBbXipkEnMCGQyFlw,78296 +sklearn/cluster/_dbscan_inner.pyx,sha256=JQ2riqW6JizG8wgHc2i_eKZUnNK_clS8dGE60NMCp1U,1318 +sklearn/cluster/_feature_agglomeration.py,sha256=2wo8vtVMqz0pwMb4cYVUTXcswjeGdkFbs3XNlaJMJn4,2426 +sklearn/cluster/_hdbscan/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/cluster/_hdbscan/__pycache__/__init__.cpython-310.pyc,, +sklearn/cluster/_hdbscan/__pycache__/hdbscan.cpython-310.pyc,, +sklearn/cluster/_hdbscan/_linkage.cpython-310-x86_64-linux-gnu.so,sha256=Vrq8F-CeFjw9REGdeUL9ISypd9vy27HxexPt0B9ndkM,134968 +sklearn/cluster/_hdbscan/_linkage.pyx,sha256=bDCkEDXyZ8M0t8wIp1uUiQWNrNuLVHaGQVw_NDFXpvU,10252 +sklearn/cluster/_hdbscan/_reachability.cpython-310-x86_64-linux-gnu.so,sha256=8K5lkp80gYMhJR505z7QWzZ4Fatl_WD5qQgc0_t42bE,246376 +sklearn/cluster/_hdbscan/_reachability.pyx,sha256=Ap27H1gEE43fLRtR4RqtJ5BnBSWoxeUKYhj4u2OtqHU,7774 +sklearn/cluster/_hdbscan/_tree.cpython-310-x86_64-linux-gnu.so,sha256=X9zK6dEIPe9nFJ5MjikSQx8F8dzxStr14e-iqN6-GaQ,274384 +sklearn/cluster/_hdbscan/_tree.pxd,sha256=Nm7ghFqifD2vLnyBoCQCn9eFsmoB8ITpEuCMItJZoM4,2150 +sklearn/cluster/_hdbscan/_tree.pyx,sha256=Fs7cI-3EjHEmLqFwDx4JvrO_vuil32llUG9w4-ElaSs,27781 +sklearn/cluster/_hdbscan/hdbscan.py,sha256=oQuEMFAJZWjbfOf34ghlgVEmn5tQBtSepy90scB0HVI,41019 +sklearn/cluster/_hdbscan/meson.build,sha256=7qnGFFfS5OsBpQLS6xdBUVIcUjzd_VZrkG8Sg1WEw0Y,492 +sklearn/cluster/_hdbscan/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cluster/_hdbscan/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/cluster/_hdbscan/tests/__pycache__/test_reachibility.cpython-310.pyc,, +sklearn/cluster/_hdbscan/tests/test_reachibility.py,sha256=HCzRrBdtYARu83re_Z-Mu-hEZzhVWKNzCDpuZD_M3rM,2065 +sklearn/cluster/_hierarchical_fast.cpython-310-x86_64-linux-gnu.so,sha256=yPwjWcimFSJZYEyI377sDPVgq0cF5nyY9igGDJCeOhU,215856 +sklearn/cluster/_hierarchical_fast.pxd,sha256=JlWtArNtEgc2RBeCJRADftNTPwNV_M-OAsAJz7lHqzY,245 +sklearn/cluster/_hierarchical_fast.pyx,sha256=DJe1c-WdbgLaClMwJjucwFwbGePFqnm0vPSdXyGQy2U,15927 +sklearn/cluster/_k_means_common.cpython-310-x86_64-linux-gnu.so,sha256=T-FhZqvhw_qEs2I9PlUeW2uBLaZyunPGbWlLAEZTLg0,409177 +sklearn/cluster/_k_means_common.pxd,sha256=6QW18TtC1wGpyTd0cdG9PxSYTiP4ZN3hj6ltJWrdaic,887 +sklearn/cluster/_k_means_common.pyx,sha256=w8e0U721_57eE97moyGYtGEULsDA1LhsHzqR6pvrD0s,10206 +sklearn/cluster/_k_means_elkan.cpython-310-x86_64-linux-gnu.so,sha256=6eUqCQhuc15jSsqeUNotqLTqd7ecVYtXOP0-UzFje9s,401169 +sklearn/cluster/_k_means_elkan.pyx,sha256=9qqaR6NCvT994gFfZVVV5nQ7qZOdYsr1UgPdXad_dQs,28164 +sklearn/cluster/_k_means_lloyd.cpython-310-x86_64-linux-gnu.so,sha256=QwaL6fNhS9mEwXJWmickNIiwJS57eMoH1GbNmXjGk1s,273993 +sklearn/cluster/_k_means_lloyd.pyx,sha256=Ns8rod9sRad_un-fpePHDOqwM6MB6lT-0_Fivhmm9E4,16472 +sklearn/cluster/_k_means_minibatch.cpython-310-x86_64-linux-gnu.so,sha256=R_LXAxPFP7nLKOLCGhruG8oBulFe7UI19ULYYXNYP_g,203921 +sklearn/cluster/_k_means_minibatch.pyx,sha256=ytlKAPQuIgC54Wc8t8OlzeS8qi6HMALyKcun4lWOjR4,8156 +sklearn/cluster/_kmeans.py,sha256=Lg2oA_QcyHDfRcWaJM0tqTxG0GR4X-8-jVwuSRLZyAM,81743 +sklearn/cluster/_mean_shift.py,sha256=r5TJitv8uVAwAP_15btOVXNzZzhwSzJOqXB4rDp-hwA,20284 +sklearn/cluster/_optics.py,sha256=E7IWBHG9ygbfgzKOumTQo-ft33nStfTDaDzoiuvF8xs,44932 +sklearn/cluster/_spectral.py,sha256=siT1f8-8plaS2L0f36nC1wrq1i1OShU_KPdFjRksuOA,30936 +sklearn/cluster/meson.build,sha256=UBtHRFqB7JvZQ3o6rP4FsLccnPlbs5WYYBXNlO1dfmQ,975 +sklearn/cluster/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cluster/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/common.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_affinity_propagation.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_bicluster.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_birch.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_bisect_k_means.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_dbscan.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_feature_agglomeration.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_hdbscan.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_hierarchical.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_k_means.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_mean_shift.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_optics.cpython-310.pyc,, +sklearn/cluster/tests/__pycache__/test_spectral.cpython-310.pyc,, +sklearn/cluster/tests/common.py,sha256=1jmt9fXRXYt9TYCwJFcgDGV10slNNjJW7_2tRCSzJBY,880 +sklearn/cluster/tests/test_affinity_propagation.py,sha256=p-q92owXh0cG1oPD3d5VZOfQoZMwEeDfRlTAS25NTa0,11898 +sklearn/cluster/tests/test_bicluster.py,sha256=JJjahw-5rSvyNcKpz0ZtM1jl07jvLAB5D9zdzcqMXU4,9126 +sklearn/cluster/tests/test_birch.py,sha256=0c5tVBWc7lY4R-7oBwF8cpvI3-qploOHWp5poqF9KaY,8857 +sklearn/cluster/tests/test_bisect_k_means.py,sha256=1hf2vfXJ_0aIncY-bZMgx5TXTzGI49YCfVxChYrsLno,5139 +sklearn/cluster/tests/test_dbscan.py,sha256=8T5QOHsOI7ZnCYcBgcRE1AMT9IUanlFImxxsr3TKi1E,15704 +sklearn/cluster/tests/test_feature_agglomeration.py,sha256=V1apZXrZLF631DBVs72OMsZFGTHmb-ZdhvwXGd1vew0,1965 +sklearn/cluster/tests/test_hdbscan.py,sha256=xLYnVvA0Yo1KLqh_axWs1eQCqWGlJbaSiKVhf2QszpA,19401 +sklearn/cluster/tests/test_hierarchical.py,sha256=70Nqw-utJHu80ixqqOL2NC3nxZFOm-oBDaV2y1VIZtU,32118 +sklearn/cluster/tests/test_k_means.py,sha256=mmTpatBS9EzfckKi84LghrIIX30tbews5dUdYX4irsU,48754 +sklearn/cluster/tests/test_mean_shift.py,sha256=g6nBLNG0dPijUCTeM6ScqUpI9irAOv6tEG3U0n-rh-Y,7081 +sklearn/cluster/tests/test_optics.py,sha256=cPi7JaLpTVLTq37t0n6ZZyaPlLl4IckHTyuEA6aTG80,24537 +sklearn/cluster/tests/test_spectral.py,sha256=fDPwNrFgA-3PLF9RFNxhVvu5seE5c8bTDpoZxqHSvUM,11763 +sklearn/compose/__init__.py,sha256=XU4j8dd7SFuy5r0AfTLZ36XsEcIP_IqjQWNGC7Grs0g,631 +sklearn/compose/__pycache__/__init__.cpython-310.pyc,, +sklearn/compose/__pycache__/_column_transformer.cpython-310.pyc,, +sklearn/compose/__pycache__/_target.cpython-310.pyc,, +sklearn/compose/_column_transformer.py,sha256=Njlf2U8ar_L2mA-aXkttTluWJ7tu2HlN3cwKYpt9qeQ,63644 +sklearn/compose/_target.py,sha256=Nsyro18C-s-eUBDai6TItOx94Wms9KH_E_0Ut7Srzgo,14572 +sklearn/compose/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/compose/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/compose/tests/__pycache__/test_column_transformer.cpython-310.pyc,, +sklearn/compose/tests/__pycache__/test_target.cpython-310.pyc,, +sklearn/compose/tests/test_column_transformer.py,sha256=cu5MEHZoKfYI3cbn_oTaV06G_4ODZ8Xe36JFhOgeiaU,94973 +sklearn/compose/tests/test_target.py,sha256=VaXk9tAcmkzckaqS5h7iwvW3MS-hcOzr7NhSsZs3nDE,14098 +sklearn/conftest.py,sha256=TH7GXOavTSmLTwOGKqaGldetfIOGMEja4RZ8bepC9jo,13083 +sklearn/covariance/__init__.py,sha256=IsRnf4hz1aAODGnrFiF3VaptjqC0NRqrHPW1iiDOj3s,1171 +sklearn/covariance/__pycache__/__init__.cpython-310.pyc,, +sklearn/covariance/__pycache__/_elliptic_envelope.cpython-310.pyc,, +sklearn/covariance/__pycache__/_empirical_covariance.cpython-310.pyc,, +sklearn/covariance/__pycache__/_graph_lasso.cpython-310.pyc,, +sklearn/covariance/__pycache__/_robust_covariance.cpython-310.pyc,, +sklearn/covariance/__pycache__/_shrunk_covariance.cpython-310.pyc,, +sklearn/covariance/_elliptic_envelope.py,sha256=z0xSxlDx7IyvjXkmHH9wiYeM3Ub7oz6abuCG913aqJ8,9055 +sklearn/covariance/_empirical_covariance.py,sha256=8nmLvVu9kDRWrAIYPv0-maCUr6b3_HS8zcA4CF4i-wI,12297 +sklearn/covariance/_graph_lasso.py,sha256=ty136Rp5nd73TVUcbQn-0VpS8uzQFbYe4j9JTKhg8Ik,40298 +sklearn/covariance/_robust_covariance.py,sha256=q4Fu19fLfu9MI42icnD4g1Q73Qapvzj30DniREhvJ24,34403 +sklearn/covariance/_shrunk_covariance.py,sha256=4l2H9FOzMbdUUsOVaDLdfWjq3Xoi35epcEguU0uQzJ4,28038 +sklearn/covariance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/covariance/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/covariance/tests/__pycache__/test_covariance.cpython-310.pyc,, +sklearn/covariance/tests/__pycache__/test_elliptic_envelope.cpython-310.pyc,, +sklearn/covariance/tests/__pycache__/test_graphical_lasso.cpython-310.pyc,, +sklearn/covariance/tests/__pycache__/test_robust_covariance.cpython-310.pyc,, +sklearn/covariance/tests/test_covariance.py,sha256=eAp8bVdc5VYO7-LakTQBEl8Bku-I1xcstkp-wn2sbm8,14038 +sklearn/covariance/tests/test_elliptic_envelope.py,sha256=xCxtRYDNADB22KJURLGRqI2OoWh4LLfazpjgsIWOzH4,1587 +sklearn/covariance/tests/test_graphical_lasso.py,sha256=WqWXj3Hxd_Q9jxFL2Jn3r_5lgYXAz7qESoI49wwEOzg,10972 +sklearn/covariance/tests/test_robust_covariance.py,sha256=IUtakkWbJCbM753mqbIs76536SHWZ4uK6sgXv-9qIUY,6370 +sklearn/cross_decomposition/__init__.py,sha256=o35MjQxe2HkuWiAEgtgMGvy3ui_Otfo8opg0yw2uUh8,244 +sklearn/cross_decomposition/__pycache__/__init__.cpython-310.pyc,, +sklearn/cross_decomposition/__pycache__/_pls.cpython-310.pyc,, +sklearn/cross_decomposition/_pls.py,sha256=fPyCpmeF6GKzRAVawwZc4Ffa56R26526DD9OUS2rSfI,36972 +sklearn/cross_decomposition/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cross_decomposition/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/cross_decomposition/tests/__pycache__/test_pls.cpython-310.pyc,, +sklearn/cross_decomposition/tests/test_pls.py,sha256=NK4bLt4YwegJnyDR7F7XuNwPeQ4WR-AqXVve1I5vkEY,23488 +sklearn/datasets/__init__.py,sha256=OIl-zBuJJkFSHzL6ZFJfB1EJ1s-j1adLtEyFaakQxy8,5186 +sklearn/datasets/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/__pycache__/_arff_parser.cpython-310.pyc,, +sklearn/datasets/__pycache__/_base.cpython-310.pyc,, +sklearn/datasets/__pycache__/_california_housing.cpython-310.pyc,, +sklearn/datasets/__pycache__/_covtype.cpython-310.pyc,, +sklearn/datasets/__pycache__/_kddcup99.cpython-310.pyc,, +sklearn/datasets/__pycache__/_lfw.cpython-310.pyc,, +sklearn/datasets/__pycache__/_olivetti_faces.cpython-310.pyc,, +sklearn/datasets/__pycache__/_openml.cpython-310.pyc,, +sklearn/datasets/__pycache__/_rcv1.cpython-310.pyc,, +sklearn/datasets/__pycache__/_samples_generator.cpython-310.pyc,, +sklearn/datasets/__pycache__/_species_distributions.cpython-310.pyc,, +sklearn/datasets/__pycache__/_svmlight_format_io.cpython-310.pyc,, +sklearn/datasets/__pycache__/_twenty_newsgroups.cpython-310.pyc,, +sklearn/datasets/_arff_parser.py,sha256=tjZDgNyIqQ1I6zPIwkxZyCXcrW1p_QNy9xLSO9_ZMMY,19160 +sklearn/datasets/_base.py,sha256=l8clUavEbBSMCMoOzIhfT5WWSnnFGIjG4G_BPrYYgM0,53388 +sklearn/datasets/_california_housing.py,sha256=W8PzRJpPbAp69aMcsSEGnIJyxe672j_gvW3wmLug34Y,7279 +sklearn/datasets/_covtype.py,sha256=icC_R-02b83gIWJQq53E4_6Q8n8UiAOzFKHzRsSYFYY,8075 +sklearn/datasets/_kddcup99.py,sha256=1f1Ss2pFpnsVmSZOSWGGZw7pvpLIBwR__jPevyfg0Lo,13961 +sklearn/datasets/_lfw.py,sha256=wObR1RrTviwH_K0RAFa_GjOAlZ74a6q2rszhrSq2J4o,22588 +sklearn/datasets/_olivetti_faces.py,sha256=_JgWZdUL7j51hNnquvZw76yvXChFhQnS-wSNBREoDUY,6075 +sklearn/datasets/_openml.py,sha256=ZWrtcER7wBb8m1qFtwlvgkfaDcpgk2j7Klz8rnTUKf4,41634 +sklearn/datasets/_rcv1.py,sha256=oBpLrSj4ENcQAmKBpakBYZIm7Ao-7CGqKET7J6HbWzg,11861 +sklearn/datasets/_samples_generator.py,sha256=0tJqRu2coJB9E_LAetgzVK13nc4HE_w3x9aHcepuCDQ,76834 +sklearn/datasets/_species_distributions.py,sha256=ZJjzcktxxA6hHOVb8y9CkiDolZtKlGO4GCUCQAIU1qc,9407 +sklearn/datasets/_svmlight_format_fast.cpython-310-x86_64-linux-gnu.so,sha256=FrW-5Ufx1F4nxUu-qbruUCd2SPwGRVO5pn-f6mKTUQE,471272 +sklearn/datasets/_svmlight_format_fast.pyx,sha256=9eDLPP_HvkuCzJbFH4hmlrsuAlYcD7CtEujLyET0yz0,7196 +sklearn/datasets/_svmlight_format_io.py,sha256=515Dc6TLQnKu_2HbNQTnZhZK5oHH2CwBHA03Y49EKdY,20839 +sklearn/datasets/_twenty_newsgroups.py,sha256=JOjPJEx34HAMdjcgGfd4r5SwUOL95OoePSUdzcyfTbs,20808 +sklearn/datasets/data/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/data/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/data/breast_cancer.csv,sha256=_tPrctBXXvYZIpP1CTxugBsUdrV30Dhr9EVVBFIhcu0,119913 +sklearn/datasets/data/diabetes_data_raw.csv.gz,sha256=o-lMx86gD4qE-l9jRSA5E6aO-kLfGPh935vq1yG_1QM,7105 +sklearn/datasets/data/diabetes_target.csv.gz,sha256=jlP2XrgR30PCBvNTS7OvDl_tITvDfta6NjEBV9YCOAM,1050 +sklearn/datasets/data/digits.csv.gz,sha256=CfZubeve4s0rWuWeDWq7tz_CsOAYXS4ZV-nrtR4jqiI,57523 +sklearn/datasets/data/iris.csv,sha256=8T_6j91W_Y5sjRbUCBo_vTEUvNCq5CVsQyBRac2dFEk,2734 +sklearn/datasets/data/linnerud_exercise.csv,sha256=y42MJJN2Q_okWWgu-4bF5me81t2TEJ7vgZZNnp8Rv4w,212 +sklearn/datasets/data/linnerud_physiological.csv,sha256=K_fgXBzX0K3w7KHkVpQfYkvtCk_JZpTWDQ_3hT7F_Pc,219 +sklearn/datasets/data/wine_data.csv,sha256=EOioApCLNPhuXajOli88gGaUvJhFChj2GFGvWfMkvt4,11157 +sklearn/datasets/descr/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/descr/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/descr/breast_cancer.rst,sha256=PFhVGCpE0SyR8fsnOIdB-3C0uSukD7dC3Km15ATGjxk,4794 +sklearn/datasets/descr/california_housing.rst,sha256=Cr6d8BzCwbHjKZi21qSNLumQppwIzjx4zGIJbxSUEfE,1720 +sklearn/datasets/descr/covtype.rst,sha256=C6DmczitjtnrO-XhCIi8WqNT0uPgYnPWNYtKwJTwcn4,1191 +sklearn/datasets/descr/diabetes.rst,sha256=B9z8E5V6gkhb385Ers_7py55d1lZZtEYuB8WLLgn44E,1455 +sklearn/datasets/descr/digits.rst,sha256=jn5Y1hKVj32bDeGTHtaLIRcD7rI56Ajz2CxfCDfMAiI,2007 +sklearn/datasets/descr/iris.rst,sha256=cfhnSai8Uo0ht9sPlTMuMjDRMjGgXCcg5TeyxaqO9ek,2656 +sklearn/datasets/descr/kddcup99.rst,sha256=qRz2X8XmUh8IZKjzT1OAJd5sj91bBo0xpdcV5rS2Jko,3919 +sklearn/datasets/descr/lfw.rst,sha256=8sj8ApMwZDHabnaksL4S-WHS1V-k-divU7DGPJWP7Aw,4409 +sklearn/datasets/descr/linnerud.rst,sha256=jDI-AIsVeZZTVVWSiUztp5lEL4H2us847bgF3FSGb1s,704 +sklearn/datasets/descr/olivetti_faces.rst,sha256=i8Y7-g4fOPdLvupgJ8i_ze1pA0hGpfDgAoPCGvCPFxI,1834 +sklearn/datasets/descr/rcv1.rst,sha256=mLj4WU7aEVqaJg7hgSSe81oI74L6_pGECR72O8dEMZ4,2455 +sklearn/datasets/descr/species_distributions.rst,sha256=L80eaLcb9ymJOZyFLoQhDykU9dwiouRFRTD-_IrKFsI,1648 +sklearn/datasets/descr/twenty_newsgroups.rst,sha256=Z5efG4-mdET4H4sXgCt37IHL08EUzKbnucB190o_GD8,10923 +sklearn/datasets/descr/wine_data.rst,sha256=R4crlpp_b1Q_B9Jo2-Jq-3djwbQO5qpBTtee9y6t6cY,3355 +sklearn/datasets/images/README.txt,sha256=PH7xWh-iW5mNOMkhMjeGNZVare3B3PPkDmPcAJj2uPc,709 +sklearn/datasets/images/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/images/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/images/china.jpg,sha256=g3gCWtJRnWSdAuMr2YmQ20q1cjV9nwmEHC-_u0_vrSk,196653 +sklearn/datasets/images/flower.jpg,sha256=p39uxB41Ov34vf8uqYGylVU12NgylPjPpJz05CPdVjg,142987 +sklearn/datasets/meson.build,sha256=Vx9GBA1WjNkluNaLNncDqp7NsZ6jTw3Ymw7htBHfy2M,173 +sklearn/datasets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_20news.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_arff_parser.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_california_housing.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_covtype.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_kddcup99.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_lfw.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_olivetti_faces.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_openml.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_rcv1.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_samples_generator.cpython-310.pyc,, +sklearn/datasets/tests/__pycache__/test_svmlight_format.cpython-310.pyc,, +sklearn/datasets/tests/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_1/api-v1-jd-1.json.gz,sha256=hi4IUgokM6SVo7066f2ebHxUCpxjLbKbuCUnhMva13k,1786 +sklearn/datasets/tests/data/openml/id_1/api-v1-jdf-1.json.gz,sha256=qWba1Yz1-8kUo3StVVbAQU9e2WIjftVaN5_pbjCNAN4,889 +sklearn/datasets/tests/data/openml/id_1/api-v1-jdq-1.json.gz,sha256=hKhybSw_i7ynnVTYsZEVh0SxmTFG-PCDsRGo6nhTYFc,145 +sklearn/datasets/tests/data/openml/id_1/data-v1-dl-1.arff.gz,sha256=z-iUW5SXcLDaQtr1jOZ9HF_uJc97T9FFFhg3wqvAlCk,1841 +sklearn/datasets/tests/data/openml/id_1119/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1119/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_1119/api-v1-jd-1119.json.gz,sha256=xB5fuz5ZzU3oge18j4j5sDp1DVN7pjWByv3mqv13rcE,711 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdf-1119.json.gz,sha256=gviZ7cWctB_dZxslaiKOXgbfxeJMknEudQBbJRsACGU,1108 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdl-dn-adult-census-l-2-dv-1.json.gz,sha256=Sl3DbKl1gxOXiyqdecznY8b4TV2V8VrFV7PXSC8i7iE,364 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdl-dn-adult-census-l-2-s-act-.json.gz,sha256=bsCVV4iRT6gfaY6XpNGv93PXoSXtbnacYnGgtI_EAR0,363 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdq-1119.json.gz,sha256=73y8tYwu3P6kXAWLdR-vd4PnEEYqkk6arK2NR6fp-Us,1549 +sklearn/datasets/tests/data/openml/id_1119/data-v1-dl-54002.arff.gz,sha256=aTGvJWGV_N0uR92LD57fFvvwOxmOd7cOPf2Yd83wlRU,1190 +sklearn/datasets/tests/data/openml/id_1590/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1590/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_1590/api-v1-jd-1590.json.gz,sha256=mxBa3-3GtrgvRpXKm_4jI5MDTN95gDUj85em3Fv4JNE,1544 +sklearn/datasets/tests/data/openml/id_1590/api-v1-jdf-1590.json.gz,sha256=BG9eYFZGk_DzuOOCclyAEsPgWGRxOcJGhc7JhOQPzQA,1032 +sklearn/datasets/tests/data/openml/id_1590/api-v1-jdq-1590.json.gz,sha256=RLmw0pCh4zlpWkMUOPhAgAccVjUWHDl33Rf0wnsAo0o,1507 +sklearn/datasets/tests/data/openml/id_1590/data-v1-dl-1595261.arff.gz,sha256=7h3N9Y8vEHL33RtDOIlpxRvGz-d24-lGWuanVuXdsQo,1152 +sklearn/datasets/tests/data/openml/id_2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_2/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_2/api-v1-jd-2.json.gz,sha256=pnLUNbl6YDPf0dKlyCPSN60YZRAb1eQDzZm1vguk4Ds,1363 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdf-2.json.gz,sha256=wbg4en0IAUocCYB65FjKdmarijxXnL-xieCcbX3okqY,866 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdl-dn-anneal-l-2-dv-1.json.gz,sha256=6QCxkHlSJP9I5GocArEAINTJhroUKIDALIbwtHLe08k,309 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdl-dn-anneal-l-2-s-act-.json.gz,sha256=_2Ily5gmDKTr7AFaGidU8qew2_tNDxfc9nJ1QhVOKhA,346 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdq-2.json.gz,sha256=xG9sXyIdh33mBLkGQDsgy99nTxIlvNuz4VvRiCpppHE,1501 +sklearn/datasets/tests/data/openml/id_2/data-v1-dl-1666876.arff.gz,sha256=1XsrBMrlJjBmcONRaYncoyyIwVV4EyXdrELkPcIyLDA,1855 +sklearn/datasets/tests/data/openml/id_292/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_292/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_292/api-v1-jd-292.json.gz,sha256=Hmo4152PnlOizhG2i0FTBi1OluwLNo0CsuZPGzPFFpM,551 +sklearn/datasets/tests/data/openml/id_292/api-v1-jd-40981.json.gz,sha256=wm3L4wz7ORYfMFsrPUOptQrcizaNB0lWjEcQbL2yCJc,553 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdf-292.json.gz,sha256=JVwW8z7Sln_hAM2AEafmn3iWA3JLHsLs-R3-tyBnwZA,306 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdf-40981.json.gz,sha256=JVwW8z7Sln_hAM2AEafmn3iWA3JLHsLs-R3-tyBnwZA,306 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-dv-1-s-dact.json.gz,sha256=jvYCVCX9_F9zZVXqOFJSr1vL9iODYV24JIk2bU-WoKc,327 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-dv-1.json.gz,sha256=naCemmAx0GDsQW9jmmvzSYnmyIzmQdEGIeuQa6HYwpM,99 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-s-act-.json.gz,sha256=NYkNCBZcgEUmtIqtRi18zAnoCL15dbpgS9YSuWCHl6w,319 +sklearn/datasets/tests/data/openml/id_292/data-v1-dl-49822.arff.gz,sha256=t-4kravUqu1kGbQ_6dP4bVX89L7g8WmK4h2GwnATFOM,2532 +sklearn/datasets/tests/data/openml/id_3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_3/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_3/api-v1-jd-3.json.gz,sha256=BmohZnmxl8xRlG4X7pouKCFUJZkbDOt_EJiMFPfz-Gk,2473 +sklearn/datasets/tests/data/openml/id_3/api-v1-jdf-3.json.gz,sha256=7E8ta8TfOIKwi7oBVx4HkqVveeCpItmEiXdzrNKEtCY,535 +sklearn/datasets/tests/data/openml/id_3/api-v1-jdq-3.json.gz,sha256=Ce8Zz60lxd5Ifduu88TQaMowY3d3MKKI39b1CWoMb0Y,1407 +sklearn/datasets/tests/data/openml/id_3/data-v1-dl-3.arff.gz,sha256=xj_fiGF2HxynBQn30tFpp8wFOYjHt8CcCabbYSTiCL4,19485 +sklearn/datasets/tests/data/openml/id_40589/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40589/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_40589/api-v1-jd-40589.json.gz,sha256=WdGqawLSNYwW-p5Pvv9SOjvRDr04x8NxkR-oM1573L8,598 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdf-40589.json.gz,sha256=gmurBXo5KfQRibxRr6ChdSaV5jzPIOEoymEp6eMyH8I,856 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdl-dn-emotions-l-2-dv-3.json.gz,sha256=Geayoqj-xUA8FGZCpNwuB31mo6Gsh-gjm9HdMckoq5w,315 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdl-dn-emotions-l-2-s-act-.json.gz,sha256=TaY6YBYzQLbhiSKr_n8fKnp9oj2mPCaTJJhdYf-qYHU,318 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdq-40589.json.gz,sha256=0PeXMZPrNdGemdHYvKPH86i40EEFCK80rVca7o7FqwU,913 +sklearn/datasets/tests/data/openml/id_40589/data-v1-dl-4644182.arff.gz,sha256=LEImVQgnzv81CcZxecRz4UOFzuIGU2Ni5XxeDfx3Ub8,4344 +sklearn/datasets/tests/data/openml/id_40675/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40675/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_40675/api-v1-jd-40675.json.gz,sha256=p4d3LWD7_MIaDpb9gZBvA1QuC5QtGdzJXa5HSYlTpP0,323 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdf-40675.json.gz,sha256=1I2WeXida699DTw0bjV211ibZjw2QJQvnB26duNV-qo,307 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-dv-1-s-dact.json.gz,sha256=Ie0ezF2HSVbpUak2HyUa-yFlrdqSeYyJyl4vl66A3Y8,317 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-dv-1.json.gz,sha256=rQpKVHdgU4D4gZzoQNu5KKPQhCZ8US9stQ1b4vfHa8I,85 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-s-act-.json.gz,sha256=FBumMOA56kS7rvkqKI4tlk_Dqi74BalyO0qsc4ompic,88 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdq-40675.json.gz,sha256=iPzcOm_tVpfzbcJi9pv_-4FHZ84zb_KKId7zqsk3sIw,886 +sklearn/datasets/tests/data/openml/id_40675/data-v1-dl-4965250.arff.gz,sha256=VD0IhzEvQ9n2Wn4dCL54okNjafYy1zgrQTTOu1JaSKM,3000 +sklearn/datasets/tests/data/openml/id_40945/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40945/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_40945/api-v1-jd-40945.json.gz,sha256=AogsawLE4GjvKxbzfzOuPV6d0XyinQFmLGkk4WQn610,437 +sklearn/datasets/tests/data/openml/id_40945/api-v1-jdf-40945.json.gz,sha256=lfCTjf3xuH0P_E1SbyyR4JfvdolIC2k5cBJtkI8pEDA,320 +sklearn/datasets/tests/data/openml/id_40945/api-v1-jdq-40945.json.gz,sha256=nH5aRlVKtqgSGDLcDNn3pg9QNM7xpafWE0a72RJRa1Q,1042 +sklearn/datasets/tests/data/openml/id_40945/data-v1-dl-16826755.arff.gz,sha256=UW6WH1GYduX4mzOaA2SgjdZBYKw6TXbV7GKVW_1tbOU,32243 +sklearn/datasets/tests/data/openml/id_40966/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40966/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_40966/api-v1-jd-40966.json.gz,sha256=NsY8OsjJ21mRCsv0x3LNUwQMzQ6sCwRSYR3XrY2lBHQ,1660 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdf-40966.json.gz,sha256=itrI4vjLy_qWd6zdSSepYUMEZdLJlAGDIWC-RVz6ztg,3690 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdl-dn-miceprotein-l-2-dv-4.json.gz,sha256=8MIDtGJxdc679SfYGRekmZEa-RX28vRu5ySEKKlI1gM,325 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdl-dn-miceprotein-l-2-s-act-.json.gz,sha256=MBOWtKQsgUsaFQON38vPXIWQUBIxdH0NwqUAuEsv0N8,328 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdq-40966.json.gz,sha256=Pe6DmH__qOwg4js8q8ANQr63pGmva9gDkJmYwWh_pjQ,934 +sklearn/datasets/tests/data/openml/id_40966/data-v1-dl-17928620.arff.gz,sha256=HF_ZP_7H3rY6lA_WmFNN1-u32zSfwYOTAEHL8X5g4sw,6471 +sklearn/datasets/tests/data/openml/id_42074/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_42074/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_42074/api-v1-jd-42074.json.gz,sha256=T8shVZW7giMyGUPw31D1pQE0Rb8YGdU9PLW_qQ2eecA,595 +sklearn/datasets/tests/data/openml/id_42074/api-v1-jdf-42074.json.gz,sha256=OLdOfwKmH_Vbz6xNhxA9W__EP-uwwBnZqqFi-PdpMGg,272 +sklearn/datasets/tests/data/openml/id_42074/api-v1-jdq-42074.json.gz,sha256=h0KnS9W8EgrNkYbIqHN8tCDtmwCfreALJOfOUhd5fyw,722 +sklearn/datasets/tests/data/openml/id_42074/data-v1-dl-21552912.arff.gz,sha256=9iPnd8CjaubIL64Qp8IIjLODKY6iRFlb-NyVRJyb5MQ,2326 +sklearn/datasets/tests/data/openml/id_42585/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_42585/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_42585/api-v1-jd-42585.json.gz,sha256=fMvxOOBmOJX5z1ERNrxjlcFT9iOK8urLajZ-huFdGnE,1492 +sklearn/datasets/tests/data/openml/id_42585/api-v1-jdf-42585.json.gz,sha256=CYUEWkVMgYa05pDr77bOoe98EyksmNUKvaRwoP861CU,312 +sklearn/datasets/tests/data/openml/id_42585/api-v1-jdq-42585.json.gz,sha256=Nzbn_retMMaGdcLE5IqfsmLoAwjJCDsQDd0DOdofwoI,348 +sklearn/datasets/tests/data/openml/id_42585/data-v1-dl-21854866.arff.gz,sha256=yNAMZpBXap7Dnhy3cFThMpa-D966sPs1pkoOhie25vM,4519 +sklearn/datasets/tests/data/openml/id_561/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_561/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_561/api-v1-jd-561.json.gz,sha256=odOP3WAbZ7ucbRYVL1Pd8Wagz8_vT6hkOOiZv-RJImw,1798 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdf-561.json.gz,sha256=QHQk-3nMMLjp_5CQCzvykkSsfzeX8ni1vmAoQ_lZtO4,425 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdl-dn-cpu-l-2-dv-1.json.gz,sha256=BwOwriC5_3UIfcYBZA7ljxwq1naIWOohokUVHam6jkw,301 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdl-dn-cpu-l-2-s-act-.json.gz,sha256=cNRZath5VHhjEJ2oZ1wreJ0H32a1Jtfry86WFsTJuUw,347 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdq-561.json.gz,sha256=h0Oy2T0sYqgvtH4fvAArl-Ja3Ptb8fyya1itC-0VvUg,1074 +sklearn/datasets/tests/data/openml/id_561/data-v1-dl-52739.arff.gz,sha256=6WFCteAN_sJhewwi1xkrNAriwo7D_8OolMW-dGuXClk,3303 +sklearn/datasets/tests/data/openml/id_61/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_61/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_61/api-v1-jd-61.json.gz,sha256=pcfnmqQe9YCDj7n8GQYoDwdsR74XQf3dUATdtQDrV_4,898 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdf-61.json.gz,sha256=M8vWrpRboElpNwqzVgTpNjyHJWOTSTOCtRGKidWThtY,268 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdl-dn-iris-l-2-dv-1.json.gz,sha256=C84gquf9kDeW2W1bOjZ3twWPvF8_4Jlu6dSR5O4j0TI,293 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdl-dn-iris-l-2-s-act-.json.gz,sha256=qfS5MXmX32PtjSuwc6OQY0TA4L4Bf9OE6uw2zti5S64,330 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdq-61.json.gz,sha256=QkzUfBKlHHu42BafrID7VgHxUr14RoskHUsRW_fSLyA,1121 +sklearn/datasets/tests/data/openml/id_61/data-v1-dl-61.arff.gz,sha256=r-RzaSRgZjiYTlcyNRkQJdQZxUXTHciHTJa3L17F23M,2342 +sklearn/datasets/tests/data/openml/id_62/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_62/__pycache__/__init__.cpython-310.pyc,, +sklearn/datasets/tests/data/openml/id_62/api-v1-jd-62.json.gz,sha256=fvNVGtR9SAI8Wh8c8HcEeppLlVRLuR1Khgl_i1dPjQc,656 +sklearn/datasets/tests/data/openml/id_62/api-v1-jdf-62.json.gz,sha256=SJsXcSbLfzNcsiBwkjO5RtOgrXHTi7ptSLeRhxRuWFo,817 +sklearn/datasets/tests/data/openml/id_62/api-v1-jdq-62.json.gz,sha256=J4pSpS1WnwfRTGp4d7EEdix32qxCn7H9mBegN41uxjQ,805 +sklearn/datasets/tests/data/openml/id_62/data-v1-dl-52352.arff.gz,sha256=-1gwyCES9ipADIKsHxtethwpwKfMcrpW0q7_D66KYPk,1625 +sklearn/datasets/tests/data/svmlight_classification.txt,sha256=an5ZLlFP2RJkK0iT8V6B5NLpNvZFUEzpTonY-Frcv0o,253 +sklearn/datasets/tests/data/svmlight_invalid.txt,sha256=ueCvdPekdiYpH8FAH_AW9MHiyMd9SulhrkJ8FQm3ol8,54 +sklearn/datasets/tests/data/svmlight_invalid_order.txt,sha256=xSNKVNcM7TuWkTyTZnQSTTcoBdERxUKoM2yz_gFCaHA,23 +sklearn/datasets/tests/data/svmlight_multilabel.txt,sha256=DsT6kKm83Ac7HLhmw6d6P0e2YNSdL7-ES3lgk7BozW4,104 +sklearn/datasets/tests/test_20news.py,sha256=-EdeU6SLVlTPCGtatJRplVBvPrt6AygXgeNz_9JF-8Y,5340 +sklearn/datasets/tests/test_arff_parser.py,sha256=n9WpxiBJ_AvltjDGmH8VLJyX6EXLWzhQQoGKTLYYbEI,8196 +sklearn/datasets/tests/test_base.py,sha256=ARlzPUqsECOclOcFbmglzjEAKIAlKYcEWDcpLEU5ppE,23022 +sklearn/datasets/tests/test_california_housing.py,sha256=-kGKf35jMxfB9PgvNryrL3Xqil_CVhoWFPqRGoCdBoU,1369 +sklearn/datasets/tests/test_common.py,sha256=F2J7ng0CH0Izs6yJ979ZrTfR_LO9stx_WoiE9y-kwgc,4392 +sklearn/datasets/tests/test_covtype.py,sha256=rnS0G-zkPov-roszvXRwiNBG50tciwMKe-D_RKe2OYY,1757 +sklearn/datasets/tests/test_kddcup99.py,sha256=5rw4Pva1EC2CO7imU9NVe0OqTrmTCu_4hElGpvZkUfk,2601 +sklearn/datasets/tests/test_lfw.py,sha256=YWNdfvIMcBbCfBfDSlaKBB1_9Q9qBXGe9VOaUUTFXac,7796 +sklearn/datasets/tests/test_olivetti_faces.py,sha256=d2r43YseviKoA9OyX6JvDyXvY8lFRfV__j5hippkYY0,919 +sklearn/datasets/tests/test_openml.py,sha256=RrIu0XL_1PqpegUN3pYJa5FM9QNCyCMX0kiIS_5DYvQ,54546 +sklearn/datasets/tests/test_rcv1.py,sha256=_MI_VuGKrZIIV-WMVxOEKMh94DqzhCrxV7l1E3NGkNM,2343 +sklearn/datasets/tests/test_samples_generator.py,sha256=CBWSP9td7WpU1vi8e2XiuMqauhXzvHHnXYJKZ22-56U,23846 +sklearn/datasets/tests/test_svmlight_format.py,sha256=mqKurK216uySN6hE-DAfHRt-6NHEGm4fBWyBIHpKCx0,20222 +sklearn/decomposition/__init__.py,sha256=joTYvN7TfssMwqycJWm9QjQqMknhLhm4CvpA3Xi3Jgg,1325 +sklearn/decomposition/__pycache__/__init__.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_base.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_dict_learning.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_factor_analysis.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_fastica.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_incremental_pca.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_kernel_pca.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_lda.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_nmf.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_pca.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_sparse_pca.cpython-310.pyc,, +sklearn/decomposition/__pycache__/_truncated_svd.cpython-310.pyc,, +sklearn/decomposition/_base.py,sha256=ghws6Nz8rN3zWFhk1DXbHAe-5oz9gpofEKey9G2Izx4,7147 +sklearn/decomposition/_cdnmf_fast.cpython-310-x86_64-linux-gnu.so,sha256=RYykXCL5MyiH_fkcA5LVszSHaIPZjyp9qGj9JZ0602w,117512 +sklearn/decomposition/_cdnmf_fast.pyx,sha256=ONJUPP9uKUn6uyUJHwHBD3qQeJmtM-7GFjFA8qCniJQ,1128 +sklearn/decomposition/_dict_learning.py,sha256=rIy4gcLaY434hn9jwcYQXTDvm0EB1ALgJ6cBoz8mDQ0,77761 +sklearn/decomposition/_factor_analysis.py,sha256=T-DH7_Wz9l3RJTDwzRTaPfQUCX9trhde3A_Yu3h9ysI,15245 +sklearn/decomposition/_fastica.py,sha256=araRZCVXh0BwHsGv4vp9p07DodDqTSrGPhKz77oFWwU,26553 +sklearn/decomposition/_incremental_pca.py,sha256=6dXXSI5OsmTXy8Ou8KhayQwf6n6wavqDiFa4z8C2fDg,16434 +sklearn/decomposition/_kernel_pca.py,sha256=-cHsZgTl-xECobnYgtUUnQQ2nrB5oEtFVhRHF_gWjWA,22379 +sklearn/decomposition/_lda.py,sha256=p26x3ZNmzm1bQeIkJTGxYrWiURgMdI-t4T2E3_nfXOM,34068 +sklearn/decomposition/_nmf.py,sha256=VY9hCOD73XvG06K934LiaZykUpGEgu_cXp--9xJ0-EA,81455 +sklearn/decomposition/_online_lda_fast.cpython-310-x86_64-linux-gnu.so,sha256=3OQSkA4qmhoyVOuYDLUwj-ZaBVWaJTYvhUg9nKeTf5w,174504 +sklearn/decomposition/_online_lda_fast.pyx,sha256=AMEYftJohmE84AayqSAn0CXbAb2ac_QAL_OSbjOsFJw,2842 +sklearn/decomposition/_pca.py,sha256=VjuYIRlMY-K79harkthjzcgFL7rlkkLOoAwNfECIlZw,34601 +sklearn/decomposition/_sparse_pca.py,sha256=Zjhwze5bnNBOnmU-aBK8KEcSbDj4hRDSZ-B8CC-UfiY,17916 +sklearn/decomposition/_truncated_svd.py,sha256=n-I_HryY_AvycFDZWt8wKWEZ8nGUM8BZYuYBHtb6qj0,11708 +sklearn/decomposition/meson.build,sha256=Ou8NjxEeKMUenExdqgH-7ijrec-bkKI8Q_21ScKCjzM,322 +sklearn/decomposition/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/decomposition/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_dict_learning.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_factor_analysis.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_fastica.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_incremental_pca.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_kernel_pca.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_nmf.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_online_lda.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_pca.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_sparse_pca.cpython-310.pyc,, +sklearn/decomposition/tests/__pycache__/test_truncated_svd.cpython-310.pyc,, +sklearn/decomposition/tests/test_dict_learning.py,sha256=ZnG9Zi7tcVkk8t1jwHjlHVqMt2-Zax0-R9Co7zgY3vo,30587 +sklearn/decomposition/tests/test_factor_analysis.py,sha256=WOiKlwnFn4WqelwLa8ERB4dZsUbjF32h3-OTtRgRzZA,4032 +sklearn/decomposition/tests/test_fastica.py,sha256=BFYeGAA9DGiZb4tVFpKi__0QtECEFk47qkPQFZ4xOus,15916 +sklearn/decomposition/tests/test_incremental_pca.py,sha256=Oa2iwpd53fnOFsVX_U6-54AFjRDS8gs84alpmqMKwOY,16897 +sklearn/decomposition/tests/test_kernel_pca.py,sha256=8h17WzyseYxwyMbR1LIweP_yF6CXkoIYEbLJBYto9T8,21021 +sklearn/decomposition/tests/test_nmf.py,sha256=TcuG7v5R864EHgikwlA3LtueImtLhVc0NU0D1YbAqYs,32219 +sklearn/decomposition/tests/test_online_lda.py,sha256=HQz3SUqlQ1BVMwhymTGcx1BdOliU_C1Y0RKrHR6XT4A,16023 +sklearn/decomposition/tests/test_pca.py,sha256=madikr_ZdbbGxMJRoV3mbMxm3IcaLcvXraAf-4uvMFw,41916 +sklearn/decomposition/tests/test_sparse_pca.py,sha256=BZiQPrCsQuk0k1gVt8769hhLZJdoKgrffGW0sxsbJYI,12077 +sklearn/decomposition/tests/test_truncated_svd.py,sha256=ZVJ_Jv-HX-3YM5uDZ4rA_U6SOxC6kRQGCIe-vxAgYj0,7242 +sklearn/discriminant_analysis.py,sha256=StVAOtSw-kEHgW4UunwMyrWx5aTcbDzkfurKH5uxYsI,40512 +sklearn/dummy.py,sha256=BGnZaLCwgpPTHZZy9qjvy2NFSTX4xQZXrsBhQgdVtJk,24507 +sklearn/ensemble/__init__.py,sha256=emYX8q4bOw4SGxORFbVKSzOsRZwhm2H04PqFab1_-oY,1374 +sklearn/ensemble/__pycache__/__init__.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_bagging.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_base.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_forest.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_gb.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_iforest.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_stacking.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_voting.cpython-310.pyc,, +sklearn/ensemble/__pycache__/_weight_boosting.cpython-310.pyc,, +sklearn/ensemble/_bagging.py,sha256=h4FpdoDbNDT_JjCXGbUe0_CGpD_mmrRROKoXNGI589E,52239 +sklearn/ensemble/_base.py,sha256=-CfPYQHpnf0RJ-mU0WZyENQWpIyLHMYPXoJNAAyBpPY,10543 +sklearn/ensemble/_forest.py,sha256=xaQjDmN1PZZGdDY728RxeTQPKEMxMxctzh7O9Oc7buQ,117697 +sklearn/ensemble/_gb.py,sha256=Ao8IAFBxxZmOtu9EFACfs5G8svxz-OGFhPbx0Km0BZ0,87765 +sklearn/ensemble/_gradient_boosting.cpython-310-x86_64-linux-gnu.so,sha256=xB-0sP67L7r96jvrSjaHJbetUwmG_6yGbZSHbquYB2w,127136 +sklearn/ensemble/_gradient_boosting.pyx,sha256=Emsc3f3sNgCb7RgQV5f_mnXfDHPAI0N1gvQe6NaINwQ,8562 +sklearn/ensemble/_hist_gradient_boosting/__init__.py,sha256=CjfoMHKJd5hxBLWAbtW-lN4WAoAjhWpw8RwtcWmuX-s,246 +sklearn/ensemble/_hist_gradient_boosting/__pycache__/__init__.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/__pycache__/binning.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/__pycache__/gradient_boosting.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/__pycache__/grower.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/__pycache__/predictor.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/__pycache__/utils.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/_binning.cpython-310-x86_64-linux-gnu.so,sha256=5y3SoWn1amuZOrZcdT7Re6YGcg3DszXB9p3wYjXojE4,92641 +sklearn/ensemble/_hist_gradient_boosting/_binning.pyx,sha256=iQH5QwuI4ia7LHDw8RclXqhwscEWTc2H7vshtH5usOE,2786 +sklearn/ensemble/_hist_gradient_boosting/_bitset.cpython-310-x86_64-linux-gnu.so,sha256=q9ceGvcfECb-jJcmig9nPxRDtyZUu_XlUWDq8mJ_DGc,80024 +sklearn/ensemble/_hist_gradient_boosting/_bitset.pxd,sha256=_5y92vr1nOs5_KyCfs2-E-hTnpEW5KTGjUTXMwthIQ0,708 +sklearn/ensemble/_hist_gradient_boosting/_bitset.pyx,sha256=Jyt_GO23ad6ZM7XKlEKxQlWV_j-s7cbVn83P44mr6d0,2540 +sklearn/ensemble/_hist_gradient_boosting/_gradient_boosting.cpython-310-x86_64-linux-gnu.so,sha256=5r3v3NUICZNXz5a64tUo6obwSzo3sjjNKqt4Haykl6w,96865 +sklearn/ensemble/_hist_gradient_boosting/_gradient_boosting.pyx,sha256=-0EYKZFppgamkie2aaQii29psvZjmd6g6rTAkeLOOos,1990 +sklearn/ensemble/_hist_gradient_boosting/_predictor.cpython-310-x86_64-linux-gnu.so,sha256=wu1OAJ9ecgbIKHkcJcYS7zpubXq17s7kJNx8GWXY2vc,133897 +sklearn/ensemble/_hist_gradient_boosting/_predictor.pyx,sha256=oBIH9D7SzdCdiv7n0hZA-o54TI4kFFDdkc-GUtN_1f0,9575 +sklearn/ensemble/_hist_gradient_boosting/binning.py,sha256=7ZuQXsKA4FbHw8xI6dm7puW_0xjsCFTzgDdlRC0RwI0,13925 +sklearn/ensemble/_hist_gradient_boosting/common.cpython-310-x86_64-linux-gnu.so,sha256=2OUg6YuO6SkFZ7eceqRPscPyGXqThxKMObxTuQkFLnM,45824 +sklearn/ensemble/_hist_gradient_boosting/common.pxd,sha256=MLDp9cP2k6UeUENyhJKBynnwTSoUnfAG-J32TucOZpk,1244 +sklearn/ensemble/_hist_gradient_boosting/common.pyx,sha256=FSvUdsBMiLIAmvk1eke3C0PBo0dcmUIJ1omN7a-B0kY,1747 +sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py,sha256=-I7BWx-GHWrIUXuOPlNIu0Ui6-MSfF2JEnUCB4tCf9w,97143 +sklearn/ensemble/_hist_gradient_boosting/grower.py,sha256=QM1v8cGeXwdP3pOUnQ3PjKAPJ9NX6g9T-MsuFSBacsk,32674 +sklearn/ensemble/_hist_gradient_boosting/histogram.cpython-310-x86_64-linux-gnu.so,sha256=3ewoECAzxvpN0N0QJ2AKIIofpnuUXur-g3rrBa_l1Gk,228569 +sklearn/ensemble/_hist_gradient_boosting/histogram.pyx,sha256=BB-f6QgDOPiQ_vUSUKTANZgpeeGON7Elpe6-yz8WwG0,20651 +sklearn/ensemble/_hist_gradient_boosting/meson.build,sha256=aNiFEGgexu7353QjlQ_MgjSL1hQK6wlBgLytMaZEgwE,979 +sklearn/ensemble/_hist_gradient_boosting/predictor.py,sha256=oBStnOotKnJcUp-lJCigLNOeOO4U0KywfrKo4zFBBzE,5029 +sklearn/ensemble/_hist_gradient_boosting/splitting.cpython-310-x86_64-linux-gnu.so,sha256=GTBeprW5smLCVXKjp2nF6VsqQps3gCYVMAT3vunHCkI,261345 +sklearn/ensemble/_hist_gradient_boosting/splitting.pyx,sha256=edDtSh-xGAJq7X7IWp-_hoTGpc-zaGFfcETr3WH7YXk,52287 +sklearn/ensemble/_hist_gradient_boosting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_binning.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_bitset.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_compare_lightgbm.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_gradient_boosting.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_grower.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_histogram.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_monotonic_constraints.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_predictor.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_splitting.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/__pycache__/test_warm_start.cpython-310.pyc,, +sklearn/ensemble/_hist_gradient_boosting/tests/test_binning.py,sha256=aNXHw7u7IRAdEfHO2TWdjAmlj9y_SdhJir-w0yQ-fkc,16252 +sklearn/ensemble/_hist_gradient_boosting/tests/test_bitset.py,sha256=5QHny5G3p9tyExBsdsUVV2vFKgPI-vYDt-zvLpMBHXQ,2100 +sklearn/ensemble/_hist_gradient_boosting/tests/test_compare_lightgbm.py,sha256=yaUeaZ8g4F5J3Vrct3mfcR9djCMV2gKvn7ITF4QZtVM,10592 +sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py,sha256=P8jW5nyqUKP9iIpDIL6kooPxgxyv_3DlZbPiErP3_Vk,63145 +sklearn/ensemble/_hist_gradient_boosting/tests/test_grower.py,sha256=mDda3Xp-vF2Kgqdz3bj5UUtC4jUZR--dCesLwmDI50c,23152 +sklearn/ensemble/_hist_gradient_boosting/tests/test_histogram.py,sha256=PBoacgv-6rOI5lTpzCyaafC9eDvyA6tb94RnDw_wLhs,8681 +sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_constraints.py,sha256=ucsF7gy_hskZ1oDK6GSD1lr9ypKNqadkEFXRGeaNHfQ,16940 +sklearn/ensemble/_hist_gradient_boosting/tests/test_predictor.py,sha256=wq5vXIMwh7Fr3wDeHGO2F-oNNXEH_hUdyOyS7SIGXpE,6345 +sklearn/ensemble/_hist_gradient_boosting/tests/test_splitting.py,sha256=nkX5rAlTeO6tPR4_K4Gc9bvViPu1HUboA7-vRdiTETo,38639 +sklearn/ensemble/_hist_gradient_boosting/tests/test_warm_start.py,sha256=3Q_3ZhKf94uvmADlNMj0Vpyp7gqjDd1czBzFW8pUuAQ,7933 +sklearn/ensemble/_hist_gradient_boosting/utils.py,sha256=RiXIru1WQYuMxoj7Ko141DeH32WctBmQZeTfKwYdRcA,5523 +sklearn/ensemble/_iforest.py,sha256=o3PaqtullSmCARwGI0MsVukYzMRopDdeVnhvM3VW2Lw,24264 +sklearn/ensemble/_stacking.py,sha256=Z8D7xDNAg2nxEM_B4us7RoYz7_1OcF-p9lj3izrcM6U,43546 +sklearn/ensemble/_voting.py,sha256=WL7_PjWtf8V4fdf9fOpIYB79qAxaTp8U4q1hjfrxsu4,24834 +sklearn/ensemble/_weight_boosting.py,sha256=Cl1PzwH0DG5CGcOH22Eacd_cmkysykABwgIgD99aTYA,41097 +sklearn/ensemble/meson.build,sha256=7nR6oq_djKOBo-7Yxc-UmB6uWVai4w5By9i10tBX4hE,224 +sklearn/ensemble/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/ensemble/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_bagging.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_forest.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_gradient_boosting.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_iforest.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_stacking.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_voting.cpython-310.pyc,, +sklearn/ensemble/tests/__pycache__/test_weight_boosting.cpython-310.pyc,, +sklearn/ensemble/tests/test_bagging.py,sha256=YS38i0QjCIZ_XY3YSwstXAo5pa_65v6JHcreb5T2NfQ,33682 +sklearn/ensemble/tests/test_base.py,sha256=dCynI18UuKx7HpGwnUSjfUR2GlTfhGRmO_UA_-kDu6A,3667 +sklearn/ensemble/tests/test_common.py,sha256=kUynrJPb67QHmQZaVC0KPWvJkZAhTEKEF5WFSO8pM2k,9106 +sklearn/ensemble/tests/test_forest.py,sha256=Oko4dDMgB2z6H-UbGaCxKiwG2ezbhqiGKjPnc2QyJAM,62801 +sklearn/ensemble/tests/test_gradient_boosting.py,sha256=zrUVq7La0QRrA34MccQBCqatrPJlOwzHxy34AG-h5YA,58761 +sklearn/ensemble/tests/test_iforest.py,sha256=s2wpk7-N9Hr6hRWYvOAhsbQTkrRqXbu3CYisUNud6nQ,13539 +sklearn/ensemble/tests/test_stacking.py,sha256=Gqiay4pCaaZ68F-jDcTixcEKb7te7ztR-w9W2xqYHEU,33490 +sklearn/ensemble/tests/test_voting.py,sha256=HLY47XeqyoSuHR5jAD25TIcLAvFt4Kjt0MxXNEUVkR8,27499 +sklearn/ensemble/tests/test_weight_boosting.py,sha256=EPyS-E7pWkcs4-bJGzM2gE1rDpTGshTTki4kXAf593U,21928 +sklearn/exceptions.py,sha256=CaaFS4DVbqhqUidiA-cMvTP6DR6qIFmEUzuiMS5HDec,7703 +sklearn/experimental/__init__.py,sha256=0SSV8qXhFfA8-T9zvuWasIT8bNbPXLUX4ZQZp0CoDzk,305 +sklearn/experimental/__pycache__/__init__.cpython-310.pyc,, +sklearn/experimental/__pycache__/enable_halving_search_cv.cpython-310.pyc,, +sklearn/experimental/__pycache__/enable_hist_gradient_boosting.cpython-310.pyc,, +sklearn/experimental/__pycache__/enable_iterative_imputer.cpython-310.pyc,, +sklearn/experimental/enable_halving_search_cv.py,sha256=4s1q_AiYCx7jiZGmc7uieges2_MsYh8ykfUi3UC4qMw,1290 +sklearn/experimental/enable_hist_gradient_boosting.py,sha256=0vehofwAKeWQbeQO0B0E0lulIWUk1q4pwBCMFERSm3Q,826 +sklearn/experimental/enable_iterative_imputer.py,sha256=IgDLGeBd6XtbGp-K5xuef6edPfHGaLNakuDMfE_Vj9A,768 +sklearn/experimental/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/experimental/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/experimental/tests/__pycache__/test_enable_hist_gradient_boosting.cpython-310.pyc,, +sklearn/experimental/tests/__pycache__/test_enable_iterative_imputer.cpython-310.pyc,, +sklearn/experimental/tests/__pycache__/test_enable_successive_halving.cpython-310.pyc,, +sklearn/experimental/tests/test_enable_hist_gradient_boosting.py,sha256=cAFugPf0tYSd-P2-GlcfvhG7YnKlfMoqE8Pff7yXG-4,672 +sklearn/experimental/tests/test_enable_iterative_imputer.py,sha256=LWtq99MTXXga2dq_ZcB0korId_7ctVxKtZLrFNZvFns,1689 +sklearn/experimental/tests/test_enable_successive_halving.py,sha256=MVt6aApWKiR3VnVRnY7GEoQdI8w-f2M--w60vS0B5vA,1896 +sklearn/externals/README,sha256=GFbJH7vHxxuzJLaVlul1GkfwjREK64RyEXUCWL1NSxk,270 +sklearn/externals/__init__.py,sha256=jo7XxwlsquXvHghwURnScmXn3XraDerjG1fNR_e11-U,42 +sklearn/externals/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/__pycache__/_arff.cpython-310.pyc,, +sklearn/externals/__pycache__/_array_api_compat_vendor.cpython-310.pyc,, +sklearn/externals/__pycache__/conftest.cpython-310.pyc,, +sklearn/externals/_arff.py,sha256=YXR8xgF1IxyugQV70YHNjmza2yuz86zhVM1i6AI-RSA,38341 +sklearn/externals/_array_api_compat_vendor.py,sha256=Gb7C65qVPo5gbKKlpq4jHtXWkgsN0wIrTQAaFBbais0,198 +sklearn/externals/_packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_packaging/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/_packaging/__pycache__/_structures.cpython-310.pyc,, +sklearn/externals/_packaging/__pycache__/version.cpython-310.pyc,, +sklearn/externals/_packaging/_structures.py,sha256=Ofe3RryZqacr5auj4s7MsEylGigfeyf8sagFvK-rPv0,2922 +sklearn/externals/_packaging/version.py,sha256=IDbp4Q6S9OZ3mP57YCDerh4Xm0s6AUqSi6CbFJ3eQyI,16134 +sklearn/externals/_scipy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_scipy/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/_scipy/sparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_scipy/sparse/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/_scipy/sparse/csgraph/__init__.py,sha256=GMAcZXBWt9Dp0QEOeCsQglt8CWB6_stqr7Wf_LfH0tE,34 +sklearn/externals/_scipy/sparse/csgraph/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/_scipy/sparse/csgraph/__pycache__/_laplacian.cpython-310.pyc,, +sklearn/externals/_scipy/sparse/csgraph/_laplacian.py,sha256=l1bAYnntljvIXc8mwJqSpLS6EBTjzMTb0XrW2_S1A1k,18166 +sklearn/externals/array_api_compat/LICENSE,sha256=T_2Xjj-hjQWNmMZncc_qftY0qvcCPPlhK4tV7umo8P4,1097 +sklearn/externals/array_api_compat/README.md,sha256=YjsmsQ3VNuGPaD7I6a_lvqGBVNBhm-k5ty-yWwIjjRY,67 +sklearn/externals/array_api_compat/__init__.py,sha256=zk6TZdJLBzT7Td3TKbCkYA1KIxKOsa-CKqDn0JCUq2I,992 +sklearn/externals/array_api_compat/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/__pycache__/_internal.cpython-310.pyc,, +sklearn/externals/array_api_compat/_internal.py,sha256=pfbMacXgxBaLmhueWE54mtXrbBdxyLd2Gc7dHrxYtGk,1412 +sklearn/externals/array_api_compat/common/__init__.py,sha256=4IcMWP5rARLYe2_pgXDWEuj2YpM0c1G6Pb5pkbQ0QS8,38 +sklearn/externals/array_api_compat/common/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/__pycache__/_aliases.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/__pycache__/_fft.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/__pycache__/_helpers.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/__pycache__/_linalg.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/__pycache__/_typing.cpython-310.pyc,, +sklearn/externals/array_api_compat/common/_aliases.py,sha256=xvZcAGCBbbujmjh76EvaYDzgPfQhaHK8QH--CQI906U,19644 +sklearn/externals/array_api_compat/common/_fft.py,sha256=ckCR2uHtz0iaOkcuvqVunhz1khIdxQNKuVU0x1bfrq8,4669 +sklearn/externals/array_api_compat/common/_helpers.py,sha256=zIz2QmS4LEI-aT05xMzXTgZ6Y6aULKbxlZxWa_R-lb4,31586 +sklearn/externals/array_api_compat/common/_linalg.py,sha256=Wdf0FzzxJNEiGhOOsQKg8PnMusM3fVeN5CA4RBItF_Y,6856 +sklearn/externals/array_api_compat/common/_typing.py,sha256=Z5N8fYR_54UorD4IXFdOOigqYRDp6mNa-iA7703PKf4,4358 +sklearn/externals/array_api_compat/cupy/__init__.py,sha256=8KfEs6ULcXuZ4AUKBD_7L3XZfW8TOQayZPerR_YLeSI,390 +sklearn/externals/array_api_compat/cupy/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/__pycache__/_aliases.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/__pycache__/_info.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/__pycache__/_typing.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/__pycache__/fft.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/__pycache__/linalg.cpython-310.pyc,, +sklearn/externals/array_api_compat/cupy/_aliases.py,sha256=OgOoVRk-TI9t0hCsI82VLkebkZRdN7aXjamWMRw0yYQ,4842 +sklearn/externals/array_api_compat/cupy/_info.py,sha256=g3DwO5ps4bSlFU2pc_f4XTaLrkCYuSDlCw0Ql2wuqM8,10125 +sklearn/externals/array_api_compat/cupy/_typing.py,sha256=dkA_sAAgU1Zb1PNopuOsywbLeFK-rLWAY4V4Vj3-x0I,628 +sklearn/externals/array_api_compat/cupy/fft.py,sha256=xCAC42CNAwAyVW7uCREsSoAV23R3rL2dqrT7w877zuE,842 +sklearn/externals/array_api_compat/cupy/linalg.py,sha256=nKOM-_wcOHzHhEeV9KBzcMVNlviJK4nP1nFBUtvnjTM,1444 +sklearn/externals/array_api_compat/dask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_compat/dask/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/__init__.py,sha256=OkadrcCZUdp3KsB5q2fhTyAACW12gDXxW_A4ANGcAqY,320 +sklearn/externals/array_api_compat/dask/array/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/__pycache__/_aliases.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/__pycache__/_info.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/__pycache__/fft.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/__pycache__/linalg.cpython-310.pyc,, +sklearn/externals/array_api_compat/dask/array/_aliases.py,sha256=ZmoAVGbsj04gcfE7R0V6N_7AXCZrhYSFXXfzJfJ5O4Y,10668 +sklearn/externals/array_api_compat/dask/array/_info.py,sha256=rpfvNrS4ZaZMEcaomlRFxx7Dqb_tohhDFvI6qYoaivI,12618 +sklearn/externals/array_api_compat/dask/array/fft.py,sha256=OZxTcLBCXKgVpbMo7Oqn9NH_7_9ZUHQdB6iP8WSYVfY,589 +sklearn/externals/array_api_compat/dask/array/linalg.py,sha256=AtkHftJ3hufuuSlZhRxR0RH9IureEet387rpn1h38XU,2451 +sklearn/externals/array_api_compat/numpy/__init__.py,sha256=7SOguTm7-yJgJPnFTlbk_4bPTltsgKLbkO59ZmoCODg,853 +sklearn/externals/array_api_compat/numpy/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/__pycache__/_aliases.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/__pycache__/_info.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/__pycache__/_typing.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/__pycache__/fft.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/__pycache__/linalg.cpython-310.pyc,, +sklearn/externals/array_api_compat/numpy/_aliases.py,sha256=SKaCfzc2eY1eAu3Yzm3JVuR3uUqL7PoXf6GyYyXpcw4,5715 +sklearn/externals/array_api_compat/numpy/_info.py,sha256=8KNJ09jKFfMH20wff67GJVPyoZ-e8-OUHF88THx-1Cs,10782 +sklearn/externals/array_api_compat/numpy/_typing.py,sha256=O03YoguInLXMcL5Q0JKHxRXSREgE0DCusVAZKAv-l10,626 +sklearn/externals/array_api_compat/numpy/fft.py,sha256=7oxAzAnFwsAH0J43eXFKRkJ_GKCVEC-7G_lz56pVBz8,779 +sklearn/externals/array_api_compat/numpy/linalg.py,sha256=ORu4MhuN6F5EXOy-lYHxfMHkRpVRx2VEC29rRwB8Bws,4039 +sklearn/externals/array_api_compat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_compat/torch/__init__.py,sha256=o351abwQmNWcX00GBnGYHrpfM8pFiieFWRaf0NI-KFg,549 +sklearn/externals/array_api_compat/torch/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/__pycache__/_aliases.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/__pycache__/_info.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/__pycache__/_typing.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/__pycache__/fft.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/__pycache__/linalg.cpython-310.pyc,, +sklearn/externals/array_api_compat/torch/_aliases.py,sha256=w_exCqFcAuB3TXtiqk_NpSHJ8D3ZawulLdjFxTvujQc,30261 +sklearn/externals/array_api_compat/torch/_info.py,sha256=-H2xD9z9SMf3GjIOW0jeRTUOvh4s8E9p9u_4LqawRZM,11889 +sklearn/externals/array_api_compat/torch/_typing.py,sha256=-uCkuTie1g9hb4vwPLK9eEnir9Zp67wAhrfaI_o-35E,108 +sklearn/externals/array_api_compat/torch/fft.py,sha256=9YO23YEbQr49gq_DrfJ7V0G41G7WlJC6rJAeqqOP7dw,1738 +sklearn/externals/array_api_compat/torch/linalg.py,sha256=acbcg80CjamMQ0JDAkrWL7FkyEW5MfmGVzQsrRT00jM,4799 +sklearn/externals/array_api_extra/LICENSE,sha256=WElDmP4Uf9znamiy3s1MCM46HqI3ttZ4UAHBX4IsbtY,1097 +sklearn/externals/array_api_extra/README.md,sha256=hujBWt3i3o5AkT4rUqbVle7qQ3LhbaSwl1VYPT33rig,66 +sklearn/externals/array_api_extra/__init__.py,sha256=Xsj-UtwQSb-PYz6mcJ76Bj0NPKmzOXcTzBeMBZY7EV8,660 +sklearn/externals/array_api_extra/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_extra/__pycache__/_delegation.cpython-310.pyc,, +sklearn/externals/array_api_extra/__pycache__/testing.cpython-310.pyc,, +sklearn/externals/array_api_extra/_delegation.py,sha256=1biTXOZj5KyDCG2JEOgCGasKHu6n1UMF_9iuB8YP-wI,6345 +sklearn/externals/array_api_extra/_lib/__init__.py,sha256=GCx2h0v6DbmpkC0XDJkRzbZWcUqwwHEuVDoAeX7FrAI,91 +sklearn/externals/array_api_extra/_lib/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/__pycache__/_at.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/__pycache__/_backends.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/__pycache__/_funcs.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/__pycache__/_lazy.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/__pycache__/_testing.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/_at.py,sha256=-ZPik4faGK6D_WvsCg9V926C0oF_x6Cmw8i3yfjvTyM,14970 +sklearn/externals/array_api_extra/_lib/_backends.py,sha256=MJ4r-NYRF9gSZkLJviYq7DeioyfgIWM9ErIkCavNANU,1754 +sklearn/externals/array_api_extra/_lib/_funcs.py,sha256=5kXrbceGaV7v2PlZiweQo7CXesB1TqXJzYegZib2yrk,28982 +sklearn/externals/array_api_extra/_lib/_lazy.py,sha256=abt1ee49uFMx3Nws8-BKee2j1qLwVUGx2trJfTHusGY,13682 +sklearn/externals/array_api_extra/_lib/_testing.py,sha256=TH7--PHPinrQ6gRZWMiCSyZGgTaZyXZ6AOSbvtPsAYQ,7658 +sklearn/externals/array_api_extra/_lib/_utils/__init__.py,sha256=8ICffM2MprXpWZd8ia0-5ZTnKtDfeZD0gExLveDrXZs,49 +sklearn/externals/array_api_extra/_lib/_utils/__pycache__/__init__.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/_utils/__pycache__/_compat.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/_utils/__pycache__/_helpers.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/_utils/__pycache__/_typing.cpython-310.pyc,, +sklearn/externals/array_api_extra/_lib/_utils/_compat.py,sha256=l_4tKMzUsfG3f_ZjczhYpuwhq8G76GtzxkgJ1fEhrzk,1724 +sklearn/externals/array_api_extra/_lib/_utils/_compat.pyi,sha256=M0UcaeFCqLPgGsF8N5mHw7s3bsrlbDfJY2uhLozJ97I,1675 +sklearn/externals/array_api_extra/_lib/_utils/_helpers.py,sha256=7rZIEG-g6xqVKs7erLlt-nLDTOomUmAh45rXP42fVW4,8234 +sklearn/externals/array_api_extra/_lib/_utils/_typing.py,sha256=FCc9Ocs2akiX3Wzwv7gWb737Azt_RRmsbEVT9dc9WU8,213 +sklearn/externals/array_api_extra/_lib/_utils/_typing.pyi,sha256=-XcCOYxOoKjgPeo3w9Pqg1KyYm6JTKm6aO_jD22CGoU,4725 +sklearn/externals/array_api_extra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_extra/testing.py,sha256=mw_0y_TzMmTP2wUV_ofAwcih0R_ymhUkQBrHBj9k_gM,11940 +sklearn/externals/conftest.py,sha256=8wfDBd_pWHl3PsD3IOGeZT4z0U-q2895fYvApMzq5gg,312 +sklearn/feature_extraction/__init__.py,sha256=I44s-WIjNSCKkaMvQ6k60KFhHD3Je34kYW5ebV4TYTk,396 +sklearn/feature_extraction/__pycache__/__init__.cpython-310.pyc,, +sklearn/feature_extraction/__pycache__/_dict_vectorizer.cpython-310.pyc,, +sklearn/feature_extraction/__pycache__/_hash.cpython-310.pyc,, +sklearn/feature_extraction/__pycache__/_stop_words.cpython-310.pyc,, +sklearn/feature_extraction/__pycache__/image.cpython-310.pyc,, +sklearn/feature_extraction/__pycache__/text.cpython-310.pyc,, +sklearn/feature_extraction/_dict_vectorizer.py,sha256=jjMZ8gPjjPihpP-sOjM1sFI_xt5WeqDem54-ZLQUZxw,16030 +sklearn/feature_extraction/_hash.py,sha256=R84FrVMR4ZvQ1vIg4P_rSg-sK2wb4werrEyfiq_wKFI,7795 +sklearn/feature_extraction/_hashing_fast.cpython-310-x86_64-linux-gnu.so,sha256=KK8D4ISTB3V4Gaeo_pYTZwuw2kYhO6_W_WXZOCPdCBc,94200 +sklearn/feature_extraction/_hashing_fast.pyx,sha256=V-PISJDpipnfNlxj6NxYhdq4LsaYwpudjdzSim1OKiw,3027 +sklearn/feature_extraction/_stop_words.py,sha256=ZEfwEZHSNFr0id2pPdBlZq-E9j6VFQM8S86gubzOweo,5725 +sklearn/feature_extraction/image.py,sha256=pAx59y5gZ0WDgExj7SnHstWEHTOvmZD_MyqoHfdX-BY,23563 +sklearn/feature_extraction/meson.build,sha256=5D4WuiUPvXvqJcQj-yqpkmQ2yxJ9yVr6T-_Q5Gk0Tw8,192 +sklearn/feature_extraction/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/feature_extraction/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/feature_extraction/tests/__pycache__/test_dict_vectorizer.cpython-310.pyc,, +sklearn/feature_extraction/tests/__pycache__/test_feature_hasher.cpython-310.pyc,, +sklearn/feature_extraction/tests/__pycache__/test_image.cpython-310.pyc,, +sklearn/feature_extraction/tests/__pycache__/test_text.cpython-310.pyc,, +sklearn/feature_extraction/tests/test_dict_vectorizer.py,sha256=sJfcwyId7Xjrs3aakS-HjkPvt0dzuVLqIuCQmpxnN5U,8256 +sklearn/feature_extraction/tests/test_feature_hasher.py,sha256=WT6h7r7k7gwS3-CvxO4F4ssw4jXSfptTGQKJL9i4D58,5046 +sklearn/feature_extraction/tests/test_image.py,sha256=lALrGDEr4LzX0HCMKNtcrhzHrtqrL-j_QDsk8_zLfcU,12303 +sklearn/feature_extraction/tests/test_text.py,sha256=Yh1wrcKr7l2Axtuc-KsFwguAzG5UqxryV3_VzTbashY,52240 +sklearn/feature_extraction/text.py,sha256=v9e4fUrhPNj4sNfkGPSbrIKjVKrWIhDY0EPr5ZmoSIE,77382 +sklearn/feature_selection/__init__.py,sha256=_G69r4hI6pPZ_6Da8uPNMW1MBzdulQfgcrJuinCJ6Mc,1128 +sklearn/feature_selection/__pycache__/__init__.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_base.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_from_model.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_mutual_info.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_rfe.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_sequential.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_univariate_selection.cpython-310.pyc,, +sklearn/feature_selection/__pycache__/_variance_threshold.cpython-310.pyc,, +sklearn/feature_selection/_base.py,sha256=lwJe6Qub7zyF6LHHrT3jZ4Y2asC2Eb6oPWC6hh2gwGQ,9426 +sklearn/feature_selection/_from_model.py,sha256=heQ8iO367qKPUYrHf2MgiSwyIXbtoDu_i5yht2hF80M,18651 +sklearn/feature_selection/_mutual_info.py,sha256=e8tJ69GdZu4VgpRW85nlxAXrY3Y4I8xg68xe79y2uDQ,19968 +sklearn/feature_selection/_rfe.py,sha256=Y8w0_KQLZtFAKvu2Q8tHNAGwFCGAN3abiXyjd39HR9o,37652 +sklearn/feature_selection/_sequential.py,sha256=c6dilPIHBKOCAYTGYWSWSg4VlCEsxXrg2MYMO7DfRaQ,13904 +sklearn/feature_selection/_univariate_selection.py,sha256=dnj6zNvGKDlnH7R-iOgco6a0U4OmCBg0zL8TtJSI5M4,40735 +sklearn/feature_selection/_variance_threshold.py,sha256=uxrTWbLzgx_b74XKUGmNwrSWMOjP0LDq7kXUD_mLQxY,4639 +sklearn/feature_selection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/feature_selection/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_chi2.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_feature_select.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_from_model.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_mutual_info.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_rfe.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_sequential.cpython-310.pyc,, +sklearn/feature_selection/tests/__pycache__/test_variance_threshold.cpython-310.pyc,, +sklearn/feature_selection/tests/test_base.py,sha256=ythpS8iRYjFm3VP581ikkgPobz12JrlTAYBTATIKKBU,4832 +sklearn/feature_selection/tests/test_chi2.py,sha256=c6L3cs9DYulMNUTjnZJo7VURucjhUHLYzG2EaRE9N1c,3139 +sklearn/feature_selection/tests/test_feature_select.py,sha256=59hWeQqIEOZJGcE5IL5y3jMnlBwFbpuwH855OKUgpsA,32507 +sklearn/feature_selection/tests/test_from_model.py,sha256=qAQAdvrS7SwnXpNY53qexquuMoWFAZyO_AZQVNdSKUk,23841 +sklearn/feature_selection/tests/test_mutual_info.py,sha256=IyCSjjXPkQez915cjtshElj_9xQVHY84a5aiCJMFP4s,9853 +sklearn/feature_selection/tests/test_rfe.py,sha256=xCDzFtO6UnnoApmEmPMHR61iii_IImfA1AZcwKR2xIo,25270 +sklearn/feature_selection/tests/test_sequential.py,sha256=9Z-naJRDVboKShzMI4xcWekQjwktpUwKT2hmaalAS3Y,10906 +sklearn/feature_selection/tests/test_variance_threshold.py,sha256=tKaSBkRgVBzo3xC0lT6nLNNzKW4M-5t_sAFJgUmr--g,2640 +sklearn/frozen/__init__.py,sha256=7zBEBZHkRwHUBRG1VAn6kPYJeFjFkktqSpLATohnI7o,148 +sklearn/frozen/__pycache__/__init__.cpython-310.pyc,, +sklearn/frozen/__pycache__/_frozen.cpython-310.pyc,, +sklearn/frozen/_frozen.py,sha256=hKGn7cTTiE6Db0cBoehUCyRNbRcOObRPeBM7V0X_XC4,4985 +sklearn/frozen/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/frozen/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/frozen/tests/__pycache__/test_frozen.cpython-310.pyc,, +sklearn/frozen/tests/test_frozen.py,sha256=u7WjplRRlNCjNx77UMkBbVpXhuKFdM6TgVSmvwEzI_4,7069 +sklearn/gaussian_process/__init__.py,sha256=pK0Xi-lrrByscZP7Brgk92RC5Qy4AIDrOtFb71bpQ58,330 +sklearn/gaussian_process/__pycache__/__init__.cpython-310.pyc,, +sklearn/gaussian_process/__pycache__/_gpc.cpython-310.pyc,, +sklearn/gaussian_process/__pycache__/_gpr.cpython-310.pyc,, +sklearn/gaussian_process/__pycache__/kernels.cpython-310.pyc,, +sklearn/gaussian_process/_gpc.py,sha256=iL9epkfnD-q4-n0cpLf5ClLiV_2pWDNDkFqxGRfoxMw,39297 +sklearn/gaussian_process/_gpr.py,sha256=zpKQWpQkjfBtXtwxz3R_SWSGEixKsjKoMkhOCVsAM_U,28314 +sklearn/gaussian_process/kernels.py,sha256=l0uLSlAi-Mrax0cAPSDPZ3N7osIViS2rSQIFr7rrV3o,85106 +sklearn/gaussian_process/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/gaussian_process/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/gaussian_process/tests/__pycache__/_mini_sequence_kernel.cpython-310.pyc,, +sklearn/gaussian_process/tests/__pycache__/test_gpc.cpython-310.pyc,, +sklearn/gaussian_process/tests/__pycache__/test_gpr.cpython-310.pyc,, +sklearn/gaussian_process/tests/__pycache__/test_kernels.cpython-310.pyc,, +sklearn/gaussian_process/tests/_mini_sequence_kernel.py,sha256=YpD-vtJFSVdzVmJxHDmEdFGl6cOQ4J98mLpjFCFThys,1571 +sklearn/gaussian_process/tests/test_gpc.py,sha256=XZIDGXYMKKgjB3Tn53Dnyit4oPrpDOE5GSlC61a-L0Y,11251 +sklearn/gaussian_process/tests/test_gpr.py,sha256=yTJz72nlINDcPygRoAjQTSZ8Mv79DoAhUq7CiqM4lXk,29682 +sklearn/gaussian_process/tests/test_kernels.py,sha256=izel3Fru6VdgNRGHxnwVqmVENxy06sYjDTF03iRI9mQ,14492 +sklearn/impute/__init__.py,sha256=ps33PrOn-LYpyam1EVU7mur1eIlt8huormM1mbDV1UI,1031 +sklearn/impute/__pycache__/__init__.cpython-310.pyc,, +sklearn/impute/__pycache__/_base.cpython-310.pyc,, +sklearn/impute/__pycache__/_iterative.cpython-310.pyc,, +sklearn/impute/__pycache__/_knn.cpython-310.pyc,, +sklearn/impute/_base.py,sha256=8xtPfsw7_5doIKVZeusk0F4_C3MLebx1TYbRIyt1AO8,42913 +sklearn/impute/_iterative.py,sha256=cgIyfyJjaV5HySQ5TxaX97Aywa8VpSkr5UJAtNd1iWI,40184 +sklearn/impute/_knn.py,sha256=1kvnVdpDEHsm-pZBTYhbNWIAtcMzWoDPJkLpIdeJFGo,14905 +sklearn/impute/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/impute/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/impute/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/impute/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/impute/tests/__pycache__/test_impute.cpython-310.pyc,, +sklearn/impute/tests/__pycache__/test_knn.cpython-310.pyc,, +sklearn/impute/tests/test_base.py,sha256=L-RND6V8s4g40Uy65BIdQG1oEtHgOWBliBH4bUVdVQc,3367 +sklearn/impute/tests/test_common.py,sha256=G7WzU8u9bItkql-tlSTnRdekw0HPeCDfObCYQwcV63w,7616 +sklearn/impute/tests/test_impute.py,sha256=pUplIQ0TmQR21lY5SJoQ0ll21FmGoXrZtqPXVv_Ik9k,66345 +sklearn/impute/tests/test_knn.py,sha256=4FL0dBxzW_FooUpFuzgR6uYDH2Y4l9pLGJ1zkgy9b4Q,17540 +sklearn/inspection/__init__.py,sha256=Sb9g89Bjofq0OCfNUQlC3rfvHyGE__zWiXA2yvPhib8,485 +sklearn/inspection/__pycache__/__init__.cpython-310.pyc,, +sklearn/inspection/__pycache__/_partial_dependence.cpython-310.pyc,, +sklearn/inspection/__pycache__/_pd_utils.cpython-310.pyc,, +sklearn/inspection/__pycache__/_permutation_importance.cpython-310.pyc,, +sklearn/inspection/_partial_dependence.py,sha256=BAR29VAvZyQ6bn6cNoEJmUQSWjF5uie6vjR-5RmxgrM,33439 +sklearn/inspection/_pd_utils.py,sha256=m01ubgd8W-ThbL95ATj7dRWK6nACermQBc0MrEPPQr8,2218 +sklearn/inspection/_permutation_importance.py,sha256=XqjjABRNDScQUAoJos5hhruGuRE1EdLLZTyc15BPblo,11395 +sklearn/inspection/_plot/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/inspection/_plot/__pycache__/__init__.cpython-310.pyc,, +sklearn/inspection/_plot/__pycache__/decision_boundary.cpython-310.pyc,, +sklearn/inspection/_plot/__pycache__/partial_dependence.cpython-310.pyc,, +sklearn/inspection/_plot/decision_boundary.py,sha256=fJgQNeItWQB70nngLb-2_AJeOkL-Gq9P7eXng1_kPBI,22072 +sklearn/inspection/_plot/partial_dependence.py,sha256=kg2frhpo2AMCmhZWirf6aCSIbaD5svA0lqT6ee79e6Y,61426 +sklearn/inspection/_plot/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/inspection/_plot/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/inspection/_plot/tests/__pycache__/test_boundary_decision_display.cpython-310.pyc,, +sklearn/inspection/_plot/tests/__pycache__/test_plot_partial_dependence.cpython-310.pyc,, +sklearn/inspection/_plot/tests/test_boundary_decision_display.py,sha256=kuwvW1ND7hZwEHz3dooYVl47rjVLy_gjuGqfGWMg_bs,24640 +sklearn/inspection/_plot/tests/test_plot_partial_dependence.py,sha256=KHHwbby3GhtkD_4x6fLlR0ZVU2nokGX2eDrDMW-JA9w,41417 +sklearn/inspection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/inspection/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/inspection/tests/__pycache__/test_partial_dependence.cpython-310.pyc,, +sklearn/inspection/tests/__pycache__/test_pd_utils.cpython-310.pyc,, +sklearn/inspection/tests/__pycache__/test_permutation_importance.cpython-310.pyc,, +sklearn/inspection/tests/test_partial_dependence.py,sha256=7Bu-KuVvLuCZtQjBi6ZP-2sDmrN46isUtByOzd-pHLw,40976 +sklearn/inspection/tests/test_pd_utils.py,sha256=t-8K4YbQAbVK4pcI1P9hr8-0iEgc72x_1-868HAhLBg,1640 +sklearn/inspection/tests/test_permutation_importance.py,sha256=wDt75_tkjpDMffkcYn7jz6WeKZrkXgsBhtAO6nAa7WY,19840 +sklearn/isotonic.py,sha256=dIKBLNb4TNGdTKRJbP4iR0PM_MfLy4BlnZ4dF40zkAI,17371 +sklearn/kernel_approximation.py,sha256=AgM5jql4nyxrTD5zlw0DC1pROLRtlL2ttUOSI0B0hqw,39676 +sklearn/kernel_ridge.py,sha256=b9dyensnC3vnJhkIJSzoAtVO5-QDYP4cl7GGD_01IUE,9211 +sklearn/linear_model/__init__.py,sha256=m1s3A4BrvReDX5PliDalY53YhPfvh1s-x4efa1flIaE,2411 +sklearn/linear_model/__pycache__/__init__.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_base.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_bayes.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_coordinate_descent.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_huber.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_least_angle.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_linear_loss.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_logistic.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_omp.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_passive_aggressive.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_perceptron.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_quantile.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_ransac.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_ridge.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_sag.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_stochastic_gradient.cpython-310.pyc,, +sklearn/linear_model/__pycache__/_theil_sen.cpython-310.pyc,, +sklearn/linear_model/_base.py,sha256=HS_9ugy0UJ0RmD-oe6QYfURpycGun9kZNGEoGyCzUTM,28901 +sklearn/linear_model/_bayes.py,sha256=5maNoqyc8gZCDHs7h-PVipebFfQW4_e7lxkW6Ef5r-c,29016 +sklearn/linear_model/_cd_fast.cpython-310-x86_64-linux-gnu.so,sha256=ASiBbaSGJnuF7N7PfZhW1KNJGALoBiYsj66XNHQwPd4,378624 +sklearn/linear_model/_cd_fast.pyx,sha256=ssLdsiFVGST1OFCXtygffSYCs1W-KQXClCY3662q99E,32804 +sklearn/linear_model/_coordinate_descent.py,sha256=3IOmDXrTDKvNPa571XyoYMrgeEbK-Zh_ijCFn-WtGg4,118398 +sklearn/linear_model/_glm/__init__.py,sha256=BmGWcP-GtYkT0WWUcbku9vHCWfCV6V8pniulKsbyrvU,318 +sklearn/linear_model/_glm/__pycache__/__init__.cpython-310.pyc,, +sklearn/linear_model/_glm/__pycache__/_newton_solver.cpython-310.pyc,, +sklearn/linear_model/_glm/__pycache__/glm.cpython-310.pyc,, +sklearn/linear_model/_glm/_newton_solver.py,sha256=ijZCrGUrsNQwgs1cRRRQpWq7oSJ7TUKmBPLXKOz3YsA,24477 +sklearn/linear_model/_glm/glm.py,sha256=6riFlb2eKwjvQUY_o_r4b8a6V5LCHMMqpNIwMAA7y3Y,32206 +sklearn/linear_model/_glm/tests/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/linear_model/_glm/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/linear_model/_glm/tests/__pycache__/test_glm.cpython-310.pyc,, +sklearn/linear_model/_glm/tests/test_glm.py,sha256=OSNL5u1UYyCUnn02iorwIUzkgyAwV55euP3SB67usrw,42223 +sklearn/linear_model/_huber.py,sha256=7Cj6NId-S-iRPZtO6VAiyJDqczWKkAekEo7wqwLgStM,12690 +sklearn/linear_model/_least_angle.py,sha256=4546YB9iYpnryuxiVpb1piQXyTxQmba6Ki7qS90vw-s,82966 +sklearn/linear_model/_linear_loss.py,sha256=qQSX-bxYyAKk8bQ2z33b_pHBMLp1wXVmj5Zz5aCgEzQ,34113 +sklearn/linear_model/_logistic.py,sha256=_TM-DMQ83cxyJVQkVBaRvbwghoWEKRf9b55VC_TTAOU,90722 +sklearn/linear_model/_omp.py,sha256=--eDGLDRu_s8p_veALnGprMyzsIGV3AytfSuGXcfHPQ,38267 +sklearn/linear_model/_passive_aggressive.py,sha256=zF7znXaTn5M5cMRpHr6rNYllZoaD4Ohk6IXOE-skNBE,19264 +sklearn/linear_model/_perceptron.py,sha256=dZkROr_kx5MLVdiP9nTaHiIdQX9_q330-7SXrgV3pjk,7564 +sklearn/linear_model/_quantile.py,sha256=lIfK-QCEa0zNqZKed6ayrfU6QdKC9UKePFZPq4MD5aA,10471 +sklearn/linear_model/_ransac.py,sha256=gJgPVGQFGpREoSQSBAz-f9Ar8j-WgRLQ38bf9HitbkI,25733 +sklearn/linear_model/_ridge.py,sha256=oKjF4mYRwbyRqXroXWrdWbmKL6XJL_fCVGfqiL1uTOM,103515 +sklearn/linear_model/_sag.py,sha256=56X90dePIvQEQG6TDDr6PWMnoL6yYUQ10NQF49JiGhU,12286 +sklearn/linear_model/_sag_fast.cpython-310-x86_64-linux-gnu.so,sha256=p963OXcpCwwNnxcVNmeav3BlFjtxyMKLPFOTHVve_No,164992 +sklearn/linear_model/_sag_fast.pyx.tp,sha256=FFxDn4DS3e8zt5VfK9ZRIDIn0xusZJwbGWsd7QuX5Ks,24277 +sklearn/linear_model/_sgd_fast.cpython-310-x86_64-linux-gnu.so,sha256=ILJnUyc0xgR7946MrvhUFYCJA53kncPd2PrGiZBUV4E,239728 +sklearn/linear_model/_sgd_fast.pyx.tp,sha256=5ewo_7gSyywxQBqtEBL6V_eBmtJORSadRPURK2ZEFB0,20671 +sklearn/linear_model/_stochastic_gradient.py,sha256=3LoT_LCt_2ftIk78NJiw08zeoZFabk81kEQmt1Qd_TY,90146 +sklearn/linear_model/_theil_sen.py,sha256=krSq8Hr9ilc4EHCdiH5fm3pn_N3PgndHu-LCZUHIlCE,16405 +sklearn/linear_model/meson.build,sha256=UTRL_xsxXtXpbw4pokGzMgLjeuEj0HGgzGoQebqUZ3M,929 +sklearn/linear_model/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/linear_model/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_bayes.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_coordinate_descent.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_huber.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_least_angle.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_linear_loss.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_logistic.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_omp.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_passive_aggressive.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_perceptron.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_quantile.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_ransac.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_ridge.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_sag.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_sgd.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_sparse_coordinate_descent.cpython-310.pyc,, +sklearn/linear_model/tests/__pycache__/test_theil_sen.cpython-310.pyc,, +sklearn/linear_model/tests/test_base.py,sha256=XMSu3aWOFjB6o_0jG6plMG6TWjpuag1TOrQZCODz2Vo,27048 +sklearn/linear_model/tests/test_bayes.py,sha256=YrINPjqB0laIJxpcrVPI3uG7qUWJkE-Mntzp9P6Xf0I,11078 +sklearn/linear_model/tests/test_common.py,sha256=fUPlV7x4PGbNE6YloDEo2VsX2r4dPzc_B84w7MwefC8,7303 +sklearn/linear_model/tests/test_coordinate_descent.py,sha256=CmCoLW0BEYWwA7TqE1l3g-1llmKMldM3p_1korNXodc,63282 +sklearn/linear_model/tests/test_huber.py,sha256=au_AulAuqWt1XACGbWr5_1tw12M_g7Vi2U3LxvyflgM,7615 +sklearn/linear_model/tests/test_least_angle.py,sha256=Du1rm-UVjzDTjztRw_S-_OacyOscAg5yqAmSKwsrMbo,29609 +sklearn/linear_model/tests/test_linear_loss.py,sha256=2zMWRVfYQM_sVuvzTOYeG9Kl4N9XhyxSSbACCU_BUx4,17912 +sklearn/linear_model/tests/test_logistic.py,sha256=MEVeBDmj8qdMrHk03PdnD3wa8FPlonMf07sNdWOeHyY,85696 +sklearn/linear_model/tests/test_omp.py,sha256=ZG03dTxyJGmeajIo4fA8SN4Kxwz_rzcOTeEVkS6g3HY,9344 +sklearn/linear_model/tests/test_passive_aggressive.py,sha256=oylJ8F5LNg0br4zX2LXZRU8FMUECnhsaVL0r8mxmd1Q,8994 +sklearn/linear_model/tests/test_perceptron.py,sha256=rsNfXmS37bAZeZ04kRNhc2PXr4WjjTWDaxW_gNmMCkI,2608 +sklearn/linear_model/tests/test_quantile.py,sha256=JiOfB1V2NwuWeK-ed6hKmOpHPHj7CNEHp_ZZuGt4CZk,10689 +sklearn/linear_model/tests/test_ransac.py,sha256=bDDkKflBMv5vTMjAZPfjC0qvlA_VNNDhS9bYC6T3g2M,16790 +sklearn/linear_model/tests/test_ridge.py,sha256=Hpk-qE-_QoDKtaYezNqAhgBUxkQjRn4gAkrQU3fzI8U,81605 +sklearn/linear_model/tests/test_sag.py,sha256=ksURaaSDjzvHB203ZH6bRxd1s9fUkPhcAb62XAXjk7o,25807 +sklearn/linear_model/tests/test_sgd.py,sha256=4Lc9pjw9E9x5BA8KmkmKkqALcZzR2GFl0NCMQWQUZbo,69765 +sklearn/linear_model/tests/test_sparse_coordinate_descent.py,sha256=2_IRPgEBCa6eWM_vtHfVHqX8-LDN7pj027WSpFHjWys,12654 +sklearn/linear_model/tests/test_theil_sen.py,sha256=UIfe_oW99MnoSeNZeqf2nfzuMd2zzDq5a6rjxpHRkl4,10135 +sklearn/manifold/__init__.py,sha256=gl4f7rOHDtrpOYrQb3eQeUrRRT2Y5TZpwrgDfG-d0uE,565 +sklearn/manifold/__pycache__/__init__.cpython-310.pyc,, +sklearn/manifold/__pycache__/_isomap.cpython-310.pyc,, +sklearn/manifold/__pycache__/_locally_linear.cpython-310.pyc,, +sklearn/manifold/__pycache__/_mds.cpython-310.pyc,, +sklearn/manifold/__pycache__/_spectral_embedding.cpython-310.pyc,, +sklearn/manifold/__pycache__/_t_sne.cpython-310.pyc,, +sklearn/manifold/_barnes_hut_tsne.cpython-310-x86_64-linux-gnu.so,sha256=4T9MoxWDRENcQKuxQMdXqkfIEMjNs_Lg1gKiyBex1Xc,134145 +sklearn/manifold/_barnes_hut_tsne.pyx,sha256=W2mN6eXTRn8kuBLdAV5S2LVyPxG6WemhA0z0CuLBuU8,11264 +sklearn/manifold/_isomap.py,sha256=h-X6biNZS5G33-1thUSn-qnDsRxg09kqMu5hD49WhV4,15686 +sklearn/manifold/_locally_linear.py,sha256=otP2a25Y6Tgf95IuhQB-rfJnZUzrMPHCYuOFTshu5XE,30541 +sklearn/manifold/_mds.py,sha256=_UaYQVKWUAFs_NuGS3vYL_58AE7gqFvX0oosb1ejSFc,26025 +sklearn/manifold/_spectral_embedding.py,sha256=5arXNyMZyZhEXQddoJgH6uahUiHxIeuw9-t0HZhekWY,29916 +sklearn/manifold/_t_sne.py,sha256=Vce22nRw2xWDeBiELqjA-vM17plPUqbRFl4kKz2pYEE,44265 +sklearn/manifold/_utils.cpython-310-x86_64-linux-gnu.so,sha256=D9F-Sd1d32rtDMY090dcbbfRsonFOqPnSKaHI8PexQ4,89520 +sklearn/manifold/_utils.pyx,sha256=o8U-cGOuCt2W0uJ6GTvTgALOmtPoUMyM4ZXsg0hmou0,3908 +sklearn/manifold/meson.build,sha256=sCySiLhLC1RumNBhRsAFZFM7GyU88lQxCrBVcZhWgtU,314 +sklearn/manifold/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/manifold/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/manifold/tests/__pycache__/test_isomap.cpython-310.pyc,, +sklearn/manifold/tests/__pycache__/test_locally_linear.cpython-310.pyc,, +sklearn/manifold/tests/__pycache__/test_mds.cpython-310.pyc,, +sklearn/manifold/tests/__pycache__/test_spectral_embedding.cpython-310.pyc,, +sklearn/manifold/tests/__pycache__/test_t_sne.cpython-310.pyc,, +sklearn/manifold/tests/test_isomap.py,sha256=Wl4voE-7ZRpK6YS6JKyEpAhccA0ReBlGyLNU-p4hQWc,12074 +sklearn/manifold/tests/test_locally_linear.py,sha256=yxsUuJ7vzm2VxiLi1fuZjzICS_0mXrwIicJHLC79eDM,5772 +sklearn/manifold/tests/test_mds.py,sha256=Q77vGfH1pB_LAiYYoKSAt7jaBGabAUtHyCwvMHmEz0k,7197 +sklearn/manifold/tests/test_spectral_embedding.py,sha256=YvmsFIvLYGZqn2FSXhSxU80P_vmYv4kVv7sR4jDouIQ,17775 +sklearn/manifold/tests/test_t_sne.py,sha256=xZPO7J--r2m3_bqMP4Odnz3_A16mmxu5A2aX_Bx4in4,39057 +sklearn/meson.build,sha256=ihYL4bRJExpefJ5vOIOJPU2Ufm1-jfhVN5Sht_E9nog,9808 +sklearn/metrics/__init__.py,sha256=mQOOWIFYPSJ60bPWNUH8RxXqL0K71gz9NYwj41YW6YM,4633 +sklearn/metrics/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/__pycache__/_base.cpython-310.pyc,, +sklearn/metrics/__pycache__/_classification.cpython-310.pyc,, +sklearn/metrics/__pycache__/_ranking.cpython-310.pyc,, +sklearn/metrics/__pycache__/_regression.cpython-310.pyc,, +sklearn/metrics/__pycache__/_scorer.cpython-310.pyc,, +sklearn/metrics/__pycache__/pairwise.cpython-310.pyc,, +sklearn/metrics/_base.py,sha256=ppcQ_yli1Z3SgwSvy7boSIDW4el9bmtp0ZxXr3XlQsw,6987 +sklearn/metrics/_classification.py,sha256=b-iLrbaQ3fNw-MVgqShvYFRKxQhiV2sz-kdXarWNnhQ,139502 +sklearn/metrics/_dist_metrics.cpython-310-x86_64-linux-gnu.so,sha256=n3H4fMRtfUiQCcQK9bosXvPj6q0ylI6fwV_UKmydor8,644840 +sklearn/metrics/_dist_metrics.pxd,sha256=U4vH-mgokzVA5-li0CRbFICY4gyJ8gOjtpQs6bQg7G8,7330 +sklearn/metrics/_dist_metrics.pxd.tp,sha256=YI-GhztvViANTOCY4cjexOnxGJNVdVN1tH2l7yyCV00,4378 +sklearn/metrics/_dist_metrics.pyx.tp,sha256=L9frrbHm0t3l6KXz_T_W9aEg7g0wagXDyHzJmzoMqJA,92197 +sklearn/metrics/_pairwise_distances_reduction/__init__.py,sha256=tUkZS268OxDpX4rYbbw8a0zG8W03xtpxo0lqIvIdZmI,5132 +sklearn/metrics/_pairwise_distances_reduction/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/_pairwise_distances_reduction/__pycache__/_dispatcher.cpython-310.pyc,, +sklearn/metrics/_pairwise_distances_reduction/_argkmin.cpython-310-x86_64-linux-gnu.so,sha256=TDBt5qsl7VNZwg10FwHD-lc_tK9B8yOkIITlnj31BFA,269969 +sklearn/metrics/_pairwise_distances_reduction/_argkmin.pxd.tp,sha256=eLGvaqpxdaoT1CgTTtzn9_PlCJ7fLMmZ_vqcDsTeBI0,979 +sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp,sha256=2qtw0fq-UuAkxkz1nKLUOE-wSXosKNHrK8biI6ICxQs,19783 +sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.cpython-310-x86_64-linux-gnu.so,sha256=rzVc2X64m3hKTmZu5StJ1bzr3xjKp1_3cOX89ZxS0Ns,171217 +sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp,sha256=KHXJwDRXq58rYQYnjlgE8k8NUXl0Qz9vrpwKTee8z_M,6432 +sklearn/metrics/_pairwise_distances_reduction/_base.cpython-310-x86_64-linux-gnu.so,sha256=_liZ-V1SjJ0zdsv2-l-VfGterV65qA_6TxbHhhN_3Ck,237345 +sklearn/metrics/_pairwise_distances_reduction/_base.pxd.tp,sha256=vIOGH_zE7b8JUZ3DOC0ieX18ea7clFZzd1B2AnrYeek,3563 +sklearn/metrics/_pairwise_distances_reduction/_base.pyx.tp,sha256=h4sPRzksjOO35w6ByoulBx8wtb3zV44flEWYXXyaEAY,18353 +sklearn/metrics/_pairwise_distances_reduction/_classmode.pxd,sha256=DndeCKL21LyIGbp42nlWI9CKoyErDByZyQawUagL1XE,151 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.cpython-310-x86_64-linux-gnu.so,sha256=omf1lvpmHzl3gg1_wHPwe67GJhuwToK7crAzIA2Tj2s,386600 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.pxd.tp,sha256=7BR2LUjE2MELP3fV9OZH9tXakpsw8QQumBFi_CjMU0U,1948 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.pyx.tp,sha256=ipbswS5TSNvw9lO_6tN-7E8ruDS5HbMDumfoxr5h0H0,15087 +sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py,sha256=UsK6BZyrKlmIMxJy82Y65HfbEuegPLcODsZ8J5io3eo,29806 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.cpython-310-x86_64-linux-gnu.so,sha256=LYFwMV_BVYYtzzoLYG4ui1DWdOxAwth0qq9Q2u5HR6k,385752 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.pxd.tp,sha256=bsr7Pmqj-09ciVAh5bMfyc6A8KgcQ_3WlPC0dBoWwfI,5925 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.pyx.tp,sha256=2RUfNdjCB6aJU4b42nNYZb2ILAYY74A9SGfu3zzn8Gc,20344 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.cpython-310-x86_64-linux-gnu.so,sha256=xqGlfyydwpse40znpGu5kwnBYd2MReLZXOw88qCzkdM,296385 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pxd.tp,sha256=gaUTpGpL4dPmjcwjnIrjlOs7RX4pUe9-T-6QDspl5No,3254 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp,sha256=105e6MGHtvVGqQs3JkpD7BnYFcn8G1TPeQ4VIPGiF_4,19423 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.cpython-310-x86_64-linux-gnu.so,sha256=xjhU9vo3xPAL64-qc2VLdiHaDwVPTR9weB2G1QdOi0c,204113 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp,sha256=J-hJcHrpN0bsPMGIMfFu_RYDVFaayvl4M8GMteOHRzA,7353 +sklearn/metrics/_pairwise_distances_reduction/meson.build,sha256=tlbyYpIVeIOkS1_9LoBk5br8jscbwBss7cFIO86uMyw,7540 +sklearn/metrics/_pairwise_fast.cpython-310-x86_64-linux-gnu.so,sha256=KAEOs4Fgzu_w6UjAZqVAYJWmcycbWQP444pNmIyFJpc,179401 +sklearn/metrics/_pairwise_fast.pyx,sha256=LmzoEGFiL-shm5pOwGHBR8Pue8Qz_cY0rNStYVSWxVQ,3460 +sklearn/metrics/_plot/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/metrics/_plot/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/_plot/__pycache__/confusion_matrix.cpython-310.pyc,, +sklearn/metrics/_plot/__pycache__/det_curve.cpython-310.pyc,, +sklearn/metrics/_plot/__pycache__/precision_recall_curve.cpython-310.pyc,, +sklearn/metrics/_plot/__pycache__/regression.cpython-310.pyc,, +sklearn/metrics/_plot/__pycache__/roc_curve.cpython-310.pyc,, +sklearn/metrics/_plot/confusion_matrix.py,sha256=YfXTo3iYAY0fokkz77iUbRgp75CfrbO8cWU9og6z4Ws,17330 +sklearn/metrics/_plot/det_curve.py,sha256=s7-5iGGsKSUlGxiGwK2UNKJ9C1L9MESU_XKNHHHaahE,12595 +sklearn/metrics/_plot/precision_recall_curve.py,sha256=xnFVrHXNK46OFsIqOMJBmHC4BDd2YRMLSNGk-uxjMQ8,19414 +sklearn/metrics/_plot/regression.py,sha256=_6smop2JeU3aS1LbKCZCNbjIVpQbFF4WR8mlyFnVOLw,14691 +sklearn/metrics/_plot/roc_curve.py,sha256=b3_-RNwVCVbXG7MxRf_8piAQrXwesJvRLl3IejDoR4A,28572 +sklearn/metrics/_plot/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/_plot/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_common_curve_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_confusion_matrix_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_det_curve_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_precision_recall_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_predict_error_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/__pycache__/test_roc_curve_display.cpython-310.pyc,, +sklearn/metrics/_plot/tests/test_common_curve_display.py,sha256=oq5eSPrDGbksf-x7TeNfXtvqZPJQwWG_nY6ixkumx2A,9825 +sklearn/metrics/_plot/tests/test_confusion_matrix_display.py,sha256=E1xT26QRcOHH8VvuLShq8ev5YQ_8bLsJmfkHW7dTiiE,13487 +sklearn/metrics/_plot/tests/test_det_curve_display.py,sha256=j8LJA2Ms-IoZjip1zvymtwORF6uTe4MBx6ldGAaqj_U,3633 +sklearn/metrics/_plot/tests/test_precision_recall_display.py,sha256=d5-4E3HOQXvMshtrKOUt7NIwwqe7-eknw8wouX_Y6TE,13884 +sklearn/metrics/_plot/tests/test_predict_error_display.py,sha256=3PnOYrgBf7bnw1zHCPWm28tVCeuZlR4hIQD2fR-9RfM,6007 +sklearn/metrics/_plot/tests/test_roc_curve_display.py,sha256=YiS8sF4nL9cZZE5iFxBimqO_prRgOgg__WgAN-bVAug,34828 +sklearn/metrics/_ranking.py,sha256=ofwC8LBCo5RcN2ksk3enRoAN0qkM3CJ99SaPIYvqP3Q,78995 +sklearn/metrics/_regression.py,sha256=c5RpBgYy5F6Ck-RdsRxCYIinZpZDKjJyAdJ9RsqeO8s,65002 +sklearn/metrics/_scorer.py,sha256=tvOiC__wKam3Th2I0E0NTtEUcl4Sq04l3QtWim4kEpI,41073 +sklearn/metrics/cluster/__init__.py,sha256=xQxpw9CyuWGDuqYT0Mrl82HTdseiQCGJD8wpEf_e1wQ,1415 +sklearn/metrics/cluster/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/cluster/__pycache__/_bicluster.cpython-310.pyc,, +sklearn/metrics/cluster/__pycache__/_supervised.cpython-310.pyc,, +sklearn/metrics/cluster/__pycache__/_unsupervised.cpython-310.pyc,, +sklearn/metrics/cluster/_bicluster.py,sha256=gb0X2lNFVTCqmdi1YutW3c_W4ZqjiBKmgYWs57lxEUc,3637 +sklearn/metrics/cluster/_expected_mutual_info_fast.cpython-310-x86_64-linux-gnu.so,sha256=0tCuoh8vTr4-EtbKOovSsVh6qTQs_EnJDH3U9qYLo5s,107888 +sklearn/metrics/cluster/_expected_mutual_info_fast.pyx,sha256=UWIcBVPgxQ6dD99fmNtP_QdmK4jd-im2zIB4R0gqPMc,2687 +sklearn/metrics/cluster/_supervised.py,sha256=6sdhfvWBGmRz6KINd5Dnr3xK2QDPRDmNcc2PMLCmo34,45333 +sklearn/metrics/cluster/_unsupervised.py,sha256=JT-IKuqoswGQ3_w16yLb0qm773K_WQUDjkl4m8c35FE,17019 +sklearn/metrics/cluster/meson.build,sha256=j4LJu4kH3dRH5UZN-2B62MM4m3mXwq44hEIeBRLgd2w,164 +sklearn/metrics/cluster/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/cluster/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/cluster/tests/__pycache__/test_bicluster.cpython-310.pyc,, +sklearn/metrics/cluster/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/metrics/cluster/tests/__pycache__/test_supervised.cpython-310.pyc,, +sklearn/metrics/cluster/tests/__pycache__/test_unsupervised.cpython-310.pyc,, +sklearn/metrics/cluster/tests/test_bicluster.py,sha256=KecSxviHfRfUMNVZ0g77Ykx96QAuKoax0YUY8paQjFg,1719 +sklearn/metrics/cluster/tests/test_common.py,sha256=sUBhJJbGfo2zPA-JJ73xXDXc7c_VI1870kdBxwkIoEk,8201 +sklearn/metrics/cluster/tests/test_supervised.py,sha256=GWrG7Suowna_Emut6bsGzf3z3ie5SDvh7Szs2OkrXJs,19370 +sklearn/metrics/cluster/tests/test_unsupervised.py,sha256=eAic9M_89S8Xbk1hEX0xyIeBW2GrAwPOTpNuNob3TaU,12269 +sklearn/metrics/meson.build,sha256=OH0IO4OSp5gSFTXVRZkj8zvyidbgtaoLJ2kZmON2PxE,1510 +sklearn/metrics/pairwise.py,sha256=zZsB-LFPhVvNWCG2w7w00Mnmv1Vgx7Z9-oT3hhKAbnI,91691 +sklearn/metrics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_classification.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_dist_metrics.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_pairwise.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_pairwise_distances_reduction.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_ranking.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_regression.cpython-310.pyc,, +sklearn/metrics/tests/__pycache__/test_score_objects.cpython-310.pyc,, +sklearn/metrics/tests/test_classification.py,sha256=R4nU1hSRhS82WIhhEjVkJyZd5zuVEWRHOxMZxB5WH5E,120654 +sklearn/metrics/tests/test_common.py,sha256=2Coo4Hhq4TF3Oy4CFg_npBsubJDdFDo82x_ptuWFcTg,77273 +sklearn/metrics/tests/test_dist_metrics.py,sha256=vMpc3Q1sgD6nuWNXkKVQD46eGRkzn1S_w1w72ACLP2I,14995 +sklearn/metrics/tests/test_pairwise.py,sha256=f5VoV6kv-F-pEzPpmSdaM_jfuSts4X4RZDLQFrgPoME,58631 +sklearn/metrics/tests/test_pairwise_distances_reduction.py,sha256=t-ZNZ7RyXOkybyFvCxm7VU2UWgOjY2_roOE8UO89aig,53061 +sklearn/metrics/tests/test_ranking.py,sha256=PJH9La5JdFhTQJCTrgE7YrV_rkEv5zY9dUkZj30VnA0,83993 +sklearn/metrics/tests/test_regression.py,sha256=W8spjLxk7WZO42-gVGwnkapTCbvPVJoHQ1HFzOrIJgA,25924 +sklearn/metrics/tests/test_score_objects.py,sha256=JhAQazrHgDR2hx1mFUO6G2XR_A-tyAmkCktKduuPHY4,59004 +sklearn/mixture/__init__.py,sha256=o0w1PyZ4gXtcQcpvrGrYEn226ugS_Ne3kQ3ZJPGt6-8,276 +sklearn/mixture/__pycache__/__init__.cpython-310.pyc,, +sklearn/mixture/__pycache__/_base.cpython-310.pyc,, +sklearn/mixture/__pycache__/_bayesian_mixture.cpython-310.pyc,, +sklearn/mixture/__pycache__/_gaussian_mixture.cpython-310.pyc,, +sklearn/mixture/_base.py,sha256=JloA758DB-woL_K_MymnLwN7wN6GS7gap5YFN8olobY,19241 +sklearn/mixture/_bayesian_mixture.py,sha256=PmRf4Dyqb0k995eZMi57_cvaIl_mqZ6Cr4zVX4oziCY,33573 +sklearn/mixture/_gaussian_mixture.py,sha256=xJ5DG7DKrD_cx61FepCZaJesbWuumhAXAMjfBrTFxq0,32736 +sklearn/mixture/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/mixture/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/mixture/tests/__pycache__/test_bayesian_mixture.cpython-310.pyc,, +sklearn/mixture/tests/__pycache__/test_gaussian_mixture.cpython-310.pyc,, +sklearn/mixture/tests/__pycache__/test_mixture.cpython-310.pyc,, +sklearn/mixture/tests/test_bayesian_mixture.py,sha256=23_HO3xRwo4hMpF60yjCVixeCsx4T4bZcd_OebiqmPA,17040 +sklearn/mixture/tests/test_gaussian_mixture.py,sha256=TJPWI0kChKEMvdF8RYDAa14Co_VhI_wAIIm0x4DJflY,50021 +sklearn/mixture/tests/test_mixture.py,sha256=ar7zjdUa-fsJQlroNmS8-Mj0brARolFyLEZrLdOIrWM,993 +sklearn/model_selection/__init__.py,sha256=EyWSMWF2i6eVm4VtUqG3e9xtniasEDVLt8Am-wUs4Io,2660 +sklearn/model_selection/__pycache__/__init__.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_classification_threshold.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_plot.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_search.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_search_successive_halving.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_split.cpython-310.pyc,, +sklearn/model_selection/__pycache__/_validation.cpython-310.pyc,, +sklearn/model_selection/_classification_threshold.py,sha256=NaKa_yc3fo3rn49nxSmTm0_ZYGzn0XK9AjImz78P2ws,32637 +sklearn/model_selection/_plot.py,sha256=J2LntPSgwlkDTYDkkh1Wn__ZZavYUup-yfqkg7b_MIw,34579 +sklearn/model_selection/_search.py,sha256=4bkqRGyiGWHShE9Y7outfmvbxzLtsmcDm2Yxk_If3TE,79924 +sklearn/model_selection/_search_successive_halving.py,sha256=AuDSR5cEbOvr1rAMOST1f1DvPNibA-PzZyCDYd0XwR4,45154 +sklearn/model_selection/_split.py,sha256=zrfGqOcIwVv6sCFiGVTy0YjCmCa0VWuMtM-P7tkPTcU,109612 +sklearn/model_selection/_validation.py,sha256=gFPz1-o_mkOmFStvMRBjnr42EjATs2XqBiLMgSGyvN4,95908 +sklearn/model_selection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/model_selection/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/common.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_classification_threshold.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_plot.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_search.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_split.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_successive_halving.cpython-310.pyc,, +sklearn/model_selection/tests/__pycache__/test_validation.cpython-310.pyc,, +sklearn/model_selection/tests/common.py,sha256=PrR7WoVcn4MdG4DPrOvuZ1jrOIZPFPok20zannr4dwI,641 +sklearn/model_selection/tests/test_classification_threshold.py,sha256=dZ_fWiDJdiz9DFa3ZGBH1u5-cljeXrOY5DYixxw5rP4,23299 +sklearn/model_selection/tests/test_plot.py,sha256=goA_s29K0admCpVCSnWisPzLVf5-XvbTfwxev-DcDZ8,18456 +sklearn/model_selection/tests/test_search.py,sha256=I9KuCvt7N30CMiEdoue3BvMixiQ6JtPSQkJ7X_Yp-TQ,99221 +sklearn/model_selection/tests/test_split.py,sha256=m_eeANTxv3ZqIjLBy8L5ibfzgUMDNtEbsqdeJ8zlTN0,74292 +sklearn/model_selection/tests/test_successive_halving.py,sha256=liuAL9oXGDJM8a63Jpqjp_UmMTehJNnFlHZvjK08gRI,29010 +sklearn/model_selection/tests/test_validation.py,sha256=zY5nRfnG-Nl4Ljhbev2nPaaMNDLDXjwgFnjLtKrRLxk,92511 +sklearn/multiclass.py,sha256=X_8kN37ayDtZOe9k1AeAhz8Ttfvc9YBbr2isVTOabPk,44339 +sklearn/multioutput.py,sha256=cGafTqvPvgTLrZoDXWltAfJ8p_eTvA1xKgPsPJx2Zxs,45541 +sklearn/naive_bayes.py,sha256=7iZcCCF7K0oFQ72YTLEalitNL0yagVAzLoTxNktdN-w,55949 +sklearn/neighbors/__init__.py,sha256=AGlC69XvpiUJ2KeIs96WoogAF5GdQFv48c1BUnOR4tk,1251 +sklearn/neighbors/__pycache__/__init__.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_base.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_classification.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_graph.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_kde.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_lof.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_nca.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_nearest_centroid.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_regression.cpython-310.pyc,, +sklearn/neighbors/__pycache__/_unsupervised.cpython-310.pyc,, +sklearn/neighbors/_ball_tree.cpython-310-x86_64-linux-gnu.so,sha256=uEYZsfmFJIVyqpiMSwDKo3v1NtAKT-xweWTkJgtU4dw,664400 +sklearn/neighbors/_ball_tree.pyx.tp,sha256=xqeo6v1L72VcadqIVGX7db8fNX9wI0X5tP3u3uz30UQ,9321 +sklearn/neighbors/_base.py,sha256=Zobunz82rnq5tXKpCTHmiTRejVq2sYeTYfsuoVmQ2eU,52312 +sklearn/neighbors/_binary_tree.pxi.tp,sha256=-TY-H2YOsJbKLfI4Xg9G6yc8tUujBszvEPeKs8UtEt4,100641 +sklearn/neighbors/_classification.py,sha256=TUGUxAjnEvwWrFvg2bqC9Wj8fWbQkvInIz9waKaDIew,35044 +sklearn/neighbors/_graph.py,sha256=SvWpzfkH-5xcG8TjkVT0cVNYbQf_kDtBPB1GC-iNfDw,24611 +sklearn/neighbors/_kd_tree.cpython-310-x86_64-linux-gnu.so,sha256=fBR5X_FTeUu-9WNv_kKl1VSoUZLzX88hx9RCyeU1eIY,674424 +sklearn/neighbors/_kd_tree.pyx.tp,sha256=ZM4_DcS7eUkXbpxBJ4OLXR6exLbkGUB7Kmd_Ou2H4N0,11117 +sklearn/neighbors/_kde.py,sha256=g3Tsl0vWeKT_rE8UYQI8mc8chg2KzJtRQtr0kirgHW4,12272 +sklearn/neighbors/_lof.py,sha256=oFd01Nt9be1BN09osbZ4xfZy0ehFTN3qnr54QapLTmM,19957 +sklearn/neighbors/_nca.py,sha256=rk8JChFYShvGqSwpLVjl9MaXZI_zc6BkIzCmk95wdb4,19864 +sklearn/neighbors/_nearest_centroid.py,sha256=a85jX0t6z64tI6afHzSqjW0rB0uwhABQV9DU10O0LIQ,13048 +sklearn/neighbors/_partition_nodes.cpython-310-x86_64-linux-gnu.so,sha256=-u91LgE-xVvocjeCjLmpwYSkifIOjhtstP5EROWD62w,28920 +sklearn/neighbors/_partition_nodes.pxd,sha256=rngZZqkJWPnBW8BRvk0FgM817-lcHgCoBWEd91X0Dbc,288 +sklearn/neighbors/_partition_nodes.pyx,sha256=iJw0PB95n4VgXORPMjDzLr0DJKgdfzoz_PUKyi0MelY,4120 +sklearn/neighbors/_quad_tree.cpython-310-x86_64-linux-gnu.so,sha256=LFyV0U6C8QuTi4TlNcGWA4nvUnuL-BfOWUU-N5A2AZk,191440 +sklearn/neighbors/_quad_tree.pxd,sha256=olKQpppK6rZ_HKcXS3swAb7dq_aTyOlcilY8bg_d1mw,4232 +sklearn/neighbors/_quad_tree.pyx,sha256=pztvIhqbZHC6iGre_wMDKY7o3qSZzu-bZDSzgB7ggCc,23664 +sklearn/neighbors/_regression.py,sha256=r5dKgxNNjwjdfuDzsOOeXsQ7S7tv3viPWjEReBjrEqg,18313 +sklearn/neighbors/_unsupervised.py,sha256=OXFrGjh8anfiEEflMk6fmZj0sZ6Ie4J-zfArTEhVQvM,6260 +sklearn/neighbors/meson.build,sha256=hZzIPGmdgKDqcNMm_WajOYj9-2gwXFpTxpAAdNWCzKM,1634 +sklearn/neighbors/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/neighbors/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_ball_tree.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_graph.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_kd_tree.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_kde.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_lof.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_nca.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_nearest_centroid.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_neighbors.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_neighbors_pipeline.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_neighbors_tree.cpython-310.pyc,, +sklearn/neighbors/tests/__pycache__/test_quad_tree.cpython-310.pyc,, +sklearn/neighbors/tests/test_ball_tree.py,sha256=hpoJiFpMrGxw0FEhd-KghO8zWtonaqSr1JgbKB7sdN0,7097 +sklearn/neighbors/tests/test_graph.py,sha256=QdJvyK2N138biDPhixx_Z9xbJ7R-aSxz5mhSSvh-HRg,3547 +sklearn/neighbors/tests/test_kd_tree.py,sha256=4cE2XJO0umuWnWPQluOMR9jfeJKDXmFETowsLElwKCI,3898 +sklearn/neighbors/tests/test_kde.py,sha256=kEZsv-8U0oWrkAVuzRidsqL5w1jQZ2b7tK9pFZYnm44,9745 +sklearn/neighbors/tests/test_lof.py,sha256=x0h5dzFQpRqSG91CviJa_cytpemWv2HPSe45leX-p60,13746 +sklearn/neighbors/tests/test_nca.py,sha256=CAT5f0TpDPc8hvpPHobj2y-41xuQDjk3d1dNmdiTeCg,19506 +sklearn/neighbors/tests/test_nearest_centroid.py,sha256=Uo0oebJiyjCnrkSx9mDN89EPbFjhmhV2c8boUEIXRyQ,7572 +sklearn/neighbors/tests/test_neighbors.py,sha256=Yi42k3z52_Qp96nIUnuAHPAbN4hd0yHlHrZM1zqaXH0,86776 +sklearn/neighbors/tests/test_neighbors_pipeline.py,sha256=CwllxS4T9cP2utY-xuui3GhgtjRBkA7759byS4LdQ3U,8147 +sklearn/neighbors/tests/test_neighbors_tree.py,sha256=8OagtxQTE0jNy7-rTbl4L9lEbCgarf6n_jkx1woYlOs,9297 +sklearn/neighbors/tests/test_quad_tree.py,sha256=y_WE4jNxliYos_SiICl_miGIya2IJlu71rXzwvQw2qk,4856 +sklearn/neural_network/__init__.py,sha256=p9-lqKAT-q-6wCIj0R97J1cflbINXL4-0X60SF3hhmY,276 +sklearn/neural_network/__pycache__/__init__.cpython-310.pyc,, +sklearn/neural_network/__pycache__/_base.cpython-310.pyc,, +sklearn/neural_network/__pycache__/_multilayer_perceptron.cpython-310.pyc,, +sklearn/neural_network/__pycache__/_rbm.cpython-310.pyc,, +sklearn/neural_network/__pycache__/_stochastic_optimizers.cpython-310.pyc,, +sklearn/neural_network/_base.py,sha256=bp4Z3TxnFtzH7VinDh9GAuywChqyz3NohImLLymG9jg,7983 +sklearn/neural_network/_multilayer_perceptron.py,sha256=q1Kcv3H4gaQR6so_P77eopnoWGm269xxuCM_LPLCIi0,65995 +sklearn/neural_network/_rbm.py,sha256=Bi37Of-5A-gfCoEBo62QbANnPD9WBdW_MVcYfYsey4s,14968 +sklearn/neural_network/_stochastic_optimizers.py,sha256=ldZWuIL10VpHq4tZ2PzJrTSWzAQjdpbzx7iJEbbFyMw,8838 +sklearn/neural_network/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/neural_network/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/neural_network/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/neural_network/tests/__pycache__/test_mlp.cpython-310.pyc,, +sklearn/neural_network/tests/__pycache__/test_rbm.cpython-310.pyc,, +sklearn/neural_network/tests/__pycache__/test_stochastic_optimizers.cpython-310.pyc,, +sklearn/neural_network/tests/test_base.py,sha256=jSriY_p7h95ngec0Iggh2IwDpeR67RirQ5-q2RrP9Zc,1566 +sklearn/neural_network/tests/test_mlp.py,sha256=ICYTyg2Bf15s4nPhxBEjq5rUj38d8Dgr5dA2_PARTAM,36232 +sklearn/neural_network/tests/test_rbm.py,sha256=Ucezw6y1X0HU9PEC9lniKrqXplVXjfX5yjWueHIPPkg,8048 +sklearn/neural_network/tests/test_stochastic_optimizers.py,sha256=9JhAPo1Qc0sA735qPORoKtS04bCTts9lQ65P9Qlhtyo,4137 +sklearn/pipeline.py,sha256=CyAjrCn3Pr_-DUX-LSbKaqZO1edSCSeKxHSS4oCWJlk,84473 +sklearn/preprocessing/__init__.py,sha256=IW0_AGxFmhh1JCuwnVwBPv43_M_zkvoXO5aorgz82iQ,1503 +sklearn/preprocessing/__pycache__/__init__.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_data.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_discretization.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_encoders.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_function_transformer.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_label.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_polynomial.cpython-310.pyc,, +sklearn/preprocessing/__pycache__/_target_encoder.cpython-310.pyc,, +sklearn/preprocessing/_csr_polynomial_expansion.cpython-310-x86_64-linux-gnu.so,sha256=nTaj2rnmzZmJpFUGE4DkCigI12bHDH3kdQEqqy4vN8E,359576 +sklearn/preprocessing/_csr_polynomial_expansion.pyx,sha256=vbTDWGOdzC6xb_AxTj-WeYWghXDO0RYqlxKx7V-N3uw,9154 +sklearn/preprocessing/_data.py,sha256=ElUECg65yZ9Yd11VzzT2dc9hU598Cg3Q1qNssfDaHhU,127903 +sklearn/preprocessing/_discretization.py,sha256=wN5OGmOv7ZMIVa_UtqGIbfe7mye3cz100R36IpVTQzk,20951 +sklearn/preprocessing/_encoders.py,sha256=GCrYWMY88owm1rMyJQAGwFKlAuSDdAtbI2o8jnST25c,68416 +sklearn/preprocessing/_function_transformer.py,sha256=CPbugGU8C9ee1DkB6s5PaEtqFUMb5skgMxOnFmhDrpk,16990 +sklearn/preprocessing/_label.py,sha256=90zXmJLk9cFQSka5K9-QZ6WM4iA2_dxtctBkXML1gD8,31271 +sklearn/preprocessing/_polynomial.py,sha256=JysoQGJRO-QPba9ahIUMvNPotzBGCsnI7iU11bxR8Ys,46303 +sklearn/preprocessing/_target_encoder.py,sha256=bmH3lffWPuc1JhurIGfWC4Y1pG4orwX5HF1Jjdh2yro,20612 +sklearn/preprocessing/_target_encoder_fast.cpython-310-x86_64-linux-gnu.so,sha256=5Ee2kcX8ptIAFA75FM75ltPeCJ2cemLCRa25IYuNxwI,456536 +sklearn/preprocessing/_target_encoder_fast.pyx,sha256=svYh2Yd1T1ursqdyVJmR8CUIKIbVV-AyIFHw9AAHJ4g,5941 +sklearn/preprocessing/meson.build,sha256=D5DfSN_SRseZ2iljBqU9IKnue8HeH-27TnVDTQY1uhU,357 +sklearn/preprocessing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/preprocessing/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_data.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_discretization.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_encoders.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_function_transformer.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_label.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_polynomial.cpython-310.pyc,, +sklearn/preprocessing/tests/__pycache__/test_target_encoder.cpython-310.pyc,, +sklearn/preprocessing/tests/test_common.py,sha256=1gLqwBEMTpCJOMsftAwACox0d8wbqfB9z-JtRLlx9NM,6793 +sklearn/preprocessing/tests/test_data.py,sha256=vh5nbrVqCjfgJQNnaUzUanqhzeAZCZadqqQzznqdXhc,98518 +sklearn/preprocessing/tests/test_discretization.py,sha256=B6drMSsu7tEiQ6wwdZ0QxULFvPwu_tQAmVsEu-o_Vx8,21803 +sklearn/preprocessing/tests/test_encoders.py,sha256=U5K77uivcfbz8er4roPXvIkcaUumoUAaBgld0SpXrJc,79539 +sklearn/preprocessing/tests/test_function_transformer.py,sha256=xPDnZiJwQx7WNDTPsaw1p1zXijcGdIvIYUDYY4tCX-I,19272 +sklearn/preprocessing/tests/test_label.py,sha256=7aCITA2EprYNs0yC0RzXfcFlu7wvk3VEZZ6MIqvpyXU,25641 +sklearn/preprocessing/tests/test_polynomial.py,sha256=VwYFsVxt0tzhm-ptXdLAzIh10QNG40R1h3oV1E5BUAw,41236 +sklearn/preprocessing/tests/test_target_encoder.py,sha256=WADxAKbtWNAIIoP30C_uEC-kfTnYR2Hf1hL6qV1YlbE,27802 +sklearn/random_projection.py,sha256=-aCd6ZoTf6iRFkZhws-IEmt_P7OctlVn9gQQyTo6Sfk,28351 +sklearn/semi_supervised/__init__.py,sha256=Qkdt7JhsauqNEyletlbs7aU9RPxnpghyTw_WionXWC4,435 +sklearn/semi_supervised/__pycache__/__init__.cpython-310.pyc,, +sklearn/semi_supervised/__pycache__/_label_propagation.cpython-310.pyc,, +sklearn/semi_supervised/__pycache__/_self_training.cpython-310.pyc,, +sklearn/semi_supervised/_label_propagation.py,sha256=NBZSDxTeFPwGbt_j8rCLggiVU35pDZpgZs4kxLdUSpM,21448 +sklearn/semi_supervised/_self_training.py,sha256=wUXp_i3rvYLw1bMSa6M-1WjtvW9uImMn8sM9u1bPjJo,22014 +sklearn/semi_supervised/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/semi_supervised/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/semi_supervised/tests/__pycache__/test_label_propagation.cpython-310.pyc,, +sklearn/semi_supervised/tests/__pycache__/test_self_training.cpython-310.pyc,, +sklearn/semi_supervised/tests/test_label_propagation.py,sha256=1dxD9IP2hGuUybuPkdMuJRFnoC1Dlz_Fti4_EJRpbxE,8801 +sklearn/semi_supervised/tests/test_self_training.py,sha256=9NxdemICSM3BO9Jw77PzhuY4iQXmv4Jzoa9_9YfEyMo,14428 +sklearn/svm/__init__.py,sha256=UKj8B0uoG71h0SR5mcFPTxPp84peaKEjf1QWQaUwwSA,454 +sklearn/svm/__pycache__/__init__.cpython-310.pyc,, +sklearn/svm/__pycache__/_base.cpython-310.pyc,, +sklearn/svm/__pycache__/_bounds.cpython-310.pyc,, +sklearn/svm/__pycache__/_classes.cpython-310.pyc,, +sklearn/svm/_base.py,sha256=LUZHaaTiHTAQ_3TLO0KQVDpj2j4yyCk4skCQ0-qsWmk,42956 +sklearn/svm/_bounds.py,sha256=_BV2163ys85SYXAFw4SoF80rkCpyERqd0d3L8jI4aW4,3459 +sklearn/svm/_classes.py,sha256=7dpD6qGKXhFiTIw1JvA7m_9ObxbQZUvyroLfy5uh36Y,66217 +sklearn/svm/_liblinear.cpython-310-x86_64-linux-gnu.so,sha256=V8hFxuxeU14npjjDZeGcqI-YFtgzSdtsciN1_BPEZmA,186840 +sklearn/svm/_liblinear.pxi,sha256=H5Li48ad7cS3z_jZu1lAJDByXVT9kA78pEVQ-AJCerI,1719 +sklearn/svm/_liblinear.pyx,sha256=_I3KvUevamU1X-7Ev21XNcdlfu8z1Jbd3IOEXcjUOwE,4101 +sklearn/svm/_libsvm.cpython-310-x86_64-linux-gnu.so,sha256=3gS53v5mbv2SX8wxdKHutfIMSgJG2k8u8mIuH8A4Vas,449200 +sklearn/svm/_libsvm.pxi,sha256=cV0nEGKq3yrtKsNxHpioX0MOmwO_4dURv9gR7Ci8TKM,3186 +sklearn/svm/_libsvm.pyx,sha256=xG6wFD9ciyARvXbOliyAm2KJK7WR4dskyq5DwbTMRhg,26669 +sklearn/svm/_libsvm_sparse.cpython-310-x86_64-linux-gnu.so,sha256=p6pN_i8KLB6O-Eg-Rm-fnFrK8DUxsF0sOimjRnkGYlw,380448 +sklearn/svm/_libsvm_sparse.pyx,sha256=tDSRkgykLtwTg5rZGGMezynJCeJeln950PL-D1zZ4kY,18886 +sklearn/svm/_newrand.cpython-310-x86_64-linux-gnu.so,sha256=1xyZRqmqk040vkWHxqPaCk12uSTuzOKrqX7vhuZ7b_s,56616 +sklearn/svm/_newrand.pyx,sha256=9Wgz24TrfT03OhvSrJ50LOq-6dznY73cXToi_seg0hg,298 +sklearn/svm/meson.build,sha256=yS7MPRrPJ5pxtVemPkGo7wNzNVfIVzivnmP_elfNd3M,1218 +sklearn/svm/src/liblinear/COPYRIGHT,sha256=NvBI21ZR3UUPA-UTAWt3A2zJmkSmay_c7PT2QYZX4OE,1486 +sklearn/svm/src/liblinear/_cython_blas_helpers.h,sha256=x7EL4uLM9u9v0iJmEaQDFJgXEhxM-3lWQ1ax-78gtlE,458 +sklearn/svm/src/liblinear/liblinear_helper.c,sha256=9rtFOnID6rSuKKkkj1kGLhPAqbA01-pYIB_14JtlREw,6380 +sklearn/svm/src/liblinear/linear.cpp,sha256=-eupquURUIdGa-8VKFJpvXNP2Fl-DpC8fhZLOI8t9IM,62634 +sklearn/svm/src/liblinear/linear.h,sha256=w70N_Hu8NaTsmxYTffXfOTgpbK1nbcpzVAiT1OOsiNs,2458 +sklearn/svm/src/liblinear/tron.cpp,sha256=meJe2MJ4b5dOutshAAxU1i9EKZ1lXYp4dXbiL_zgyP4,4940 +sklearn/svm/src/liblinear/tron.h,sha256=rX95I3vubCVFvoPaI8vE6jqdsWTOvq5GHx8FUcOiRFE,768 +sklearn/svm/src/libsvm/LIBSVM_CHANGES,sha256=n5OrHZ65A9CqDFxpGfph5_tWGAuiRhdBI0xAGWoYx9I,769 +sklearn/svm/src/libsvm/_svm_cython_blas_helpers.h,sha256=H25CeF4GM3FQq0B6u3cQp1FZGAiGlbOOhgFqn4RIAFk,217 +sklearn/svm/src/libsvm/libsvm_helper.c,sha256=fVUEDyWrrX65g3pstPpnxWdvWZlIsB4BoD4XCQ5gy-c,11718 +sklearn/svm/src/libsvm/libsvm_sparse_helper.c,sha256=fWKVM9H_TNNUcVhymn678X2PYCM4S1KrD6ArcRbdW1I,13247 +sklearn/svm/src/libsvm/libsvm_template.cpp,sha256=de-H2Nxv6VI2P8KXyAirKS8IAdtJYKfqPoDn3mMaIyM,173 +sklearn/svm/src/libsvm/svm.cpp,sha256=kOPTJGIi9eDqTR9xRZ_lu0KxL9fDW799-6inxngLu88,69105 +sklearn/svm/src/libsvm/svm.h,sha256=Vhf4LRfqLp7dE8swI2LmAKF3lf6ZOjC6L10k1IXJ96I,6262 +sklearn/svm/src/newrand/newrand.h,sha256=VGF__VxEdrYCRWeldvGF2AQfmb6DTH2bwR3QnsAmhQg,1840 +sklearn/svm/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/svm/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/svm/tests/__pycache__/test_bounds.cpython-310.pyc,, +sklearn/svm/tests/__pycache__/test_sparse.cpython-310.pyc,, +sklearn/svm/tests/__pycache__/test_svm.cpython-310.pyc,, +sklearn/svm/tests/test_bounds.py,sha256=17Uej-UAlNFcsiVQRJVg7UQQY1mwXTNAS-pqIH2Sh5g,5488 +sklearn/svm/tests/test_sparse.py,sha256=7Uelip6jrKqyDj3BSio9RWeXzoIDR7Qts4bGB4Ljeok,15713 +sklearn/svm/tests/test_svm.py,sha256=i0DQrT_gyJWvtwzJpB3W7nLkiufJja7xROvWTN8otqc,49321 +sklearn/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/tests/__pycache__/metadata_routing_common.cpython-310.pyc,, +sklearn/tests/__pycache__/test_base.cpython-310.pyc,, +sklearn/tests/__pycache__/test_build.cpython-310.pyc,, +sklearn/tests/__pycache__/test_calibration.cpython-310.pyc,, +sklearn/tests/__pycache__/test_check_build.cpython-310.pyc,, +sklearn/tests/__pycache__/test_common.cpython-310.pyc,, +sklearn/tests/__pycache__/test_config.cpython-310.pyc,, +sklearn/tests/__pycache__/test_discriminant_analysis.cpython-310.pyc,, +sklearn/tests/__pycache__/test_docstring_parameters.cpython-310.pyc,, +sklearn/tests/__pycache__/test_docstring_parameters_consistency.cpython-310.pyc,, +sklearn/tests/__pycache__/test_docstrings.cpython-310.pyc,, +sklearn/tests/__pycache__/test_dummy.cpython-310.pyc,, +sklearn/tests/__pycache__/test_init.cpython-310.pyc,, +sklearn/tests/__pycache__/test_isotonic.cpython-310.pyc,, +sklearn/tests/__pycache__/test_kernel_approximation.cpython-310.pyc,, +sklearn/tests/__pycache__/test_kernel_ridge.cpython-310.pyc,, +sklearn/tests/__pycache__/test_metadata_routing.cpython-310.pyc,, +sklearn/tests/__pycache__/test_metaestimators.cpython-310.pyc,, +sklearn/tests/__pycache__/test_metaestimators_metadata_routing.cpython-310.pyc,, +sklearn/tests/__pycache__/test_min_dependencies_readme.cpython-310.pyc,, +sklearn/tests/__pycache__/test_multiclass.cpython-310.pyc,, +sklearn/tests/__pycache__/test_multioutput.cpython-310.pyc,, +sklearn/tests/__pycache__/test_naive_bayes.cpython-310.pyc,, +sklearn/tests/__pycache__/test_pipeline.cpython-310.pyc,, +sklearn/tests/__pycache__/test_public_functions.cpython-310.pyc,, +sklearn/tests/__pycache__/test_random_projection.cpython-310.pyc,, +sklearn/tests/metadata_routing_common.py,sha256=SRXRZEbTLYwnN7oEgRBPV7kpjESVwULuIOSj1gt6DcQ,20209 +sklearn/tests/test_base.py,sha256=VrVLt_AaDs8x11DdWZFpAeOWeOUvSEE5Kcx3stUBRIU,33694 +sklearn/tests/test_build.py,sha256=n9OrwEnrrozhLRaYwJNWYHrL65d7UHpmLbTDfVNtpmg,1181 +sklearn/tests/test_calibration.py,sha256=fkdT26HTVN4YMQtsrAUBJu_M0WPI3UKDFc64lnFsHYE,42703 +sklearn/tests/test_check_build.py,sha256=udkTjCgk_hJbkNmyyWxCZaf3-rfGCNudkHY_dH3lXj0,300 +sklearn/tests/test_common.py,sha256=Lc6qj8HJ1vOQ26ku2NbX2sPoaEaFemZApoxMq3z1-Wg,13255 +sklearn/tests/test_config.py,sha256=IOKEMY6L4Aq9Ykp90fjo7cuj9ReTWu7DQuEUeKTh4mI,5787 +sklearn/tests/test_discriminant_analysis.py,sha256=VaWug3CYu_OLTFrJ_AM1IALfWTjldYc03ntlpWzyfxo,22689 +sklearn/tests/test_docstring_parameters.py,sha256=zdFK0a9qLNGSePIx5T3CsF1iAjHDBSogOQpDiMY2iQc,11672 +sklearn/tests/test_docstring_parameters_consistency.py,sha256=MEG4Rut5qEKBgrkImKzRvr-iNEqZUO4iyG1SK9KYSIc,4171 +sklearn/tests/test_docstrings.py,sha256=t1zNwka5FH2Y1r5uweYuZwHR6RuicG5VJfdaK8YerNc,6853 +sklearn/tests/test_dummy.py,sha256=kSSm0v9b-rcoGR5fCETFmd2qYOoBcYSUyNZ1T4nYcdY,22085 +sklearn/tests/test_init.py,sha256=0cET-MRZ2v0MeHqLg9XqubgyWB03xsD3QmE-vBKF73A,476 +sklearn/tests/test_isotonic.py,sha256=YnhVVK8aTb5liVOKnzIy67aXw4Hk9GabGzuFd22zF9Y,22331 +sklearn/tests/test_kernel_approximation.py,sha256=h52dmpdAJyRzf_tVYYIAlu7Ac3gC8jv1_DDYw9E8U7E,16579 +sklearn/tests/test_kernel_ridge.py,sha256=qkwUUjuY5O1uMiXi9gAS-wXOCHa62F5T7VJnNdZdGOE,2888 +sklearn/tests/test_metadata_routing.py,sha256=nWWuf5wDbvi-r9ZyYHXUGLmMVhjm4UXqYeLN2Xbjnzo,40635 +sklearn/tests/test_metaestimators.py,sha256=6DVdtUKP7r5y3VafOX5SlaTimtxuT-OlQKYRLBD-HnE,11471 +sklearn/tests/test_metaestimators_metadata_routing.py,sha256=BKNAA6CAKly7n1CPHKdccZdRHcf2taSEubRtzdHTbls,32009 +sklearn/tests/test_min_dependencies_readme.py,sha256=BprXGDEgArPAoWLvBP8YPefv5vvKPzONzB1bSrRnzaE,4576 +sklearn/tests/test_multiclass.py,sha256=w2LmmymBo9K4ZyfFmMX27nvHIljeoaZlTi6WJUx-Pns,33934 +sklearn/tests/test_multioutput.py,sha256=3enAkYXMhvEFxC2zeyD_nZl5afjhF-eFtMRLwYYo93I,30553 +sklearn/tests/test_naive_bayes.py,sha256=SePTMoTagDqYQznjFrc01tIBTpMiWSK9MKvbdBnL9rg,35184 +sklearn/tests/test_pipeline.py,sha256=ZKOndIopvrdYgcHQ-Kxq7sODSgQfys8bMP2Ol3Ut7QA,80841 +sklearn/tests/test_public_functions.py,sha256=sCP84pcI2ok33NM2n8kllIDxxinIoDmffbjuj7cohN0,16738 +sklearn/tests/test_random_projection.py,sha256=PHgMtjxt5qvy6IM0YY6eWmNLelxdT2H4kF8BQbUBeRc,19583 +sklearn/tree/__init__.py,sha256=w6EhQ5jlcvPSer8w3GTWY0RPufTiCmp2IaRqhYYdmbY,572 +sklearn/tree/__pycache__/__init__.cpython-310.pyc,, +sklearn/tree/__pycache__/_classes.cpython-310.pyc,, +sklearn/tree/__pycache__/_export.cpython-310.pyc,, +sklearn/tree/__pycache__/_reingold_tilford.cpython-310.pyc,, +sklearn/tree/_classes.py,sha256=DygqR1NKHxRMECLn888NTnF7KX21HJ5kg1BwA4tIu9g,77648 +sklearn/tree/_criterion.cpython-310-x86_64-linux-gnu.so,sha256=HPNOrqLvfUSn0kl2JZrd3ULCZ3d07YRSgpDDCnhBhz8,213496 +sklearn/tree/_criterion.pxd,sha256=K_TRUrtxRiX_4Q_AltNDYtkhYLerlREjq9F14hcGJrs,4491 +sklearn/tree/_criterion.pyx,sha256=ujjfJAUnJ2y2rpHiJe5xumhuLC5S1eSqTyFmpyELN28,61626 +sklearn/tree/_export.py,sha256=Bnz5BYL-MXWgX9nYQYEzUQqWazLLFYwT8vRKTB2zWfQ,40733 +sklearn/tree/_partitioner.cpython-310-x86_64-linux-gnu.so,sha256=M_2p9x7OePRoMW7uNLbuApEON9OcQPlF_oThJBZdn7k,182352 +sklearn/tree/_partitioner.pxd,sha256=Wlf1kIFiFeykzrO1YYXOcGd_RZwnUTO4YXTwNCVfXNA,4939 +sklearn/tree/_partitioner.pyx,sha256=CvptDNtr-F7WkHuOk0PVN8USOAczQ-U9jbmG-oCIOhs,31975 +sklearn/tree/_reingold_tilford.py,sha256=ImilHGv15TI5inwyBar79zEy-V-TxD5A9NUk0sBM91A,5157 +sklearn/tree/_splitter.cpython-310-x86_64-linux-gnu.so,sha256=SD-zMrv1sP3sumZReN5S7SFo1Rv4FRHC5P-Dlwfe-Nw,157232 +sklearn/tree/_splitter.pxd,sha256=Yq0osi__MEU1QIJ_rz6Oq940Eu5srHBUMTQptnnWRUY,4436 +sklearn/tree/_splitter.pyx,sha256=Gj0B-hAU-TBKi90MRwZu9_WXy8v0lQonNoinb1QlauE,33411 +sklearn/tree/_tree.cpython-310-x86_64-linux-gnu.so,sha256=T3JI8QhNKc6Ty2OxROGs-Bjyj6nXMWgoc-nYcnHBshY,512600 +sklearn/tree/_tree.pxd,sha256=0z7WppVbOyb-1kOv0eKSO6iBrySonlhScaPjf_YWlsw,5431 +sklearn/tree/_tree.pyx,sha256=gKlP2sGFwwKXm3BS8kHoFXAMPhVczyLp3qFBX9G1rnM,73917 +sklearn/tree/_utils.cpython-310-x86_64-linux-gnu.so,sha256=mxd2bVVZTIhKbLpBZisNyKaL3QRd3nenqET3mwJz424,147576 +sklearn/tree/_utils.pxd,sha256=x-vTBBqxgTB-py3mJ8QQ3fqDfEeexkzsLnKbXcgk-Z4,3622 +sklearn/tree/_utils.pyx,sha256=k-viNXwSoiZ8Xe-S9BxyBVIWQW8nFuN6TInVpDJVCDA,16609 +sklearn/tree/meson.build,sha256=h_vVyJ3Uap4rs3v7OnDqMq8gV0bxOmsYYdCZAi_G1tE,899 +sklearn/tree/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/tree/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/tree/tests/__pycache__/test_export.cpython-310.pyc,, +sklearn/tree/tests/__pycache__/test_monotonic_tree.cpython-310.pyc,, +sklearn/tree/tests/__pycache__/test_reingold_tilford.cpython-310.pyc,, +sklearn/tree/tests/__pycache__/test_tree.cpython-310.pyc,, +sklearn/tree/tests/test_export.py,sha256=ClpF28hE3ZKLU90Q1ncPw6mlZO_Gv2BTlgThBo3AzqE,21026 +sklearn/tree/tests/test_monotonic_tree.py,sha256=-UtlxTxsTe-30KfK7KsfniieAN2Iks8Z6l23UTjKoz4,18612 +sklearn/tree/tests/test_reingold_tilford.py,sha256=xRt_Hlm-fGJ2onva4L9eL5mNdcHwWhPEppwNjP4VEJs,1461 +sklearn/tree/tests/test_tree.py,sha256=QIq_89dUB30m99D7XbS2EqhiU48sqDZd1_QUz_36Jh8,99442 +sklearn/utils/__init__.py,sha256=3Nz2hkOhi9RofSV3J-1KDnEfiXXIosfxhCLVlclJ5X0,2134 +sklearn/utils/__pycache__/__init__.cpython-310.pyc,, +sklearn/utils/__pycache__/_arpack.cpython-310.pyc,, +sklearn/utils/__pycache__/_array_api.cpython-310.pyc,, +sklearn/utils/__pycache__/_available_if.cpython-310.pyc,, +sklearn/utils/__pycache__/_bunch.cpython-310.pyc,, +sklearn/utils/__pycache__/_chunking.cpython-310.pyc,, +sklearn/utils/__pycache__/_encode.cpython-310.pyc,, +sklearn/utils/__pycache__/_estimator_html_repr.cpython-310.pyc,, +sklearn/utils/__pycache__/_indexing.cpython-310.pyc,, +sklearn/utils/__pycache__/_mask.cpython-310.pyc,, +sklearn/utils/__pycache__/_metadata_requests.cpython-310.pyc,, +sklearn/utils/__pycache__/_missing.cpython-310.pyc,, +sklearn/utils/__pycache__/_mocking.cpython-310.pyc,, +sklearn/utils/__pycache__/_optional_dependencies.cpython-310.pyc,, +sklearn/utils/__pycache__/_param_validation.cpython-310.pyc,, +sklearn/utils/__pycache__/_plotting.cpython-310.pyc,, +sklearn/utils/__pycache__/_pprint.cpython-310.pyc,, +sklearn/utils/__pycache__/_response.cpython-310.pyc,, +sklearn/utils/__pycache__/_set_output.cpython-310.pyc,, +sklearn/utils/__pycache__/_show_versions.cpython-310.pyc,, +sklearn/utils/__pycache__/_tags.cpython-310.pyc,, +sklearn/utils/__pycache__/_testing.cpython-310.pyc,, +sklearn/utils/__pycache__/_unique.cpython-310.pyc,, +sklearn/utils/__pycache__/_user_interface.cpython-310.pyc,, +sklearn/utils/__pycache__/class_weight.cpython-310.pyc,, +sklearn/utils/__pycache__/deprecation.cpython-310.pyc,, +sklearn/utils/__pycache__/discovery.cpython-310.pyc,, +sklearn/utils/__pycache__/estimator_checks.cpython-310.pyc,, +sklearn/utils/__pycache__/extmath.cpython-310.pyc,, +sklearn/utils/__pycache__/fixes.cpython-310.pyc,, +sklearn/utils/__pycache__/graph.cpython-310.pyc,, +sklearn/utils/__pycache__/metadata_routing.cpython-310.pyc,, +sklearn/utils/__pycache__/metaestimators.cpython-310.pyc,, +sklearn/utils/__pycache__/multiclass.cpython-310.pyc,, +sklearn/utils/__pycache__/optimize.cpython-310.pyc,, +sklearn/utils/__pycache__/parallel.cpython-310.pyc,, +sklearn/utils/__pycache__/random.cpython-310.pyc,, +sklearn/utils/__pycache__/sparsefuncs.cpython-310.pyc,, +sklearn/utils/__pycache__/stats.cpython-310.pyc,, +sklearn/utils/__pycache__/validation.cpython-310.pyc,, +sklearn/utils/_arpack.py,sha256=dB4rJYnuwSUXl73JoLISQbYHSXeco10y3gjNKGGEAig,1209 +sklearn/utils/_array_api.py,sha256=hJTLyn5kuMCwGD5-C5peyDEWR2jEY1lZUzNYc2En-6A,34748 +sklearn/utils/_available_if.py,sha256=CUJT-FoWEUiSCJ7BnfBFZ__74shIuMbHSBhQpwbVgnE,2945 +sklearn/utils/_bunch.py,sha256=_QRWzRU0TcO0Suv-mUFfuvuNrvP0Avp-PI0RY7uxdbA,2176 +sklearn/utils/_chunking.py,sha256=fpnjaJDWTLndUv4bHfIlt2gk0YmPYdArtYljwVA0KsM,5438 +sklearn/utils/_cython_blas.cpython-310-x86_64-linux-gnu.so,sha256=NNZq1v68T-6LvXO4h5g_9_hLGaySWtDTbhzOebp0u0c,353320 +sklearn/utils/_cython_blas.pxd,sha256=Kx-TV-Wy3JD8JAROmcAB3623tmk01WnffCiFLResUZI,1565 +sklearn/utils/_cython_blas.pyx,sha256=c9hEUrULMKXia5j3Ia88YDaJ7Lv4RGsqqxY6HIF9oQY,8282 +sklearn/utils/_encode.py,sha256=bptNb3r5s1VW1eI--TJM0S-feBJ9ozOceq9ju1DioWs,11797 +sklearn/utils/_estimator_html_repr.py,sha256=fgZ19z0W2bb51uWISQtaOUYxD2nvPx-EHgIuc4jHiO0,898 +sklearn/utils/_fast_dict.cpython-310-x86_64-linux-gnu.so,sha256=FRoBZdRHbZ1Tl7bQ7G67JW1U8huv9TTlCWgylo_zg0w,172864 +sklearn/utils/_fast_dict.pxd,sha256=IyPazoB2nBPCRf-TrfMqGDl9xQSM9QmnNx1nDUcSNCo,516 +sklearn/utils/_fast_dict.pyx,sha256=H4RiRkSLH3syEzlAR54xArEAWURDmp8U4S17Adxbf2s,4652 +sklearn/utils/_heap.cpython-310-x86_64-linux-gnu.so,sha256=wx-gD3OmEpUqpS73BQRh78Cc4L1spDs_Dv3iGodDzSw,23448 +sklearn/utils/_heap.pxd,sha256=FXcpp-JAYxvFGZqLZ6IrJieDZ9_W2hP4sVOLY4fzJAQ,256 +sklearn/utils/_heap.pyx,sha256=ca-rKqGzTbGz7X-HuLF9VzkZ3CfNEiIF2Bh7QjfZQ7s,2253 +sklearn/utils/_indexing.py,sha256=WpNkZhzQf0SQkNGDhZT0bum6BgxpWFmYciTWQQaYLcU,26366 +sklearn/utils/_isfinite.cpython-310-x86_64-linux-gnu.so,sha256=3YF16I2TB44Qkf2wAZbhrarMkUKpJjgrX64gGzZ7aCc,117888 +sklearn/utils/_isfinite.pyx,sha256=PFLLYo0BWaxpfNP6t0O6r0cLY9KXZSzHQmVYQKYbBtI,1414 +sklearn/utils/_mask.py,sha256=QoXi1rB6ZLp5GfOmv5jY47Wv2IS20-NS7bTt1Phz8Wc,4890 +sklearn/utils/_metadata_requests.py,sha256=ZJOYUDL-YjvElm1jLLwTKndWzIW1n_ri57TqUJqxiU0,58245 +sklearn/utils/_missing.py,sha256=SerUx-LWOIZFw3i6uxWQ9KkJX5n3BWZJZFt6lELH1TE,1479 +sklearn/utils/_mocking.py,sha256=J7wTGzJL364cLCYeIfxNxmtPYSvM_gKRAcJ7WqFvRvg,13661 +sklearn/utils/_openmp_helpers.cpython-310-x86_64-linux-gnu.so,sha256=9xf4yOXso0OblMdFXejezXmoZ8uSEUv4V-bejNds8vk,84625 +sklearn/utils/_openmp_helpers.pxd,sha256=ORtNXjPXDBOmoHW6--54wwrMEIZptCAZ6T8CJPCuJ-0,1069 +sklearn/utils/_openmp_helpers.pyx,sha256=6NgzGt7XMaLuzqqigYqJzERWbpvW-pDJ36L8OAVfdKw,3143 +sklearn/utils/_optional_dependencies.py,sha256=ppUWhMBeNGhVcPjZDqrmDOujWZ_qApndumj6OesynOA,1300 +sklearn/utils/_param_validation.py,sha256=H3vhAp9Cbn-7JuTGozS-MwGoOlEcd1fHKmP0oq0Z2UY,28578 +sklearn/utils/_plotting.py,sha256=Tu8t4k0EhWACBvsH5t0TlFOVdeJ4wI9PXub8ZLdyadQ,15370 +sklearn/utils/_pprint.py,sha256=QtAc-rPoco7xOuky6RLmMafpzPKsxud9LEgCGAEh9gg,18520 +sklearn/utils/_random.cpython-310-x86_64-linux-gnu.so,sha256=z6at-CVlRFXhYH8CeH6vfHpQsN7h-Tl9j2gHXbNgE9g,242720 +sklearn/utils/_random.pxd,sha256=_9sOwgmCxQ3rJCvVPplc7FJ-2iJgXZxeU3q8bo2oXXE,1250 +sklearn/utils/_random.pyx,sha256=H1plEnif12DxB2ZKB8H_mkC5WxXrPHpeFRbTLSxZQUI,12589 +sklearn/utils/_repr_html/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/utils/_repr_html/__pycache__/__init__.cpython-310.pyc,, +sklearn/utils/_repr_html/__pycache__/base.cpython-310.pyc,, +sklearn/utils/_repr_html/__pycache__/estimator.cpython-310.pyc,, +sklearn/utils/_repr_html/__pycache__/params.cpython-310.pyc,, +sklearn/utils/_repr_html/base.py,sha256=ZrLqweuvYizwi5HzCx8261BkyF7ot9iQ9Xf2zJcOx-o,6146 +sklearn/utils/_repr_html/estimator.css,sha256=cypIOeM_ga4IcYEXJfke90HNLJ1bHmhM-uUh52wQER4,11237 +sklearn/utils/_repr_html/estimator.js,sha256=TeUu7jCSDl2Af2v9C8I2qGOMw-LCHbh7ED2EMsyQaEs,1730 +sklearn/utils/_repr_html/estimator.py,sha256=N-c-YguOE554YmjZ5In6k0J6Bsz4HJkZmqyeaFNf844,18069 +sklearn/utils/_repr_html/params.css,sha256=kbxocqXiZSXRJB697838Kl3bTBJ3PiZC3lWU3ZGNXJY,1896 +sklearn/utils/_repr_html/params.py,sha256=pUWHGeI_EWfN3Wxum5HCOlevm_qOMuEdgiIBNbPjhvs,2651 +sklearn/utils/_repr_html/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/utils/_repr_html/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/utils/_repr_html/tests/__pycache__/test_estimator.cpython-310.pyc,, +sklearn/utils/_repr_html/tests/__pycache__/test_params.cpython-310.pyc,, +sklearn/utils/_repr_html/tests/test_estimator.py,sha256=EuxDDZhcJDR_Wlawk4vFMsXPXlqS2XUvZI1oWZfdCr4,21420 +sklearn/utils/_repr_html/tests/test_params.py,sha256=qimQsD0wAD3249bS-_9nvMg8m-_u4igjzXeBvO5utw4,2356 +sklearn/utils/_response.py,sha256=_-mr2y7YAVXx_lrZ2Cz2RUj54LVC4BVhzQzL7Qy6fco,12117 +sklearn/utils/_seq_dataset.cpython-310-x86_64-linux-gnu.so,sha256=3s_DgK_Gub2n0Dc7wC2VbfZxeWIHexlN5LcOLaeeab4,226520 +sklearn/utils/_seq_dataset.pxd.tp,sha256=XWHP_pzN2o5rQkKSgnZWO_VMdASTPOQap35ebvWnRXw,2567 +sklearn/utils/_seq_dataset.pyx.tp,sha256=tkpcGPtSGLrAJxE4GGzXydoMvvn_6jWs4EKLUGEWio4,12252 +sklearn/utils/_set_output.py,sha256=l8xL8wklyS8hBdF-6HYry4m8HlSrRQiGe58JIja758A,14793 +sklearn/utils/_show_versions.py,sha256=GL9Ca3wwOStKVdOYIqTv2vRB5BWCTwiJTBiQSIaYEmI,2548 +sklearn/utils/_sorting.cpython-310-x86_64-linux-gnu.so,sha256=v1nrqxAs8iSskH23v4tGyba40AvhtIsfGrOv7pBqn_k,28120 +sklearn/utils/_sorting.pxd,sha256=i8Bkh1j07pgP6pIvzFxFIZ7uAlR1fQOCbIHh7v04xw8,161 +sklearn/utils/_sorting.pyx,sha256=Q-F_hwd8KFokcfaVXOswOWXVjdIjiQREoQRLaRxl9dY,3280 +sklearn/utils/_tags.py,sha256=B4xmNHIdYlVdrFI1n5ji1_qZYdVcr_SWcAX6Ck_ZNns,12283 +sklearn/utils/_test_common/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/utils/_test_common/__pycache__/__init__.cpython-310.pyc,, +sklearn/utils/_test_common/__pycache__/instance_generator.cpython-310.pyc,, +sklearn/utils/_test_common/instance_generator.py,sha256=36b7kvFHnuBhRyUgb49OLNYfUaHKR0bhc2aP3Pw55m8,50205 +sklearn/utils/_testing.py,sha256=1m-_LOB2ml38qvcf1QQ1Xd2ArwwmP7RpXR3e0P7tC-c,50818 +sklearn/utils/_typedefs.cpython-310-x86_64-linux-gnu.so,sha256=mCtEJBe1lMNEsL1guapFcvqptUtYUIaYmHY3gWI86-Q,153240 +sklearn/utils/_typedefs.pxd,sha256=gew7YuCZWwpo-JWXGDIrwJ2-K_6mB-C4Ghd_Zu9Gd-o,2090 +sklearn/utils/_typedefs.pyx,sha256=rX9ZIRqg-XFgtM4L3Mh0YAsmRHSnccxdg2nEs9_2Zns,428 +sklearn/utils/_unique.py,sha256=IBhmM0fwGmuhcNjtSf_1-OryOx1plPOGwXVE4sndDyM,2972 +sklearn/utils/_user_interface.py,sha256=dzS5H5O6prEkNLholFgBLWOfFp4u0Mw61mDBQFh5KZ4,1485 +sklearn/utils/_vector_sentinel.cpython-310-x86_64-linux-gnu.so,sha256=iB_UTmbxZhWqIKeanSKEhxGsMleZDxzTIgFwQtfnT2g,166544 +sklearn/utils/_vector_sentinel.pxd,sha256=G_im5dT6DaREJgMAGu2MCd-tj5E-elc5mYX4sulSYW0,296 +sklearn/utils/_vector_sentinel.pyx,sha256=H1GeEQ7qOSSwgo55sNUaiWzVb1vbAqOr03hnfR-B-o8,4458 +sklearn/utils/_weight_vector.cpython-310-x86_64-linux-gnu.so,sha256=yaAmPriUpETldyQvjtucTxjL36jaMlaB5A1dsa2bCpk,93152 +sklearn/utils/_weight_vector.pxd.tp,sha256=VXw0bYJBkzy0rFFI_wfwPFZsAnfdykJz0W_svYGXiKM,1389 +sklearn/utils/_weight_vector.pyx.tp,sha256=8NR10zND_aAQt2iKtOXIGm7nu665cVsXoKmaZioWH1I,6901 +sklearn/utils/arrayfuncs.cpython-310-x86_64-linux-gnu.so,sha256=UfXpGRSzyhG7c0-RMc8kuPLhizSgzVJIsoDXjqY1bLA,188200 +sklearn/utils/arrayfuncs.pyx,sha256=vw-BXUbdkWBH6l5XAGRMPB0Ek8lRdFouVrVPjPD-iBg,2908 +sklearn/utils/class_weight.py,sha256=zcSNeTVb852HxMJlTUj86mbuWJdWA2dSMS-ft3ESYZM,8722 +sklearn/utils/deprecation.py,sha256=wi1BfyMwrwx1vdgSsw42Vser4aeKnKsDdlNh2OIrhY0,4374 +sklearn/utils/discovery.py,sha256=vQzoj_8YHjepxOlGT1YbF2C871mjkUuYbQRpM6Dho4s,8698 +sklearn/utils/estimator_checks.py,sha256=9el1g3Vc_qXy2Dmo6sUGlco4lv9NMHL8qwFx7vZbtm4,193210 +sklearn/utils/extmath.py,sha256=ZfCd1ARG9C2HP5Oss244N9rf7yawYz-hSIBsiPkFXPQ,48516 +sklearn/utils/fixes.py,sha256=6U_XXwI976vOFzT2MkxeuFbtkOnbtWmpNsqv-RvqSgY,15211 +sklearn/utils/graph.py,sha256=Jorg2G33rvJEC9ySKfQ30Siu_KGZ3VLARo8U9xAKxAc,5696 +sklearn/utils/meson.build,sha256=B12-imQSrpXZL81WVtFk7kGtjI6yZPIW4hzqYdH3Djs,2575 +sklearn/utils/metadata_routing.py,sha256=yOqxU3x6s2TAQ35XN9uTR8DLQtAucuXbzjrjlLIyxKY,578 +sklearn/utils/metaestimators.py,sha256=OvXMa6tex9Gog2wzaHZqoMd1DYHEp02-Ubrz2czXWyE,5827 +sklearn/utils/multiclass.py,sha256=pAwNDXiul4QCW6UJP1MJGNLgsM8JBNo7vDtVpidihQE,20391 +sklearn/utils/murmurhash.cpython-310-x86_64-linux-gnu.so,sha256=uJdpVAqIXgqcKw9Nsy7_9J39rToAKxy-mcjri1zhRlM,138952 +sklearn/utils/murmurhash.pxd,sha256=Z8mj3dEhTQN1MdzvlHA7jS9lA6fjkqYFK1YTeVwC10o,876 +sklearn/utils/murmurhash.pyx,sha256=bRyDiVMurmKHJW32MNMqzmEA9Mj-eNR6zBoj5C6M4MU,4530 +sklearn/utils/optimize.py,sha256=GX3H4bzCVBZ1Fv6eUcgvU2d7zx-xBclNMRFCcqLuWn8,12298 +sklearn/utils/parallel.py,sha256=0f3yUjH024aVyf6zauOj1vhOmwlibQs7CFQUJCkoa00,6082 +sklearn/utils/random.py,sha256=8fAjBUbjcuhHypUwQ7X7GB7D-k-Y-RfhhOiZyQ182Sg,3683 +sklearn/utils/sparsefuncs.py,sha256=msAts1ikks1NEGG1EvTZhcp2VfIEsEEh3rkPAYlgLLo,22598 +sklearn/utils/sparsefuncs_fast.cpython-310-x86_64-linux-gnu.so,sha256=eInKQheiyUv5a4dDH8hI-j7pOGiOxZloCf2HhyBXMe0,765624 +sklearn/utils/sparsefuncs_fast.pyx,sha256=XcHvxCBHTlxwhn4kZX5FSrOl36ziouVbBJDhKpN5KtA,21795 +sklearn/utils/src/MurmurHash3.cpp,sha256=5BI_ft6ZWDOlbpDI-U1MM-bvqH5G2ssGgfLJWD5bozU,7968 +sklearn/utils/src/MurmurHash3.h,sha256=vX2iW09b4laQOwIwXSiTu14wfdkowndTzKgDAmHQPi4,1155 +sklearn/utils/stats.py,sha256=B7SSDY9D-KSUd9AdmsjIVtr5zWulMypyLmj7m9pLgqs,5035 +sklearn/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/utils/tests/__pycache__/__init__.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_arpack.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_array_api.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_arrayfuncs.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_bunch.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_chunking.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_class_weight.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_cython_blas.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_deprecation.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_encode.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_estimator_checks.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_estimator_html_repr.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_extmath.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_fast_dict.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_fixes.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_graph.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_indexing.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_mask.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_metaestimators.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_missing.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_mocking.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_multiclass.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_murmurhash.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_optimize.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_parallel.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_param_validation.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_plotting.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_pprint.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_random.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_response.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_seq_dataset.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_set_output.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_shortest_path.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_show_versions.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_sparsefuncs.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_stats.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_tags.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_testing.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_typedefs.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_unique.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_user_interface.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_validation.cpython-310.pyc,, +sklearn/utils/tests/__pycache__/test_weight_vector.cpython-310.pyc,, +sklearn/utils/tests/test_arpack.py,sha256=EL3_6a1iDpl8Q-0A8iv6YrwycX0zBwWsL_6cEm3i6lo,490 +sklearn/utils/tests/test_array_api.py,sha256=X079ez3xRzqPQ-6RhM0nJjVWhm8z-nu6ZtZASHBotGA,21062 +sklearn/utils/tests/test_arrayfuncs.py,sha256=DGbK5ejSO-_ibZDoeM5RlDNY7a8Z8eGScnq3cThQ1Js,1326 +sklearn/utils/tests/test_bunch.py,sha256=QZXKwtgneO2wcnnrbMVM_QNfVlVec8eLw0JYtL_ExMI,813 +sklearn/utils/tests/test_chunking.py,sha256=4ygjiWbrLWxqgYYKPZ2aHKRzJ93MD32kEajbgIO0C6s,2371 +sklearn/utils/tests/test_class_weight.py,sha256=K-vCvXz5GlqOl1h6QdPXRGs-svR82ql2fD6ndzPhsXI,12957 +sklearn/utils/tests/test_cython_blas.py,sha256=COhzY-WHwQLIF5qn_XvBPggWn9OfMw7J2IOLV63MKaw,6709 +sklearn/utils/tests/test_deprecation.py,sha256=BRp5wLhsGtSdDt1cWwF72kqqsqbNDvggGs_kETP0g0U,2294 +sklearn/utils/tests/test_encode.py,sha256=QiiG0ArBGF7ENYrvcgPGwjYgUdn3W6Ch_GE9VEF2DWI,9603 +sklearn/utils/tests/test_estimator_checks.py,sha256=_5OzYdijgENQQfL_zDN8TTntCCjXVq-MLtwqGnvHOhI,58138 +sklearn/utils/tests/test_estimator_html_repr.py,sha256=YeI-j1_CkTR82HpHi4vZflc9zshWQRBx_PT9qiWz_6Y,614 +sklearn/utils/tests/test_extmath.py,sha256=6lQ03gV-8CD0-02tJU7eVz4ORNgi00WfxDuHDhj2D1Q,39056 +sklearn/utils/tests/test_fast_dict.py,sha256=Y4wCGUJ4Wb1SkePK4HJsqQa3iL9rTqsbByU2X3P8KQY,1355 +sklearn/utils/tests/test_fixes.py,sha256=8w_0PiyUlBq1EebvaCMJdttuCFStZykRYGQ7sTCdzPs,5328 +sklearn/utils/tests/test_graph.py,sha256=0FGOXawAnpEg2wYW5PEkJsLmIlz1zVTIgFP5IJqdXpc,3047 +sklearn/utils/tests/test_indexing.py,sha256=P5ulIjFw0gkoHFRGSORlhCFt6_y7EbvcdHTNHOuM-jM,23721 +sklearn/utils/tests/test_mask.py,sha256=eEsLP_o7OqGGFt5Kj9vnobvHw4sNaVFzHCuE4rlyEd4,537 +sklearn/utils/tests/test_metaestimators.py,sha256=x_0agW4puaVCmqPwBrk3FrWIZeK3qgM9eNJWUxYD640,2107 +sklearn/utils/tests/test_missing.py,sha256=3lPgYdyvRkzPH-Bw82N282i_5_aYN7hHK-bkoPBw_Jg,709 +sklearn/utils/tests/test_mocking.py,sha256=S0W07EnpATWo5sy1V-FAoPpyhRT1DHOveb9PyXa7ibQ,5898 +sklearn/utils/tests/test_multiclass.py,sha256=h0GSlMbftD2sVtel14MDHd38u_7tVMGHI9uRJN8kIk4,22059 +sklearn/utils/tests/test_murmurhash.py,sha256=b-WKvPEJmp8XiIjGVDv_c_6mGOL-nz9XOvMFNXPpXeA,2516 +sklearn/utils/tests/test_optimize.py,sha256=FWWUjF2yJ_zIn41UCmik8iTKMeww7mp16Djq1UhPFKs,7603 +sklearn/utils/tests/test_parallel.py,sha256=ldqH6MqBTZCM6EIjHySSzlx3wRwWD0_cdV4DzQwpKcQ,5665 +sklearn/utils/tests/test_param_validation.py,sha256=fApmDQ01VznF_uaA4bKch0IqHHfBMLxAa1VMa_c7su4,24407 +sklearn/utils/tests/test_plotting.py,sha256=UWU43kvMkBbNoYJUAqDdgSHwINeiPxv3dnWc37PGy7E,19938 +sklearn/utils/tests/test_pprint.py,sha256=Bg9Sv8uNPfM3bDtWrIeahnXacdyGSjliIecPyHCqmTc,27858 +sklearn/utils/tests/test_random.py,sha256=wzhfCP5lhSm-5PaCvbcgqvsnuINkvJNVSanQZiRmc0s,7149 +sklearn/utils/tests/test_response.py,sha256=JW7hWzu3l8_c0GNU3AHxbPynknD7zaqXuvbGPLsFHEI,14142 +sklearn/utils/tests/test_seq_dataset.py,sha256=Nr1MGuCWVEM6T7OWPKDUxss7XzAaX1qFZwjN5KC_sFU,5868 +sklearn/utils/tests/test_set_output.py,sha256=sPtTyGoAsIXO4IkT6TYhSgXPeQu_Qt-kZ9gxErWeeos,16131 +sklearn/utils/tests/test_shortest_path.py,sha256=XN1SF7TfMo8tQCC-bUV2wK99jR32hEM7xZOl54NbIoQ,1846 +sklearn/utils/tests/test_show_versions.py,sha256=eMzrmzaMs6TO7JSMSfSokfAVW_daMms-7Xel5XyqKZc,1001 +sklearn/utils/tests/test_sparsefuncs.py,sha256=mnmWRDHuvKHT-rgkHoC48edn-sAguWAYzA-OfplR_4w,34943 +sklearn/utils/tests/test_stats.py,sha256=r1kIVHmemK0scdg_1M93HQXEjFCn3Lv9GkC6xj5-D7M,12576 +sklearn/utils/tests/test_tags.py,sha256=oQf8mu-ooCy2EZUiYsJer6se4BTwS93mx4IepysvU6A,4644 +sklearn/utils/tests/test_testing.py,sha256=LdQzf2249lqmo48vHNcNnFeQCt6PL-V09iLWTiRR1bQ,33119 +sklearn/utils/tests/test_typedefs.py,sha256=gc_bm54uF15dtX5rz0Cmw4OQQhscTHACRhjdkEkMx8o,735 +sklearn/utils/tests/test_unique.py,sha256=UMMRUrDYiTzxcf49N_ddIWWyvSySFBTzrPK7JY94fGU,1820 +sklearn/utils/tests/test_user_interface.py,sha256=Pn0bUwodt-TCy7f2KdYFOXQZ-2c2BI98rhpXTpCW4uE,1772 +sklearn/utils/tests/test_validation.py,sha256=95Bo4XIy3w4fGMLK7obcs73vWMFHwVj8aGYT3Hf0qd0,80550 +sklearn/utils/tests/test_weight_vector.py,sha256=eay4_mfrN7vg2ZGoXmZ06cU9CLQYBJKMR_dK6s2Wyic,665 +sklearn/utils/validation.py,sha256=rjyXLmReLQugBFno-CrFQPCmcPR0zCk4dalfTWPwzMI,108488 diff --git a/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/WHEEL b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..4e4c38ae320920b8f083b87f408214cdecd350d2 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_learn-1.7.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: meson +Root-Is-Purelib: false +Tag: cp310-cp310-manylinux_2_17_x86_64 +Tag: cp310-cp310-manylinux2014_x86_64 + diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/DESCRIPTION.rst b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/DESCRIPTION.rst new file mode 100644 index 0000000000000000000000000000000000000000..bfa917d5ce96748461e08b7787c227eabf823236 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This library provides easy access to common as well as +state-of-the-art video processing routines. Check out the +website for more details. + + + diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/INSTALLER b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/METADATA b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..298b38c098434ff74f3c60e6bed0f3973711bb98 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/METADATA @@ -0,0 +1,33 @@ +Metadata-Version: 2.0 +Name: scikit-video +Version: 1.1.11 +Summary: Video Processing in Python +Home-page: http://scikit-video.org/ +Author: ('Todd Goodall',) +Author-email: info@scikit-video.org +License: BSD +Download-URL: https://github.com/scikit-video/scikit-video +Description-Content-Type: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: MacOS +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Topic :: Multimedia :: Video +Classifier: Topic :: Scientific/Engineering +Requires-Dist: numpy +Requires-Dist: pillow +Requires-Dist: scipy + +This library provides easy access to common as well as +state-of-the-art video processing routines. Check out the +website for more details. + + + diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/RECORD b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..ab1a131899750c9da317e03ba906dbbd8a8f5cc2 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/RECORD @@ -0,0 +1,116 @@ +scikit_video-1.1.11.dist-info/DESCRIPTION.rst,sha256=iOZTVGWLIqe2Mq3SzBfrUR1n1_rHLl2VWrA1RZ71hMk,142 +scikit_video-1.1.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +scikit_video-1.1.11.dist-info/METADATA,sha256=EvN8JAojYGtTP3fyEDDg7Vz7TcS57ccuK8AQI6ba_f4,1084 +scikit_video-1.1.11.dist-info/RECORD,, +scikit_video-1.1.11.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scikit_video-1.1.11.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110 +scikit_video-1.1.11.dist-info/metadata.json,sha256=jgcoMXj5DM3ss7He5KuoEEG1Fp0DWf2jS4Zyq120-pQ,1059 +scikit_video-1.1.11.dist-info/top_level.txt,sha256=sFcPKt1c2OIATzJhWvClOk9dP_GL0NMm7j7O-ixwh4I,8 +skvideo/__init__.py,sha256=BwOyCFbVaTHkj_XhDX2tTLnAgAWHzTqFvxgIkdkWyaE,14599 +skvideo/__pycache__/__init__.cpython-310.pyc,, +skvideo/__pycache__/setup.cpython-310.pyc,, +skvideo/datasets/__init__.py,sha256=I2J9CNvY4FoqHlgSAfA9rwf_OwjeElZvKNGd9rLuN4U,1264 +skvideo/datasets/__pycache__/__init__.cpython-310.pyc,, +skvideo/datasets/__pycache__/setup.cpython-310.pyc,, +skvideo/datasets/data/bigbuckbunny.mp4,sha256=8lsx8VWXDEYwCTS9pKds0vWBrKtFxJdigy_9_dvPn90,1055736 +skvideo/datasets/data/bikes.mp4,sha256=kQKPnWxyzIE32L0FZ4vfz1q3yP2de3fecM56Ot4le7U,509868 +skvideo/datasets/data/carphone_distorted.mp4,sha256=RgUaO5BgWZ11MG9oKvkZJ_M-I7aNFMFcCXjh8FcuwF4,7019 +skvideo/datasets/data/carphone_pristine.mp4,sha256=HErdeDiwe01lrZ1m6UkXWMfbtscXSQ20t57Pn_grqyg,588804 +skvideo/datasets/setup.py,sha256=ZW73PzfaVcNEzV33ksKPMK_ctvazy2pe-NUubkmkvc4,227 +skvideo/io/__init__.py,sha256=9zhCNeb1NZ_3RtlpYaIL3QXGsQT7FtJEay-NgVoSPRI,360 +skvideo/io/__pycache__/__init__.cpython-310.pyc,, +skvideo/io/__pycache__/abstract.cpython-310.pyc,, +skvideo/io/__pycache__/avconv.cpython-310.pyc,, +skvideo/io/__pycache__/avprobe.cpython-310.pyc,, +skvideo/io/__pycache__/ffmpeg.cpython-310.pyc,, +skvideo/io/__pycache__/ffprobe.cpython-310.pyc,, +skvideo/io/__pycache__/io.cpython-310.pyc,, +skvideo/io/__pycache__/mprobe.cpython-310.pyc,, +skvideo/io/abstract.py,sha256=24uohGUZ43_PcQ5rVG-aSqyF1vZZSSnwyY9E4ebfAIc,19351 +skvideo/io/avconv.py,sha256=zS2xN6ZDezhIFAkWHWn7w-60s6ZKo1ydgZmzvS_r7HE,4581 +skvideo/io/avprobe.py,sha256=BQHTPLfE-u_mFRKHbO1elFoOYWwHBcIxGLKe_TySq4E,1444 +skvideo/io/ffmpeg.py,sha256=lqwmbu32KqQ6f9ymldn8hDqTNFLKqdzwuhb4mENhxKA,3958 +skvideo/io/ffprobe.py,sha256=DgFIIiU3Jiz-goXMcPD48GuzIfuopo7mxuAp_xrNgzs,1417 +skvideo/io/io.py,sha256=FAwSKELtjDHut6ZrUzlQGFBlrX531hpLi219I-PXi2k,8598 +skvideo/io/mprobe.py,sha256=DcUGwJezjoaleUZhlJ23QwgM3CXiCkcTShYUrXJBEeA,1763 +skvideo/measure/Li3DDCT.py,sha256=f0-l3N357_JmLWuEMfgq62EIU2NurWHvl1L4FwV9o80,4090 +skvideo/measure/__init__.py,sha256=N3suX-_cS4jvrEX1_B6rW1Zq5R5MtZ2XhU2Aro-e254,547 +skvideo/measure/__pycache__/Li3DDCT.cpython-310.pyc,, +skvideo/measure/__pycache__/__init__.cpython-310.pyc,, +skvideo/measure/__pycache__/brisque.cpython-310.pyc,, +skvideo/measure/__pycache__/mad.cpython-310.pyc,, +skvideo/measure/__pycache__/mae.cpython-310.pyc,, +skvideo/measure/__pycache__/mse.cpython-310.pyc,, +skvideo/measure/__pycache__/msssim.cpython-310.pyc,, +skvideo/measure/__pycache__/niqe.cpython-310.pyc,, +skvideo/measure/__pycache__/psnr.cpython-310.pyc,, +skvideo/measure/__pycache__/scene.cpython-310.pyc,, +skvideo/measure/__pycache__/setup.cpython-310.pyc,, +skvideo/measure/__pycache__/ssim.cpython-310.pyc,, +skvideo/measure/__pycache__/strred.cpython-310.pyc,, +skvideo/measure/__pycache__/videobliinds.cpython-310.pyc,, +skvideo/measure/__pycache__/viideo.cpython-310.pyc,, +skvideo/measure/brisque.py,sha256=ldiF59V8qumoAmmK5DrcDqCN644dPK_PRGkqZqpzlyM,2570 +skvideo/measure/data/frames_modelparameters.mat,sha256=YefZ4xjlDO-U3wQasLDv59sSrm-lGE_0ujNZDwg0Bro,8351 +skvideo/measure/data/niqe_cov_96.pkl,sha256=2aTXPCO9plz7OVubw12slIaHN1y1CD0YpbbUoxINPx0,5507 +skvideo/measure/data/niqe_image_params.mat,sha256=JpzS1f5M-q5EGIKxBuqEm7sKJ8FxdqPqOu2bvi89-18,10912 +skvideo/measure/data/niqe_mu_96.pkl,sha256=afgs7D45UJ5ByADqGdH5q2lEfXk9HDAU3I_7Rt0Gn_A,440 +skvideo/measure/mad.py,sha256=NqJXz1zPbcNpn1XSQu7QJhA2228BB97gvaUFYqbpLoM,1531 +skvideo/measure/mae.py,sha256=eWhcBpG_d4XNzj2B2pVputac7yk11iod-UZ75FAeSvA,1527 +skvideo/measure/mse.py,sha256=XfEEoRkhZtYCqNnePS6t0WqQJZnx6elHm8ANkREhCu4,1523 +skvideo/measure/msssim.py,sha256=wxiqrHA8ir-sp021scUhJiBAmhikfAvAzFb_bxaMkSg,3840 +skvideo/measure/niqe.py,sha256=snCHzctgIZWPGpkJWB6Hufg_kk_fWKqm5dleWDmMmTk,4897 +skvideo/measure/niqe_original.py,sha256=HorQN1p86bDH2bOLigtEOhDYbORR8_pI2Ai4Wr_S8zk,5686 +skvideo/measure/psnr.py,sha256=TwP64lzUCje1-kVdVU0Ika2PFZvQxKpSZ5g3WXG5i3Y,1757 +skvideo/measure/runb.py,sha256=1_wsR5M_pVsxcQv23M9Nm17w7VLj51Uyj02diwLW9Ik,542 +skvideo/measure/scene.py,sha256=f2ehHR8lRcKQnGN6mcfRhTkilVJFdwQuv9RcdgNOJYE,5248 +skvideo/measure/setup.py,sha256=rxFHBtCQKISUck8jf-ad1I5Q8bpTtYm-ybjwvoCkCcM,226 +skvideo/measure/ssim.py,sha256=XJZeE_0cSD2cL2ogN1uy7602zQj7gwRn8i8KCxj1Uow,8260 +skvideo/measure/strred.py,sha256=0T5GkdD2r5dcrD43XHAT94NxNV1IHWziAWpmMfxv1RQ,5590 +skvideo/measure/videobliinds.py,sha256=sIHkAoUASSohF1RUONrTebR4qJCooBRx_UcaNiQW6WU,12703 +skvideo/measure/viideo.py,sha256=-PGKdf77f85F-HmtR7xRm6Je6w0fazlHIv6E1OWUrc8,6845 +skvideo/motion/__init__.py,sha256=tkhJRLs-hiC_0pe9LS8yYOvzklxd9ZR5n3w1VA-S3hE,158 +skvideo/motion/__pycache__/__init__.cpython-310.pyc,, +skvideo/motion/__pycache__/block.cpython-310.pyc,, +skvideo/motion/__pycache__/gme.cpython-310.pyc,, +skvideo/motion/block.py,sha256=g3z9mL_UGgscYiUu1Bpdfsf_woVHFqCxl40a1N2WUrs,39474 +skvideo/motion/gme.py,sha256=xs-LKxYUMZYGkgaWEFkvmq7L6y5-BWkEO4aZCL7WHeU,3203 +skvideo/setup.py,sha256=py7SZ5MoMfcnZopxCjQEUkIwK6cw3r0hZG0wJkDATCs,405 +skvideo/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +skvideo/tests/__pycache__/__init__.cpython-310.pyc,, +skvideo/tests/__pycache__/test_IOfail.cpython-310.pyc,, +skvideo/tests/__pycache__/test_ffmpeg.cpython-310.pyc,, +skvideo/tests/__pycache__/test_libav.cpython-310.pyc,, +skvideo/tests/__pycache__/test_measure.cpython-310.pyc,, +skvideo/tests/__pycache__/test_measure2.cpython-310.pyc,, +skvideo/tests/__pycache__/test_motion.cpython-310.pyc,, +skvideo/tests/__pycache__/test_path.cpython-310.pyc,, +skvideo/tests/__pycache__/test_pattern.cpython-310.pyc,, +skvideo/tests/__pycache__/test_scenedet.cpython-310.pyc,, +skvideo/tests/__pycache__/test_vread.cpython-310.pyc,, +skvideo/tests/__pycache__/test_vreader.cpython-310.pyc,, +skvideo/tests/__pycache__/test_vwrite.cpython-310.pyc,, +skvideo/tests/test_IOfail.py,sha256=lUDnWaTXjmFm-IUNjp85kYYqgHAl6kx1leuGCHeLJpA,944 +skvideo/tests/test_ffmpeg.py,sha256=Cp100W0XBGMs16GHLIvrhBjN9rIyKwj1CdQK4ww_72E,3908 +skvideo/tests/test_libav.py,sha256=zZGTfrFd8vM4N254ABTGwDjr8KRNRkJ2r_Kj3wMwjpU,5650 +skvideo/tests/test_measure.py,sha256=G5j-jIaqU4mLcIndejzQXtay4JmntC96sfhAXdAJhQw,6485 +skvideo/tests/test_measure2.py,sha256=ovVUnAr32tBzC4QlRdXMU-r3-Hf6iO2nY52kmssvPQo,674 +skvideo/tests/test_motion.py,sha256=gIpnhmCIMaRV_8EvwglH-6OTxd26g3unLYO7BJx-26o,3166 +skvideo/tests/test_path.py,sha256=PqTGJp2qRYpAZYHEBH_e4XG-4nFY8hUhRWKzrtWGQuM,1929 +skvideo/tests/test_pattern.py,sha256=kHXDCRA8-e3TNRmLo4C4y4jWbKQc1mfAEgfoAf2IOYY,3444 +skvideo/tests/test_scenedet.py,sha256=1kt7JbtxyZwA4SmT8gM1UI-dqqanOd03Bgyj_m4hTDE,886 +skvideo/tests/test_vread.py,sha256=WHZRHrpIokvo1mFUqAicqGQjfeRWKPQ7qIWXbesjyUs,5660 +skvideo/tests/test_vreader.py,sha256=JdXZGv3czufQubLkRX2OEyvYlmfDjztq5KSRE1nsLQ8,1092 +skvideo/tests/test_vwrite.py,sha256=iYiQdj6Q0Aztp7BDiKYFPutaolQElax81YYdfd-ypAo,1261 +skvideo/utils/__init__.py,sha256=VJVEiCUd_NubH2wl0ZTeaOpV47nrJSSf9gLF3Jye8Mw,10232 +skvideo/utils/__pycache__/__init__.cpython-310.pyc,, +skvideo/utils/__pycache__/edge.cpython-310.pyc,, +skvideo/utils/__pycache__/mscn.cpython-310.pyc,, +skvideo/utils/__pycache__/stats.cpython-310.pyc,, +skvideo/utils/__pycache__/stpyr.cpython-310.pyc,, +skvideo/utils/__pycache__/xmltodict.cpython-310.pyc,, +skvideo/utils/edge.py,sha256=2AU1JkW5ZQa_-E147zrdOsGAjLpNSVvm8UwNvZGVFlY,5405 +skvideo/utils/mscn.py,sha256=0cymVLbWVk2s3SR8_CAoC6dQATSJLQLCCsWxZ1GfO1Y,1284 +skvideo/utils/stats.py,sha256=aJCfSNxRMaJQ4bp1Z9PONtBlzUlOIp11aCOE26c4plU,2206 +skvideo/utils/stpyr.py,sha256=Jh3i-oKEmdjCo7p-AavT5apjNtHPj65p6_Rc7t2d7Pc,19304 +skvideo/utils/xmltodict.py,sha256=cg_dY9XbmYD86z8ju0tYsoZiPcylZgToEJ4FB_WG-xQ,15157 diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/REQUESTED b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/WHEEL b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..8b6dd1b5a884bfc07b5f771ab5dc56ed02323531 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.29.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/metadata.json b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..80070a16111409d479e7525ce9ddb3f503c5fa81 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/metadata.json @@ -0,0 +1 @@ +{"classifiers": ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: POSIX :: Linux", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.6", "Topic :: Multimedia :: Video", "Topic :: Scientific/Engineering"], "description_content_type": "UNKNOWN", "download_url": "https://github.com/scikit-video/scikit-video", "extensions": {"python.details": {"contacts": [{"email": "info@scikit-video.org", "name": "('Todd Goodall',)", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://scikit-video.org/"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "license": "BSD", "metadata_version": "2.0", "name": "scikit-video", "run_requires": [{"requires": ["numpy", "pillow", "scipy"]}], "summary": "Video Processing in Python", "version": "1.1.11"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/top_level.txt b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..ee4392a62487bda2beceab6b76cd62b830b340c5 --- /dev/null +++ b/lib/python3.10/site-packages/scikit_video-1.1.11.dist-info/top_level.txt @@ -0,0 +1 @@ +skvideo diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/INSTALLER b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/LICENSE.txt b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..117117616e806a9bb960b1f32a552c8c912b0497 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/LICENSE.txt @@ -0,0 +1,30 @@ +Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/METADATA b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..7c782d553b632e71908629316b57ef304bac93e3 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/METADATA @@ -0,0 +1,178 @@ +Metadata-Version: 2.1 +Name: scipy +Version: 1.15.2 +Summary: Fundamental algorithms for scientific computing in Python +Maintainer-Email: SciPy Developers +License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: C +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: Topic :: Software Development :: Libraries +Classifier: Topic :: Scientific/Engineering +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +Project-URL: homepage, https://scipy.org/ +Project-URL: documentation, https://docs.scipy.org/doc/scipy/ +Project-URL: source, https://github.com/scipy/scipy +Project-URL: download, https://github.com/scipy/scipy/releases +Project-URL: tracker, https://github.com/scipy/scipy/issues +Requires-Python: >=3.10 +Requires-Dist: numpy<2.5,>=1.23.5 +Provides-Extra: test +Requires-Dist: pytest; extra == "test" +Requires-Dist: pytest-cov; extra == "test" +Requires-Dist: pytest-timeout; extra == "test" +Requires-Dist: pytest-xdist; extra == "test" +Requires-Dist: asv; extra == "test" +Requires-Dist: mpmath; extra == "test" +Requires-Dist: gmpy2; extra == "test" +Requires-Dist: threadpoolctl; extra == "test" +Requires-Dist: scikit-umfpack; extra == "test" +Requires-Dist: pooch; extra == "test" +Requires-Dist: hypothesis>=6.30; extra == "test" +Requires-Dist: array-api-strict<2.1.1,>=2.0; extra == "test" +Requires-Dist: Cython; extra == "test" +Requires-Dist: meson; extra == "test" +Requires-Dist: ninja; sys_platform != "emscripten" and extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx<8.0.0,>=5.0.0; extra == "doc" +Requires-Dist: intersphinx_registry; extra == "doc" +Requires-Dist: pydata-sphinx-theme>=0.15.2; extra == "doc" +Requires-Dist: sphinx-copybutton; extra == "doc" +Requires-Dist: sphinx-design>=0.4.0; extra == "doc" +Requires-Dist: matplotlib>=3.5; extra == "doc" +Requires-Dist: numpydoc; extra == "doc" +Requires-Dist: jupytext; extra == "doc" +Requires-Dist: myst-nb; extra == "doc" +Requires-Dist: pooch; extra == "doc" +Requires-Dist: jupyterlite-sphinx>=0.16.5; extra == "doc" +Requires-Dist: jupyterlite-pyodide-kernel; extra == "doc" +Provides-Extra: dev +Requires-Dist: mypy==1.10.0; extra == "dev" +Requires-Dist: typing_extensions; extra == "dev" +Requires-Dist: types-psutil; extra == "dev" +Requires-Dist: pycodestyle; extra == "dev" +Requires-Dist: ruff>=0.0.292; extra == "dev" +Requires-Dist: cython-lint>=0.12.2; extra == "dev" +Requires-Dist: rich-click; extra == "dev" +Requires-Dist: doit>=0.36.0; extra == "dev" +Requires-Dist: pydevtool; extra == "dev" +Description-Content-Type: text/x-rst + +.. image:: https://raw.githubusercontent.com/scipy/scipy/main/doc/source/_static/logo.svg + :target: https://scipy.org + :width: 110 + :height: 110 + :align: left + +.. image:: https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A + :target: https://numfocus.org + +.. image:: https://img.shields.io/pypi/dm/scipy.svg?label=Pypi%20downloads + :target: https://pypi.org/project/scipy/ + +.. image:: https://img.shields.io/conda/dn/conda-forge/scipy.svg?label=Conda%20downloads + :target: https://anaconda.org/conda-forge/scipy + +.. image:: https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg + :target: https://stackoverflow.com/questions/tagged/scipy + +.. image:: https://img.shields.io/badge/DOI-10.1038%2Fs41592--019--0686--2-blue.svg + :target: https://www.nature.com/articles/s41592-019-0686-2 + +SciPy (pronounced "Sigh Pie") is an open-source software for mathematics, +science, and engineering. It includes modules for statistics, optimization, +integration, linear algebra, Fourier transforms, signal and image processing, +ODE solvers, and more. + +- **Website:** https://scipy.org +- **Documentation:** https://docs.scipy.org/doc/scipy/ +- **Development version of the documentation:** https://scipy.github.io/devdocs +- **SciPy development forum:** https://discuss.scientific-python.org/c/contributor/scipy +- **Stack Overflow:** https://stackoverflow.com/questions/tagged/scipy +- **Source code:** https://github.com/scipy/scipy +- **Contributing:** https://scipy.github.io/devdocs/dev/index.html +- **Bug reports:** https://github.com/scipy/scipy/issues +- **Code of Conduct:** https://docs.scipy.org/doc/scipy/dev/conduct/code_of_conduct.html +- **Report a security vulnerability:** https://tidelift.com/docs/security +- **Citing in your work:** https://www.scipy.org/citing-scipy/ + +SciPy is built to work with +NumPy arrays, and provides many user-friendly and efficient numerical routines, +such as routines for numerical integration and optimization. Together, they +run on all popular operating systems, are quick to install, and are free of +charge. NumPy and SciPy are easy to use, but powerful enough to be depended +upon by some of the world's leading scientists and engineers. If you need to +manipulate numbers on a computer and display or publish the results, give +SciPy a try! + +For the installation instructions, see `our install +guide `__. + + +Call for Contributions +---------------------- + +We appreciate and welcome contributions. Small improvements or fixes are always appreciated; issues labeled as "good +first issue" may be a good starting point. Have a look at `our contributing +guide `__. + +Writing code isn’t the only way to contribute to SciPy. You can also: + +- review pull requests +- triage issues +- develop tutorials, presentations, and other educational materials +- maintain and improve `our website `__ +- develop graphic design for our brand assets and promotional materials +- help with outreach and onboard new contributors +- write grant proposals and help with other fundraising efforts + +If you’re unsure where to start or how your skills fit in, reach out! You can +ask on the `forum `__ +or here, on GitHub, by leaving a comment on a relevant issue that is already +open. + +If you are new to contributing to open source, `this +guide `__ helps explain why, what, +and how to get involved. diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/RECORD b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..c0e1ccd427eb10c4ab36b8be5ce6e555db629bf0 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/RECORD @@ -0,0 +1,1300 @@ +scipy-1.15.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +scipy-1.15.2.dist-info/LICENSE.txt,sha256=wp-45ZNvMyN6ixUla0xukwN_Bbv1jD17V1Wu3kwgAXM,1536 +scipy-1.15.2.dist-info/METADATA,sha256=2166gH1PtGwU2A-pNAQaxvOtTnNGbN2Bbh7djb4euHs,8511 +scipy-1.15.2.dist-info/RECORD,, +scipy-1.15.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy-1.15.2.dist-info/WHEEL,sha256=JzzI8nVJN3Hvt8OYHYee974X3qSPVns_-ot71BuB6Rc,88 +scipy-1.15.2.dist-info/direct_url.json,sha256=di_z4l6Jw-NsNBKG9RZgR3T3e9jn1c79zSxvjZdM3Do,329 +scipy/__config__.py,sha256=CAurmxkhF5DrCa4yaYMO1hZauS9RbyusVuVSwad670g,16678 +scipy/__init__.py,sha256=GFkTqhB1Evr9XPid_UUqhxm0Wm66gz4tzuLL_Ri0u-U,4153 +scipy/__pycache__/__config__.cpython-310.pyc,, +scipy/__pycache__/__init__.cpython-310.pyc,, +scipy/__pycache__/_distributor_init.cpython-310.pyc,, +scipy/__pycache__/conftest.cpython-310.pyc,, +scipy/__pycache__/version.cpython-310.pyc,, +scipy/_distributor_init.py,sha256=zJThN3Fvof09h24804pNDPd2iN-lCHV3yPlZylSefgQ,611 +scipy/_lib/__init__.py,sha256=CXrH_YBpZ-HImHHrqXIhQt_vevp4P5NXClp7hnFMVLM,353 +scipy/_lib/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/__pycache__/_array_api.cpython-310.pyc,, +scipy/_lib/__pycache__/_array_api_no_0d.cpython-310.pyc,, +scipy/_lib/__pycache__/_bunch.cpython-310.pyc,, +scipy/_lib/__pycache__/_ccallback.cpython-310.pyc,, +scipy/_lib/__pycache__/_disjoint_set.cpython-310.pyc,, +scipy/_lib/__pycache__/_docscrape.cpython-310.pyc,, +scipy/_lib/__pycache__/_elementwise_iterative_method.cpython-310.pyc,, +scipy/_lib/__pycache__/_finite_differences.cpython-310.pyc,, +scipy/_lib/__pycache__/_gcutils.cpython-310.pyc,, +scipy/_lib/__pycache__/_pep440.cpython-310.pyc,, +scipy/_lib/__pycache__/_testutils.cpython-310.pyc,, +scipy/_lib/__pycache__/_threadsafety.cpython-310.pyc,, +scipy/_lib/__pycache__/_tmpdirs.cpython-310.pyc,, +scipy/_lib/__pycache__/_util.cpython-310.pyc,, +scipy/_lib/__pycache__/decorator.cpython-310.pyc,, +scipy/_lib/__pycache__/deprecation.cpython-310.pyc,, +scipy/_lib/__pycache__/doccer.cpython-310.pyc,, +scipy/_lib/__pycache__/uarray.cpython-310.pyc,, +scipy/_lib/_array_api.py,sha256=gtUAF6O-i8eBiTl_cQHOLBv8q_EMbmkNx6Zi6qXRZNE,22051 +scipy/_lib/_array_api_no_0d.py,sha256=zVB7D070dZ9Rc-7mXvlkqpv75TgcvCy_7PL0q6yZsbg,4453 +scipy/_lib/_bunch.py,sha256=WooFxHL6t0SwjcwMDECM5wcWWLIS0St8zP3urDVK-V0,8120 +scipy/_lib/_ccallback.py,sha256=N9CO7kJYzk6IWQR5LHf_YA1-Oq48R38UIhJFIlJ2Qyc,7087 +scipy/_lib/_ccallback_c.cpython-310-x86_64-linux-gnu.so,sha256=hdHZtR0gW_U_ql83Jal_ZOzovm50ZBi3zxKxpi-880s,100512 +scipy/_lib/_disjoint_set.py,sha256=o_EUHZwnnI1m8nitEf8bSkF7TWZ65RSiklBN4daFruA,6160 +scipy/_lib/_docscrape.py,sha256=OUfg01moyk_U05boFoyiwKdpUe44iiqKcSkKVHNQsYY,23808 +scipy/_lib/_elementwise_iterative_method.py,sha256=79M1Rrgx01KoBKAgxjnY_QwbVerbnt_UpmgOYt97pwg,15277 +scipy/_lib/_finite_differences.py,sha256=llaIPvCOxpE4VA8O8EycPEU8i6LHJyOD-y7Y9OvQHt0,4172 +scipy/_lib/_fpumode.cpython-310-x86_64-linux-gnu.so,sha256=MfPxCVzonQuRnBHrBBB5zeq6b1G40seAwZshSY81pO0,15880 +scipy/_lib/_gcutils.py,sha256=hajQd-HUw9ckK7QeBaqXVRpmnxPgyXO3QqqniEh7tRk,2669 +scipy/_lib/_pep440.py,sha256=vo3nxbfjtMfGq1ektYzHIzRbj8W-NHOMp5WBRjPlDTg,14005 +scipy/_lib/_testutils.py,sha256=5Ua6vjKp02oRGpWX1icBHh1NjlgVCPRIVIrdgb9VSyc,12067 +scipy/_lib/_threadsafety.py,sha256=ttPEh64SKLjhQGZIYSm_9d5bW4cjAXoRZCA_a5-nK9M,1453 +scipy/_lib/_tmpdirs.py,sha256=z3IYpzACnWdN_BMjOvqYbkTvYyUbfbQvfehq7idENSo,2374 +scipy/_lib/_uarray/LICENSE,sha256=yAw5tfzga6SJfhTgsKiLVEWDNNlR6xNhQC_60s-4Y7Q,1514 +scipy/_lib/_uarray/__init__.py,sha256=Rww7wLA7FH6Yong7oMgl_sHPpjcRslRaTjh61W_xVg4,4493 +scipy/_lib/_uarray/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/_uarray/__pycache__/_backend.cpython-310.pyc,, +scipy/_lib/_uarray/_backend.py,sha256=LZnSLJ2UK209jrMtocOMoc5grlNoob3tbb1HbW0XlAQ,20531 +scipy/_lib/_uarray/_uarray.cpython-310-x86_64-linux-gnu.so,sha256=6u9m3xHRVwtyv8QCmZXHOzezTU-MWsI4geDAC-1NNNI,108752 +scipy/_lib/_util.py,sha256=yEp-zOqfklOTMcvzAL0S9dTffhuJDOiYchIYxWBkbFE,44605 +scipy/_lib/array_api_compat/__init__.py,sha256=jjRoCLlFhQjrHK2xCR3aHoUVjovGKMBSBsHZmi6yjjI,969 +scipy/_lib/array_api_compat/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/__pycache__/_internal.cpython-310.pyc,, +scipy/_lib/array_api_compat/_internal.py,sha256=0GHLUJRbBHZLsbgRYE0OCtxAKdYuLtr1qzh70N5vBQI,1010 +scipy/_lib/array_api_compat/common/__init__.py,sha256=HB4vvyS0GnH6JQSEgAC75oa-s2WBIiQQebpgXnW00N0,37 +scipy/_lib/array_api_compat/common/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_helpers.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/_aliases.py,sha256=Vr_64oTgASVrbawHA2oJjJhYXLPx7tXii8vFPyG8D98,17875 +scipy/_lib/array_api_compat/common/_fft.py,sha256=qZvAveqXFwEQxCbTNx9l_41EpQpAwMfwS2GqWKEVwow,4520 +scipy/_lib/array_api_compat/common/_helpers.py,sha256=4gXCgC9TRmgFlXxfHtznk6Jv7MOZ03e3xE1f7jQKaC0,23956 +scipy/_lib/array_api_compat/common/_linalg.py,sha256=BebUx7WRkz9DAx9lrrP8d57-uN0VobwLGX0xbvI-7Wg,6142 +scipy/_lib/array_api_compat/common/_typing.py,sha256=KBJcLRAG2MeID9V38-GBipfpsFWGGrxOKkgfSQmgjXE,414 +scipy/_lib/array_api_compat/cupy/__init__.py,sha256=3079YH9uF2HoG8E27bp_1lsIVvYsdrq8hKMk_jT3NFs,442 +scipy/_lib/array_api_compat/cupy/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/_aliases.py,sha256=aCmWDlvcdhagje7QDxgF-jqTmUk6mnVIl2hOky1IpBE,4538 +scipy/_lib/array_api_compat/cupy/_info.py,sha256=kdUS8xcIVg_0Mgg2qSzuqOrXgopaHO_G8JmGBB-4qOM,9805 +scipy/_lib/array_api_compat/cupy/_typing.py,sha256=oDhrZB8R-D6wvee7tR4YkyBhTq93M0fFi3Tv-lpN_Dg,617 +scipy/_lib/array_api_compat/cupy/fft.py,sha256=xCAC42CNAwAyVW7uCREsSoAV23R3rL2dqrT7w877zuE,842 +scipy/_lib/array_api_compat/cupy/linalg.py,sha256=nKOM-_wcOHzHhEeV9KBzcMVNlviJK4nP1nFBUtvnjTM,1444 +scipy/_lib/array_api_compat/dask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/_lib/array_api_compat/dask/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__init__.py,sha256=7_FttjbrGeKtPFGS_CA85WZZmbxPwkpxvsMS8KTMEFw,242 +scipy/_lib/array_api_compat/dask/array/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/_aliases.py,sha256=ERdXHmeTGKxBMSiSt_VlxsnZ0sLh9K8bxkWQT1OKqMM,6549 +scipy/_lib/array_api_compat/dask/array/_info.py,sha256=D8hG1uRNsF31_WX5bnulbdl75Jkd6G2DbkmhXXTplEs,10410 +scipy/_lib/array_api_compat/dask/array/fft.py,sha256=FWXfXVz9zUGKVtYJWl-xSb9BUp7UIewQ89FzGimwOOA,553 +scipy/_lib/array_api_compat/dask/array/linalg.py,sha256=5E3wSAXmiZJ5rf69u6Pzw1Xs0lCdMpiVBnheA4lzY4E,2441 +scipy/_lib/array_api_compat/numpy/__init__.py,sha256=uxjYAO4xcDhTQPbrD2XmkWT5TyZsjpwc5FD-ViHxN-c,831 +scipy/_lib/array_api_compat/numpy/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/_aliases.py,sha256=ZrddTjHOVUNvDM1h9p7NqXqaODVJKkKu2fTyPClCmXg,4485 +scipy/_lib/array_api_compat/numpy/_info.py,sha256=GAD-zNvAMUSeUJfjABY6p_eYkG--KBBgz1vdQkL2-UA,10384 +scipy/_lib/array_api_compat/numpy/_typing.py,sha256=OFRXfhT8-snL_4VeOjbOCd_yYIGqVS-IRrZoWNcL3v4,618 +scipy/_lib/array_api_compat/numpy/fft.py,sha256=vlrYUcv2VV5mOOEb5R4u83nFSSDmE-nfJYM-lmq1Dao,679 +scipy/_lib/array_api_compat/numpy/linalg.py,sha256=ne4h3Ui1esyzD9p7Ko2IueJvgpSUmfF_Z5aWbiBKJc0,3256 +scipy/_lib/array_api_compat/torch/__init__.py,sha256=sk32NV12KrlR8a-UjiBdjJspUcex5j7REAGgSJoI3do,591 +scipy/_lib/array_api_compat/torch/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/_aliases.py,sha256=kCIeFyzzUqNh86Byo5Ai2s1guK2-OkXg62chBCN_kgU,28559 +scipy/_lib/array_api_compat/torch/_info.py,sha256=rnInxwjMErvcHLI4S6fzom7N43hoAqS0rysw1K8Riyw,11413 +scipy/_lib/array_api_compat/torch/fft.py,sha256=AVHOwIxM-t9_w-FjVF79RrzeC5wYc5g97WPUp7bIHlA,1794 +scipy/_lib/array_api_compat/torch/linalg.py,sha256=dJ0o1gCbSDtklpvgZCxx3gbHXW9q3I4u8ZLFPW24dJs,4770 +scipy/_lib/array_api_extra/__init__.py,sha256=916j5GLpulyZZsUQa-I_r510XDVbap_aIrVpCVn_PIk,266 +scipy/_lib/array_api_extra/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_extra/__pycache__/_funcs.cpython-310.pyc,, +scipy/_lib/array_api_extra/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_extra/_funcs.py,sha256=T5nPgBxYOb8DkNHlEM52Qf70Nf7Qb6lFtlDtuvmEk4c,14906 +scipy/_lib/array_api_extra/_typing.py,sha256=E3XJz5PbjXP-ckQMQLi_nOJPLr-B0cm_EVArRwY-7FY,193 +scipy/_lib/cobyqa/__init__.py,sha256=9Gj-EtpYGRmh0-ADiX0t0psItcvMgzIMwFDzlvOzcE8,578 +scipy/_lib/cobyqa/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/framework.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/main.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/models.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/problem.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/settings.cpython-310.pyc,, +scipy/_lib/cobyqa/framework.py,sha256=lIeKCkDLxHbMmSTiMcyasvVe77jVvh_YTOYX0HnK4Qk,38900 +scipy/_lib/cobyqa/main.py,sha256=wz0M2iqFfzeTaZUq_j1TkF_9V_SJ1t73A-0fdH0eSs4,57527 +scipy/_lib/cobyqa/models.py,sha256=cAM8_np_xFSRwKsjaMRZu9Dc9xQOQPAZVWxsvR_7qjE,50656 +scipy/_lib/cobyqa/problem.py,sha256=SiPgmiFTxiW5yJ_FVf37Z9GQGo6Gx_fJ3RXMzhsrn40,40203 +scipy/_lib/cobyqa/settings.py,sha256=ogfiShxuPHsMfW16OGSwB9-mIPRiuWZSGXBOCO2HDvw,3826 +scipy/_lib/cobyqa/subsolvers/__init__.py,sha256=VmFBpi-_tNa8yzNmu_fufewmPTnCU6ycNCGcN34UBcc,341 +scipy/_lib/cobyqa/subsolvers/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/__pycache__/geometry.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/__pycache__/optim.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/geometry.py,sha256=dgS-C0QBUhkzPhHULFIRbnbFOIEB005GyPYE-i-cuFY,14173 +scipy/_lib/cobyqa/subsolvers/optim.py,sha256=hIseVqrPyI3ezICGNXkCtKlpqvAO2W6ZQe0n7sxfkss,45512 +scipy/_lib/cobyqa/utils/__init__.py,sha256=sw6g402vXaXwX7rMhxrNl5PD5OBs89l5f3XNcYApRHs,359 +scipy/_lib/cobyqa/utils/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/exceptions.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/math.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/versions.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/exceptions.py,sha256=N1JdmUxHnME95wEZHyeeF_M6GXPEqH5t3qzuXig49YE,483 +scipy/_lib/cobyqa/utils/math.py,sha256=beT-Tib41TJWZecjnKhSfu4foOLLaHlWj5CcyRhdSl4,1611 +scipy/_lib/cobyqa/utils/versions.py,sha256=eBOlEGAKFCfjFqVprdali3M1G7l0k_kxb7ku-Lz2bU0,1465 +scipy/_lib/decorator.py,sha256=-Rm0CvawUDXzPssHjts9vrDAC57_d_x4IfOAzgf19SQ,15021 +scipy/_lib/deprecation.py,sha256=2xwTeh_7Uc71zmnJW264zxjvh0LUWQqZsH6s95dQDyo,9840 +scipy/_lib/doccer.py,sha256=dzTRxBKnbl1wSILhYgrAj3-V0i0JvK-UhaWP0xJ7NpI,10907 +scipy/_lib/messagestream.cpython-310-x86_64-linux-gnu.so,sha256=4mg28AkUr0rUpGoslrkpKygbEZl1FfJYfJQ9yI1ABhk,84304 +scipy/_lib/uarray.py,sha256=4X0D3FBQR6HOYcwMftjH-38Kt1nkrS-eD4c5lWL5DGo,815 +scipy/cluster/__init__.py,sha256=pgzWiWR5smQ3rwud2dhnLn6dpkD5lju_moElQp_zhoE,880 +scipy/cluster/__pycache__/__init__.cpython-310.pyc,, +scipy/cluster/__pycache__/hierarchy.cpython-310.pyc,, +scipy/cluster/__pycache__/vq.cpython-310.pyc,, +scipy/cluster/_hierarchy.cpython-310-x86_64-linux-gnu.so,sha256=BxqKH0MkQ8w-JUqSGkm9l7MKI-gVlvuLzlF5H9Dcgw0,376800 +scipy/cluster/_optimal_leaf_ordering.cpython-310-x86_64-linux-gnu.so,sha256=6OPtocuKP5pUIQ2BzHUEwj_RO15MViEkoI_sV8MDK9o,317632 +scipy/cluster/_vq.cpython-310-x86_64-linux-gnu.so,sha256=Uz9-NVj9F2MDnMbImuIZ0eu0veSG7PAAW6Zgc5CZIT4,122552 +scipy/cluster/hierarchy.py,sha256=gXomjlief0U5nn-lYGxONKA6GMQB6Xtl0PAqJKm9e_E,149078 +scipy/cluster/vq.py,sha256=wa5bcXyigz2XiCNOu91qCuw0fvreoKSbHaRP0QQbOs4,30548 +scipy/conftest.py,sha256=Q3DbWzqWdFt8hkq16Bbg4MQ8WaxgKuhhKp6XEEZ8bWw,22027 +scipy/constants/__init__.py,sha256=1Iqylk8TvAxegNKIcFIUVXwiH5ItKpdKtCcVPhEBvPQ,14839 +scipy/constants/__pycache__/__init__.cpython-310.pyc,, +scipy/constants/__pycache__/_codata.cpython-310.pyc,, +scipy/constants/__pycache__/_constants.cpython-310.pyc,, +scipy/constants/__pycache__/codata.cpython-310.pyc,, +scipy/constants/__pycache__/constants.cpython-310.pyc,, +scipy/constants/_codata.py,sha256=fIhZGWMCGLGSwO3rnNmDEisAN1rGLwkNbSlwdZDpowQ,202354 +scipy/constants/_constants.py,sha256=1OBL3gWWsaid_3eR8t7DvzE-sN8B_AKiSUCY4PZOztM,10497 +scipy/constants/codata.py,sha256=ThmW8ohzndi-4-WtyVXxSrW40MnLIz1XoqRcm2RgSHw,614 +scipy/constants/constants.py,sha256=w7sGxSidD2Q9Ged0Sn1pnL-qqD1ssEP1A8sZWeLWBeI,2250 +scipy/datasets/__init__.py,sha256=X_9AbefPK1_pg-eG7g3nn--JhoHeDsrEFbJfbI5Hyak,2802 +scipy/datasets/__pycache__/__init__.cpython-310.pyc,, +scipy/datasets/__pycache__/_download_all.cpython-310.pyc,, +scipy/datasets/__pycache__/_fetchers.cpython-310.pyc,, +scipy/datasets/__pycache__/_registry.cpython-310.pyc,, +scipy/datasets/__pycache__/_utils.cpython-310.pyc,, +scipy/datasets/_download_all.py,sha256=iRPR2IUk6C3B5u2q77yOhac449MRSoRaTlCy2oCIknE,1701 +scipy/datasets/_fetchers.py,sha256=4sdEEQpTI99QCR9DoLv_D6Dwd4N9cSLRJX8cENX_QCg,6735 +scipy/datasets/_registry.py,sha256=br0KfyalEbh5yrQLznQ_QvBtmN4rMsm0UxOjnsJp4OQ,1072 +scipy/datasets/_utils.py,sha256=kdZ-Opp7Dr1pCwM285p3GVjgZTx_mKWCvETur92FWg4,2967 +scipy/differentiate/__init__.py,sha256=nZ3imDWtf1QzImE-xsrYHE4kuOa8tEuc99Hl0zAFqzI,621 +scipy/differentiate/__pycache__/__init__.cpython-310.pyc,, +scipy/differentiate/__pycache__/_differentiate.cpython-310.pyc,, +scipy/differentiate/_differentiate.py,sha256=zFkAn71YqLGg4rDufjlxFzhnXnHMuLuJCmIwNVQ1GG0,50595 +scipy/fft/__init__.py,sha256=0cjHIwyHnjoz1XUUe3OB70vrQR0-pFp8Uv34-U-FGRg,3632 +scipy/fft/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/__pycache__/_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_basic.cpython-310.pyc,, +scipy/fft/__pycache__/_basic_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_debug_backends.cpython-310.pyc,, +scipy/fft/__pycache__/_fftlog.cpython-310.pyc,, +scipy/fft/__pycache__/_fftlog_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_helper.cpython-310.pyc,, +scipy/fft/__pycache__/_realtransforms.cpython-310.pyc,, +scipy/fft/__pycache__/_realtransforms_backend.cpython-310.pyc,, +scipy/fft/_backend.py,sha256=5rBxK8GQtCMnuPHc-lNQdpH4uFFZ9_5vBukkDv6jRRA,6544 +scipy/fft/_basic.py,sha256=lGJ8qQTMXUJEbq_2vwfPPPlX7b4j358ks9LLretOtEY,62997 +scipy/fft/_basic_backend.py,sha256=Qms-BE7DCJYNSq9Vd5utnKiwVTqRIUzLYYEiMyTdpfE,7447 +scipy/fft/_debug_backends.py,sha256=RlvyunZNqaDDsI3-I6QH6GSBz_faT6EN4OONWsvMtR8,598 +scipy/fft/_fftlog.py,sha256=JeLVCAgfB99brT2Ez9tzdapmhWrTfYCUYEi2KTvPzIQ,7864 +scipy/fft/_fftlog_backend.py,sha256=UgoePwhoMoLxvQ5soSUZkVWvWWTP7y1xWVAD9BlrdJY,5304 +scipy/fft/_helper.py,sha256=wQ5ZlvOEY9snn32Yg6p0W_DcQu70JRaHTu_lrrODtlA,12385 +scipy/fft/_pocketfft/LICENSE.md,sha256=wlSytf0wrjyJ02ugYXMFY7l2D8oE8bdGobLDFX2ix4k,1498 +scipy/fft/_pocketfft/__init__.py,sha256=dROVDi9kRvkbSdynd3L09tp9_exzQ4QqG3xnNx78JeU,207 +scipy/fft/_pocketfft/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/basic.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/helper.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/realtransforms.cpython-310.pyc,, +scipy/fft/_pocketfft/basic.py,sha256=4HR-eRDb6j4YR4sqKnTikFmG0tnUIXxa0uImnB6_JVs,8138 +scipy/fft/_pocketfft/helper.py,sha256=mmiRCzeNuPSUUFYubG1VRO4nMIRDDelSGDZrdomBno0,5841 +scipy/fft/_pocketfft/pypocketfft.cpython-310-x86_64-linux-gnu.so,sha256=3XVKC2oWzBKvKuvPQVHBhT3kL0dpt4OEF4Ayo6XNYH4,1051696 +scipy/fft/_pocketfft/realtransforms.py,sha256=4TmqAkCDQK3gs1ddxXY4rOrVfvQqO8NyVtOzziUGw6E,3344 +scipy/fft/_realtransforms.py,sha256=QmO9CDqrAsvBcLNgIzFBIWBTYsSUCRJ_Cj1myv73KlE,25386 +scipy/fft/_realtransforms_backend.py,sha256=u4y4nBGCxpTLVqxK1J7xV6tcpeC3-8iiSEXLOcRM9wI,2389 +scipy/fftpack/__init__.py,sha256=rLCBFC5Dx5ij_wmL7ChiGmScYlgu0mhaWtrJaz_rBt0,3155 +scipy/fftpack/__pycache__/__init__.cpython-310.pyc,, +scipy/fftpack/__pycache__/_basic.cpython-310.pyc,, +scipy/fftpack/__pycache__/_helper.cpython-310.pyc,, +scipy/fftpack/__pycache__/_pseudo_diffs.cpython-310.pyc,, +scipy/fftpack/__pycache__/_realtransforms.cpython-310.pyc,, +scipy/fftpack/__pycache__/basic.cpython-310.pyc,, +scipy/fftpack/__pycache__/helper.cpython-310.pyc,, +scipy/fftpack/__pycache__/pseudo_diffs.cpython-310.pyc,, +scipy/fftpack/__pycache__/realtransforms.cpython-310.pyc,, +scipy/fftpack/_basic.py,sha256=Sk_gfswmWKb3za6wrU_mIrRVBl69qjzAu9ltznbDCKs,13098 +scipy/fftpack/_helper.py,sha256=8r6Hh2FA5qTzYyn8y4jfaG41FXMfqQyK6SN8x1dIbaE,3348 +scipy/fftpack/_pseudo_diffs.py,sha256=T39Owz8EgL4oqmViBT0ggen9DXOtNHWRxh-n6I7pLyw,15936 +scipy/fftpack/_realtransforms.py,sha256=2k91B3tSnFm6gKsQn-hRGx4J238CKvqwvQevKgDMuaQ,19222 +scipy/fftpack/basic.py,sha256=i2CMMS__L3UtFFqe57E0cs7AZ4U6VO-Ted1KhU7_wNc,577 +scipy/fftpack/convolve.cpython-310-x86_64-linux-gnu.so,sha256=KXJYOdyuhFt7yZgsV6UboDRA6Q2Obg2bCGVVmVUP7U4,234728 +scipy/fftpack/helper.py,sha256=M7jTN4gQIRWpkArQR13bI7WN6WcW-AabxKgrOHRvfeQ,580 +scipy/fftpack/pseudo_diffs.py,sha256=h0vkjsSqAThy7OdTkYWVxQqZ3rILohg7MXJqf5CGMTE,658 +scipy/fftpack/realtransforms.py,sha256=9-mR-VV3W14oTaD6pB5-RIDV3vkTBQmGCcxfbA8GYH0,595 +scipy/integrate/__init__.py,sha256=CmPLfkF66jXhHsKyQPOsvFEc9nxicRYwl6WDAa7cfJk,4373 +scipy/integrate/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/__pycache__/_bvp.cpython-310.pyc,, +scipy/integrate/__pycache__/_cubature.cpython-310.pyc,, +scipy/integrate/__pycache__/_lebedev.cpython-310.pyc,, +scipy/integrate/__pycache__/_ode.cpython-310.pyc,, +scipy/integrate/__pycache__/_odepack_py.cpython-310.pyc,, +scipy/integrate/__pycache__/_quad_vec.cpython-310.pyc,, +scipy/integrate/__pycache__/_quadpack_py.cpython-310.pyc,, +scipy/integrate/__pycache__/_quadrature.cpython-310.pyc,, +scipy/integrate/__pycache__/_tanhsinh.cpython-310.pyc,, +scipy/integrate/__pycache__/dop.cpython-310.pyc,, +scipy/integrate/__pycache__/lsoda.cpython-310.pyc,, +scipy/integrate/__pycache__/odepack.cpython-310.pyc,, +scipy/integrate/__pycache__/quadpack.cpython-310.pyc,, +scipy/integrate/__pycache__/vode.cpython-310.pyc,, +scipy/integrate/_bvp.py,sha256=0EazRKECaOErYe_MAAbmgRrbkdOgSXpwkQfwPLxP30I,40897 +scipy/integrate/_cubature.py,sha256=DI7iFsEgT4LpuPzXKReXqCWCwhXlsMWvhBiH_tkAKTY,25671 +scipy/integrate/_dop.cpython-310-x86_64-linux-gnu.so,sha256=D4M-mJgcn70KHcbq5V1r_MqRfIYQTZI3U17gK7OXkWw,100800 +scipy/integrate/_ivp/__init__.py,sha256=gKFR_pPjr8fRLgAGY5sOzYKGUFu2nGX8x1RrXT-GZZc,256 +scipy/integrate/_ivp/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/base.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/bdf.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/common.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/dop853_coefficients.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/ivp.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/lsoda.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/radau.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/rk.cpython-310.pyc,, +scipy/integrate/_ivp/base.py,sha256=Mlef_dgmn0wzjFxZA3oBbtHrQgrfdZw_8k1mLYNZP4A,10295 +scipy/integrate/_ivp/bdf.py,sha256=tTN2OiFRjGlIT-PkrCLi-mBfUmcAZ8NEprFSjwR_K5U,17501 +scipy/integrate/_ivp/common.py,sha256=IzV9Uo_cmUsIpvHMR1yPaqrpkqLdYHW6VYxSaFLh_oI,15751 +scipy/integrate/_ivp/dop853_coefficients.py,sha256=OrYvW0Hu6X7sOh37FU58gNkgC77KVpYclewv_ARGMAE,7237 +scipy/integrate/_ivp/ivp.py,sha256=DqTbmqbGiIB33wSlwyc8Z0ZHfclneqyWIhJIxmmXHpo,31473 +scipy/integrate/_ivp/lsoda.py,sha256=t5t2jZBgBPt0G20TOI4SVXuGFAZYAhfDlJZhfCzeeDo,9927 +scipy/integrate/_ivp/radau.py,sha256=0KpFk0Me857geCXbbvAyTkqbrO8OI_2kLTdzGLpqYlY,19676 +scipy/integrate/_ivp/rk.py,sha256=-l1jAJF_T5SeaZsRb1muFHFZ1cYUfVXZQNydMwOJEFY,22800 +scipy/integrate/_lebedev.py,sha256=Tj3I_tnQ3_mfARK_scDsd9aM5dLe9To-GeaCda5OMKw,262024 +scipy/integrate/_lsoda.cpython-310-x86_64-linux-gnu.so,sha256=-MoYnuJnEUZZ40ezt3BCc3gz18VuHMLBKd2jqVo_y2M,99640 +scipy/integrate/_ode.py,sha256=Wm6XtYfe11GZWpnTA71N02ib-niAg2ytyens3YPB2Co,48299 +scipy/integrate/_odepack.cpython-310-x86_64-linux-gnu.so,sha256=MxictuxJLhkgYHvp1bzM_Fie8-s7qHkUT7bg6y0pixc,72936 +scipy/integrate/_odepack_py.py,sha256=DhHLB7rx0p6TrQQzQQlwzqcb8oMuFRDra0nIFryb0M8,11231 +scipy/integrate/_quad_vec.py,sha256=VKdZEaWLDNW0-2S3tcGKv386QIcUlwb-vpxPk0_NwGU,22024 +scipy/integrate/_quadpack.cpython-310-x86_64-linux-gnu.so,sha256=Fe3KZTzvR9hiqkk_jvnNmly81psfz1meGm71A3QSsnY,94520 +scipy/integrate/_quadpack_py.py,sha256=jOeoUlpqTEOh7Qw7RJxwxt5ojsW9iVsF0CaQ_kk0esE,53250 +scipy/integrate/_quadrature.py,sha256=6u3t4hUh4_3CtdHmaXAtKxB2-IBVPNO37CeEjZyS7rM,47907 +scipy/integrate/_rules/__init__.py,sha256=JNlDLTPYR-FVDeWbm9BHOot47OA8tvOj22g2iJlEsBg,328 +scipy/integrate/_rules/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_base.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_gauss_kronrod.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_gauss_legendre.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_genz_malik.cpython-310.pyc,, +scipy/integrate/_rules/_base.py,sha256=AWdkCdJTmI8m_jUGv7MAhuwKBySGzVwf0GP4b3qh7-s,17931 +scipy/integrate/_rules/_gauss_kronrod.py,sha256=ULpHMJRd0J99IFwNufur9BYG8EQhxlGj-OdCBgnE8yk,8473 +scipy/integrate/_rules/_gauss_legendre.py,sha256=KJSMmztXRqTvpmkB-ky-WSVIqAMg_GcWoewTcRxJ1Cw,1733 +scipy/integrate/_rules/_genz_malik.py,sha256=104fosqAnmCI992oY-Z9V_QiuG2ruWLmGS2U_EdshEw,7308 +scipy/integrate/_tanhsinh.py,sha256=ZENXy4PaSkPHOErW91DfDdY3hPLa4DyKqMlVHv9weCM,61352 +scipy/integrate/_vode.cpython-310-x86_64-linux-gnu.so,sha256=M8PTY2F2ey-NP2f7vCvsvKRV5I5giZMCoGW7g5ULrlw,147512 +scipy/integrate/dop.py,sha256=Kx5Ed_Te81X09bvGmBUq3-_kQNdTIsOdO7ykjEpEG9c,422 +scipy/integrate/lsoda.py,sha256=hUg4-tJcW3MjhLjLBsD88kzP7qGp_zLGw1AH2ZClHmw,436 +scipy/integrate/odepack.py,sha256=G5KiKninKFyYgF756_LtDGB68BGk7IwPidUOywFpLQo,545 +scipy/integrate/quadpack.py,sha256=vQNE5jQ-dFpH26er1i8LJSkylFVbeSgVGLwSRQawfYg,604 +scipy/integrate/vode.py,sha256=DPRqm2oBQx6KKi5tl9dDVpXEdAO--W0WpRQEyLeQpf4,424 +scipy/interpolate/__init__.py,sha256=QlL_nJvEkGbheWI4k2AgPf_FZ9QQdwKv807y1eFiLp4,3817 +scipy/interpolate/__pycache__/__init__.cpython-310.pyc,, +scipy/interpolate/__pycache__/_bary_rational.cpython-310.pyc,, +scipy/interpolate/__pycache__/_bsplines.cpython-310.pyc,, +scipy/interpolate/__pycache__/_cubic.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack2.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_impl.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_py.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_repro.cpython-310.pyc,, +scipy/interpolate/__pycache__/_interpolate.cpython-310.pyc,, +scipy/interpolate/__pycache__/_ndbspline.cpython-310.pyc,, +scipy/interpolate/__pycache__/_ndgriddata.cpython-310.pyc,, +scipy/interpolate/__pycache__/_pade.cpython-310.pyc,, +scipy/interpolate/__pycache__/_polyint.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rbf.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rbfinterp.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rgi.cpython-310.pyc,, +scipy/interpolate/__pycache__/dfitpack.cpython-310.pyc,, +scipy/interpolate/__pycache__/fitpack.cpython-310.pyc,, +scipy/interpolate/__pycache__/fitpack2.cpython-310.pyc,, +scipy/interpolate/__pycache__/interpnd.cpython-310.pyc,, +scipy/interpolate/__pycache__/interpolate.cpython-310.pyc,, +scipy/interpolate/__pycache__/ndgriddata.cpython-310.pyc,, +scipy/interpolate/__pycache__/polyint.cpython-310.pyc,, +scipy/interpolate/__pycache__/rbf.cpython-310.pyc,, +scipy/interpolate/_bary_rational.py,sha256=0iyVrHJy5GDXpIw7cn5TjE78xnAFsbImKOSReqD4zcg,27865 +scipy/interpolate/_bspl.cpython-310-x86_64-linux-gnu.so,sha256=VwhZqTbaR4xKCR9nRluyfs6zbISvxmfYxAJl0APlr-Q,267496 +scipy/interpolate/_bsplines.py,sha256=iISJYDQooeLPFKTyuqtW_RMPPqxNmBFQMdoDvUEYLd0,82693 +scipy/interpolate/_cubic.py,sha256=boYHRQjLhs9PlIR5WOFoky8MoH2xEwNUcIHxK3t9J-Q,37727 +scipy/interpolate/_dfitpack.cpython-310-x86_64-linux-gnu.so,sha256=YULo2Z8gzZvYG9ZUKT0433VLvYz5fYXzcXOHx6Wz0T8,325856 +scipy/interpolate/_dierckx.cpython-310-x86_64-linux-gnu.so,sha256=FYcjRy_uZU24RROyHFdA_87mofBxJaRZ6bmUGrGTk7A,67792 +scipy/interpolate/_fitpack.cpython-310-x86_64-linux-gnu.so,sha256=Yx2I55wmlEQHkh1EGFGlvCrv3-68EAFcgxlYYhlONxM,88632 +scipy/interpolate/_fitpack2.py,sha256=DS3mjEptn2DJEqQ3NQ5WZUZWNYMLdCK_YBffwDUo5dQ,89728 +scipy/interpolate/_fitpack_impl.py,sha256=hSnz9q_sibFKhgPlrhlb4a0VvanoIh8sWJjxYooibmY,28678 +scipy/interpolate/_fitpack_py.py,sha256=sCzWA-X8ulb0bn-YcaBq9Zo1fpHD0nAoKmURIMbqGek,32157 +scipy/interpolate/_fitpack_repro.py,sha256=RWdm7I9LBGm5_CBWcgJZYD7MXhppnrj0GZx4-6IAAcI,36710 +scipy/interpolate/_interpnd.cpython-310-x86_64-linux-gnu.so,sha256=8Bx7XNsuD9m9uvD6lYWUe6_S_U2dG9LUGU1-4qdsVyg,399936 +scipy/interpolate/_interpolate.py,sha256=Yqk9e3zK42-2emJcWDRzTC80tErXnyMkPkKyAs4-TYY,79656 +scipy/interpolate/_ndbspline.py,sha256=RdwKfjW87UC_oJASnDcbiiFl22DJo3Z9y1zRlMFqzVc,14900 +scipy/interpolate/_ndgriddata.py,sha256=AZk11XftWehCBhiQv7WRqUV0sLS5ltU1IUbOuHRJJN8,12093 +scipy/interpolate/_pade.py,sha256=OBorKWc3vCSGlsWrajoF1_7WeNd9QtdbX0wOHLdRI2A,1827 +scipy/interpolate/_polyint.py,sha256=jnfDD6IpNvu2OeL4x7bVL1icdKNW1-EPKLDTdTBbHwA,36366 +scipy/interpolate/_ppoly.cpython-310-x86_64-linux-gnu.so,sha256=rl8m_zVq8J6WQh3qvZ0TDvZ_rIBXUulDTv4i0rKaooc,419736 +scipy/interpolate/_rbf.py,sha256=tBeBsMEe_NO1yxEv8PsX8ngVearEn1VfOyrCqEfr_Uc,11674 +scipy/interpolate/_rbfinterp.py,sha256=bzuAuZpojP-cKCukD3jVekbQzZfHnrUT13Sex5pkKOI,19723 +scipy/interpolate/_rbfinterp_pythran.cpython-310-x86_64-linux-gnu.so,sha256=fCCvcuQ9Nf7-_wW0RDDQ-a98TsV2YoX6BhYNsQw5pDA,219960 +scipy/interpolate/_rgi.py,sha256=M1RJ3ftZ4xfM3teo_UWt-ga7gn47yfJNm4BWmmqNqBU,31001 +scipy/interpolate/_rgi_cython.cpython-310-x86_64-linux-gnu.so,sha256=X5AFRoYL4E5Yl8mb33LactZ7OPAjvemfX5DB4O9I-1I,257360 +scipy/interpolate/dfitpack.py,sha256=z3AS0QKeTqVA-yV2RpSdmYAhL5g5sKud3c-0BcXLexA,915 +scipy/interpolate/fitpack.py,sha256=aCH6A3dRouuXW47tK5lEdd2pJa39LCkewY-1zTlI8Hc,702 +scipy/interpolate/fitpack2.py,sha256=P15_3gM5eZQYb_-K3c70xKdeIGM81u5WAkVhY8ei4N0,817 +scipy/interpolate/interpnd.py,sha256=FDGYwstwT7H3KxD0YcQdbRLti8QkuuMlT7MUdgYRixQ,683 +scipy/interpolate/interpolate.py,sha256=Aiu_dJ_oxq-Y1VXns5N5u5K1Wng2hzCgRgRiDhTAiVI,754 +scipy/interpolate/ndgriddata.py,sha256=VbvvoDPdWmrk8871y5olPS9StX0S_B27j_oGMAyj8QQ,636 +scipy/interpolate/polyint.py,sha256=ek1EtbIbLLwehb8XDSKeNvIdjTfDQoQ9CSu4TbY8Vbo,672 +scipy/interpolate/rbf.py,sha256=6oBxdpsKY8bH36nQnRNiLB9C1bNri8b2PHz9IsUIr-w,519 +scipy/io/__init__.py,sha256=XegFIpTjKz9NXsHPLcvnYXT-mzUrMqPJUD7a8dhUK_0,2735 +scipy/io/__pycache__/__init__.cpython-310.pyc,, +scipy/io/__pycache__/_fortran.cpython-310.pyc,, +scipy/io/__pycache__/_idl.cpython-310.pyc,, +scipy/io/__pycache__/_mmio.cpython-310.pyc,, +scipy/io/__pycache__/_netcdf.cpython-310.pyc,, +scipy/io/__pycache__/harwell_boeing.cpython-310.pyc,, +scipy/io/__pycache__/idl.cpython-310.pyc,, +scipy/io/__pycache__/mmio.cpython-310.pyc,, +scipy/io/__pycache__/netcdf.cpython-310.pyc,, +scipy/io/__pycache__/wavfile.cpython-310.pyc,, +scipy/io/_fast_matrix_market/__init__.py,sha256=EmT5UuApydDttAWNYvZw3lbBuJMkw73dloawtX0o3uQ,17123 +scipy/io/_fast_matrix_market/__pycache__/__init__.cpython-310.pyc,, +scipy/io/_fast_matrix_market/_fmm_core.cpython-310-x86_64-linux-gnu.so,sha256=w3qTv1rPLQrZUj6ddfdz8c8RSGcXRZ51Fov8dG0qOow,4074120 +scipy/io/_fortran.py,sha256=pgbB0LbOKEfPk07y-9IQXUyT7Kx_wHP0AyGPLtC53yM,10893 +scipy/io/_harwell_boeing/__init__.py,sha256=90qYbBzDEoTMG8ouVLGnTU2GMsY4BYOOtwJdoKT3Zz8,164 +scipy/io/_harwell_boeing/__pycache__/__init__.cpython-310.pyc,, +scipy/io/_harwell_boeing/__pycache__/_fortran_format_parser.cpython-310.pyc,, +scipy/io/_harwell_boeing/__pycache__/hb.cpython-310.pyc,, +scipy/io/_harwell_boeing/_fortran_format_parser.py,sha256=beJJq2mckeU_Hu4ZM_WvrHCICJOvghI4R4bAvOnH48Q,9025 +scipy/io/_harwell_boeing/hb.py,sha256=e4FbmYCXO4omXFcMW2n6qk_Cdcwx1eKHyUD5H-B71fc,19515 +scipy/io/_idl.py,sha256=-31PPsVEtNR8It3clEfZuGRCzeBrB9OSQdkeOwNpsu0,27075 +scipy/io/_mmio.py,sha256=Pk9Qmf4r-g7-ZQE9cCsu9_BaqiQJDRcnYlJL840WeQo,32094 +scipy/io/_netcdf.py,sha256=wSulfl-YWbyIxhwF4w5gDpINzUAsvOXRXa4rWHSz8p0,39223 +scipy/io/arff/__init__.py,sha256=czaV8hvY6JnmEn2qyU3_fzcy_P55aXVT09OzGnhJT9I,805 +scipy/io/arff/__pycache__/__init__.cpython-310.pyc,, +scipy/io/arff/__pycache__/_arffread.cpython-310.pyc,, +scipy/io/arff/__pycache__/arffread.cpython-310.pyc,, +scipy/io/arff/_arffread.py,sha256=uOomT89u1pVrDdGKujArTE_e6Xz3Cw2f2ACPTPS6DlY,25752 +scipy/io/arff/arffread.py,sha256=KW6mASZrW2J1wmC3GYucy1EO7y-rg5MgcGDMyMTpfw4,575 +scipy/io/harwell_boeing.py,sha256=BzISbfgVnrO3vYx-mP2xkLqh9r3oq64NNPbEY03P6v0,538 +scipy/io/idl.py,sha256=A1QV5h6xBa1cTIejjsc1NfjG0MqMbxqFqXicC2OLNrM,504 +scipy/io/matlab/__init__.py,sha256=z1F-sXRyay69RcZUHjWSFe0IVKNKQbbMwQMrGD8i4qI,2156 +scipy/io/matlab/__pycache__/__init__.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_byteordercodes.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio4.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio5.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio5_params.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_miobase.cpython-310.pyc,, +scipy/io/matlab/__pycache__/byteordercodes.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio4.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5_params.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5_utils.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio_utils.cpython-310.pyc,, +scipy/io/matlab/__pycache__/miobase.cpython-310.pyc,, +scipy/io/matlab/__pycache__/streams.cpython-310.pyc,, +scipy/io/matlab/_byteordercodes.py,sha256=AUMjfdIARtCGqyMgDDJBGa_EncP5ioYrEzyZqXOLRxU,1983 +scipy/io/matlab/_mio.py,sha256=Qa_FMP-Zid7tOFTNiNjnVrYi7YkK4hKtcGJiAv884Bw,13587 +scipy/io/matlab/_mio4.py,sha256=W9FaF7ryhbT10TEgHcuovZkm7w2zIU3tDtnb5gIlYlQ,20993 +scipy/io/matlab/_mio5.py,sha256=6wfD_hwa4KdY1-pLXgjIAQfYpZO_LCCsaVMYWaV6dUI,33637 +scipy/io/matlab/_mio5_params.py,sha256=skRcKG70vOlVMSb1TO67LB5312zuOUSrcOK7mOCcUss,8201 +scipy/io/matlab/_mio5_utils.cpython-310-x86_64-linux-gnu.so,sha256=Gn1k4_OCmUxRDGP_QlOrqhGL1j1hzFx1tjaaYccwT9Y,242560 +scipy/io/matlab/_mio_utils.cpython-310-x86_64-linux-gnu.so,sha256=YvLliNpyVFlk53I-Iz8Gfl51S-7SUJ1shk9yQQDerbg,68024 +scipy/io/matlab/_miobase.py,sha256=OpKCydtebY-dqQR6GjI_8K85Zi9ZSSNBFeyUcafTjRw,13004 +scipy/io/matlab/_streams.cpython-310-x86_64-linux-gnu.so,sha256=gYAPXmsaReXYZEIMgf62TEzLppuWrMv9uws7CcBYfOQ,145904 +scipy/io/matlab/byteordercodes.py,sha256=fHZVESDgIeYzGYtRlknPQ2nUqscQQ_4FhQc_ClkjBvQ,528 +scipy/io/matlab/mio.py,sha256=2b0WwgQ0rBkoJ4X0hgPl889PpR7Q0i7ibSLtTQVuTto,539 +scipy/io/matlab/mio4.py,sha256=hkhpBa4p0euf2rUjJviBWJ4TJs1wkUads3mX1fgDYMc,508 +scipy/io/matlab/mio5.py,sha256=jEFeEEkXWOhziPreDt0SqfAtOo9JMauxoODAbbXHmoQ,638 +scipy/io/matlab/mio5_params.py,sha256=2RWROlfc8RmXmcXGyM-be107Tm55ibc_U7DztJ2b4fc,593 +scipy/io/matlab/mio5_utils.py,sha256=DYiQfx5BkyDVnK4nZ3xPa-5tbpZE7WRx4SIdBmPVfSI,520 +scipy/io/matlab/mio_utils.py,sha256=VZPx03BNFbrQjB1CNbDCvvXUuP0_VoNRFV1R0YoB2iw,518 +scipy/io/matlab/miobase.py,sha256=3qQoq8Y7ZQpHIufUCzg6RAeaLqU3qTAozmuYbaOd7BI,565 +scipy/io/matlab/streams.py,sha256=0Aww9GRGGnRmiAMBAzIAXsFGySu5YCUNG-cHP1omYjI,513 +scipy/io/mmio.py,sha256=Dc5HqR8BXOD0wir63VTVczuZcLjSxEjbSbeZd4y27po,526 +scipy/io/netcdf.py,sha256=RKhmlybZwbFNKA4US6xLX6O2IUDCmdkToosPt4bAUX0,533 +scipy/io/wavfile.py,sha256=zISeQssvUbZ1kJTqrFX0x8N8QWuriM7F_KPQvaqXPQ4,28647 +scipy/linalg/__init__.pxd,sha256=0MlO-o_Kr8gg--_ipXEHFGtB8pZdHX8VX4wLYe_UzPg,53 +scipy/linalg/__init__.py,sha256=UOFZX4GCusrQjcaPB6NNNerhsVDe707BvlfE7XB8KzU,7517 +scipy/linalg/__pycache__/__init__.cpython-310.pyc,, +scipy/linalg/__pycache__/_basic.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_cholesky.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_cossin.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_ldl.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_lu.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_polar.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_qr.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_qz.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_schur.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_svd.cpython-310.pyc,, +scipy/linalg/__pycache__/_expm_frechet.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs_inv_ssq.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs_sqrtm.cpython-310.pyc,, +scipy/linalg/__pycache__/_misc.cpython-310.pyc,, +scipy/linalg/__pycache__/_procrustes.cpython-310.pyc,, +scipy/linalg/__pycache__/_sketches.cpython-310.pyc,, +scipy/linalg/__pycache__/_solvers.cpython-310.pyc,, +scipy/linalg/__pycache__/_special_matrices.cpython-310.pyc,, +scipy/linalg/__pycache__/_testutils.cpython-310.pyc,, +scipy/linalg/__pycache__/basic.cpython-310.pyc,, +scipy/linalg/__pycache__/blas.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_cholesky.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_lu.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_qr.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_schur.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_svd.cpython-310.pyc,, +scipy/linalg/__pycache__/interpolative.cpython-310.pyc,, +scipy/linalg/__pycache__/lapack.cpython-310.pyc,, +scipy/linalg/__pycache__/matfuncs.cpython-310.pyc,, +scipy/linalg/__pycache__/misc.cpython-310.pyc,, +scipy/linalg/__pycache__/special_matrices.cpython-310.pyc,, +scipy/linalg/_basic.py,sha256=5LXUCE49zLfVNzU1V-0HrsHWkFsNe16wzZ9cu2LubW0,76085 +scipy/linalg/_blas_subroutines.h,sha256=v5j0yyW_pBFpkeccHLk4ZooAehksxRstV_A-ZlgGFy4,18190 +scipy/linalg/_cythonized_array_utils.cpython-310-x86_64-linux-gnu.so,sha256=C4JcJDmeT7Y08wIxA_SlzXPgjlgHOzNwIk0V8Xw1ZNw,575616 +scipy/linalg/_cythonized_array_utils.pxd,sha256=OlWTbJt3gmdrfRFyx_Vz7GTmDTjr8dids5HA4TfC6R0,890 +scipy/linalg/_cythonized_array_utils.pyi,sha256=HZWXvJdpXGcydTEjkaL_kXIcxpcMqBBfFz7ZhscsRNo,340 +scipy/linalg/_decomp.py,sha256=D3WgtUo43h4Cjb-9vLepEVs_7BSXX1wYLWBtdmhRO_M,61881 +scipy/linalg/_decomp_cholesky.py,sha256=pk7_zuMkd-q-8AHyrNpm0wDof4-DeWiCFA3ESBkvLSQ,13721 +scipy/linalg/_decomp_cossin.py,sha256=rf2DFhaDmpXnWr1YpL3s8-hTOlR42HfSyWN7OoWzrec,8977 +scipy/linalg/_decomp_interpolative.cpython-310-x86_64-linux-gnu.so,sha256=1tLVbNg_EIgOHQQJweavh1jNB428z8dVAowx3_FBoZE,901392 +scipy/linalg/_decomp_ldl.py,sha256=HYzVUNZgEyuC2ZoFOGneas8ZkhhOFzUGcapL3Pos_cE,12535 +scipy/linalg/_decomp_lu.py,sha256=bCwCzMX_StEoLg1vScxglenyCzqMw3-BGJQmBcNEqNM,12941 +scipy/linalg/_decomp_lu_cython.cpython-310-x86_64-linux-gnu.so,sha256=LZ1Z_2BWAoAMetQCl_SbSeJww_-Ek-kmiLAyGCNU38g,232536 +scipy/linalg/_decomp_lu_cython.pyi,sha256=EASCkhrbJcBHo4zMYCUl1qRJDvPrvCqxd1TfqMWEd_U,291 +scipy/linalg/_decomp_polar.py,sha256=arzJ40FP1-TFsRvXPCP1qdNTsT60lkBcKBHfhB2JxxY,3578 +scipy/linalg/_decomp_qr.py,sha256=PbkwukMtzEH94uVjO9IEqSg4xmi0PV-UHXg9iM15rRE,15388 +scipy/linalg/_decomp_qz.py,sha256=uH93in1ikPR-Wgi1g49EPm2XXuhKOWBzPUJEahCotx8,16330 +scipy/linalg/_decomp_schur.py,sha256=OOzr2woTgWHBrJETNRCrzdviLTjiSDcxBgM6gTVkZMY,12059 +scipy/linalg/_decomp_svd.py,sha256=Epk7P6mmLLmYDiRETZAb3O2v3wKfbOjmGseWkAUlRPM,16809 +scipy/linalg/_decomp_update.cpython-310-x86_64-linux-gnu.so,sha256=L_8RRy6apLD4CHFk5CQKVwSTtO_Mrv397pNFQGXNOz4,335672 +scipy/linalg/_expm_frechet.py,sha256=Yc6J9HICUULvXcYBUaCyoOPFhXwjkIFi7TdrcNeVEmo,12326 +scipy/linalg/_fblas.cpython-310-x86_64-linux-gnu.so,sha256=ZxGY_SCMd8D1obJV-SywEC5bR4OJlnGd6orFS_ww7uY,621344 +scipy/linalg/_flapack.cpython-310-x86_64-linux-gnu.so,sha256=xkcbRoVRVAO7z1sr52zUinRrQIsQRALvQjKuwG6upOU,2130016 +scipy/linalg/_lapack_subroutines.h,sha256=Wk88h_VA1tkF168pjCl8E8UVFbUTm8jWbI2hH8NZ12c,239333 +scipy/linalg/_linalg_pythran.cpython-310-x86_64-linux-gnu.so,sha256=smVjRnnYCgqTHHC80LACGg6-ITdBHSiDGi_wPdX4bBc,108048 +scipy/linalg/_matfuncs.py,sha256=oOrSsB4tKtgGwFV2YJSUf0I3rTl9ZqCpF2WgHleDn70,25177 +scipy/linalg/_matfuncs_expm.cpython-310-x86_64-linux-gnu.so,sha256=OSFKNG_BtC72BYbBfqGeYORo7nSLrPOXV_6Wg0q7Pc0,84448 +scipy/linalg/_matfuncs_expm.pyi,sha256=wZAZfVtEbB78ljXgQoiL0I4yaPhmHOqIpGBYGQPvS6k,178 +scipy/linalg/_matfuncs_inv_ssq.py,sha256=8dL7xD6DU8D4h2YyHcYjRhZQvv1pSOEzMuKlGP6zonw,28095 +scipy/linalg/_matfuncs_sqrtm.py,sha256=-qdBb42d2HvSkyVi-90N4Ai5vzwkqwGL00duzi_V1jM,6268 +scipy/linalg/_matfuncs_sqrtm_triu.cpython-310-x86_64-linux-gnu.so,sha256=O8FrXzoWv5V3-_IKj7w_VmIBtk3ZH9cG0rzpCpqsajk,242720 +scipy/linalg/_misc.py,sha256=udhvxGfEHxhS3ecQBuwQ65W9ezVQIaVBw8JOmfqH_oE,6301 +scipy/linalg/_procrustes.py,sha256=uqPSMCxvqdbYMv3YEGUvwhnZnyIaApknfJcNAfyiTBQ,3520 +scipy/linalg/_sketches.py,sha256=6XwvmXh2zHjUFFsTYmoBYzhAUfZG2hwtdKR-YOzcDDQ,6117 +scipy/linalg/_solve_toeplitz.cpython-310-x86_64-linux-gnu.so,sha256=9ySxKHTKRyMQqTLiQe4L5ele8Z3_tu8spUFvAHr_bpY,262280 +scipy/linalg/_solvers.py,sha256=zwhrz0DbJ6wf9fY7B0pZMvMoC-cHo1VYXd3DyHk7pTg,28800 +scipy/linalg/_special_matrices.py,sha256=ZmOTcoJfbsR3baZgHWQ80extNyYJeSo8Tx81nUmzkyc,40697 +scipy/linalg/_testutils.py,sha256=IWA5vvdZ8yaHeXo2IxpQLqG9q54YIomHscYs85q9pd0,1807 +scipy/linalg/basic.py,sha256=AuNvDlH8mnAJScycj4mV-Iq1M0bXxidpY4Vud_lRJlM,753 +scipy/linalg/blas.py,sha256=-D-IH0bah8h2SmrdVA4xPfIqmKiPTkVC14GJ3czelLA,11685 +scipy/linalg/cython_blas.cpython-310-x86_64-linux-gnu.so,sha256=7-2qj_jLfhQZX5f4gSzxjodpY3RquDMZ2CdmpsX8olQ,299192 +scipy/linalg/cython_blas.pxd,sha256=DCPBxNWP-BvdT_REj6_a4TjUrNaf6sCq_XoxU3pEbfc,15592 +scipy/linalg/cython_blas.pyx,sha256=9iUdRoyiHzu6mFbMUEQnhCqkpqD6bDo_QPnVwIOy-3g,65304 +scipy/linalg/cython_lapack.cpython-310-x86_64-linux-gnu.so,sha256=iFW2jY2g3SpSUmnJekxJTROhv1bUX7u8d0Xf3zdyupc,752016 +scipy/linalg/cython_lapack.pxd,sha256=Ld5hPwcYxpOPahFNsfNomsp0_DY8BfG-W8TmZxh-iYM,204556 +scipy/linalg/cython_lapack.pyx,sha256=odVC_GknEWmSo9tDA7wucppRlFV8fbO9KBaw94iD_2M,707012 +scipy/linalg/decomp.py,sha256=w9HTI1OxXpX_rL72qcmykc5dUWal7lTlAU8k-9Eq7Dg,708 +scipy/linalg/decomp_cholesky.py,sha256=1g45oc115ZZR3CfMW1bCPseF5ATz4Xf6Ih26NRqyjfs,649 +scipy/linalg/decomp_lu.py,sha256=FPo9NHe9wg1FhCaoVV1_4mdfNj0S4plT4dHr4vMl1U8,593 +scipy/linalg/decomp_qr.py,sha256=EJNpu6lSa36Eo-e4rbYu5kDlRTMse2mmGul_PLRFXHs,567 +scipy/linalg/decomp_schur.py,sha256=vkVK3y-055523Q__ptxVNatDebPBE1HD-DFBe7kEh3w,602 +scipy/linalg/decomp_svd.py,sha256=HrJqbmgde7d7EWxCsa9XkS9QuWgPYMFOHiF4NcAL_Qg,631 +scipy/linalg/interpolative.py,sha256=8kCZv1z3UtzBuPvompAUUjHToLta4ffvOjVVLSaRLeQ,32757 +scipy/linalg/lapack.py,sha256=0bytum8c_A1Xdt5NH5dcol7GjFtrkjuAnH_cnLWr07g,15805 +scipy/linalg/matfuncs.py,sha256=vYw39D2LukCRCFJpx0qx8tgHlRZEDZI2wZfZwhh-Ubo,744 +scipy/linalg/misc.py,sha256=uxpR80jJ5w5mslplWlL6tIathas8mEXvRIwDXYMcTOk,592 +scipy/linalg/special_matrices.py,sha256=OXkkDj-ypZHiC17RUerraAzO8dC9aDuVujzb3Ft3GDY,757 +scipy/misc/__init__.py,sha256=dVfULY959nFwpl5NCxyCpiHyNcSNaR7HYOg7QU21a5s,135 +scipy/misc/__pycache__/__init__.cpython-310.pyc,, +scipy/misc/__pycache__/common.cpython-310.pyc,, +scipy/misc/__pycache__/doccer.cpython-310.pyc,, +scipy/misc/common.py,sha256=nAGQOVR9ZEAb703uhOVQZqf-z0iCM4EDhbHK4_h_Tdc,142 +scipy/misc/doccer.py,sha256=wHbpGV8todadz6MIzJHalDfRjiKI164qs6iMcHgsVu0,142 +scipy/ndimage/__init__.py,sha256=w4dCQCzsFmzAs7xF18MCTf5ld8HdIFfZjoRxuLQeqwg,5154 +scipy/ndimage/__pycache__/__init__.cpython-310.pyc,, +scipy/ndimage/__pycache__/_delegators.cpython-310.pyc,, +scipy/ndimage/__pycache__/_filters.cpython-310.pyc,, +scipy/ndimage/__pycache__/_fourier.cpython-310.pyc,, +scipy/ndimage/__pycache__/_interpolation.cpython-310.pyc,, +scipy/ndimage/__pycache__/_measurements.cpython-310.pyc,, +scipy/ndimage/__pycache__/_morphology.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ndimage_api.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ni_docstrings.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ni_support.cpython-310.pyc,, +scipy/ndimage/__pycache__/_support_alternative_backends.cpython-310.pyc,, +scipy/ndimage/__pycache__/filters.cpython-310.pyc,, +scipy/ndimage/__pycache__/fourier.cpython-310.pyc,, +scipy/ndimage/__pycache__/interpolation.cpython-310.pyc,, +scipy/ndimage/__pycache__/measurements.cpython-310.pyc,, +scipy/ndimage/__pycache__/morphology.cpython-310.pyc,, +scipy/ndimage/_delegators.py,sha256=NBf6hkZ7pyrELlhUpP-CvuvBPEgO77FgAfhD38KEk-Q,9256 +scipy/ndimage/_filters.py,sha256=6CH71a4VDcn9thauWiE1BJBOVBb-vN5CFznz_lJ2nAw,70982 +scipy/ndimage/_fourier.py,sha256=SoAYRx7ax7Tv51MyYzDlZ3fN682x4T6N8yReX2La4-I,11266 +scipy/ndimage/_interpolation.py,sha256=Zlb4ZRJbTOrf21dedO28GHTXA0Kh9hMCDWBdGvRbco4,36670 +scipy/ndimage/_measurements.py,sha256=eyBWnB0x1CxseFOMPXkfpuu48nhkMuK24hZPBla2wVs,56113 +scipy/ndimage/_morphology.py,sha256=LF91gKJcHIWoD9ath_9-Y7HgUwQbA0ELqgVYvm1YAWA,100762 +scipy/ndimage/_nd_image.cpython-310-x86_64-linux-gnu.so,sha256=BO4JPwJuNtS508V9G2Yc_R3Na9TStztm9u642Q0B5BI,137680 +scipy/ndimage/_ndimage_api.py,sha256=ZozKmpYXU6AG3WnkgJQUPXQ39V2obSFTwC_N9LCtG64,536 +scipy/ndimage/_ni_docstrings.py,sha256=TxAEkoC5ysA5JuK8IM2xoq60yddVWqOXsmxYXIr-4_E,8542 +scipy/ndimage/_ni_label.cpython-310-x86_64-linux-gnu.so,sha256=fa15qF23gnSoKuX1ITBLgCErVG0F-XYGMR1pSsaZkJU,398672 +scipy/ndimage/_ni_support.py,sha256=weYLkgApaf0WG54dksxJnFEY2ToCT9O3XNP4d4pppFM,5308 +scipy/ndimage/_rank_filter_1d.cpython-310-x86_64-linux-gnu.so,sha256=kTbiijPPLyis5KaLB057VC46tf-0-p7ZAuzGI7TLhu8,25800 +scipy/ndimage/_support_alternative_backends.py,sha256=G9J6cBRmZ0VFkAQ72uGdsiQ9-4ZlqTZ4KsX8cs_QZXg,2603 +scipy/ndimage/filters.py,sha256=cAv2zezrTJEm9JzKPV_pmXzZcgczCK_VaYJ4mdNW3FM,976 +scipy/ndimage/fourier.py,sha256=gnifi4S_Epyu4DpNsebz4A5BKzBWoGf11FkXWeXsoqY,599 +scipy/ndimage/interpolation.py,sha256=GHYvxCyQsLfKtNUc8AUN_vqmBhmAPwNnxm2-VpFMayk,664 +scipy/ndimage/measurements.py,sha256=xdSs52Y5RjURLP710iGURXWQFeS3ok4WjoYufKh9OeA,788 +scipy/ndimage/morphology.py,sha256=yFWSo7o_7PuYq61WGQOCIgMppneNLxqhJocyN0bMsVA,965 +scipy/odr/__init__.py,sha256=CErxMJ0yBfu_cvCoKJMu9WjqUaohLIqqf228Gm9XWJI,4325 +scipy/odr/__odrpack.cpython-310-x86_64-linux-gnu.so,sha256=1WX35XjvvQsel4TiAZJWFDsuY8_RmKmE8TU185lxNNk,221432 +scipy/odr/__pycache__/__init__.cpython-310.pyc,, +scipy/odr/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/odr/__pycache__/_models.cpython-310.pyc,, +scipy/odr/__pycache__/_odrpack.cpython-310.pyc,, +scipy/odr/__pycache__/models.cpython-310.pyc,, +scipy/odr/__pycache__/odrpack.cpython-310.pyc,, +scipy/odr/_add_newdocs.py,sha256=GeWL4oIb2ydph_K3qCjiIbPCM3QvpwP5EZwEJVOzJrQ,1128 +scipy/odr/_models.py,sha256=tfOLgqnV4LR3VKi7NAg1g1Jp_Zw8lG_PA5BHwU_pTH0,7800 +scipy/odr/_odrpack.py,sha256=n30DVx78Oh0zDItjKdqDaJpiXSyVPqHYGk63a1-5NZg,42496 +scipy/odr/models.py,sha256=Fcdj-P9rJ_B-Ct8bh3RrusnapeHLysVaDsM26Q8fHFo,590 +scipy/odr/odrpack.py,sha256=OlRlBxKlzp5VDi2fnnA-Jdl6G0chDt95JNCvJYg2czs,632 +scipy/optimize/__init__.pxd,sha256=kFYBK9tveJXql1KXuOkKGvj4Fu67GmuyRP5kMVkMbyk,39 +scipy/optimize/__init__.py,sha256=7ZzePqFF1X1377f_s3dpVdeg51I3YwManuh8Pl4M1mE,13279 +scipy/optimize/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/__pycache__/_basinhopping.cpython-310.pyc,, +scipy/optimize/__pycache__/_bracket.cpython-310.pyc,, +scipy/optimize/__pycache__/_chandrupatla.cpython-310.pyc,, +scipy/optimize/__pycache__/_cobyla_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_cobyqa_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_constraints.cpython-310.pyc,, +scipy/optimize/__pycache__/_dcsrch.cpython-310.pyc,, +scipy/optimize/__pycache__/_differentiable_functions.cpython-310.pyc,, +scipy/optimize/__pycache__/_differentialevolution.cpython-310.pyc,, +scipy/optimize/__pycache__/_direct_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_dual_annealing.cpython-310.pyc,, +scipy/optimize/__pycache__/_elementwise.cpython-310.pyc,, +scipy/optimize/__pycache__/_hessian_update_strategy.cpython-310.pyc,, +scipy/optimize/__pycache__/_isotonic.cpython-310.pyc,, +scipy/optimize/__pycache__/_lbfgsb_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_linesearch.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_doc.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_highs.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_ip.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_rs.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_simplex.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_util.cpython-310.pyc,, +scipy/optimize/__pycache__/_milp.cpython-310.pyc,, +scipy/optimize/__pycache__/_minimize.cpython-310.pyc,, +scipy/optimize/__pycache__/_minpack_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_nnls.cpython-310.pyc,, +scipy/optimize/__pycache__/_nonlin.cpython-310.pyc,, +scipy/optimize/__pycache__/_numdiff.cpython-310.pyc,, +scipy/optimize/__pycache__/_optimize.cpython-310.pyc,, +scipy/optimize/__pycache__/_qap.cpython-310.pyc,, +scipy/optimize/__pycache__/_remove_redundancy.cpython-310.pyc,, +scipy/optimize/__pycache__/_root.cpython-310.pyc,, +scipy/optimize/__pycache__/_root_scalar.cpython-310.pyc,, +scipy/optimize/__pycache__/_shgo.cpython-310.pyc,, +scipy/optimize/__pycache__/_slsqp_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_spectral.cpython-310.pyc,, +scipy/optimize/__pycache__/_tnc.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_dogleg.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_exact.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_krylov.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_ncg.cpython-310.pyc,, +scipy/optimize/__pycache__/_tstutils.cpython-310.pyc,, +scipy/optimize/__pycache__/_zeros_py.cpython-310.pyc,, +scipy/optimize/__pycache__/cobyla.cpython-310.pyc,, +scipy/optimize/__pycache__/elementwise.cpython-310.pyc,, +scipy/optimize/__pycache__/lbfgsb.cpython-310.pyc,, +scipy/optimize/__pycache__/linesearch.cpython-310.pyc,, +scipy/optimize/__pycache__/minpack.cpython-310.pyc,, +scipy/optimize/__pycache__/minpack2.cpython-310.pyc,, +scipy/optimize/__pycache__/moduleTNC.cpython-310.pyc,, +scipy/optimize/__pycache__/nonlin.cpython-310.pyc,, +scipy/optimize/__pycache__/optimize.cpython-310.pyc,, +scipy/optimize/__pycache__/slsqp.cpython-310.pyc,, +scipy/optimize/__pycache__/tnc.cpython-310.pyc,, +scipy/optimize/__pycache__/zeros.cpython-310.pyc,, +scipy/optimize/_basinhopping.py,sha256=Ug6gQH56vjrs-6RwGZKyCgVzjkT9rgqOPH-sJSaWtmM,29778 +scipy/optimize/_bglu_dense.cpython-310-x86_64-linux-gnu.so,sha256=0LcJgMNK7QeB7rP9Z-QpiAmdd0TSdGUtF4Ldat11d9E,313832 +scipy/optimize/_bracket.py,sha256=hEml-Fciyx1NZfKS1cCtSieBufNvFrLZiVgSGIg_ZtI,29802 +scipy/optimize/_chandrupatla.py,sha256=cmgXWc33PxEUUVn2Bh5Go4XPx_K7Hzihb2DyUAn8C80,24639 +scipy/optimize/_cobyla.cpython-310-x86_64-linux-gnu.so,sha256=m_osiyXWYA-VlTsgnhozK7PNVIE8orFS9xfwU_XnIpE,88720 +scipy/optimize/_cobyla_py.py,sha256=_HUCEYEEFxNBniaw56eZqmjsrwCOMbOTdFaYUv5UqUI,10867 +scipy/optimize/_cobyqa_py.py,sha256=_zejgs3XKkieGiMlRVn1x12cyWoulaPP2SpvxA4zK3k,2971 +scipy/optimize/_constraints.py,sha256=K37Le2W-pA7fsR39wXiC3L60QZGFN_-EUhtmGie-qn4,22895 +scipy/optimize/_cython_nnls.cpython-310-x86_64-linux-gnu.so,sha256=rD88pptpfnK7qPY1LqSmLTLZNwEKl5G52Q69RtqWM34,103568 +scipy/optimize/_dcsrch.py,sha256=D5I9G4oH5kFD2Rrb61gppXFMwwz6JiQBYPvW3vbR5Gs,25235 +scipy/optimize/_differentiable_functions.py,sha256=aYwpOvlHfQ7j-BO15VcL1v5XLR36tr_OPmf1eCWLuHY,24922 +scipy/optimize/_differentialevolution.py,sha256=UrTsxsTC1ddNoBsZ2tnNI0Lpz4HUC0QlmcaA1wCiQPc,86506 +scipy/optimize/_direct.cpython-310-x86_64-linux-gnu.so,sha256=unMOIDN6LKLcMIbxFhGltO3cs4VgIK0PYv3t5M471PM,46616 +scipy/optimize/_direct_py.py,sha256=-tEx51_9jg63zmDcSmmqeMtTlxXpci8fSh9TR_dFD4M,11849 +scipy/optimize/_dual_annealing.py,sha256=Zr5O-Juk2lslIlneQ4J9sgmDlPKh6sRZ9ytZZ9i-x7U,31121 +scipy/optimize/_elementwise.py,sha256=2CYFgK7uYw0St-T5M-GAhh8zgB3yU0mHmjS1Q6YYrNA,33136 +scipy/optimize/_group_columns.cpython-310-x86_64-linux-gnu.so,sha256=bt7qBbA8ydZINH4UtMB4jllSkjwE7h0WjzWjJTX6iFY,68048 +scipy/optimize/_hessian_update_strategy.py,sha256=xmtREKGlLgVvlBynjb5eCnPbsH-xbPcprS-ZoziG80M,18423 +scipy/optimize/_highspy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/_highspy/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_highspy/__pycache__/_highs_wrapper.cpython-310.pyc,, +scipy/optimize/_highspy/_core.cpython-310-x86_64-linux-gnu.so,sha256=mmjwDPRlJD7TKDvQvnmNP0kxVp9lswuh6Fv_A0BJCnw,5293088 +scipy/optimize/_highspy/_highs_options.cpython-310-x86_64-linux-gnu.so,sha256=aCHKq6WujK4q1VDk33vIuOuq-wJW5Z6nv3WGoUpVWpo,318456 +scipy/optimize/_highspy/_highs_wrapper.py,sha256=26lybYeLKk_tVx8j9Q8oEBrx8QM2uRK2kS-Q1jKen68,11212 +scipy/optimize/_isotonic.py,sha256=WY-9jtT5VVafVALYIp6lJPQnBfYVNDP9oJpg-kErYYI,6077 +scipy/optimize/_lbfgsb.cpython-310-x86_64-linux-gnu.so,sha256=54zWw_SP_xANceEVrM1ujxLwwd8gt-JQ_K3ixbLNr-U,41648 +scipy/optimize/_lbfgsb_py.py,sha256=KgLYyR-UeQg8chw-ttdarm5blMuop5lY4KqI_Hqk-2c,21047 +scipy/optimize/_linesearch.py,sha256=sZ45z0K3l6LLURdAfzO5CI5DctDlXqD92PCaz9mKzYE,27215 +scipy/optimize/_linprog.py,sha256=TGl9k9Ioh-hgHYgtndN5BNcU4vqfpZm8whRK2f4ehQQ,30262 +scipy/optimize/_linprog_doc.py,sha256=AeDv_zu0iU_oV0vxSrdzzY5GkKzOVx-5nmBgFB_UXhA,61942 +scipy/optimize/_linprog_highs.py,sha256=-r2tkn0Wii6b6zS21uCxj0z2HiUs-hKOGm8PJ6K5H10,17027 +scipy/optimize/_linprog_ip.py,sha256=dEaU1pqYXRxWvH91Zxm4tMQ7813QNhjIB8Yj8Nb3cPY,45784 +scipy/optimize/_linprog_rs.py,sha256=wRVGZxCSpo4ttw4CPpmXozSvM9WRXD179fGiGh8gOQ4,23146 +scipy/optimize/_linprog_simplex.py,sha256=9_nxcVl-ofHN9p_dDyC1C6jHlPttSfO9kp8WF1ST4JM,24748 +scipy/optimize/_linprog_util.py,sha256=Xka58MQ9BUFAnLVCshJvlMGP0Dn_ahV_VTNn5fnZKFA,62747 +scipy/optimize/_lsap.cpython-310-x86_64-linux-gnu.so,sha256=uppiVR053h9Pq7lm1kIUmxoRBh0ffPcpHCgEA9qcccg,30408 +scipy/optimize/_lsq/__init__.py,sha256=Yk4FSVEqe1h-qPqVX7XSkQNBYDtZO2veTmMAebCxhIQ,172 +scipy/optimize/_lsq/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/bvls.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/common.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/dogbox.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/least_squares.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/lsq_linear.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/trf.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/trf_linear.cpython-310.pyc,, +scipy/optimize/_lsq/bvls.py,sha256=7u5B8LfUbv3ZRZ8DAZKuDTSNRfDEBmTsn25VZtMMsKk,5195 +scipy/optimize/_lsq/common.py,sha256=kNsAyIAPFPTEJqQCKUwR8NEbYWtgINDoF76SBg-rU6Y,20476 +scipy/optimize/_lsq/dogbox.py,sha256=97htRlr-Yt-u4Ob3ks7avAMdnjJsO83uHUMjMYrhyjc,11682 +scipy/optimize/_lsq/givens_elimination.cpython-310-x86_64-linux-gnu.so,sha256=m-ur7EdmOJTTGWSKEsfD8E9iVKLVbgOmyK586_Ph1yU,193496 +scipy/optimize/_lsq/least_squares.py,sha256=M_bznCB4ueIt9hklMVu4mCXskIKkZe1AVBL5biaSvTY,39302 +scipy/optimize/_lsq/lsq_linear.py,sha256=JWhGY2GJmeQoi7ZU0dg-TFSRIGvdNAgHhIaPK9GNOUA,15037 +scipy/optimize/_lsq/trf.py,sha256=ElVHnB2Un3eaQ4jJ8KHHp-hwXfYHMypnSthfRO33P90,19477 +scipy/optimize/_lsq/trf_linear.py,sha256=jIs7WviOu_8Kpb7sTln8W7YLgkcndv0eGIP15g_mC4g,7642 +scipy/optimize/_milp.py,sha256=KYJlJ0NulFZoO6d1yactJmhryLuPzmiRS8GIxqWXxbU,15227 +scipy/optimize/_minimize.py,sha256=MGd3sP6LNwpElRiW85iHxBEinhaohly0gfOLxhtUs7s,50135 +scipy/optimize/_minpack.cpython-310-x86_64-linux-gnu.so,sha256=LbItXjfaLAW_WelUiLOQpFDFiNFKxZ8CZYds9CaZsYo,76696 +scipy/optimize/_minpack_py.py,sha256=sjx90i41TQ9CzXtr2LVkxP-woc2L_8v7YHVXidSpRK0,45028 +scipy/optimize/_moduleTNC.cpython-310-x86_64-linux-gnu.so,sha256=bCXKrQnubSwLCLlitrf0tn_nLQ0svdI0PRwitHf3lAY,142816 +scipy/optimize/_nnls.py,sha256=td0FOAvUICeUTGrXqFmdV6UXGi_Cy0PrG8hQviDsqe4,3233 +scipy/optimize/_nonlin.py,sha256=BtDRlEwSlvOhxo04mXQHpzytoV-FI_K5yVs0RAX8eBI,50177 +scipy/optimize/_numdiff.py,sha256=CpeUGKWHTsAk-JnvtbDBjpXvlI8pch1oXIPj40CNY2c,28931 +scipy/optimize/_optimize.py,sha256=AzljBSSf7wAO_G9W8pkg-o3IdlHzMdp5JulhMGcoORM,147685 +scipy/optimize/_pava_pybind.cpython-310-x86_64-linux-gnu.so,sha256=MsLzKh_BjHd_c4th1llYGBvuyOPgIqYEniFXSD7Rtg0,171208 +scipy/optimize/_qap.py,sha256=6bIzIiLwD4V2MCJrqQBOJ2h7uycy0qx01mkl-CR1U3I,29390 +scipy/optimize/_remove_redundancy.py,sha256=JqaQo5XclDpilSzc1BFv4Elxr8CXlFlgV45ypUwALyc,18769 +scipy/optimize/_root.py,sha256=Zh-WttrslloClCDg7VKhrVbRkDHBRkS4-ijJkI-_twg,28714 +scipy/optimize/_root_scalar.py,sha256=PIVT37WbcUwytG0WsU_t_pkUiluqZcJUan61ErBo_7I,20391 +scipy/optimize/_shgo.py,sha256=y5ET23yh6LS0yltoVaeM3CH7gundIfAfUhOEKq09ksw,62399 +scipy/optimize/_shgo_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/_shgo_lib/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_shgo_lib/__pycache__/_complex.cpython-310.pyc,, +scipy/optimize/_shgo_lib/__pycache__/_vertex.cpython-310.pyc,, +scipy/optimize/_shgo_lib/_complex.py,sha256=Ivs6HoVpIaVrS1wMiJC5FhV3N8VZKvoVSkcZ8YA191s,50224 +scipy/optimize/_shgo_lib/_vertex.py,sha256=I2TAqEEdTK66Km6UIkrDm2-tKpeJUuFX7DAfTk3XvUg,13996 +scipy/optimize/_slsqp.cpython-310-x86_64-linux-gnu.so,sha256=CyPzOmCGGXGKR3LoseVK5Zy2-ig3yuMy6BHcCQvWEPc,76616 +scipy/optimize/_slsqp_py.py,sha256=8KNFRiJlhinsqSMIp3-lzjrrw4lcrV7CADf1N6k87LA,19066 +scipy/optimize/_spectral.py,sha256=cgBoHOh5FcTqQ0LD5rOx4K7ECc7sbnODvcrn15_QeTI,8132 +scipy/optimize/_tnc.py,sha256=hmnQHaS5FLoaLzPHLcIVU2NPeO_-EQuJCc1Z8RLqDVs,17009 +scipy/optimize/_trlib/__init__.py,sha256=cNGWE1VffijqhPtSaqwagtBJvjJK-XrJ6K80RURLd48,524 +scipy/optimize/_trlib/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_trlib/_trlib.cpython-310-x86_64-linux-gnu.so,sha256=Pg_pOUEekG4rftC7ZOn94g5GHeKAcSrzVF7yOfhnQj8,336376 +scipy/optimize/_trustregion.py,sha256=z3yOE3-PGbIviDYTqpPQqa5wQhTMqc-LvssbY9Eou0A,10801 +scipy/optimize/_trustregion_constr/__init__.py,sha256=c8J2wYGQZr9WpLIT4zE4MUgEj4YNbHEWYYYsFmxAeXI,180 +scipy/optimize/_trustregion_constr/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/canonical_constraint.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/equality_constrained_sqp.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/minimize_trustregion_constr.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/projections.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/qp_subproblem.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/report.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/tr_interior_point.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/canonical_constraint.py,sha256=lWdsJ7WNTDm17jD-Omf5lflSMfcvdZWpReCND2CyjI0,12549 +scipy/optimize/_trustregion_constr/equality_constrained_sqp.py,sha256=eJc1Y25WhSLC6OGNJSFw0uA0c6LSUgfTQzmyXsXqVog,9154 +scipy/optimize/_trustregion_constr/minimize_trustregion_constr.py,sha256=WpVDoMk7rFHJI2KSG2YWiBm6bli180KvLneK9TVfz9Y,26145 +scipy/optimize/_trustregion_constr/projections.py,sha256=EO0uHULrNw8pm99vY-gd3pOFQEqrqk_13lVde9iUjTA,13169 +scipy/optimize/_trustregion_constr/qp_subproblem.py,sha256=EtAhRcEtSnGsEeEZ2HGEzm-7r0pnXMCgl9NemKWvdzg,22592 +scipy/optimize/_trustregion_constr/report.py,sha256=_L-HrO5C1lzvKvaijgkOYD210dvM4PkrhBSEQrMhVlw,1782 +scipy/optimize/_trustregion_constr/tr_interior_point.py,sha256=rRly3wy-O-MQ0dF2lc7b1IwTYWYXE_k87MzYnAW7EJw,14400 +scipy/optimize/_trustregion_dogleg.py,sha256=HS783IZYHE-EEuF82c4rkFp9u3MNKUdCeynZ6ap8y8s,4389 +scipy/optimize/_trustregion_exact.py,sha256=zaMQc5wUhZSnpxyXWwcqIh0O9bctOU4R-isaeblvSNc,15558 +scipy/optimize/_trustregion_krylov.py,sha256=KGdudJsoXXROXAc82aZ8ACojD3rimvyx5PYitbo4UzQ,3030 +scipy/optimize/_trustregion_ncg.py,sha256=y7b7QjFBfnB1wDtbwnvKD9DYpz7y7NqVrJ9RhNPcipw,4580 +scipy/optimize/_tstutils.py,sha256=BBaThpZNuwIQBqtVMOEB4bUHk3QdG2NpuLJBum8P6ak,34047 +scipy/optimize/_zeros.cpython-310-x86_64-linux-gnu.so,sha256=2XLUSOXb4oem5NnJVWCfTnzDS-BmiaAPRf-owLUI0V0,20808 +scipy/optimize/_zeros_py.py,sha256=pN0GMI_qHtor8BnY73B49bDZiiSYAxY1EtsQ3Kf0BJ0,52066 +scipy/optimize/cobyla.py,sha256=k2io8SM0vahYT5Zu4nS4yfa05_gyH0y-jVVxdWkC4dU,557 +scipy/optimize/cython_optimize.pxd,sha256=ecYJEpT0CXN-2vtaZfGCChD-oiIaJyRDIsTHE8eUG5M,442 +scipy/optimize/cython_optimize/__init__.py,sha256=eehEQNmLGy3e_XjNh6t5vQIC9l_OREeE4tYRRaFZdNs,4887 +scipy/optimize/cython_optimize/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/cython_optimize/_zeros.cpython-310-x86_64-linux-gnu.so,sha256=z18jEIXxzB2Y_D9uF9OgsiXzcsq_UiJXL9ugfIVX3Q4,97704 +scipy/optimize/cython_optimize/_zeros.pxd,sha256=anyu-MgWhq24f1bywI4TlohvJjOnpNpkCtSzpKBJSSo,1239 +scipy/optimize/cython_optimize/c_zeros.pxd,sha256=6Gc0l1q-1nlCO9uKrYeXFiHsbimRZzU3t6EoTa8MVvA,1118 +scipy/optimize/elementwise.py,sha256=8eEQW_PeNkr49YBTROr5xWDLgeJd7rxtdQk3tVuEECQ,1190 +scipy/optimize/lbfgsb.py,sha256=XT7kclUTtom8JASPYyAScx-5irlBd9s9yEnZzRwFqu8,601 +scipy/optimize/linesearch.py,sha256=w5OhOofynUbz7IzHAGEc6huLKV_rMR5eUq77VcskA9o,535 +scipy/optimize/minpack.py,sha256=2S9tkmBI670qqeDN7k_1-ZLYsFZV1yXaDMkrCvMETiQ,664 +scipy/optimize/minpack2.py,sha256=IPIduBcu0LRo75GJ9SiMa_GjfdKCOYzsWUs61_d1HR8,514 +scipy/optimize/moduleTNC.py,sha256=qTEQ4IWtv_LT6fH3-iYmYNwrtrjG1gS4KFbZ73iDcd0,507 +scipy/optimize/nonlin.py,sha256=uoKIYAdmhwNrC6zFbUIBCNdM1a59nn7hb5jxSOuK3rs,710 +scipy/optimize/optimize.py,sha256=SivH06ZYrbIwJLTQj3ZShU4FXft7w2y1a2uYE9ILIMo,877 +scipy/optimize/slsqp.py,sha256=K7nXxF99sjaI3_eoOm9w0VnrbaQXgnHlvvgs8lNa0zA,582 +scipy/optimize/tnc.py,sha256=aEKhka8wryg4mVlbrGFwzTJF_KYB49joMkSxKgh1KnA,560 +scipy/optimize/zeros.py,sha256=Sc06-J8JUazdfR36UamHhPndJoPK0FkOzHR-unHWoBw,620 +scipy/signal/__init__.py,sha256=tcYF8m39SxVh_JUIRVh8BdupHM3Gz8V6aJ_Y1Xorptg,13479 +scipy/signal/__pycache__/__init__.cpython-310.pyc,, +scipy/signal/__pycache__/_arraytools.cpython-310.pyc,, +scipy/signal/__pycache__/_czt.cpython-310.pyc,, +scipy/signal/__pycache__/_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/_fir_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/_lti_conversion.cpython-310.pyc,, +scipy/signal/__pycache__/_ltisys.cpython-310.pyc,, +scipy/signal/__pycache__/_max_len_seq.cpython-310.pyc,, +scipy/signal/__pycache__/_peak_finding.cpython-310.pyc,, +scipy/signal/__pycache__/_savitzky_golay.cpython-310.pyc,, +scipy/signal/__pycache__/_short_time_fft.cpython-310.pyc,, +scipy/signal/__pycache__/_signaltools.cpython-310.pyc,, +scipy/signal/__pycache__/_spectral_py.cpython-310.pyc,, +scipy/signal/__pycache__/_spline_filters.cpython-310.pyc,, +scipy/signal/__pycache__/_upfirdn.cpython-310.pyc,, +scipy/signal/__pycache__/_waveforms.cpython-310.pyc,, +scipy/signal/__pycache__/_wavelets.cpython-310.pyc,, +scipy/signal/__pycache__/bsplines.cpython-310.pyc,, +scipy/signal/__pycache__/filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/fir_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/lti_conversion.cpython-310.pyc,, +scipy/signal/__pycache__/ltisys.cpython-310.pyc,, +scipy/signal/__pycache__/signaltools.cpython-310.pyc,, +scipy/signal/__pycache__/spectral.cpython-310.pyc,, +scipy/signal/__pycache__/spline.cpython-310.pyc,, +scipy/signal/__pycache__/waveforms.cpython-310.pyc,, +scipy/signal/__pycache__/wavelets.cpython-310.pyc,, +scipy/signal/_arraytools.py,sha256=k3kHbl9RzcqsyftIYSFJZvJFL4zlcMAHyaRFUkFxOXY,8294 +scipy/signal/_czt.py,sha256=t5P1kRCM3iw3eCaL9hTgctMfQKezkqnjbghLjCkffQE,19445 +scipy/signal/_filter_design.py,sha256=4k8U0EV4ySo5c5NsvLkleFftDomEBRdl7gg1qdGBn4s,187997 +scipy/signal/_fir_filter_design.py,sha256=LEazCRvJAG9fyirZDqEnrgUpyv3ukl0r_SAOlUNQQw4,49741 +scipy/signal/_lti_conversion.py,sha256=ZLlxEy1TrxvSXvAeDDSxgvKHHv5_lXxxJUjwIgbfpQE,16057 +scipy/signal/_ltisys.py,sha256=sOxEME3e4217x6gFg7anY08p4CWoTS0jm6Np9IpsTM4,118051 +scipy/signal/_max_len_seq.py,sha256=8QkMWoYY3qy3bCKfsuXaS93Bnb2zd-ue6j5i5-3_hi0,5060 +scipy/signal/_max_len_seq_inner.cpython-310-x86_64-linux-gnu.so,sha256=k8YZhWPIf_h8zuhzHlglpGve3Q5dsUTr5AznWuP4zzc,44520 +scipy/signal/_peak_finding.py,sha256=e9vpWL98OQ9Ik1f7gwLl4d5feTAiyLwPm_yarJq3T_8,48856 +scipy/signal/_peak_finding_utils.cpython-310-x86_64-linux-gnu.so,sha256=Wp9WcirigIrbLu72nYZR9PxqKoKzhAR8DgyCpVTYGso,259376 +scipy/signal/_savitzky_golay.py,sha256=AahANBsLy8d6FKmVgteGiAw1l_4wWWItZYSyOVnj_nk,13447 +scipy/signal/_short_time_fft.py,sha256=lRvNtvsinMq4Is6jjfu1g7Nt3kUOZm0Q-GXNfFuRrcM,75332 +scipy/signal/_signaltools.py,sha256=zLnQL_DG5oHlStH9jA7_jfPIrccY1jUsIRIzdwa8COU,176273 +scipy/signal/_sigtools.cpython-310-x86_64-linux-gnu.so,sha256=q1_1dY-N722Ov84OOi8jKcQC6eMBYEorfHKiMVo_yBg,98944 +scipy/signal/_sosfilt.cpython-310-x86_64-linux-gnu.so,sha256=GuLWxPxbOnArUGYLijcEe0SfPFCcwyHWa2waDMZNxJU,269312 +scipy/signal/_spectral_py.py,sha256=h0BILp8mj4Txrj7aNC3GWNviR8oKpxTNBHd-vgoGCqM,86897 +scipy/signal/_spline.cpython-310-x86_64-linux-gnu.so,sha256=_rZhgvboXiHdae-46DD-MgGjNZdr7lAjaC0rSOkXEKM,42240 +scipy/signal/_spline.pyi,sha256=9tWZQCI7D84ONLwICZG6psBGtwKxAvLF7JaZ1tQUKoY,948 +scipy/signal/_spline_filters.py,sha256=t1HWc3YEhDu6AtXo8z1CLTkFYpcbYvpOIRIMPiRMEGM,24487 +scipy/signal/_upfirdn.py,sha256=ODSw2x1KHXN0vdKHm4vnovZxkoafcwIdUek0N8Edu5g,7882 +scipy/signal/_upfirdn_apply.cpython-310-x86_64-linux-gnu.so,sha256=xSTlimGzih5sPCLBLuuYS5qWKYbR74kJWRWaTKdmOTU,345344 +scipy/signal/_waveforms.py,sha256=Ca551WqyDWTrQrQ4hOwHl2dpHS1FSWg_SKyz1XObQrU,23089 +scipy/signal/_wavelets.py,sha256=QTjAp83C2V2sxIkUsITWLw3ceIRkmBJ5CYtwW_3szCU,873 +scipy/signal/bsplines.py,sha256=G1sa6en1z_41sU7ckRY8-flJjUKSqJJihaxBlwzUd3s,651 +scipy/signal/filter_design.py,sha256=EyHs8OX4mdeUi6e3Zf7IWuz6r5Re2eR_t0Bi10JuntM,1112 +scipy/signal/fir_filter_design.py,sha256=0BxZF7tqewVQ4J1u-Ls-DZfC25rIcizwr9v6WaxkS0k,640 +scipy/signal/lti_conversion.py,sha256=6uQ1qaT7XI75DoFmtRqRS94Hkpm-Qvy66CRNhmQ-Lbw,639 +scipy/signal/ltisys.py,sha256=TFul9jyL0ujEIchiOnDdIiJKIXZ8SSgOV066DvmX_QA,869 +scipy/signal/signaltools.py,sha256=I7U_hMuMf02zpdNi0LcPogucTDf0nUVUSkMZ1eAoq3E,1038 +scipy/signal/spectral.py,sha256=RA3jj6AWV6ptNwXfpVrbuyxxed8P7nWw8bLsD0iZIgw,662 +scipy/signal/spline.py,sha256=S54RVqPeA7nnGzLgICi-2rl3Ei3roPaDAJ6ihTeZSwk,747 +scipy/signal/waveforms.py,sha256=jfOXW7kgtGdh1nrMo1YLAh79W_Ln3WgzEN2esrp70wE,599 +scipy/signal/wavelets.py,sha256=7pA7HVMiXwG4fZZ0Q4nzz47hWWALMTYtxwGrIqV3bNE,510 +scipy/signal/windows/__init__.py,sha256=BUSXzc_D5Agp59RacDdG6EE9QjkXXtlcfQrTop_IJwo,2119 +scipy/signal/windows/__pycache__/__init__.cpython-310.pyc,, +scipy/signal/windows/__pycache__/_windows.cpython-310.pyc,, +scipy/signal/windows/__pycache__/windows.cpython-310.pyc,, +scipy/signal/windows/_windows.py,sha256=Scga4uJiDNUrH-p3ddILShNzXPmSOaA0Zvc6GOVyy6w,83594 +scipy/signal/windows/windows.py,sha256=FI6w8mt0V1221Rqv3Do3LuWRWrtKo3hYYTvpB_5UB1c,839 +scipy/sparse/__init__.py,sha256=OShVd94qpqQr4HMNPAvbMRQKf0Z6cL7bfRSbxcx99YQ,9361 +scipy/sparse/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/__pycache__/_base.cpython-310.pyc,, +scipy/sparse/__pycache__/_bsr.cpython-310.pyc,, +scipy/sparse/__pycache__/_compressed.cpython-310.pyc,, +scipy/sparse/__pycache__/_construct.cpython-310.pyc,, +scipy/sparse/__pycache__/_coo.cpython-310.pyc,, +scipy/sparse/__pycache__/_csc.cpython-310.pyc,, +scipy/sparse/__pycache__/_csr.cpython-310.pyc,, +scipy/sparse/__pycache__/_data.cpython-310.pyc,, +scipy/sparse/__pycache__/_dia.cpython-310.pyc,, +scipy/sparse/__pycache__/_dok.cpython-310.pyc,, +scipy/sparse/__pycache__/_extract.cpython-310.pyc,, +scipy/sparse/__pycache__/_index.cpython-310.pyc,, +scipy/sparse/__pycache__/_lil.cpython-310.pyc,, +scipy/sparse/__pycache__/_matrix.cpython-310.pyc,, +scipy/sparse/__pycache__/_matrix_io.cpython-310.pyc,, +scipy/sparse/__pycache__/_spfuncs.cpython-310.pyc,, +scipy/sparse/__pycache__/_sputils.cpython-310.pyc,, +scipy/sparse/__pycache__/base.cpython-310.pyc,, +scipy/sparse/__pycache__/bsr.cpython-310.pyc,, +scipy/sparse/__pycache__/compressed.cpython-310.pyc,, +scipy/sparse/__pycache__/construct.cpython-310.pyc,, +scipy/sparse/__pycache__/coo.cpython-310.pyc,, +scipy/sparse/__pycache__/csc.cpython-310.pyc,, +scipy/sparse/__pycache__/csr.cpython-310.pyc,, +scipy/sparse/__pycache__/data.cpython-310.pyc,, +scipy/sparse/__pycache__/dia.cpython-310.pyc,, +scipy/sparse/__pycache__/dok.cpython-310.pyc,, +scipy/sparse/__pycache__/extract.cpython-310.pyc,, +scipy/sparse/__pycache__/lil.cpython-310.pyc,, +scipy/sparse/__pycache__/sparsetools.cpython-310.pyc,, +scipy/sparse/__pycache__/spfuncs.cpython-310.pyc,, +scipy/sparse/__pycache__/sputils.cpython-310.pyc,, +scipy/sparse/_base.py,sha256=JqCa11sV9NR6-FeG7zUUBkQNfmXbXTwJGFsuYkISWi0,49272 +scipy/sparse/_bsr.py,sha256=7qZwcg8KeP-E-zYJn8uTcd9UqjP2NyyQ0CaqPcieWQA,30934 +scipy/sparse/_compressed.py,sha256=-WyLaP_KTsCedtm2wahWC9SOP712l5T30jumRR5P4hk,58983 +scipy/sparse/_construct.py,sha256=d044HGf_0-UqzsmifsAKCw2bPbQLTD1-vIFJOhxbTks,47960 +scipy/sparse/_coo.py,sha256=Oiyq04Pe0CPnEvYK-6Mtdo7XuQT8b1mkL7dx6Mze3To,64224 +scipy/sparse/_csc.py,sha256=KKVzIuWFCRlWGNCQMZpZp-_es0RefHimb-DW2AhNj6U,11142 +scipy/sparse/_csparsetools.cpython-310-x86_64-linux-gnu.so,sha256=od9lixV9eGyaHrZDpjuhlW40ZiSALTR0dFi8-75nojM,671264 +scipy/sparse/_csr.py,sha256=HbHai24yw-JPg9PZrgcFLEdfqQfj1BjmvNF_01qj-os,18156 +scipy/sparse/_data.py,sha256=NpxPIjJbmJDM_3AbRndYN55ffhz4j2aYV2ABgL3yM0c,20488 +scipy/sparse/_dia.py,sha256=suqsKGsedO5vruYCs4O6T_AJtM_E4Q9Gwn4H1DHG2Zg,20179 +scipy/sparse/_dok.py,sha256=tbmVoRu0-ECKB12hXW61qU82-kA6rcQhYQRJ3zzqoU4,23011 +scipy/sparse/_extract.py,sha256=0NWW00hxjk5gl4CjNRHtvcqsx54yNei2VVbqARMOlAo,5058 +scipy/sparse/_index.py,sha256=Mu4nOO8s0bq0O0l7NXUBuNMhdaal9tXYcxlRzqotYb0,16376 +scipy/sparse/_lil.py,sha256=uS3i5M_yhLjTDk9xySG_4COGgJA2QcwIpKphuwhcCV4,21125 +scipy/sparse/_matrix.py,sha256=-iZoYGC2dchFI3QKhmOpOCZgousk6vTO95jKgNDorg4,4427 +scipy/sparse/_matrix_io.py,sha256=0ZEoczSQq59zOGd_eWk6sfACt62vdQmth3ia7uqWFTM,5960 +scipy/sparse/_sparsetools.cpython-310-x86_64-linux-gnu.so,sha256=iSwW4QNbX2EmFugPr76u9u7G16B_S4I-ZteGGQ4mrQ4,4370896 +scipy/sparse/_spfuncs.py,sha256=lDVTp6CiQIuMxTfSzOi3-k6p97ayXJxdKPTf7j_4GWc,1987 +scipy/sparse/_sputils.py,sha256=xTe_MUII85GErqsA-DbOMdUQ1UFuOWxyyWB82xS_rBg,20750 +scipy/sparse/base.py,sha256=8Yx-QLKSRu9LJjgG-y8VqsRnsjImB2iKoJFxTgKGFsI,791 +scipy/sparse/bsr.py,sha256=CsYirxoLqHwBiEyNbOgGdZMx4Lt3adKZ-7uVv1gpzCY,811 +scipy/sparse/compressed.py,sha256=rbaz4AoTJvNnfnwEx4ocDXlkHJPOxe9DzqxCcJoHY2g,1009 +scipy/sparse/construct.py,sha256=i9lHBSRsDkvoNCbF9b7mZ0C2fHCjKU5CKCE30c-CxMc,925 +scipy/sparse/coo.py,sha256=VRF6kaYsVtyprwYrEuy1gRcCU5G7xsKyY0L1zJ_9JiQ,844 +scipy/sparse/csc.py,sha256=EV_LxYjPiRsTV6-J8kUefNna-R0tdI5uBt9Fj_XWlwc,609 +scipy/sparse/csgraph/__init__.py,sha256=znrEd48JFLdlcevl8IFDSM104Yl1YvXC0O_f8OUWATs,7842 +scipy/sparse/csgraph/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/csgraph/__pycache__/_laplacian.cpython-310.pyc,, +scipy/sparse/csgraph/__pycache__/_validation.cpython-310.pyc,, +scipy/sparse/csgraph/_flow.cpython-310-x86_64-linux-gnu.so,sha256=8DNyo4nvfgc32aR2Z99X0PYMBshJ-EeHOlV5bFCJwe4,308104 +scipy/sparse/csgraph/_laplacian.py,sha256=bpCduRWjIhcDpclvPbftx74PExTiW0P3EE6_Ztiop1Y,18273 +scipy/sparse/csgraph/_matching.cpython-310-x86_64-linux-gnu.so,sha256=TTeFJ2Vl3P-4KjRVD_o1KJxxZpj84kpTeaAYhA8rWdo,315064 +scipy/sparse/csgraph/_min_spanning_tree.cpython-310-x86_64-linux-gnu.so,sha256=QTIZrZZdG_fD6QkgTjoo79BMyD2XanV98zPt8ShNtzQ,234080 +scipy/sparse/csgraph/_reordering.cpython-310-x86_64-linux-gnu.so,sha256=XKG7Bo3qIUwgaJgWSWJtKLn0P3eyP94lU0jN9U9UyfM,293680 +scipy/sparse/csgraph/_shortest_path.cpython-310-x86_64-linux-gnu.so,sha256=Er-_dCfmMv6ZVa6jQFCuiJ_RqaQ3G3ZV3fHdQTHW7SM,518208 +scipy/sparse/csgraph/_tools.cpython-310-x86_64-linux-gnu.so,sha256=_f2vZpRAAjRvGrJibOOsq9wBKEwaGj3eNrgH9lmyQbs,200880 +scipy/sparse/csgraph/_traversal.cpython-310-x86_64-linux-gnu.so,sha256=5QWz8uNspoUOyu0ACvZEvr55skYhry92_-Pb1yvM4uQ,592200 +scipy/sparse/csgraph/_validation.py,sha256=SxINtd4jYyH0YWdzspr8JR0syZfO3nMj7C60GWBUr6k,2629 +scipy/sparse/csr.py,sha256=9UrWUoq5-hSl9bcaVeWxN4tmPJisTQ_6JiISCyrlMCw,658 +scipy/sparse/data.py,sha256=qGDAuAvTASgQ7wXXZ9t2JPp0rNBNVxObTTzXNHDRSEo,573 +scipy/sparse/dia.py,sha256=0y5_QfvVeU5doVbngvf8G36qVGU-FlnUxRChQ43e1aU,689 +scipy/sparse/dok.py,sha256=LMnaLFd266EZ3p4D1ZgOICGRZkY6s7YM0Wvlr6ylRn0,733 +scipy/sparse/extract.py,sha256=6qT2PNOilsEhDWl6MhmgpveIuQr4QCs3LATwIrBroOQ,567 +scipy/sparse/lil.py,sha256=Gve3XHYPYZavcUPJz1TSOhjv6AtPpkKBHTzCK6FG8ek,562 +scipy/sparse/linalg/__init__.py,sha256=KL54k4eDwEf7mHbL21uZe87S2rnSPIFcEI-pT3UpLIw,4111 +scipy/sparse/linalg/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_expm_multiply.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_interface.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_matfuncs.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_norm.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_onenormest.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_special_sparse_arrays.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_svdp.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/dsolve.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/eigen.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/interface.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/isolve.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/matfuncs.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__init__.py,sha256=PIX7n_d0LOMZZZ65Dz4Mgz9trjKGB2kLaF16PQLkAIs,2039 +scipy/sparse/linalg/_dsolve/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__pycache__/linsolve.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/_add_newdocs.py,sha256=4Nm6RAKQlKI4lQt4z20v0D6m0Vk8eqp0mIzEk5gfztA,3743 +scipy/sparse/linalg/_dsolve/_superlu.cpython-310-x86_64-linux-gnu.so,sha256=oS2RSbuBX2l8yPj-c8QQ_xfrmhA9uF3aHHi19i7I6vs,318352 +scipy/sparse/linalg/_dsolve/linsolve.py,sha256=F-KfpTKnlUl-ZXoDPnQ_2jY9NmsgByAiDsMaPHnHRFg,30697 +scipy/sparse/linalg/_eigen/__init__.py,sha256=SwNho3iWZu_lJvcdSomA5cQdcDU8gocKbmRnm6Bf9-0,460 +scipy/sparse/linalg/_eigen/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/__pycache__/_svds.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/__pycache__/_svds_doc.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/_svds.py,sha256=niV8PR0Aonw85rbiSPpL-RswAn9TltpUwcni3Qu_kl8,19908 +scipy/sparse/linalg/_eigen/_svds_doc.py,sha256=0_sC8kKbu3b5BYpGl16sPLrZu6mDxiFhj8xkbG2w5-U,15003 +scipy/sparse/linalg/_eigen/arpack/COPYING,sha256=CSZWb59AYXjRIU-Mx5bhZrEhPdfAXgxbRhqLisnlC74,1892 +scipy/sparse/linalg/_eigen/arpack/__init__.py,sha256=zDxf9LokyPitn3_0d-PUXoBCh6tWK0eUSvsAj6nkXI0,562 +scipy/sparse/linalg/_eigen/arpack/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/__pycache__/arpack.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/_arpack.cpython-310-x86_64-linux-gnu.so,sha256=k5zZgv1xofb9b1yVlzxo0OoGOiiBWUCbBiIkqx5GfBU,470024 +scipy/sparse/linalg/_eigen/arpack/arpack.py,sha256=CR5Wpf8vtkekS_tM-pIypaIFDlKXiWhqEOLV8e-jaYY,67129 +scipy/sparse/linalg/_eigen/lobpcg/__init__.py,sha256=E5JEPRoVz-TaLrj_rPm5LP3jCwei4XD-RxbcxYwf5lM,420 +scipy/sparse/linalg/_eigen/lobpcg/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/__pycache__/lobpcg.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py,sha256=vMsZlXCgKzn8l0PzHQFFadrAGfG9Fp0aTxwihATTqKM,41951 +scipy/sparse/linalg/_expm_multiply.py,sha256=zSeO3Nl5hyAZutMZjMq3e7_-ur43aJbNmUzx68n_kzM,26291 +scipy/sparse/linalg/_interface.py,sha256=akxeuwxWt859aHcmpyLI5oBQ7EeV0dHrD3ijIKgqkXI,29170 +scipy/sparse/linalg/_isolve/__init__.py,sha256=Z_eQUYbe6RWMSNi09T9TfPEWm8RsVxcIKYAlihM-U-c,479 +scipy/sparse/linalg/_isolve/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/_gcrotmk.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/iterative.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lgmres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lsmr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lsqr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/minres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/tfqmr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/utils.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/_gcrotmk.py,sha256=Qm8Y9J6kbGwvsuD_JF4OTLLLx6_7twygIXe5vKpeaOw,15740 +scipy/sparse/linalg/_isolve/iterative.py,sha256=Vhk3_ozYf8Pscte_Vkl_u9AAlFyJxVNpe8jAqviHlF4,33861 +scipy/sparse/linalg/_isolve/lgmres.py,sha256=A-mgYLEvzt5n10yMDoo3ZPNweULpp52aVAMhpTrbOe0,8695 +scipy/sparse/linalg/_isolve/lsmr.py,sha256=8MRtv-FJa7nOHlJ7MZ4TsQiWAkZwntD0r55SOQuRqvI,15650 +scipy/sparse/linalg/_isolve/lsqr.py,sha256=Ca2SjyNwMFXSckUTW_LqYFkFc5CWOaZ1yiYB0tK2uB8,21322 +scipy/sparse/linalg/_isolve/minres.py,sha256=3heKvLLuULWhdCrhbhaanZvu5J6-EbQEtwOIzI6uEFs,10887 +scipy/sparse/linalg/_isolve/tfqmr.py,sha256=_Uyy3skUaIHpqBD18H-poX8Tot1IfqkMmnF6h0iU6TY,6240 +scipy/sparse/linalg/_isolve/utils.py,sha256=I-Fjco_b83YKUtZPVdobTjPyY41-2SHruVvKZVOIXaU,3598 +scipy/sparse/linalg/_matfuncs.py,sha256=JaiiIwtP6Uzk6Jal8D9Ep9jTCxSyJZIdKamfzJN8wlA,29338 +scipy/sparse/linalg/_norm.py,sha256=MizhY4JL8pqcuP2suUlP1hMkwL1fIoyYHkiaEKuKqTQ,6163 +scipy/sparse/linalg/_onenormest.py,sha256=BkWu89ffmifkBdLH--IQ7DiW0hvDkVEiudUx4HRVmcI,15480 +scipy/sparse/linalg/_propack/_cpropack.cpython-310-x86_64-linux-gnu.so,sha256=LZeYX9PJdVoq6O_-xY9r5g-MEF78-UA7c9AO_ovEon8,121560 +scipy/sparse/linalg/_propack/_dpropack.cpython-310-x86_64-linux-gnu.so,sha256=h8vVzts3Blq0DDvnZXznxIW8Slw7pTt3ck3wADkL5QE,112872 +scipy/sparse/linalg/_propack/_spropack.cpython-310-x86_64-linux-gnu.so,sha256=z4yRbdvEffkVQPyi2fg_gTKJRBacIDim7simSbyV6SY,112872 +scipy/sparse/linalg/_propack/_zpropack.cpython-310-x86_64-linux-gnu.so,sha256=Hc6sv2yWpPAY9HamHe3nyPZxv-P5i2GmmpJzXaZ6XT4,121752 +scipy/sparse/linalg/_special_sparse_arrays.py,sha256=e7Y4OOurKa3eMyOnWaN-e6YQOM17onTESURjDpWUYS4,34225 +scipy/sparse/linalg/_svdp.py,sha256=dUr5v53cR5S40r71QCAVy0qUdKMxOviaWAT0ptrcjTQ,11200 +scipy/sparse/linalg/dsolve.py,sha256=fvCzVUda-h-WzwGWDss4FVuv6TVE-OKHzARBlUCDIJw,654 +scipy/sparse/linalg/eigen.py,sha256=4BTo8Tc9SNQaruyrF4gRdFE5NstiA0XH9I44IyikZQ4,626 +scipy/sparse/linalg/interface.py,sha256=_KXBkGhZWvY_ZmGixqWMZe6J64bCPdjtrqr63HvicUI,573 +scipy/sparse/linalg/isolve.py,sha256=diSAxpbYg8PeH75QOEE-CREO8p39f4BZK2dGynJDKIc,649 +scipy/sparse/linalg/matfuncs.py,sha256=H2qJl4ZZqZ4bI-E9NCbu1oFfto0EdFxCTKTugMPHRHg,570 +scipy/sparse/sparsetools.py,sha256=pCcuyQYvIahrvr43V398XHyqwcGtWCPLFH6n1uSYmB8,516 +scipy/sparse/spfuncs.py,sha256=TWpfkZk3JErNajVFUH5B85d3r6UuSv0Rsx0lMtUSac0,508 +scipy/sparse/sputils.py,sha256=PsqT7RUmiO8ph5jG8GHYmPbacDQFljjc0SL7RMxweJU,508 +scipy/spatial/__init__.py,sha256=-FVg_WjbK0J0U2kyei6Fz6NgqEso5cipWZ5gHnqjErs,3731 +scipy/spatial/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/__pycache__/_geometric_slerp.cpython-310.pyc,, +scipy/spatial/__pycache__/_kdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/_plotutils.cpython-310.pyc,, +scipy/spatial/__pycache__/_procrustes.cpython-310.pyc,, +scipy/spatial/__pycache__/_spherical_voronoi.cpython-310.pyc,, +scipy/spatial/__pycache__/ckdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/distance.cpython-310.pyc,, +scipy/spatial/__pycache__/kdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/qhull.cpython-310.pyc,, +scipy/spatial/_ckdtree.cpython-310-x86_64-linux-gnu.so,sha256=mQ3CA9XPKEd-RvVf-0kCT4gxEeUj5mauAdyKp2sjO2E,806672 +scipy/spatial/_distance_pybind.cpython-310-x86_64-linux-gnu.so,sha256=vaGgv4QaS8w7GHNrEPJmReOLDP3d0FYPq_tbvPaL9Ks,619360 +scipy/spatial/_distance_wrap.cpython-310-x86_64-linux-gnu.so,sha256=HgYcc6Ki9O-FBISIAJWpIWCZTXMfSYuk4G6pwX-LgM8,108584 +scipy/spatial/_geometric_slerp.py,sha256=d3pavtaMuIIKjupWLwFLt7WrfqvtT18u7wcsBdnuOTs,7951 +scipy/spatial/_hausdorff.cpython-310-x86_64-linux-gnu.so,sha256=XZYXH5sUaicaORPniSNiCqYLlyd3X5Loq9pUpMODyVM,211728 +scipy/spatial/_kdtree.py,sha256=ImDiR14DOAhwK--x9VhMjAlH_uhumsKuMin1Np63O7Q,33479 +scipy/spatial/_plotutils.py,sha256=cp94kSvt1QzWV6YWjeTrLh0lbWoVQu_0-iagVpoIgMo,7557 +scipy/spatial/_procrustes.py,sha256=qvhHPHt_OIKo-ge_k19S4VWqbP6ZgMXLVnNey0JxTb8,4427 +scipy/spatial/_qhull.cpython-310-x86_64-linux-gnu.so,sha256=F9eJ0RC6w5brC69hUIyoegQ4kLcxpvl8OpNe11ihITI,1021080 +scipy/spatial/_qhull.pyi,sha256=dmvze3QcaoA_Be6H8zswajVatOPwtJFIFxoZFE9qR-A,5969 +scipy/spatial/_spherical_voronoi.py,sha256=v1XkbWI7yoXQ6EJmJHs185vl0qHV8yfRrm3c_gBGyzg,13577 +scipy/spatial/_voronoi.cpython-310-x86_64-linux-gnu.so,sha256=AYtA21z2OvZjB4iAYLlF6YKhf2mC8Ey2wud-I4WJLPM,202608 +scipy/spatial/_voronoi.pyi,sha256=aAOiF4fvHz18hmuSjieKkRItssD443p2_w1ggXOIs1g,126 +scipy/spatial/ckdtree.py,sha256=0IssUT415ieBOJuvfZJxIra-TeYyd0KxDGLrXDZ_GGw,523 +scipy/spatial/distance.py,sha256=h_8YsmV28ycxIm3k9-o3EYeiHBrRc7uoUj5hMg_jC6s,98001 +scipy/spatial/distance.pyi,sha256=rVZpbHbTPWeqYN7aBSDBDIt3MLQWbUIYmgwzWJiODjE,5238 +scipy/spatial/kdtree.py,sha256=ZYJL8A_WpLyEH29aFQGLbxd9ttFdGBgdglbgAfsvhz8,636 +scipy/spatial/qhull.py,sha256=aFE-KscuINt6QIhFC2dqhwFCYu3HSBkVXDH5exHH71s,622 +scipy/spatial/qhull_src/COPYING.txt,sha256=NNsMDE-TGGHXIFVcnNei4ijRKQuimvDy7oDEG7IDivs,1635 +scipy/spatial/transform/__init__.py,sha256=vkvtowJUcu-FrMMXjEiyfnG94Cqwl000z5Nwx2F8OX0,700 +scipy/spatial/transform/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/_rotation_groups.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/_rotation_spline.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/rotation.cpython-310.pyc,, +scipy/spatial/transform/_rotation.cpython-310-x86_64-linux-gnu.so,sha256=3Fs2RjP8SqRClYpW3h-pycLHZU5S-dajpG7mQuyAz6Y,935512 +scipy/spatial/transform/_rotation_groups.py,sha256=XS-9K6xYnnwWywMMYMVznBYc1-0DPhADHQp_FIT3_f8,4422 +scipy/spatial/transform/_rotation_spline.py,sha256=B1wmFTqR34W-CMAggNFvFgZwVrgP2v2iFVIzjnAxnA8,14076 +scipy/spatial/transform/rotation.py,sha256=co5Bpny89EfCywilEeeLDvJPESBLrSXTCCJqRlfdYzg,556 +scipy/special/__init__.pxd,sha256=l9Y21wnx5fZLvrxCeCMUWQvBI5gHx7LBhimDWptxke8,42 +scipy/special/__init__.py,sha256=DoBkidFI8n9vihdtuv6XB_VBiz750909thSvHTOAXVs,33726 +scipy/special/__pycache__/__init__.cpython-310.pyc,, +scipy/special/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/special/__pycache__/_basic.cpython-310.pyc,, +scipy/special/__pycache__/_ellip_harm.cpython-310.pyc,, +scipy/special/__pycache__/_input_validation.cpython-310.pyc,, +scipy/special/__pycache__/_lambertw.cpython-310.pyc,, +scipy/special/__pycache__/_logsumexp.cpython-310.pyc,, +scipy/special/__pycache__/_mptestutils.cpython-310.pyc,, +scipy/special/__pycache__/_multiufuncs.cpython-310.pyc,, +scipy/special/__pycache__/_orthogonal.cpython-310.pyc,, +scipy/special/__pycache__/_sf_error.cpython-310.pyc,, +scipy/special/__pycache__/_spfun_stats.cpython-310.pyc,, +scipy/special/__pycache__/_spherical_bessel.cpython-310.pyc,, +scipy/special/__pycache__/_support_alternative_backends.cpython-310.pyc,, +scipy/special/__pycache__/_testutils.cpython-310.pyc,, +scipy/special/__pycache__/add_newdocs.cpython-310.pyc,, +scipy/special/__pycache__/basic.cpython-310.pyc,, +scipy/special/__pycache__/orthogonal.cpython-310.pyc,, +scipy/special/__pycache__/sf_error.cpython-310.pyc,, +scipy/special/__pycache__/specfun.cpython-310.pyc,, +scipy/special/__pycache__/spfun_stats.cpython-310.pyc,, +scipy/special/_add_newdocs.py,sha256=ZGPOb0r2gI8MIG9SA7_dEleWl8CHFprVyt422UabbQ8,290517 +scipy/special/_basic.py,sha256=8AwohnlJ1Z_396QgTh4L1Ba5iiVL_iewk_tg4CukAjU,112015 +scipy/special/_comb.cpython-310-x86_64-linux-gnu.so,sha256=ZQOG_DZPck9fUVW0pHjQwyFWZGoXYu6PhjBc8b6W1EQ,58224 +scipy/special/_ellip_harm.py,sha256=YHHFZXMtzdJxyjZXKsy3ocIsV-eg6ne3Up79BuFl9P8,5382 +scipy/special/_ellip_harm_2.cpython-310-x86_64-linux-gnu.so,sha256=4diaRY055d11_9vDOtKBqxmabXGk2dfi6p-8EE2ro8k,104792 +scipy/special/_gufuncs.cpython-310-x86_64-linux-gnu.so,sha256=AvE7z-uQ0VtiBtGw3fFW-hvNF-7yDuIqXY2QXeusNwA,699320 +scipy/special/_input_validation.py,sha256=ZEwg_sZaesaqzaVA_btZQAi_uPXtIViL_u3Zms6UnyQ,474 +scipy/special/_lambertw.py,sha256=-oSEnHFQWZiUZXMamxPWjfntWq5tt0rzHmI13DxGHBY,3962 +scipy/special/_logsumexp.py,sha256=CFPYc53br-qoY75-SNZGt8N27i3XEzm2LXkG91suLaI,13712 +scipy/special/_mptestutils.py,sha256=ocy_wBXqHGIg311jfjATEA8O29ICl4qPnvTgsmTm5qg,14441 +scipy/special/_multiufuncs.py,sha256=z9UQsy0fwHF-f6tUZOFAjmhw6EXx3njzA2mkyRk-Zho,18522 +scipy/special/_orthogonal.py,sha256=9RcRoMBby-UMRN8bBqK_m34b9gcAhvP3i630SzAnKJk,74230 +scipy/special/_orthogonal.pyi,sha256=a0iJfx1CdwZQjf2o8RfM7jiS2daOfXSwQ4a2hpoFhVs,8242 +scipy/special/_precompute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/special/_precompute/__pycache__/__init__.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/cosine_cdf.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/expn_asy.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/gammainc_asy.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/gammainc_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/hyp2f1_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/lambertw.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/loggamma.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/struve_convergence.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/utils.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wright_bessel.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wright_bessel_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wrightomega.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/zetac.cpython-310.pyc,, +scipy/special/_precompute/cosine_cdf.py,sha256=ZGSeDDpLRsapyx2GbIrqqYR98fvaEQrLn7IE-fuodhE,354 +scipy/special/_precompute/expn_asy.py,sha256=JAz0hY1gBJu3Q_dvscQrSJdgKuwpjqFZVwz-sOQQ21w,1265 +scipy/special/_precompute/gammainc_asy.py,sha256=P5OFRcPkkpjGQeYCaMZ8SFSUmZG_CjrEHv8OLwgcGFc,2502 +scipy/special/_precompute/gammainc_data.py,sha256=jogxBuXLr3uEpMBvpqScDz5TzEEalksH8f-cRGzasck,4077 +scipy/special/_precompute/hyp2f1_data.py,sha256=STSBybQ2pCAu6sh8c9tiHsoDOgnisnSp4tkP2cK4MuI,14707 +scipy/special/_precompute/lambertw.py,sha256=7f4F3ivouVNZwuvVX8TAi2lPB7LirPS8IfN5lEw9zI0,1961 +scipy/special/_precompute/loggamma.py,sha256=iq7ZBrUmk8pXYZwO_wINI4u8ENsLbL9VUShGjGO0Pt0,1094 +scipy/special/_precompute/struve_convergence.py,sha256=z7R0Q5_Ye-EqLI9g-yARdl_j5FooofXMRXPLVrIFJQQ,3624 +scipy/special/_precompute/utils.py,sha256=JXJuI07Jlm4bDHJFVtj0jHq05p-V1ofeXZB16Y05kzI,887 +scipy/special/_precompute/wright_bessel.py,sha256=7z2W3spGANZO31r_xauMA6hIQ0eseRlXx-zJW6du5tU,12868 +scipy/special/_precompute/wright_bessel_data.py,sha256=f1id2Gk5TPyUmSt-Evhoq2_hfRgLUU7Qu_mELKtaXGg,5647 +scipy/special/_precompute/wrightomega.py,sha256=YpmLwtGJ4qazMDY0RXjhnQiuRAISI-Pr9MwKc7pZlhc,955 +scipy/special/_precompute/zetac.py,sha256=LmhJP7JFg7XktHvfm-DgzuiWZFtVdpvYzzLOB1ePG1Q,591 +scipy/special/_sf_error.py,sha256=q_Rbfkws1ttgTQKYLt6zFTdY6DFX2HajJe_lXiNWC0c,375 +scipy/special/_specfun.cpython-310-x86_64-linux-gnu.so,sha256=TU04kBKkRpz1zP4Kt4nrAonT-N7-qfCIQtDLv-TAW74,261848 +scipy/special/_special_ufuncs.cpython-310-x86_64-linux-gnu.so,sha256=DZRnxfi3ji4NhWMB8lTyrSlodz7i4gmAWZfpcR1c9cw,1389712 +scipy/special/_spfun_stats.py,sha256=IjK325nhaTa7koQyvlVaeCo01TN9QWRpK6mDzkuuAq0,3779 +scipy/special/_spherical_bessel.py,sha256=E6aFHez6Ev8sUlJNLKWk5pZ0bwIp3vrafZr8Bh2Vws8,12446 +scipy/special/_support_alternative_backends.py,sha256=3Qlio4pv6iJoZvPhilpx5YZifX3R4a39k5uHbo_Vyd8,6315 +scipy/special/_testutils.py,sha256=o_h6MBVRhEubUC7flB-1LLr1GF5GJgVw9iol46H2lPs,11975 +scipy/special/_ufuncs.cpython-310-x86_64-linux-gnu.so,sha256=W0XSblISMZx5IvWRGhASuwSrd0jEcX5CqQoQYn9KPEk,1076080 +scipy/special/_ufuncs.pyi,sha256=AIHP4TNIs1CeqhIgObHyY0S2nNGBo6cICL_3hpRzj9o,8839 +scipy/special/_ufuncs.pyx,sha256=O98FaNvASL6ooj4ymS-Re2-1tZlzA6hyKwpUEdKWbEk,605812 +scipy/special/_ufuncs_cxx.cpython-310-x86_64-linux-gnu.so,sha256=t3pjMeRUMyxM33I2cybFGVDnGoyTbdTj9Cv9_qG2Yhs,1689008 +scipy/special/_ufuncs_cxx.pxd,sha256=Ltt2eonXvAbhRTKQj74VH299NBK9mCx4XYCdyUXLQ4U,5644 +scipy/special/_ufuncs_cxx.pyx,sha256=Py0yENPlxWqfc700rtXPv2ZTrL8tnh1HR-K_vWlbCKU,31470 +scipy/special/_ufuncs_cxx_defs.h,sha256=X8HIX3AK-7HXPIAPN1KGw5KOdF5GTvMmlR4Sl9nLwFU,9609 +scipy/special/_ufuncs_defs.h,sha256=h0MFUp-u8riZ6vm7y7UhcCzw4_kuGWxVc7q5IAAW1Ns,3166 +scipy/special/add_newdocs.py,sha256=Wnd-5R0wQAVxSolD4QY2CamTSbe1k48Aie3XaBWRKKc,436 +scipy/special/basic.py,sha256=LRU8rIxXx42O4eVZv21nFwswAu7JFtQ42_4xT5BwYpE,1582 +scipy/special/cython_special.cpython-310-x86_64-linux-gnu.so,sha256=gQLquk-qVQZzG3mUZku-QkXpyJ5F08sTuiw7s6lzEtM,2899376 +scipy/special/cython_special.pxd,sha256=6dBzCjT38uzfixyM49cTuB6zfUH69m2DGN2WBVVBk9I,16362 +scipy/special/cython_special.pyi,sha256=BQVUCzV8lCylnmLCtnN0Yz_ttlqyzcLc-BZx2KPXPzM,58 +scipy/special/libsf_error_state.so,sha256=QCY7NKhstBL06WTCjIPjwXsEkzjHOFG6HOTmszT4-JY,15120 +scipy/special/orthogonal.py,sha256=aLzv7PzJgsdLpyTrV6Cu-rpHNHWlUAEqOImiW4fuzuE,1724 +scipy/special/sf_error.py,sha256=wOZqzX7iipkH39hOHqBlkmretJRbYy-K7PsnZPyaJFU,573 +scipy/special/specfun.py,sha256=V1ZaKH1FFHPvzgkFa-UBVaVTLJRO4fodr7NAW_1jExo,588 +scipy/special/spfun_stats.py,sha256=ESJXGUwH7iijUk6aXZQVI1pnaWiVZ6_l0hVpC4bBSIw,535 +scipy/special/xsf/binom.h,sha256=IOVEKVugDUr9zqCLOk99Pj9LcMiGIZe4zzJCtWlYTZg,2471 +scipy/special/xsf/cdflib.h,sha256=1BrCII189UOWaBsII0H1kLgHfo8wdgaoysSbPojKIGU,4176 +scipy/special/xsf/cephes/airy.h,sha256=eTMfFrUgTjCEn0l8IiuKwBSDFHd5rZMrcTttNK0Akis,11089 +scipy/special/xsf/cephes/besselpoly.h,sha256=8MdB7tamsSebW9rpHS0TiVlq_YdkJTP1vDTrUx-i6io,1379 +scipy/special/xsf/cephes/beta.h,sha256=MDaX9iQorb6nYHKIjsS10qq0PmS-h8_f-MV3XHL35UQ,6981 +scipy/special/xsf/cephes/cbrt.h,sha256=bvmwllJjyMlgTUl9FqFXxhiGCXVan-BcrF3iF_UVEMg,3383 +scipy/special/xsf/cephes/chbevl.h,sha256=G6HJhFVbhKkXXBN_ZVborRWGBGO6PNAAQ5-zpOYoXBA,1906 +scipy/special/xsf/cephes/chdtr.h,sha256=eADp4we-EkfmgSRtjztWrkBhiad0LKfS4zCF5SLqth8,4047 +scipy/special/xsf/cephes/const.h,sha256=FfK7cYG3W8fCzBTe7M6Y8Ejfd_6OL1kzSswC9KyTNk4,3243 +scipy/special/xsf/cephes/ellie.h,sha256=ncKPlvJ2naCIouLawoGsiBlwp7hVNFMGwkLHq9Kljeg,9494 +scipy/special/xsf/cephes/ellik.h,sha256=0b40o6PlvzvUCbGnNJ-97BgE-8ZxLYjK9PuCjsoztzw,7601 +scipy/special/xsf/cephes/ellpe.h,sha256=XTCSsSMw8q1CZv19tAdzStjvZRaZ2ONEJNbccSqTiAk,3061 +scipy/special/xsf/cephes/ellpk.h,sha256=jI3WsxFmDAMsovrVyVkt_1voOsYRL2ZesgjuMKLlTpo,3392 +scipy/special/xsf/cephes/expn.h,sha256=IiyXzwtCkUT-TRz8TnMyvdoFi3g0Ri1BThEVydX3S7g,8942 +scipy/special/xsf/cephes/gamma.h,sha256=1ys_rqGE3dR_30YskFwfd8CpKXfCh7UIbZR3fxOtcPA,12004 +scipy/special/xsf/cephes/hyp2f1.h,sha256=kruh1lao3mygHmwVOfvu-MnFunbwNVdf5fZ9Gq5lydk,19986 +scipy/special/xsf/cephes/hyperg.h,sha256=q7BXWxVRmTwkHlJHqdep4CHWrYUWr1Ixv-as_xSKjBA,10458 +scipy/special/xsf/cephes/i0.h,sha256=rnsastkYnz7FPozLTZXE2NjLYjRtO2bqsCrNLmBS7V4,4548 +scipy/special/xsf/cephes/i1.h,sha256=WuxVJe6_M91pTmZgWFqqahu3slNwkDuzveUfGJlZUps,4740 +scipy/special/xsf/cephes/igam.h,sha256=w8_0jQmn-Lxtr-7NFeXKnqyo1jCRBBjup31kOJR0r0E,12877 +scipy/special/xsf/cephes/igam_asymp_coeff.h,sha256=ky3gnc7fifHIDRtamh4h5Ex2gKdBj6WPy4rmNtqD2nc,17893 +scipy/special/xsf/cephes/igami.h,sha256=B_PW8A2s1trORbnVDzKCtqdzslzWbzDsr9vKWey3pqY,12687 +scipy/special/xsf/cephes/j0.h,sha256=93xq6Budd0C4hNipx0maXQ_311NLxJMmVFzJe9jEnQk,6878 +scipy/special/xsf/cephes/j1.h,sha256=Qd9M25owFl3YOuAJ_Lr-hAh1m7bRxzFEEsOWDs6K68Y,6058 +scipy/special/xsf/cephes/jv.h,sha256=RpS_SWQlINWAr7vr7zCguo6V5zBt5o9ffBcdWLVKhzA,23130 +scipy/special/xsf/cephes/k0.h,sha256=ZeaVogEPyw0bGDFs4BFg1CR8I1WtIwqQGEPNv6M7B-w,4864 +scipy/special/xsf/cephes/k1.h,sha256=NYGMytXenLXSe2RZcRds3yGfHlvQwKmpegkDuKnDH8g,4626 +scipy/special/xsf/cephes/kn.h,sha256=SIUl7ePiFLVbXuTf2AC0VhoJkOPHTVQxkY0U5SCGYX8,6264 +scipy/special/xsf/cephes/lanczos.h,sha256=2Wp0n-MWPs2l0MtQ1RVaOvcLsC52zELOYPxYJoZK4OA,5494 +scipy/special/xsf/cephes/ndtr.h,sha256=y7RhtmvX0n61_Muy7awljyqTURnwtVLbL4Y3rwz9WCY,6681 +scipy/special/xsf/cephes/poch.h,sha256=jmJkxvIEnTcuaWPnmDH6lw5kPuE3AZGN1q7zmOaAL1s,2383 +scipy/special/xsf/cephes/polevl.h,sha256=7_WTjsgG9WKExZO0RSU8e0c_j6qvnWvDPYEa63Lq0Jk,4075 +scipy/special/xsf/cephes/psi.h,sha256=2GQCNBA4UHa-Y8bo9CE2Lm6q7HnOlOsxu1BPt9xfFdY,6291 +scipy/special/xsf/cephes/rgamma.h,sha256=zBqYhN1-xWE-Vpn2wvDsiDcGuO5qdIcsBEXCOrakwaU,3058 +scipy/special/xsf/cephes/scipy_iv.h,sha256=Tw2Ls0PAqBbZyfbcYuzNSX6NPiYQqfuwZAw2Taty2mY,25450 +scipy/special/xsf/cephes/shichi.h,sha256=wR_EwP7h-qwaqIjxb1Edn3RhhjPAEYQW5hFF1QzkMrQ,8513 +scipy/special/xsf/cephes/sici.h,sha256=7i2QVx2ij4ehnMTz4lcs3TeOInl-KPoDoQEetRtoPWI,7325 +scipy/special/xsf/cephes/sindg.h,sha256=SHZRnvwVhxjZUWNIjTd-cl4VFmZyZoG76nrUwkfyC9c,5634 +scipy/special/xsf/cephes/tandg.h,sha256=9Ko6moB_BLWq29XOWynKwp9XeTf6eQbotcKaIBPbrxQ,3391 +scipy/special/xsf/cephes/trig.h,sha256=vqygJpPKDlTylA29ejgX_cu58g76gzoWwyQvO05gwig,1340 +scipy/special/xsf/cephes/unity.h,sha256=vnNI6j6kpnkPkJuc-4gIiCOHPjPaz8TuChz7aqUzPKE,5053 +scipy/special/xsf/cephes/zeta.h,sha256=s21iDx7jlgHsOJdks6aXs2n-Z0K0A7C9Z2lLdpRtAUI,4381 +scipy/special/xsf/config.h,sha256=P5g5tNTQVAPx8P2bvxlEdT2shWQHXevshd5y91G7nt0,8438 +scipy/special/xsf/digamma.h,sha256=dt4JcA8YOwSLvJEMwglQHDjun5xH4cRZ3NU6RQU2pKk,7515 +scipy/special/xsf/error.h,sha256=UR9iGZFzuTeqAlNsqTKIRK9VaD-c70CAZLquyoAuDfA,1731 +scipy/special/xsf/evalpoly.h,sha256=JCz6KMNA4jDKenIfi0Z2KhVpVOb1bzzBltEz7oTOXlw,1119 +scipy/special/xsf/expint.h,sha256=iyJ6V4PHCOnRQRY4YWqifIF1Ri56LYNcbquMT_q5gBs,8345 +scipy/special/xsf/hyp2f1.h,sha256=FFLmMgvNMGg1xdR6ATjwpggr3lPtmM1vV0IhHzaUWVs,34727 +scipy/special/xsf/iv_ratio.h,sha256=nX7K3F8LV0zFNa3CoHC5dBMl5dAO5uH16lAskqZzARM,5674 +scipy/special/xsf/lambertw.h,sha256=Eon5lhh7L4n5ycalsiNfBjt3WiM1gd8-jR40F5g4u8Q,5411 +scipy/special/xsf/loggamma.h,sha256=GDJhdc7dldEiN7Xj2O5c91AgXCUkI4L_nFDO5FrAq-c,6209 +scipy/special/xsf/sici.h,sha256=mzu3DK3oGE7o7KMjmqfmdirWvpBuFejqQu1WKbir2vo,5854 +scipy/special/xsf/tools.h,sha256=x2ZqPsfRghqo7QJBmaCs8b7rJPDzB2VPUK92ExerRlM,16145 +scipy/special/xsf/trig.h,sha256=ZK6mxae-JxM9o8Cf4xytP5lXWhGgGQUgtm7vxsyxV2A,4362 +scipy/special/xsf/wright_bessel.h,sha256=eYkLjIiTx9iXHaAKdQXpGBWa4mmoZ0ZuQlSLGxSu53U,42619 +scipy/special/xsf/zlog1.h,sha256=tu6rdW4hOWkrEt00KTX3BWq5kD0ZPuiCIRT7G_M1pZE,965 +scipy/stats/__init__.py,sha256=CUo1rk_ClMcxEIobb_XxhRWZi1IZ--FkHazykYw8a6Q,18680 +scipy/stats/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/__pycache__/_axis_nan_policy.cpython-310.pyc,, +scipy/stats/__pycache__/_binned_statistic.cpython-310.pyc,, +scipy/stats/__pycache__/_binomtest.cpython-310.pyc,, +scipy/stats/__pycache__/_bws_test.cpython-310.pyc,, +scipy/stats/__pycache__/_censored_data.cpython-310.pyc,, +scipy/stats/__pycache__/_common.cpython-310.pyc,, +scipy/stats/__pycache__/_constants.cpython-310.pyc,, +scipy/stats/__pycache__/_continuous_distns.cpython-310.pyc,, +scipy/stats/__pycache__/_correlation.cpython-310.pyc,, +scipy/stats/__pycache__/_covariance.cpython-310.pyc,, +scipy/stats/__pycache__/_crosstab.cpython-310.pyc,, +scipy/stats/__pycache__/_discrete_distns.cpython-310.pyc,, +scipy/stats/__pycache__/_distn_infrastructure.cpython-310.pyc,, +scipy/stats/__pycache__/_distr_params.cpython-310.pyc,, +scipy/stats/__pycache__/_distribution_infrastructure.cpython-310.pyc,, +scipy/stats/__pycache__/_entropy.cpython-310.pyc,, +scipy/stats/__pycache__/_fit.cpython-310.pyc,, +scipy/stats/__pycache__/_hypotests.cpython-310.pyc,, +scipy/stats/__pycache__/_kde.cpython-310.pyc,, +scipy/stats/__pycache__/_ksstats.cpython-310.pyc,, +scipy/stats/__pycache__/_mannwhitneyu.cpython-310.pyc,, +scipy/stats/__pycache__/_mgc.cpython-310.pyc,, +scipy/stats/__pycache__/_morestats.cpython-310.pyc,, +scipy/stats/__pycache__/_mstats_basic.cpython-310.pyc,, +scipy/stats/__pycache__/_mstats_extras.cpython-310.pyc,, +scipy/stats/__pycache__/_multicomp.cpython-310.pyc,, +scipy/stats/__pycache__/_multivariate.cpython-310.pyc,, +scipy/stats/__pycache__/_new_distributions.cpython-310.pyc,, +scipy/stats/__pycache__/_odds_ratio.cpython-310.pyc,, +scipy/stats/__pycache__/_page_trend_test.cpython-310.pyc,, +scipy/stats/__pycache__/_probability_distribution.cpython-310.pyc,, +scipy/stats/__pycache__/_qmc.cpython-310.pyc,, +scipy/stats/__pycache__/_qmvnt.cpython-310.pyc,, +scipy/stats/__pycache__/_relative_risk.cpython-310.pyc,, +scipy/stats/__pycache__/_resampling.cpython-310.pyc,, +scipy/stats/__pycache__/_result_classes.cpython-310.pyc,, +scipy/stats/__pycache__/_sampling.cpython-310.pyc,, +scipy/stats/__pycache__/_sensitivity_analysis.cpython-310.pyc,, +scipy/stats/__pycache__/_stats_mstats_common.cpython-310.pyc,, +scipy/stats/__pycache__/_stats_py.cpython-310.pyc,, +scipy/stats/__pycache__/_survival.cpython-310.pyc,, +scipy/stats/__pycache__/_tukeylambda_stats.cpython-310.pyc,, +scipy/stats/__pycache__/_variation.cpython-310.pyc,, +scipy/stats/__pycache__/_warnings_errors.cpython-310.pyc,, +scipy/stats/__pycache__/_wilcoxon.cpython-310.pyc,, +scipy/stats/__pycache__/biasedurn.cpython-310.pyc,, +scipy/stats/__pycache__/contingency.cpython-310.pyc,, +scipy/stats/__pycache__/distributions.cpython-310.pyc,, +scipy/stats/__pycache__/kde.cpython-310.pyc,, +scipy/stats/__pycache__/morestats.cpython-310.pyc,, +scipy/stats/__pycache__/mstats.cpython-310.pyc,, +scipy/stats/__pycache__/mstats_basic.cpython-310.pyc,, +scipy/stats/__pycache__/mstats_extras.cpython-310.pyc,, +scipy/stats/__pycache__/mvn.cpython-310.pyc,, +scipy/stats/__pycache__/qmc.cpython-310.pyc,, +scipy/stats/__pycache__/sampling.cpython-310.pyc,, +scipy/stats/__pycache__/stats.cpython-310.pyc,, +scipy/stats/_ansari_swilk_statistics.cpython-310-x86_64-linux-gnu.so,sha256=PoAViCZExMoy3XoqwNRE6ytqlD9KfIWlF_JiIFNh6gg,239520 +scipy/stats/_axis_nan_policy.py,sha256=vtqhfxpJUrpD9GETwnB1HN7fe2NLIPt8QkGXjr3VPa8,31788 +scipy/stats/_biasedurn.cpython-310-x86_64-linux-gnu.so,sha256=uXhvy-Cz0aFs_JvW5xesDiDZHXdJ3kpZgZ2LLhGh-xQ,185240 +scipy/stats/_biasedurn.pxd,sha256=bQC6xG4RH1E5h2jCKXRMADfgGctiO5TgNlJegKrR7DY,1046 +scipy/stats/_binned_statistic.py,sha256=ATvrikTtX6zW8FKbjpV7O7IvAKSCBBLQSH1JKFR9R7Q,32702 +scipy/stats/_binomtest.py,sha256=aW6p-vRkv3pSB8_0nTfT3kNAhV8Ip44A39EEPyl9Wlc,13118 +scipy/stats/_bws_test.py,sha256=XQMGiLMPKFN3b6O4nD5tkZdcI8D8vggSx8B7XLJ5EGs,7062 +scipy/stats/_censored_data.py,sha256=Ts7GSYYti2z-8yoOJTedj6aCLnGhugLlDRdxZc4rPxs,18306 +scipy/stats/_common.py,sha256=4RqXT04Knp1CoOJuSBV6Uy_XmcmtVr0bImAbSk_VHlQ,172 +scipy/stats/_constants.py,sha256=mBeJgvWcDZBmPFStDNEjlzeZY3aMDMCHWoj7dCmgugQ,1002 +scipy/stats/_continuous_distns.py,sha256=vVY62qNTDUx2ktZk9KwIoguqwqj37n-LmcKoclk0uoA,407420 +scipy/stats/_correlation.py,sha256=TKenq2UmJ6gMligjczL1nTIXgUShprfYyBc23lhTCuo,7911 +scipy/stats/_covariance.py,sha256=g0oXQfcjugq9YpJhbmUECSOqYqPqsuDBD_69r_oGRDU,22524 +scipy/stats/_crosstab.py,sha256=djdU7xCQ-513VlxFEOvLN8oaY4QyUPHDJHWlilhyEVA,7351 +scipy/stats/_discrete_distns.py,sha256=nYPH9LKlqC0q_RFMitD4XEsP9F0pfnM-B1JnJtLwACw,65095 +scipy/stats/_distn_infrastructure.py,sha256=nfk3LYe26PjZzrTug-ZDKKCI-qsmTsQCfj99-fR9Tvw,151588 +scipy/stats/_distr_params.py,sha256=bD2Sdq0etEh0NYfi3-vFM-C7PevQfH0dRLbNnXeOtYY,9052 +scipy/stats/_distribution_infrastructure.py,sha256=yXlXMuwpT_MykLntuBKbNd4EmGjPe40e0HqC9Ia2PzI,203772 +scipy/stats/_entropy.py,sha256=hMlhLViQos20KYpBwmQf9fSfmbMzoCluF4uRg7yKxTc,15831 +scipy/stats/_fit.py,sha256=PmLg5oE25gnOIHVV-4U-nfUEsKdfgac4M9OaBSjKrow,59747 +scipy/stats/_hypotests.py,sha256=gDsPkfLiTH3oCeBL_FPOcC1Gvz53SHuda2a3YPE9hr4,79170 +scipy/stats/_kde.py,sha256=EAMQrO4MRwIcdOuQ1v-R6TP5IpAo_kZThwTEmRj8v7M,25089 +scipy/stats/_ksstats.py,sha256=JsUipfbLw0TMrmUpkvHY06Rk_eXT0l7WemK9xhVdLiA,20139 +scipy/stats/_levy_stable/__init__.py,sha256=J2Nw8Ye0e52Q9cC4o08H56QnLd1Frp_fB3WuxInP6II,45986 +scipy/stats/_levy_stable/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_levy_stable/levyst.cpython-310-x86_64-linux-gnu.so,sha256=M0BvE2Uo6aSzXByrikO-dCn34AFxagWg0eS_GMSkiiY,65296 +scipy/stats/_mannwhitneyu.py,sha256=LQII0f5CF4-OfWXqBuP4uPjNJ8IuVgPp04itqacy1EA,19330 +scipy/stats/_mgc.py,sha256=iImSUbFmYh_7Ouap70PFP6O6CVpUylf5y44z33j3obg,21359 +scipy/stats/_morestats.py,sha256=0Q1FJqhMJICguWL7HbrRKwwCyqfZUTLN7WxOeeKa2-0,170393 +scipy/stats/_mstats_basic.py,sha256=GXFCsZtbKg6kJuvXSCGRxhtme-dfzBLvl2r-g2UWGGM,122939 +scipy/stats/_mstats_extras.py,sha256=VMtwkTOFc3eBGFHiqO0cJjr98PC0fc2EIO_oKGIQJQo,16366 +scipy/stats/_multicomp.py,sha256=x9XBSCbTWl4V-hUZ_YaMYZ5smpE95qBCUic6yYygnpA,16836 +scipy/stats/_multivariate.py,sha256=V_ArfvakTKERdhchS5vob52fOnCPHqLMYcbS0FixhOY,249240 +scipy/stats/_mvn.cpython-310-x86_64-linux-gnu.so,sha256=C2xLrsVKy6SzkGsg7RJOVeCyq_2jfCCVu_VMvNrxXYM,79168 +scipy/stats/_new_distributions.py,sha256=4QuIqw-_QwJeIPsLDzFNDZBIpD7mTx4dwvEwn_5uoJk,13239 +scipy/stats/_odds_ratio.py,sha256=zZvZsD7ftKeWUrypXeUapcNoq006XldVAkMMC3RLbWE,17005 +scipy/stats/_page_trend_test.py,sha256=OvisWd3E6CF7rdFRGv46HWOfJlyHalMITt5iJPzE8LI,18987 +scipy/stats/_probability_distribution.py,sha256=xcvEl_eux4p8SSRKbTpb3Ipmfs9XAx522RK1ebkKiks,61504 +scipy/stats/_qmc.py,sha256=sJfB3Jz8unPDBe_TPN5qm1YK4emQ7lJN7iQ2_vGBO9E,107502 +scipy/stats/_qmc_cy.cpython-310-x86_64-linux-gnu.so,sha256=kJ5HmJfX8WO812ua-j6rqYAoMIF46ySIggdi19Xa0is,256248 +scipy/stats/_qmc_cy.pyi,sha256=xOpTSlaG_1YDZhkJjQQtukbcgOTAR9FpcRMkU5g9mXc,1134 +scipy/stats/_qmvnt.py,sha256=oKf0JU2bY9_oePM-sLMD_xowKjMdlXFYR5c1veeuWKw,18769 +scipy/stats/_rcont/__init__.py,sha256=dUzWdRuJNAxnGYVFjDqUB8DMYti3by1WziKEfBDOlB4,84 +scipy/stats/_rcont/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_rcont/rcont.cpython-310-x86_64-linux-gnu.so,sha256=z5QL3CNkoKya99p4a-W6_dCUGXz5InuhztIm6fy5zhA,220424 +scipy/stats/_relative_risk.py,sha256=5zeYBMshYwtomiLTkaXc1nmWYD0FsaQNjf0iuDadtSc,9571 +scipy/stats/_resampling.py,sha256=46DA0dE1CTlXR-vVBenghqptFL7wDadr2g0CKp4IMQs,104295 +scipy/stats/_result_classes.py,sha256=_ghuGdpFsCMuEmnfHg1AeorR-fASc77ACXYWEmQzXjI,1085 +scipy/stats/_sampling.py,sha256=YJ1mG2tkXW4Em-virElY-cNzMXn8lHbOxNxujqDsPY0,46408 +scipy/stats/_sensitivity_analysis.py,sha256=rSzMU4dmjN_zL-bt8tcxTTQbpRxNZuKrKn46zQtJyJc,25041 +scipy/stats/_sobol.cpython-310-x86_64-linux-gnu.so,sha256=Rjqrx-1htahh95Vj2yD2QtVfIKy2hlSwr0qwWokGWG4,357680 +scipy/stats/_sobol.pyi,sha256=TAywylI75AF9th9QZY8TYfHvIQ1cyM5QZi7eBOAkrbg,971 +scipy/stats/_sobol_direction_numbers.npz,sha256=SFmTEUfULORluGBcsnf5V9mLg50DGU_fBleTV5BtGTs,589334 +scipy/stats/_stats.cpython-310-x86_64-linux-gnu.so,sha256=5gTX1M9uxcLPhoujk3P8ZdW6arYJsDhehGBoY2f0w-c,658472 +scipy/stats/_stats.pxd,sha256=T_7IrDqgIahKMECV5WAtxtsoV91XBVRM359kAXPIhww,709 +scipy/stats/_stats_mstats_common.py,sha256=9SFbzUBOf6QpTwCiRkyXIlKAlm6B9uC8lv_VXSsiPzo,11557 +scipy/stats/_stats_py.py,sha256=AbZl_rpQP9U2hNAMpvMiVQ-kHUFOCdpIKrl_SNZLils,417517 +scipy/stats/_stats_pythran.cpython-310-x86_64-linux-gnu.so,sha256=Ttb2IlVhs-6XtZ-hvivjQbN2oZ0v80rShU1Rr-8HNHg,139608 +scipy/stats/_survival.py,sha256=JexV_eUz0H_2QSwpido_M_LJr4mkODmhHVwjzFXjgj8,25939 +scipy/stats/_tukeylambda_stats.py,sha256=eodvo09rCVfcYa1Uh6BKHKvXyY8K5Zg2uGQX1phQ6Ew,6871 +scipy/stats/_unuran/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/stats/_unuran/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_unuran/unuran_wrapper.cpython-310-x86_64-linux-gnu.so,sha256=efx4Pw61KKCExyxm5oy2i--PEZzu2WOxlzZ__SN5Pno,803192 +scipy/stats/_unuran/unuran_wrapper.pyi,sha256=TT9P08hsVQu6W7giss8kweV-FKcLffwZO9gyxmbpi2c,5588 +scipy/stats/_variation.py,sha256=2DfKIrosnZ68BzG7BLJNAAR692BN0SvZhlBs6M86l5U,4652 +scipy/stats/_warnings_errors.py,sha256=MpucxNFYEDytXh7vrZCMqTkRfuXTvvMpQ2W_Ak2OnPk,1196 +scipy/stats/_wilcoxon.py,sha256=wq_2sPwuiVA1kAFWJw3yegFp0TP5WVACPkYiTMrDs9U,9382 +scipy/stats/biasedurn.py,sha256=ECfilE4KrIhU2sK-KWtr8yxqthfVsyz_-o4F2TnMXU4,431 +scipy/stats/contingency.py,sha256=psNLzIB1A00rE4U9LwdYyt6XpYZlPRBCqQSMOEjHH04,18649 +scipy/stats/distributions.py,sha256=9Kt2fyTohorJcf6a7M9DYH8Nu4jEU66nKP01cRhKmuE,859 +scipy/stats/kde.py,sha256=8ZThSc3lz-l1Gb2jzIvy1J87_HTd7eXzxuPLClVpo7c,516 +scipy/stats/morestats.py,sha256=GdMXz4MSuPp7hsff_DoijVtFsCEyy6J3_M7BITKGiP4,973 +scipy/stats/mstats.py,sha256=aRbrykjrvl-qOBkmGjlFMH4rbWYSqBBQHReanSAomFg,2466 +scipy/stats/mstats_basic.py,sha256=PjgL37PCPwiDx_ptqnmKXc1W3QGlRjjPrG0nI5FA4So,1394 +scipy/stats/mstats_extras.py,sha256=925lNnnf_NTRoyAnXql-k9syzhv7MF6T2kPGsdE2FHc,721 +scipy/stats/mvn.py,sha256=pOcB_Dd_DHpfbYnuJKq-wqmNNGCun1M0294xK1bX0KQ,498 +scipy/stats/qmc.py,sha256=b6gLkc_FSm11Ssb9uIai4XxLk4XL_qqK6Jc2k4RSeN0,11703 +scipy/stats/sampling.py,sha256=VYwxxGosFs-T3qdCmdw4tJYEFLlegwj-JgDin7iwndE,1939 +scipy/stats/stats.py,sha256=EgWjDdnlfCRKJymUcBDvMvPn0ZLO3G_ml1XJ7wvMbCI,1512 +scipy/version.py,sha256=1ZOHRzEixbLKLzUBPsRe9DNOAf9fGp_ZvYD9DL-tGqs,318 diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/REQUESTED b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/WHEEL b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..4eaf1f8d3fed05dc2dbb9a82197b1d94fdf1dbe0 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: meson +Root-Is-Purelib: false +Tag: cp310-cp310-linux_x86_64 \ No newline at end of file diff --git a/lib/python3.10/site-packages/scipy-1.15.2.dist-info/direct_url.json b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..d9e2cb468762cdfacdf5801e7c2eb017ed879add --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.2.dist-info/direct_url.json @@ -0,0 +1 @@ +{"archive_info": {"hash": "sha256=9e52bad6c3294d1a5b04a13632118ca2157130603c6c018c2d710162b223b27e", "hashes": {"sha256": "9e52bad6c3294d1a5b04a13632118ca2157130603c6c018c2d710162b223b27e"}}, "url": "file:///home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work/dist/scipy-1.15.2-cp310-cp310-linux_x86_64.whl"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/INSTALLER b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/LICENSE.txt b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc3571daaad1bef068acf7207c655823a32947ea --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/LICENSE.txt @@ -0,0 +1,934 @@ +Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---- + +This binary distribution of SciPy can also bundle the following software +(depending on the build): + + +Name: OpenBLAS +Files: scipy.libs/libscipy_openblas*.so +Description: bundled as a dynamically linked library +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause-Attribution + Copyright (c) 2011-2014, The OpenBLAS Project + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: LAPACK +Files: scipy.libs/libscipy_openblas*.so +Description: bundled in OpenBLAS +Availability: https://github.com/OpenMathLib/OpenBLAS/ +License: BSD-3-Clause-Attribution + Copyright (c) 1992-2013 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. + Copyright (c) 2000-2013 The University of California Berkeley. All + rights reserved. + Copyright (c) 2006-2013 The University of Colorado Denver. All rights + reserved. + + $COPYRIGHT$ + + Additional copyrights may follow + + $HEADER$ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + + - Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + The copyright holders provide no reassurances that the source code + provided does not infringe any patent, copyright, or any other + intellectual property rights of third parties. The copyright holders + disclaim any liability to any recipient for claims brought against + recipient by any third party for infringement of that parties + intellectual property rights. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Name: GCC runtime library +Files: scipy.libs/libgfortran*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran +License: GPL-3.0-with-GCC-exception + Copyright (C) 2002-2017 Free Software Foundation, Inc. + + Libgfortran is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + Libgfortran is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Under Section 7 of GPL version 3, you are granted additional + permissions described in the GCC Runtime Library Exception, version + 3.1, as published by the Free Software Foundation. + + You should have received a copy of the GNU General Public License and + a copy of the GCC Runtime Library Exception along with this program; + see the files COPYING3 and COPYING.RUNTIME respectively. If not, see + . + +---- + +Full text of license texts referred to above follows (that they are +listed below does not necessarily imply the conditions apply to the +present binary release): + +---- + +GCC RUNTIME LIBRARY EXCEPTION + +Version 3.1, 31 March 2009 + +Copyright (C) 2009 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This GCC Runtime Library Exception ("Exception") is an additional +permission under section 7 of the GNU General Public License, version +3 ("GPLv3"). It applies to a given file (the "Runtime Library") that +bears a notice placed by the copyright holder of the file stating that +the file is governed by GPLv3 along with this Exception. + +When you use GCC to compile a program, GCC may combine portions of +certain GCC header files and runtime libraries with the compiled +program. The purpose of this Exception is to allow compilation of +non-GPL (including proprietary) programs to use, in this way, the +header files and runtime libraries covered by this Exception. + +0. Definitions. + +A file is an "Independent Module" if it either requires the Runtime +Library for execution after a Compilation Process, or makes use of an +interface provided by the Runtime Library, but is not otherwise based +on the Runtime Library. + +"GCC" means a version of the GNU Compiler Collection, with or without +modifications, governed by version 3 (or a specified later version) of +the GNU General Public License (GPL) with the option of using any +subsequent versions published by the FSF. + +"GPL-compatible Software" is software whose conditions of propagation, +modification and use would permit combination with GCC in accord with +the license of GCC. + +"Target Code" refers to output from any compiler for a real or virtual +target processor architecture, in executable form or suitable for +input to an assembler, loader, linker and/or execution +phase. Notwithstanding that, Target Code does not include data in any +format that is used as a compiler intermediate representation, or used +for producing a compiler intermediate representation. + +The "Compilation Process" transforms code entirely represented in +non-intermediate languages designed for human-written code, and/or in +Java Virtual Machine byte code, into Target Code. Thus, for example, +use of source code generators and preprocessors need not be considered +part of the Compilation Process, since the Compilation Process can be +understood as starting with the output of the generators or +preprocessors. + +A Compilation Process is "Eligible" if it is done using GCC, alone or +with other GPL-compatible software, or if it is done without using any +work based on GCC. For example, using non-GPL-compatible Software to +optimize any GCC intermediate representations would not qualify as an +Eligible Compilation Process. + +1. Grant of Additional Permission. + +You have permission to propagate a work of Target Code formed by +combining the Runtime Library with Independent Modules, even if such +propagation would otherwise violate the terms of GPLv3, provided that +all Target Code was generated by Eligible Compilation Processes. You +may then convey such a combination under terms of your choice, +consistent with the licensing of the Independent Modules. + +2. No Weakening of GCC Copyleft. + +The availability of this Exception does not imply any general +presumption that third-party software is unaffected by the copyleft +requirements of the license of GCC. + +---- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + + +Name: libquadmath +Files: scipy.libs/libquadmath*.so +Description: dynamically linked to files compiled with gcc +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath +License: LGPL-2.1-or-later + + GCC Quad-Precision Math Library + Copyright (C) 2010-2019 Free Software Foundation, Inc. + Written by Francois-Xavier Coudert + + This file is part of the libquadmath library. + Libquadmath is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + Libquadmath is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/METADATA b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..8a40d2e1f9797e61e4ad22383bec76baa230fcd6 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/METADATA @@ -0,0 +1,1082 @@ +Metadata-Version: 2.1 +Name: scipy +Version: 1.15.3 +Summary: Fundamental algorithms for scientific computing in Python +Maintainer-Email: SciPy Developers +License: Copyright (c) 2001-2002 Enthought, Inc. 2003-2024, SciPy Developers. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ---- + + This binary distribution of SciPy can also bundle the following software + (depending on the build): + + + Name: OpenBLAS + Files: scipy.libs/libscipy_openblas*.so + Description: bundled as a dynamically linked library + Availability: https://github.com/OpenMathLib/OpenBLAS/ + License: BSD-3-Clause-Attribution + Copyright (c) 2011-2014, The OpenBLAS Project + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Name: LAPACK + Files: scipy.libs/libscipy_openblas*.so + Description: bundled in OpenBLAS + Availability: https://github.com/OpenMathLib/OpenBLAS/ + License: BSD-3-Clause-Attribution + Copyright (c) 1992-2013 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. + Copyright (c) 2000-2013 The University of California Berkeley. All + rights reserved. + Copyright (c) 2006-2013 The University of Colorado Denver. All rights + reserved. + + $COPYRIGHT$ + + Additional copyrights may follow + + $HEADER$ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + + - Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + The copyright holders provide no reassurances that the source code + provided does not infringe any patent, copyright, or any other + intellectual property rights of third parties. The copyright holders + disclaim any liability to any recipient for claims brought against + recipient by any third party for infringement of that parties + intellectual property rights. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Name: GCC runtime library + Files: scipy.libs/libgfortran*.so + Description: dynamically linked to files compiled with gcc + Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgfortran + License: GPL-3.0-with-GCC-exception + Copyright (C) 2002-2017 Free Software Foundation, Inc. + + Libgfortran is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3, or (at your option) + any later version. + + Libgfortran is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + Under Section 7 of GPL version 3, you are granted additional + permissions described in the GCC Runtime Library Exception, version + 3.1, as published by the Free Software Foundation. + + You should have received a copy of the GNU General Public License and + a copy of the GCC Runtime Library Exception along with this program; + see the files COPYING3 and COPYING.RUNTIME respectively. If not, see + . + + ---- + + Full text of license texts referred to above follows (that they are + listed below does not necessarily imply the conditions apply to the + present binary release): + + ---- + + GCC RUNTIME LIBRARY EXCEPTION + + Version 3.1, 31 March 2009 + + Copyright (C) 2009 Free Software Foundation, Inc. + + Everyone is permitted to copy and distribute verbatim copies of this + license document, but changing it is not allowed. + + This GCC Runtime Library Exception ("Exception") is an additional + permission under section 7 of the GNU General Public License, version + 3 ("GPLv3"). It applies to a given file (the "Runtime Library") that + bears a notice placed by the copyright holder of the file stating that + the file is governed by GPLv3 along with this Exception. + + When you use GCC to compile a program, GCC may combine portions of + certain GCC header files and runtime libraries with the compiled + program. The purpose of this Exception is to allow compilation of + non-GPL (including proprietary) programs to use, in this way, the + header files and runtime libraries covered by this Exception. + + 0. Definitions. + + A file is an "Independent Module" if it either requires the Runtime + Library for execution after a Compilation Process, or makes use of an + interface provided by the Runtime Library, but is not otherwise based + on the Runtime Library. + + "GCC" means a version of the GNU Compiler Collection, with or without + modifications, governed by version 3 (or a specified later version) of + the GNU General Public License (GPL) with the option of using any + subsequent versions published by the FSF. + + "GPL-compatible Software" is software whose conditions of propagation, + modification and use would permit combination with GCC in accord with + the license of GCC. + + "Target Code" refers to output from any compiler for a real or virtual + target processor architecture, in executable form or suitable for + input to an assembler, loader, linker and/or execution + phase. Notwithstanding that, Target Code does not include data in any + format that is used as a compiler intermediate representation, or used + for producing a compiler intermediate representation. + + The "Compilation Process" transforms code entirely represented in + non-intermediate languages designed for human-written code, and/or in + Java Virtual Machine byte code, into Target Code. Thus, for example, + use of source code generators and preprocessors need not be considered + part of the Compilation Process, since the Compilation Process can be + understood as starting with the output of the generators or + preprocessors. + + A Compilation Process is "Eligible" if it is done using GCC, alone or + with other GPL-compatible software, or if it is done without using any + work based on GCC. For example, using non-GPL-compatible Software to + optimize any GCC intermediate representations would not qualify as an + Eligible Compilation Process. + + 1. Grant of Additional Permission. + + You have permission to propagate a work of Target Code formed by + combining the Runtime Library with Independent Modules, even if such + propagation would otherwise violate the terms of GPLv3, provided that + all Target Code was generated by Eligible Compilation Processes. You + may then convey such a combination under terms of your choice, + consistent with the licensing of the Independent Modules. + + 2. No Weakening of GCC Copyleft. + + The availability of this Exception does not imply any general + presumption that third-party software is unaffected by the copyleft + requirements of the license of GCC. + + ---- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for + software and other kinds of works. + + The licenses for most software and other practical works are designed + to take away your freedom to share and change the works. By contrast, + the GNU General Public License is intended to guarantee your freedom to + share and change all versions of a program--to make sure it remains free + software for all its users. We, the Free Software Foundation, use the + GNU General Public License for most of our software; it applies also to + any other work released this way by its authors. You can apply it to + your programs, too. + + When we speak of free software, we are referring to freedom, not + price. Our General Public Licenses are designed to make sure that you + have the freedom to distribute copies of free software (and charge for + them if you wish), that you receive source code or can get it if you + want it, that you can change the software or use pieces of it in new + free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you + these rights or asking you to surrender the rights. Therefore, you have + certain responsibilities if you distribute copies of the software, or if + you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether + gratis or for a fee, you must pass on to the recipients the same + freedoms that you received. You must make sure that they, too, receive + or can get the source code. And you must show them these terms so they + know their rights. + + Developers that use the GNU GPL protect your rights with two steps: + (1) assert copyright on the software, and (2) offer you this License + giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains + that there is no warranty for this free software. For both users' and + authors' sake, the GPL requires that modified versions be marked as + changed, so that their problems will not be attributed erroneously to + authors of previous versions. + + Some devices are designed to deny users access to install or run + modified versions of the software inside them, although the manufacturer + can do so. This is fundamentally incompatible with the aim of + protecting users' freedom to change the software. The systematic + pattern of such abuse occurs in the area of products for individuals to + use, which is precisely where it is most unacceptable. Therefore, we + have designed this version of the GPL to prohibit the practice for those + products. If such problems arise substantially in other domains, we + stand ready to extend this provision to those domains in future versions + of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. + States should not allow patents to restrict development and use of + software on general-purpose computers, but in those that do, we wish to + avoid the special danger that patents applied to a free program could + make it effectively proprietary. To prevent this, the GPL assures that + patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and + modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this + License. Each licensee is addressed as "you". "Licensees" and + "recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work + in a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a "modified version" of the + earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based + on the Program. + + To "propagate" a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through + a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" + to the extent that it includes a convenient and prominently visible + feature that (1) displays an appropriate copyright notice, and (2) + tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the + work under this License, and how to view a copy of this License. If + the interface presents a list of user commands or options, such as a + menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work + for making modifications to it. "Object code" means any non-source + form of a work. + + A "Standard Interface" means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that + is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other + than the work as a whole, that (a) is included in the normal form of + packaging a Major Component, but which is not part of that Major + Component, and (b) serves only to enable use of the work with that + Major Component, or to implement a Standard Interface for which an + implementation is available to the public in source code form. A + "Major Component", in this context, means a major essential component + (kernel, window system, and so on) of the specific operating system + (if any) on which the executable work runs, or a compiler used to + produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all + the source code needed to generate, install, and (for an executable + work) run the object code and to modify the work, including scripts to + control those activities. However, it does not include the work's + System Libraries, or general-purpose tools or generally available free + programs which are used unmodified in performing those activities but + which are not part of the work. For example, Corresponding Source + includes interface definition files associated with source files for + the work, and the source code for shared libraries and dynamically + linked subprograms that the work is specifically designed to require, + such as by intimate data communication or control flow between those + subprograms and other parts of the work. + + The Corresponding Source need not include anything that users + can regenerate automatically from other parts of the Corresponding + Source. + + The Corresponding Source for a work in source code form is that + same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program. The output from running a + covered work is covered by this License only if the output, given its + content, constitutes a covered work. This License acknowledges your + rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not + convey, without conditions so long as your license otherwise remains + in force. You may convey covered works to others for the sole purpose + of having them make modifications exclusively for you, or provide you + with facilities for running those works, provided that you comply with + the terms of this License in conveying all material for which you do + not control copyright. Those thus making or running the covered works + for you must do so exclusively on your behalf, under your direction + and control, on terms that prohibit them from making any copies of + your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under + the conditions stated below. Sublicensing is not allowed; section 10 + makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article + 11 of the WIPO copyright treaty adopted on 20 December 1996, or + similar laws prohibiting or restricting circumvention of such + measures. + + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention + is effected by exercising rights under this License with respect to + the covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's + users, your or third parties' legal rights to forbid circumvention of + technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; + keep intact all notices stating that this License and any + non-permissive terms added in accord with section 7 apply to the code; + keep intact all notices of the absence of any warranty; and give all + recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, + and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the + terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, + and which are not combined with it such as to form a larger program, + in or on a volume of a storage or distribution medium, is called an + "aggregate" if the compilation and its resulting copyright are not + used to limit the access or legal rights of the compilation's users + beyond what the individual works permit. Inclusion of a covered work + in an aggregate does not cause this License to apply to the other + parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms + of sections 4 and 5, provided that you also convey the + machine-readable Corresponding Source under the terms of this License, + in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be + included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, "normally used" refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as + part of a transaction in which the right of possession and use of the + User Product is transferred to the recipient in perpetuity or for a + fixed term (regardless of how the transaction is characterized), the + Corresponding Source conveyed under this section must be accompanied + by the Installation Information. But this requirement does not apply + if neither you nor any third party retains the ability to install + modified object code on the User Product (for example, the work has + been installed in ROM). + + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access to a + network may be denied when the modification itself materially and + adversely affects the operation of the network or violates the rules and + protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, + in accord with this section must be in a format that is publicly + documented (and with an implementation available to the public in + source code form), and must require no special password or key for + unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall + be treated as though they were included in this License, to the extent + that they are valid under applicable law. If additional permissions + apply only to part of the Program, that part may be used separately + under those permissions, but the entire Program remains governed by + this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option + remove any additional permissions from that copy, or from any part of + it. (Additional permissions may be written to require their own + removal in certain cases when you modify the work.) You may place + additional permissions on material, added by you to a covered work, + for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you + add to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further + restrictions" within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further + restriction, you may remove that term. If a license document contains + a further restriction but permits relicensing or conveying under this + License, you may add to a covered work material governed by the terms + of that license document, provided that the further restriction does + not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you + must place, in the relevant source files, a statement of the + additional terms that apply to those files, or a notice indicating + where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the + form of a separately written license, or stated as exceptions; + the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or + modify it is void, and will automatically terminate your rights under + this License (including any patent licenses granted under the third + paragraph of section 11). + + However, if you cease all violation of this License, then your + license from a particular copyright holder is reinstated (a) + provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and (b) permanently, if the copyright + holder fails to notify you of the violation by some reasonable means + prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is + reinstated permanently if the copyright holder notifies you of the + violation by some reasonable means, this is the first time you have + received notice of violation of this License (for any work) from that + copyright holder, and you cure the violation prior to 30 days after + your receipt of the notice. + + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or + run a copy of the Program. Ancillary propagation of a covered work + occurring solely as a consequence of using peer-to-peer transmission + to receive a copy likewise does not require acceptance. However, + nothing other than this License grants you permission to propagate or + modify any covered work. These actions infringe copyright if you do + not accept this License. Therefore, by modifying or propagating a + covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically + receives a license from the original licensors, to run, modify and + propagate that work, subject to this License. You are not responsible + for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered + work results from an entity transaction, each party to that + transaction who receives a copy of the work also receives whatever + licenses to the work the party's predecessor in interest had or could + give under the previous paragraph, plus a right to possession of the + Corresponding Source of the work from the predecessor in interest, if + the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the + rights granted or affirmed under this License. For example, you may + not impose a license fee, royalty, or other charge for exercise of + rights granted under this License, and you may not initiate litigation + (including a cross-claim or counterclaim in a lawsuit) alleging that + any patent claim is infringed by making, using, selling, offering for + sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The + work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims + owned or controlled by the contributor, whether already acquired or + hereafter acquired, that would be infringed by some manner, permitted + by this License, of making, using, or selling its contributor version, + but do not include claims that would be infringed only as a + consequence of further modification of the contributor version. For + purposes of this definition, "control" includes the right to grant + patent sublicenses in a manner consistent with the requirements of + this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to + make, use, sell, offer for sale, import and otherwise run, modify and + propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To "grant" such a patent license to a + party means to make such an agreement or commitment not to enforce a + patent against the party. + + If you convey a covered work, knowingly relying on a patent license, + and the Corresponding Source of the work is not available for anyone + to copy, free of charge and under the terms of this License, through a + publicly available network server or other readily accessible means, + then you must either (1) cause the Corresponding Source to be so + available, or (2) arrange to deprive yourself of the benefit of the + patent license for this particular work, or (3) arrange, in a manner + consistent with the requirements of this License, to extend the patent + license to downstream recipients. "Knowingly relying" means you have + actual knowledge that, but for the patent license, your conveying the + covered work in a country, or your recipient's use of the covered work + in a country, would infringe one or more identifiable patents in that + country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties + receiving the covered work authorizing them to use, propagate, modify + or convey a specific copy of the covered work, then the patent license + you grant is automatically extended to all recipients of the covered + work and works based on it. + + A patent license is "discriminatory" if it does not include within + the scope of its coverage, prohibits the exercise of, or is + conditioned on the non-exercise of one or more of the rights that are + specifically granted under this License. You may not convey a covered + work if you are a party to an arrangement with a third party that is + in the business of distributing software, under which you make payment + to the third party based on the extent of your activity of conveying + the work, and under which the third party grants, to any of the + parties who would receive the covered work from you, a discriminatory + patent license (a) in connection with copies of the covered work + conveyed by you (or copies made from those copies), or (b) primarily + for and in connection with specific products or compilations that + contain the covered work, unless you entered into that arrangement, + or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting + any implied license or other defenses to infringement that may + otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot convey a + covered work so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you may + not convey it at all. For example, if you agree to terms that obligate you + to collect a royalty for further conveying from those to whom you convey + the Program, the only way you could satisfy both those terms and this + License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have + permission to link or combine any covered work with a work licensed + under version 3 of the GNU Affero General Public License into a single + combined work, and to convey the resulting work. The terms of this + License will continue to apply to the part which is the covered work, + but the special requirements of the GNU Affero General Public License, + section 13, concerning interaction through a network will apply to the + combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of + the GNU General Public License from time to time. Such new versions will + be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. + + Each version is given a distinguishing version number. If the + Program specifies that a certain numbered version of the GNU General + Public License "or any later version" applies to it, you have the + option of following the terms and conditions either of that numbered + version or of any later version published by the Free Software + Foundation. If the Program does not specify a version number of the + GNU General Public License, you may choose any version ever published + by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future + versions of the GNU General Public License can be used, that proxy's + public statement of acceptance of a version permanently authorizes you + to choose that version for the Program. + + Later license versions may give you additional or different + permissions. However, no additional obligations are imposed on any + author or copyright holder as a result of your choosing to follow a + later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF + SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided + above cannot be given local legal effect according to their terms, + reviewing courts shall apply local law that most closely approximates + an absolute waiver of all civil liability in connection with the + Program, unless a warranty or assumption of liability accompanies a + copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest + possible use to the public, the best way to achieve this is to make it + free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest + to attach them to the start of each source file to most effectively + state the exclusion of warranty; and each file should have at least + the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short + notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + + The hypothetical commands `show w' and `show c' should show the appropriate + parts of the General Public License. Of course, your program's commands + might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, + if any, to sign a "copyright disclaimer" for the program, if necessary. + For more information on this, and how to apply and follow the GNU GPL, see + . + + The GNU General Public License does not permit incorporating your program + into proprietary programs. If your program is a subroutine library, you + may consider it more useful to permit linking proprietary applications with + the library. If this is what you want to do, use the GNU Lesser General + Public License instead of this License. But first, please read + . + + + Name: libquadmath + Files: scipy.libs/libquadmath*.so + Description: dynamically linked to files compiled with gcc + Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libquadmath + License: LGPL-2.1-or-later + + GCC Quad-Precision Math Library + Copyright (C) 2010-2019 Free Software Foundation, Inc. + Written by Francois-Xavier Coudert + + This file is part of the libquadmath library. + Libquadmath is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + Libquadmath is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: C +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: Topic :: Software Development :: Libraries +Classifier: Topic :: Scientific/Engineering +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +Project-URL: homepage, https://scipy.org/ +Project-URL: documentation, https://docs.scipy.org/doc/scipy/ +Project-URL: source, https://github.com/scipy/scipy +Project-URL: download, https://github.com/scipy/scipy/releases +Project-URL: tracker, https://github.com/scipy/scipy/issues +Requires-Python: >=3.10 +Requires-Dist: numpy<2.5,>=1.23.5 +Provides-Extra: test +Requires-Dist: pytest; extra == "test" +Requires-Dist: pytest-cov; extra == "test" +Requires-Dist: pytest-timeout; extra == "test" +Requires-Dist: pytest-xdist; extra == "test" +Requires-Dist: asv; extra == "test" +Requires-Dist: mpmath; extra == "test" +Requires-Dist: gmpy2; extra == "test" +Requires-Dist: threadpoolctl; extra == "test" +Requires-Dist: scikit-umfpack; extra == "test" +Requires-Dist: pooch; extra == "test" +Requires-Dist: hypothesis>=6.30; extra == "test" +Requires-Dist: array-api-strict<2.1.1,>=2.0; extra == "test" +Requires-Dist: Cython; extra == "test" +Requires-Dist: meson; extra == "test" +Requires-Dist: ninja; sys_platform != "emscripten" and extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx<8.0.0,>=5.0.0; extra == "doc" +Requires-Dist: intersphinx_registry; extra == "doc" +Requires-Dist: pydata-sphinx-theme>=0.15.2; extra == "doc" +Requires-Dist: sphinx-copybutton; extra == "doc" +Requires-Dist: sphinx-design>=0.4.0; extra == "doc" +Requires-Dist: matplotlib>=3.5; extra == "doc" +Requires-Dist: numpydoc; extra == "doc" +Requires-Dist: jupytext; extra == "doc" +Requires-Dist: myst-nb; extra == "doc" +Requires-Dist: pooch; extra == "doc" +Requires-Dist: jupyterlite-sphinx>=0.19.1; extra == "doc" +Requires-Dist: jupyterlite-pyodide-kernel; extra == "doc" +Provides-Extra: dev +Requires-Dist: mypy==1.10.0; extra == "dev" +Requires-Dist: typing_extensions; extra == "dev" +Requires-Dist: types-psutil; extra == "dev" +Requires-Dist: pycodestyle; extra == "dev" +Requires-Dist: ruff>=0.0.292; extra == "dev" +Requires-Dist: cython-lint>=0.12.2; extra == "dev" +Requires-Dist: rich-click; extra == "dev" +Requires-Dist: doit>=0.36.0; extra == "dev" +Requires-Dist: pydevtool; extra == "dev" +Description-Content-Type: text/x-rst + +.. image:: https://raw.githubusercontent.com/scipy/scipy/main/doc/source/_static/logo.svg + :target: https://scipy.org + :width: 110 + :height: 110 + :align: left + +.. image:: https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A + :target: https://numfocus.org + +.. image:: https://img.shields.io/pypi/dm/scipy.svg?label=Pypi%20downloads + :target: https://pypi.org/project/scipy/ + +.. image:: https://img.shields.io/conda/dn/conda-forge/scipy.svg?label=Conda%20downloads + :target: https://anaconda.org/conda-forge/scipy + +.. image:: https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg + :target: https://stackoverflow.com/questions/tagged/scipy + +.. image:: https://img.shields.io/badge/DOI-10.1038%2Fs41592--019--0686--2-blue.svg + :target: https://www.nature.com/articles/s41592-019-0686-2 + +SciPy (pronounced "Sigh Pie") is an open-source software for mathematics, +science, and engineering. It includes modules for statistics, optimization, +integration, linear algebra, Fourier transforms, signal and image processing, +ODE solvers, and more. + +- **Website:** https://scipy.org +- **Documentation:** https://docs.scipy.org/doc/scipy/ +- **Development version of the documentation:** https://scipy.github.io/devdocs +- **SciPy development forum:** https://discuss.scientific-python.org/c/contributor/scipy +- **Stack Overflow:** https://stackoverflow.com/questions/tagged/scipy +- **Source code:** https://github.com/scipy/scipy +- **Contributing:** https://scipy.github.io/devdocs/dev/index.html +- **Bug reports:** https://github.com/scipy/scipy/issues +- **Code of Conduct:** https://docs.scipy.org/doc/scipy/dev/conduct/code_of_conduct.html +- **Report a security vulnerability:** https://tidelift.com/docs/security +- **Citing in your work:** https://www.scipy.org/citing-scipy/ + +SciPy is built to work with +NumPy arrays, and provides many user-friendly and efficient numerical routines, +such as routines for numerical integration and optimization. Together, they +run on all popular operating systems, are quick to install, and are free of +charge. NumPy and SciPy are easy to use, but powerful enough to be depended +upon by some of the world's leading scientists and engineers. If you need to +manipulate numbers on a computer and display or publish the results, give +SciPy a try! + +For the installation instructions, see `our install +guide `__. + + +Call for Contributions +---------------------- + +We appreciate and welcome contributions. Small improvements or fixes are always appreciated; issues labeled as "good +first issue" may be a good starting point. Have a look at `our contributing +guide `__. + +Writing code isn’t the only way to contribute to SciPy. You can also: + +- review pull requests +- triage issues +- develop tutorials, presentations, and other educational materials +- maintain and improve `our website `__ +- develop graphic design for our brand assets and promotional materials +- help with outreach and onboard new contributors +- write grant proposals and help with other fundraising efforts + +If you’re unsure where to start or how your skills fit in, reach out! You can +ask on the `forum `__ +or here, on GitHub, by leaving a comment on a relevant issue that is already +open. + +If you are new to contributing to open source, `this +guide `__ helps explain why, what, +and how to get involved. diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/RECORD b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..a2fea4af1bb56d4f7502ff23658ad2f7e5e1571d --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/RECORD @@ -0,0 +1,2338 @@ +scipy-1.15.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +scipy-1.15.3.dist-info/LICENSE.txt,sha256=goy5pzacCp9jPOfBACP42CLw92tUziXHIzYdRbCIh5o,46845 +scipy-1.15.3.dist-info/METADATA,sha256=x1yvMrZ6E6n8FqrPmTqLuIO3ppdE_-d8u9Tzv7rSjz0,61956 +scipy-1.15.3.dist-info/RECORD,, +scipy-1.15.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy-1.15.3.dist-info/WHEEL,sha256=sZM_NeUMz2G4fDenMf11eikcCxcLaQWiYRmjwQBavQs,137 +scipy.libs/libgfortran-040039e1-0352e75f.so.5.0.0,sha256=xgkASOzMdjUiwS7wFvgdprYnyzoET1XPBHmoOcQcCYA,2833617 +scipy.libs/libgfortran-040039e1.so.5.0.0,sha256=FK-zEpsai1C8QKOwggx_EVLqm8EBIaqxUpQ_cFdHKIY,2686065 +scipy.libs/libquadmath-96973f99-934c22de.so.0.0.0,sha256=btUTf0Enga14Y0OftUNhP2ILQ8MrYykqACkkYWL1u8Y,250985 +scipy.libs/libquadmath-96973f99.so.0.0.0,sha256=k0wi3tDn0WnE1GeIdslgUa3z2UVF2pYvYLQWWbB12js,247609 +scipy.libs/libscipy_openblas-68440149.so,sha256=PnOaJDWvSdLZOHSfMWqjOMZfQcmuZhVAbnz4P-tmL0Y,22211841 +scipy/__config__.py,sha256=ucr8f6IcbvBMdAmSaZedVniAOkyDNGliOqAVyQSNz_M,5205 +scipy/__init__.py,sha256=GFkTqhB1Evr9XPid_UUqhxm0Wm66gz4tzuLL_Ri0u-U,4153 +scipy/__pycache__/__config__.cpython-310.pyc,, +scipy/__pycache__/__init__.cpython-310.pyc,, +scipy/__pycache__/_distributor_init.cpython-310.pyc,, +scipy/__pycache__/conftest.cpython-310.pyc,, +scipy/__pycache__/version.cpython-310.pyc,, +scipy/_distributor_init.py,sha256=zJThN3Fvof09h24804pNDPd2iN-lCHV3yPlZylSefgQ,611 +scipy/_lib/__init__.py,sha256=CXrH_YBpZ-HImHHrqXIhQt_vevp4P5NXClp7hnFMVLM,353 +scipy/_lib/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/__pycache__/_array_api.cpython-310.pyc,, +scipy/_lib/__pycache__/_array_api_no_0d.cpython-310.pyc,, +scipy/_lib/__pycache__/_bunch.cpython-310.pyc,, +scipy/_lib/__pycache__/_ccallback.cpython-310.pyc,, +scipy/_lib/__pycache__/_disjoint_set.cpython-310.pyc,, +scipy/_lib/__pycache__/_docscrape.cpython-310.pyc,, +scipy/_lib/__pycache__/_elementwise_iterative_method.cpython-310.pyc,, +scipy/_lib/__pycache__/_finite_differences.cpython-310.pyc,, +scipy/_lib/__pycache__/_gcutils.cpython-310.pyc,, +scipy/_lib/__pycache__/_pep440.cpython-310.pyc,, +scipy/_lib/__pycache__/_testutils.cpython-310.pyc,, +scipy/_lib/__pycache__/_threadsafety.cpython-310.pyc,, +scipy/_lib/__pycache__/_tmpdirs.cpython-310.pyc,, +scipy/_lib/__pycache__/_util.cpython-310.pyc,, +scipy/_lib/__pycache__/decorator.cpython-310.pyc,, +scipy/_lib/__pycache__/deprecation.cpython-310.pyc,, +scipy/_lib/__pycache__/doccer.cpython-310.pyc,, +scipy/_lib/__pycache__/uarray.cpython-310.pyc,, +scipy/_lib/_array_api.py,sha256=ydofyemfVuger2iLfVlUA2pseolDonoxypdAAQSTtBc,22353 +scipy/_lib/_array_api_no_0d.py,sha256=zVB7D070dZ9Rc-7mXvlkqpv75TgcvCy_7PL0q6yZsbg,4453 +scipy/_lib/_bunch.py,sha256=WooFxHL6t0SwjcwMDECM5wcWWLIS0St8zP3urDVK-V0,8120 +scipy/_lib/_ccallback.py,sha256=N9CO7kJYzk6IWQR5LHf_YA1-Oq48R38UIhJFIlJ2Qyc,7087 +scipy/_lib/_ccallback_c.cpython-310-x86_64-linux-gnu.so,sha256=cvW1nt3B5HFznbZ6ufuR8qk99dEs5SNXcTcbYGv-h2k,110176 +scipy/_lib/_disjoint_set.py,sha256=o_EUHZwnnI1m8nitEf8bSkF7TWZ65RSiklBN4daFruA,6160 +scipy/_lib/_docscrape.py,sha256=OUfg01moyk_U05boFoyiwKdpUe44iiqKcSkKVHNQsYY,23808 +scipy/_lib/_elementwise_iterative_method.py,sha256=79M1Rrgx01KoBKAgxjnY_QwbVerbnt_UpmgOYt97pwg,15277 +scipy/_lib/_finite_differences.py,sha256=llaIPvCOxpE4VA8O8EycPEU8i6LHJyOD-y7Y9OvQHt0,4172 +scipy/_lib/_fpumode.cpython-310-x86_64-linux-gnu.so,sha256=Kk1mpVY1lns4OpLjvNrW4B9W-nLAOgt6nH-0O5oSRTg,16400 +scipy/_lib/_gcutils.py,sha256=hajQd-HUw9ckK7QeBaqXVRpmnxPgyXO3QqqniEh7tRk,2669 +scipy/_lib/_pep440.py,sha256=vo3nxbfjtMfGq1ektYzHIzRbj8W-NHOMp5WBRjPlDTg,14005 +scipy/_lib/_test_ccallback.cpython-310-x86_64-linux-gnu.so,sha256=eVlhBo8MscWcnGS1WVQPtb6AsCXBqJhfY90wuHv2XTM,23232 +scipy/_lib/_test_deprecation_call.cpython-310-x86_64-linux-gnu.so,sha256=MGMi1imArTzYhEpgEOSaCD7Z9k-Y5Faq-U9ZJ6bId70,49544 +scipy/_lib/_test_deprecation_def.cpython-310-x86_64-linux-gnu.so,sha256=DbK0eJG5ZF9l8Tf_lfXNeh6TTgaman-fZlgcCIUjPws,34392 +scipy/_lib/_testutils.py,sha256=5Ua6vjKp02oRGpWX1icBHh1NjlgVCPRIVIrdgb9VSyc,12067 +scipy/_lib/_threadsafety.py,sha256=ttPEh64SKLjhQGZIYSm_9d5bW4cjAXoRZCA_a5-nK9M,1453 +scipy/_lib/_tmpdirs.py,sha256=z3IYpzACnWdN_BMjOvqYbkTvYyUbfbQvfehq7idENSo,2374 +scipy/_lib/_uarray/LICENSE,sha256=yAw5tfzga6SJfhTgsKiLVEWDNNlR6xNhQC_60s-4Y7Q,1514 +scipy/_lib/_uarray/__init__.py,sha256=Rww7wLA7FH6Yong7oMgl_sHPpjcRslRaTjh61W_xVg4,4493 +scipy/_lib/_uarray/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/_uarray/__pycache__/_backend.cpython-310.pyc,, +scipy/_lib/_uarray/_backend.py,sha256=LZnSLJ2UK209jrMtocOMoc5grlNoob3tbb1HbW0XlAQ,20531 +scipy/_lib/_uarray/_uarray.cpython-310-x86_64-linux-gnu.so,sha256=Bwt4jbURprvVw-XXxLl6zvVJdxebsHphZjxuYXouT2Y,178176 +scipy/_lib/_util.py,sha256=yEp-zOqfklOTMcvzAL0S9dTffhuJDOiYchIYxWBkbFE,44605 +scipy/_lib/array_api_compat/__init__.py,sha256=jjRoCLlFhQjrHK2xCR3aHoUVjovGKMBSBsHZmi6yjjI,969 +scipy/_lib/array_api_compat/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/__pycache__/_internal.cpython-310.pyc,, +scipy/_lib/array_api_compat/_internal.py,sha256=0GHLUJRbBHZLsbgRYE0OCtxAKdYuLtr1qzh70N5vBQI,1010 +scipy/_lib/array_api_compat/common/__init__.py,sha256=HB4vvyS0GnH6JQSEgAC75oa-s2WBIiQQebpgXnW00N0,37 +scipy/_lib/array_api_compat/common/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_helpers.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/common/_aliases.py,sha256=Vr_64oTgASVrbawHA2oJjJhYXLPx7tXii8vFPyG8D98,17875 +scipy/_lib/array_api_compat/common/_fft.py,sha256=qZvAveqXFwEQxCbTNx9l_41EpQpAwMfwS2GqWKEVwow,4520 +scipy/_lib/array_api_compat/common/_helpers.py,sha256=4gXCgC9TRmgFlXxfHtznk6Jv7MOZ03e3xE1f7jQKaC0,23956 +scipy/_lib/array_api_compat/common/_linalg.py,sha256=BebUx7WRkz9DAx9lrrP8d57-uN0VobwLGX0xbvI-7Wg,6142 +scipy/_lib/array_api_compat/common/_typing.py,sha256=KBJcLRAG2MeID9V38-GBipfpsFWGGrxOKkgfSQmgjXE,414 +scipy/_lib/array_api_compat/cupy/__init__.py,sha256=3079YH9uF2HoG8E27bp_1lsIVvYsdrq8hKMk_jT3NFs,442 +scipy/_lib/array_api_compat/cupy/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/cupy/_aliases.py,sha256=aCmWDlvcdhagje7QDxgF-jqTmUk6mnVIl2hOky1IpBE,4538 +scipy/_lib/array_api_compat/cupy/_info.py,sha256=kdUS8xcIVg_0Mgg2qSzuqOrXgopaHO_G8JmGBB-4qOM,9805 +scipy/_lib/array_api_compat/cupy/_typing.py,sha256=oDhrZB8R-D6wvee7tR4YkyBhTq93M0fFi3Tv-lpN_Dg,617 +scipy/_lib/array_api_compat/cupy/fft.py,sha256=xCAC42CNAwAyVW7uCREsSoAV23R3rL2dqrT7w877zuE,842 +scipy/_lib/array_api_compat/cupy/linalg.py,sha256=nKOM-_wcOHzHhEeV9KBzcMVNlviJK4nP1nFBUtvnjTM,1444 +scipy/_lib/array_api_compat/dask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/_lib/array_api_compat/dask/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__init__.py,sha256=7_FttjbrGeKtPFGS_CA85WZZmbxPwkpxvsMS8KTMEFw,242 +scipy/_lib/array_api_compat/dask/array/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/dask/array/_aliases.py,sha256=ERdXHmeTGKxBMSiSt_VlxsnZ0sLh9K8bxkWQT1OKqMM,6549 +scipy/_lib/array_api_compat/dask/array/_info.py,sha256=D8hG1uRNsF31_WX5bnulbdl75Jkd6G2DbkmhXXTplEs,10410 +scipy/_lib/array_api_compat/dask/array/fft.py,sha256=FWXfXVz9zUGKVtYJWl-xSb9BUp7UIewQ89FzGimwOOA,553 +scipy/_lib/array_api_compat/dask/array/linalg.py,sha256=5E3wSAXmiZJ5rf69u6Pzw1Xs0lCdMpiVBnheA4lzY4E,2441 +scipy/_lib/array_api_compat/numpy/__init__.py,sha256=uxjYAO4xcDhTQPbrD2XmkWT5TyZsjpwc5FD-ViHxN-c,831 +scipy/_lib/array_api_compat/numpy/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/numpy/_aliases.py,sha256=ZrddTjHOVUNvDM1h9p7NqXqaODVJKkKu2fTyPClCmXg,4485 +scipy/_lib/array_api_compat/numpy/_info.py,sha256=GAD-zNvAMUSeUJfjABY6p_eYkG--KBBgz1vdQkL2-UA,10384 +scipy/_lib/array_api_compat/numpy/_typing.py,sha256=OFRXfhT8-snL_4VeOjbOCd_yYIGqVS-IRrZoWNcL3v4,618 +scipy/_lib/array_api_compat/numpy/fft.py,sha256=vlrYUcv2VV5mOOEb5R4u83nFSSDmE-nfJYM-lmq1Dao,679 +scipy/_lib/array_api_compat/numpy/linalg.py,sha256=ne4h3Ui1esyzD9p7Ko2IueJvgpSUmfF_Z5aWbiBKJc0,3256 +scipy/_lib/array_api_compat/torch/__init__.py,sha256=sk32NV12KrlR8a-UjiBdjJspUcex5j7REAGgSJoI3do,591 +scipy/_lib/array_api_compat/torch/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/_aliases.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/_info.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/fft.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/__pycache__/linalg.cpython-310.pyc,, +scipy/_lib/array_api_compat/torch/_aliases.py,sha256=kCIeFyzzUqNh86Byo5Ai2s1guK2-OkXg62chBCN_kgU,28559 +scipy/_lib/array_api_compat/torch/_info.py,sha256=rnInxwjMErvcHLI4S6fzom7N43hoAqS0rysw1K8Riyw,11413 +scipy/_lib/array_api_compat/torch/fft.py,sha256=AVHOwIxM-t9_w-FjVF79RrzeC5wYc5g97WPUp7bIHlA,1794 +scipy/_lib/array_api_compat/torch/linalg.py,sha256=dJ0o1gCbSDtklpvgZCxx3gbHXW9q3I4u8ZLFPW24dJs,4770 +scipy/_lib/array_api_extra/__init__.py,sha256=916j5GLpulyZZsUQa-I_r510XDVbap_aIrVpCVn_PIk,266 +scipy/_lib/array_api_extra/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/array_api_extra/__pycache__/_funcs.cpython-310.pyc,, +scipy/_lib/array_api_extra/__pycache__/_typing.cpython-310.pyc,, +scipy/_lib/array_api_extra/_funcs.py,sha256=T5nPgBxYOb8DkNHlEM52Qf70Nf7Qb6lFtlDtuvmEk4c,14906 +scipy/_lib/array_api_extra/_typing.py,sha256=E3XJz5PbjXP-ckQMQLi_nOJPLr-B0cm_EVArRwY-7FY,193 +scipy/_lib/cobyqa/__init__.py,sha256=9Gj-EtpYGRmh0-ADiX0t0psItcvMgzIMwFDzlvOzcE8,578 +scipy/_lib/cobyqa/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/framework.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/main.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/models.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/problem.cpython-310.pyc,, +scipy/_lib/cobyqa/__pycache__/settings.cpython-310.pyc,, +scipy/_lib/cobyqa/framework.py,sha256=lIeKCkDLxHbMmSTiMcyasvVe77jVvh_YTOYX0HnK4Qk,38900 +scipy/_lib/cobyqa/main.py,sha256=wz0M2iqFfzeTaZUq_j1TkF_9V_SJ1t73A-0fdH0eSs4,57527 +scipy/_lib/cobyqa/models.py,sha256=cAM8_np_xFSRwKsjaMRZu9Dc9xQOQPAZVWxsvR_7qjE,50656 +scipy/_lib/cobyqa/problem.py,sha256=SiPgmiFTxiW5yJ_FVf37Z9GQGo6Gx_fJ3RXMzhsrn40,40203 +scipy/_lib/cobyqa/settings.py,sha256=ogfiShxuPHsMfW16OGSwB9-mIPRiuWZSGXBOCO2HDvw,3826 +scipy/_lib/cobyqa/subsolvers/__init__.py,sha256=VmFBpi-_tNa8yzNmu_fufewmPTnCU6ycNCGcN34UBcc,341 +scipy/_lib/cobyqa/subsolvers/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/__pycache__/geometry.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/__pycache__/optim.cpython-310.pyc,, +scipy/_lib/cobyqa/subsolvers/geometry.py,sha256=dgS-C0QBUhkzPhHULFIRbnbFOIEB005GyPYE-i-cuFY,14173 +scipy/_lib/cobyqa/subsolvers/optim.py,sha256=hIseVqrPyI3ezICGNXkCtKlpqvAO2W6ZQe0n7sxfkss,45512 +scipy/_lib/cobyqa/utils/__init__.py,sha256=sw6g402vXaXwX7rMhxrNl5PD5OBs89l5f3XNcYApRHs,359 +scipy/_lib/cobyqa/utils/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/exceptions.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/math.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/__pycache__/versions.cpython-310.pyc,, +scipy/_lib/cobyqa/utils/exceptions.py,sha256=N1JdmUxHnME95wEZHyeeF_M6GXPEqH5t3qzuXig49YE,483 +scipy/_lib/cobyqa/utils/math.py,sha256=beT-Tib41TJWZecjnKhSfu4foOLLaHlWj5CcyRhdSl4,1611 +scipy/_lib/cobyqa/utils/versions.py,sha256=eBOlEGAKFCfjFqVprdali3M1G7l0k_kxb7ku-Lz2bU0,1465 +scipy/_lib/decorator.py,sha256=-Rm0CvawUDXzPssHjts9vrDAC57_d_x4IfOAzgf19SQ,15021 +scipy/_lib/deprecation.py,sha256=2xwTeh_7Uc71zmnJW264zxjvh0LUWQqZsH6s95dQDyo,9840 +scipy/_lib/doccer.py,sha256=dzTRxBKnbl1wSILhYgrAj3-V0i0JvK-UhaWP0xJ7NpI,10907 +scipy/_lib/messagestream.cpython-310-x86_64-linux-gnu.so,sha256=N8Xt4BSFY-yCqAY8CdqOsY0fWIZxglqRiLRI1CgEggQ,85760 +scipy/_lib/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/_lib/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test__gcutils.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test__pep440.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test__testutils.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test__threadsafety.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test__util.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_array_api.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_bunch.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_ccallback.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_config.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_deprecation.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_doccer.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_import_cycles.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_public_api.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_scipy_version.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_tmpdirs.cpython-310.pyc,, +scipy/_lib/tests/__pycache__/test_warnings.cpython-310.pyc,, +scipy/_lib/tests/test__gcutils.py,sha256=Uadt4yXwuLDMCSbf4cpMszR_5NOeVQC1E_v4NZAeJR4,3729 +scipy/_lib/tests/test__pep440.py,sha256=u9hPoolK4AoIIS-Rq74Du5SJu5og2RxMwgaAvGgWvRo,2277 +scipy/_lib/tests/test__testutils.py,sha256=P4WDJpUgy19wD9tknQSjIivuQvZF7YUBGSBWlur2QRA,800 +scipy/_lib/tests/test__threadsafety.py,sha256=qSfCF5OG_5lbnSl-grmDN_QCU4QLe-fS3sqnwL04pf8,1322 +scipy/_lib/tests/test__util.py,sha256=-66scvDLNcoXLcVVSBFP4l6J0htlJvCEoNPHJJvMkVI,24645 +scipy/_lib/tests/test_array_api.py,sha256=wAPTUd0GySv5pQXTN7Cn75-tETWYM46y473UCxXn-nQ,8075 +scipy/_lib/tests/test_bunch.py,sha256=sViE5aFSmAccfk8kYvt6EmzR5hyQ9nOSWMcftaDYDBg,6168 +scipy/_lib/tests/test_ccallback.py,sha256=dy9g70zyd80KpawffSKgWbddsKUwNNeF5sbxMfCTk6w,6175 +scipy/_lib/tests/test_config.py,sha256=ekM39jzkDFcuk3ahIMn-j4JUz3kZeSDxxB_2WRRxULM,1275 +scipy/_lib/tests/test_deprecation.py,sha256=pIia1qGES_ABOfbqLSSlXzmLmeBjpziyvh9J2mUUcMA,390 +scipy/_lib/tests/test_doccer.py,sha256=2HGlzqu7dgJ7collFy6SunjKc4lKMFo4TZIUQCHlVoU,4053 +scipy/_lib/tests/test_import_cycles.py,sha256=K4LfxIHzFRIj4XGGmpRhYj4Kij8GXYxKGbIX8WfjUWQ,586 +scipy/_lib/tests/test_public_api.py,sha256=ZB6xJ_-qVr1paESyx0MMGJQSxdFPqJeHs2BWiwQeeUk,18066 +scipy/_lib/tests/test_scipy_version.py,sha256=kVoxuBUidCHsVpvybRPoVJzkv2hUixRwuDAEAqPgpaA,918 +scipy/_lib/tests/test_tmpdirs.py,sha256=DiSY_ReQtD9Ou01pJ49MVY1aT6L62W2Odbbr-zEm3zI,1337 +scipy/_lib/tests/test_warnings.py,sha256=ZQ_4o16m2b--0v8erteoUd2pA134GzMRZhTV9vfuhqI,4949 +scipy/_lib/uarray.py,sha256=4X0D3FBQR6HOYcwMftjH-38Kt1nkrS-eD4c5lWL5DGo,815 +scipy/cluster/__init__.py,sha256=pgzWiWR5smQ3rwud2dhnLn6dpkD5lju_moElQp_zhoE,880 +scipy/cluster/__pycache__/__init__.cpython-310.pyc,, +scipy/cluster/__pycache__/hierarchy.cpython-310.pyc,, +scipy/cluster/__pycache__/vq.cpython-310.pyc,, +scipy/cluster/_hierarchy.cpython-310-x86_64-linux-gnu.so,sha256=QNqTQXWOfXeOfFMQIH0ye3aqiDYQH06n6U6QZ7OcOeU,423504 +scipy/cluster/_optimal_leaf_ordering.cpython-310-x86_64-linux-gnu.so,sha256=ERE2l6rmQGvubAjb5muqLBlxykc5-vnLQv9evC5Dbhg,355856 +scipy/cluster/_vq.cpython-310-x86_64-linux-gnu.so,sha256=iS2u_uboSU58mHv5tKByRpu29o-rDx3qDZzHCIRFiiI,127888 +scipy/cluster/hierarchy.py,sha256=gXomjlief0U5nn-lYGxONKA6GMQB6Xtl0PAqJKm9e_E,149078 +scipy/cluster/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/cluster/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/cluster/tests/__pycache__/hierarchy_test_data.cpython-310.pyc,, +scipy/cluster/tests/__pycache__/test_disjoint_set.cpython-310.pyc,, +scipy/cluster/tests/__pycache__/test_hierarchy.cpython-310.pyc,, +scipy/cluster/tests/__pycache__/test_vq.cpython-310.pyc,, +scipy/cluster/tests/hierarchy_test_data.py,sha256=7syUYdIaDVr7hgvMliX0CW4386utjBJn1DOgX0USXls,6850 +scipy/cluster/tests/test_disjoint_set.py,sha256=EuHGBE3ZVEMnWFbCn8tjI-_6CWrNXfpnv5bUBa9qhWI,5525 +scipy/cluster/tests/test_hierarchy.py,sha256=t4pjYeKNvovBnotlUxX-m1RMBdTSVYvHslWPQ9zjCzc,52109 +scipy/cluster/tests/test_vq.py,sha256=zzM7GmiApkd3fuGYv9405vU9tNNMiFVTqHcvh2phafs,18973 +scipy/cluster/vq.py,sha256=wa5bcXyigz2XiCNOu91qCuw0fvreoKSbHaRP0QQbOs4,30548 +scipy/conftest.py,sha256=Q3DbWzqWdFt8hkq16Bbg4MQ8WaxgKuhhKp6XEEZ8bWw,22027 +scipy/constants/__init__.py,sha256=1Iqylk8TvAxegNKIcFIUVXwiH5ItKpdKtCcVPhEBvPQ,14839 +scipy/constants/__pycache__/__init__.cpython-310.pyc,, +scipy/constants/__pycache__/_codata.cpython-310.pyc,, +scipy/constants/__pycache__/_constants.cpython-310.pyc,, +scipy/constants/__pycache__/codata.cpython-310.pyc,, +scipy/constants/__pycache__/constants.cpython-310.pyc,, +scipy/constants/_codata.py,sha256=fIhZGWMCGLGSwO3rnNmDEisAN1rGLwkNbSlwdZDpowQ,202354 +scipy/constants/_constants.py,sha256=1OBL3gWWsaid_3eR8t7DvzE-sN8B_AKiSUCY4PZOztM,10497 +scipy/constants/codata.py,sha256=ThmW8ohzndi-4-WtyVXxSrW40MnLIz1XoqRcm2RgSHw,614 +scipy/constants/constants.py,sha256=w7sGxSidD2Q9Ged0Sn1pnL-qqD1ssEP1A8sZWeLWBeI,2250 +scipy/constants/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/constants/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/constants/tests/__pycache__/test_codata.cpython-310.pyc,, +scipy/constants/tests/__pycache__/test_constants.cpython-310.pyc,, +scipy/constants/tests/test_codata.py,sha256=AKabbXFbMwLw-SQKethXND34uJ5y_HUA20DgOzqSvsg,2841 +scipy/constants/tests/test_constants.py,sha256=G4ffHfFeFMIXUtQI8Kd7wZdrfNCr1sJx2-4H9-mCFzE,4675 +scipy/datasets/__init__.py,sha256=X_9AbefPK1_pg-eG7g3nn--JhoHeDsrEFbJfbI5Hyak,2802 +scipy/datasets/__pycache__/__init__.cpython-310.pyc,, +scipy/datasets/__pycache__/_download_all.cpython-310.pyc,, +scipy/datasets/__pycache__/_fetchers.cpython-310.pyc,, +scipy/datasets/__pycache__/_registry.cpython-310.pyc,, +scipy/datasets/__pycache__/_utils.cpython-310.pyc,, +scipy/datasets/_download_all.py,sha256=iRPR2IUk6C3B5u2q77yOhac449MRSoRaTlCy2oCIknE,1701 +scipy/datasets/_fetchers.py,sha256=4sdEEQpTI99QCR9DoLv_D6Dwd4N9cSLRJX8cENX_QCg,6735 +scipy/datasets/_registry.py,sha256=br0KfyalEbh5yrQLznQ_QvBtmN4rMsm0UxOjnsJp4OQ,1072 +scipy/datasets/_utils.py,sha256=kdZ-Opp7Dr1pCwM285p3GVjgZTx_mKWCvETur92FWg4,2967 +scipy/datasets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/datasets/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/datasets/tests/__pycache__/test_data.cpython-310.pyc,, +scipy/datasets/tests/test_data.py,sha256=6DJtyMDmwi_ghOrDuryVakZQExFq-MIKiuJi_Cr7kdM,4213 +scipy/differentiate/__init__.py,sha256=nZ3imDWtf1QzImE-xsrYHE4kuOa8tEuc99Hl0zAFqzI,621 +scipy/differentiate/__pycache__/__init__.cpython-310.pyc,, +scipy/differentiate/__pycache__/_differentiate.cpython-310.pyc,, +scipy/differentiate/_differentiate.py,sha256=zFkAn71YqLGg4rDufjlxFzhnXnHMuLuJCmIwNVQ1GG0,50595 +scipy/differentiate/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/differentiate/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/differentiate/tests/__pycache__/test_differentiate.cpython-310.pyc,, +scipy/differentiate/tests/test_differentiate.py,sha256=X8kfzIwvk4GFYV5nL1h86a9HJ79SgU8eDJBxiy3HUKg,28039 +scipy/fft/__init__.py,sha256=0cjHIwyHnjoz1XUUe3OB70vrQR0-pFp8Uv34-U-FGRg,3632 +scipy/fft/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/__pycache__/_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_basic.cpython-310.pyc,, +scipy/fft/__pycache__/_basic_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_debug_backends.cpython-310.pyc,, +scipy/fft/__pycache__/_fftlog.cpython-310.pyc,, +scipy/fft/__pycache__/_fftlog_backend.cpython-310.pyc,, +scipy/fft/__pycache__/_helper.cpython-310.pyc,, +scipy/fft/__pycache__/_realtransforms.cpython-310.pyc,, +scipy/fft/__pycache__/_realtransforms_backend.cpython-310.pyc,, +scipy/fft/_backend.py,sha256=5rBxK8GQtCMnuPHc-lNQdpH4uFFZ9_5vBukkDv6jRRA,6544 +scipy/fft/_basic.py,sha256=lGJ8qQTMXUJEbq_2vwfPPPlX7b4j358ks9LLretOtEY,62997 +scipy/fft/_basic_backend.py,sha256=Qms-BE7DCJYNSq9Vd5utnKiwVTqRIUzLYYEiMyTdpfE,7447 +scipy/fft/_debug_backends.py,sha256=RlvyunZNqaDDsI3-I6QH6GSBz_faT6EN4OONWsvMtR8,598 +scipy/fft/_fftlog.py,sha256=JeLVCAgfB99brT2Ez9tzdapmhWrTfYCUYEi2KTvPzIQ,7864 +scipy/fft/_fftlog_backend.py,sha256=UgoePwhoMoLxvQ5soSUZkVWvWWTP7y1xWVAD9BlrdJY,5304 +scipy/fft/_helper.py,sha256=wQ5ZlvOEY9snn32Yg6p0W_DcQu70JRaHTu_lrrODtlA,12385 +scipy/fft/_pocketfft/LICENSE.md,sha256=wlSytf0wrjyJ02ugYXMFY7l2D8oE8bdGobLDFX2ix4k,1498 +scipy/fft/_pocketfft/__init__.py,sha256=dROVDi9kRvkbSdynd3L09tp9_exzQ4QqG3xnNx78JeU,207 +scipy/fft/_pocketfft/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/basic.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/helper.cpython-310.pyc,, +scipy/fft/_pocketfft/__pycache__/realtransforms.cpython-310.pyc,, +scipy/fft/_pocketfft/basic.py,sha256=4HR-eRDb6j4YR4sqKnTikFmG0tnUIXxa0uImnB6_JVs,8138 +scipy/fft/_pocketfft/helper.py,sha256=mmiRCzeNuPSUUFYubG1VRO4nMIRDDelSGDZrdomBno0,5841 +scipy/fft/_pocketfft/pypocketfft.cpython-310-x86_64-linux-gnu.so,sha256=DI9FFpkUASTBx2XuzINF8Eocryy8axT-0BjptenzFTk,1201872 +scipy/fft/_pocketfft/realtransforms.py,sha256=4TmqAkCDQK3gs1ddxXY4rOrVfvQqO8NyVtOzziUGw6E,3344 +scipy/fft/_pocketfft/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/fft/_pocketfft/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/_pocketfft/tests/__pycache__/test_basic.cpython-310.pyc,, +scipy/fft/_pocketfft/tests/__pycache__/test_real_transforms.cpython-310.pyc,, +scipy/fft/_pocketfft/tests/test_basic.py,sha256=wG06l401F3jGl_2mzwdTU1-7X-tp54fYcMqAqId2dUw,35715 +scipy/fft/_pocketfft/tests/test_real_transforms.py,sha256=vsQ3RdHDtJKhypf4v1MLTgy782XWvFMykPHrDie0bio,16879 +scipy/fft/_realtransforms.py,sha256=QmO9CDqrAsvBcLNgIzFBIWBTYsSUCRJ_Cj1myv73KlE,25386 +scipy/fft/_realtransforms_backend.py,sha256=u4y4nBGCxpTLVqxK1J7xV6tcpeC3-8iiSEXLOcRM9wI,2389 +scipy/fft/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/fft/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/fft/tests/__pycache__/mock_backend.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_backend.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_basic.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_fftlog.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_helper.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_multithreading.cpython-310.pyc,, +scipy/fft/tests/__pycache__/test_real_transforms.cpython-310.pyc,, +scipy/fft/tests/mock_backend.py,sha256=p17Hfg6xuoF6Ldxwe1PZ-79Lf_r9FyJUR00N4TokM8k,2685 +scipy/fft/tests/test_backend.py,sha256=DFJ6OKV6gRw4p9OuVfy1ENTeLJCbYS2GuppwpJnwQGQ,4285 +scipy/fft/tests/test_basic.py,sha256=h0JPW3pX0da5F5zMaxodnGMZtmxhmQto14LkRUZ-UXI,20719 +scipy/fft/tests/test_fftlog.py,sha256=D98q61cNJx2UsQyJ-jHwGhYha_HOf0KxJqOZsljHWH8,7362 +scipy/fft/tests/test_helper.py,sha256=TZChUViGsjAWHn21OmotAQyJ4C_icojkSSnG3cO_2Uc,20187 +scipy/fft/tests/test_multithreading.py,sha256=JMSXQocScFghpsy47zov03R5MbEY0Z3ROGt6GxFeWzo,2150 +scipy/fft/tests/test_real_transforms.py,sha256=0lSYAeDXOft_wvKGlI37rIAB1OXfxl-wZVf-Grxy6yU,9287 +scipy/fftpack/__init__.py,sha256=rLCBFC5Dx5ij_wmL7ChiGmScYlgu0mhaWtrJaz_rBt0,3155 +scipy/fftpack/__pycache__/__init__.cpython-310.pyc,, +scipy/fftpack/__pycache__/_basic.cpython-310.pyc,, +scipy/fftpack/__pycache__/_helper.cpython-310.pyc,, +scipy/fftpack/__pycache__/_pseudo_diffs.cpython-310.pyc,, +scipy/fftpack/__pycache__/_realtransforms.cpython-310.pyc,, +scipy/fftpack/__pycache__/basic.cpython-310.pyc,, +scipy/fftpack/__pycache__/helper.cpython-310.pyc,, +scipy/fftpack/__pycache__/pseudo_diffs.cpython-310.pyc,, +scipy/fftpack/__pycache__/realtransforms.cpython-310.pyc,, +scipy/fftpack/_basic.py,sha256=Sk_gfswmWKb3za6wrU_mIrRVBl69qjzAu9ltznbDCKs,13098 +scipy/fftpack/_helper.py,sha256=8r6Hh2FA5qTzYyn8y4jfaG41FXMfqQyK6SN8x1dIbaE,3348 +scipy/fftpack/_pseudo_diffs.py,sha256=T39Owz8EgL4oqmViBT0ggen9DXOtNHWRxh-n6I7pLyw,15936 +scipy/fftpack/_realtransforms.py,sha256=2k91B3tSnFm6gKsQn-hRGx4J238CKvqwvQevKgDMuaQ,19222 +scipy/fftpack/basic.py,sha256=i2CMMS__L3UtFFqe57E0cs7AZ4U6VO-Ted1KhU7_wNc,577 +scipy/fftpack/convolve.cpython-310-x86_64-linux-gnu.so,sha256=G7blA59SEt72X2O7oK55lru04KhUxX63WK5qhzzSC6s,272968 +scipy/fftpack/helper.py,sha256=M7jTN4gQIRWpkArQR13bI7WN6WcW-AabxKgrOHRvfeQ,580 +scipy/fftpack/pseudo_diffs.py,sha256=h0vkjsSqAThy7OdTkYWVxQqZ3rILohg7MXJqf5CGMTE,658 +scipy/fftpack/realtransforms.py,sha256=9-mR-VV3W14oTaD6pB5-RIDV3vkTBQmGCcxfbA8GYH0,595 +scipy/fftpack/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/fftpack/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/fftpack/tests/__pycache__/test_basic.cpython-310.pyc,, +scipy/fftpack/tests/__pycache__/test_helper.cpython-310.pyc,, +scipy/fftpack/tests/__pycache__/test_import.cpython-310.pyc,, +scipy/fftpack/tests/__pycache__/test_pseudo_diffs.cpython-310.pyc,, +scipy/fftpack/tests/__pycache__/test_real_transforms.cpython-310.pyc,, +scipy/fftpack/tests/fftw_double_ref.npz,sha256=pgxklBW2RSI5JNg0LMxcCXgByGkBKHo2nlP8kln17E4,162120 +scipy/fftpack/tests/fftw_longdouble_ref.npz,sha256=pAbL1NrQTQxZ3Tj1RBb7SUJMgiKcGgdLakTsDN4gAOM,296072 +scipy/fftpack/tests/fftw_single_ref.npz,sha256=J2qRQTGOb8NuSrb_VKYbZAVO-ISbZg8XNZ5fVBtDxSY,95144 +scipy/fftpack/tests/test.npz,sha256=Nt6ASiLY_eoFRZDOSd3zyFmDi32JGTxWs7y2YMv0N5c,11968 +scipy/fftpack/tests/test_basic.py,sha256=7nJo-X2q7SHXAMha6WJZUZiufODTiVR8TnT3E8Oq7t4,30554 +scipy/fftpack/tests/test_helper.py,sha256=8JaPSJOwsk5XXOf1zFahJ_ktUTfNGSk2-k3R6e420XI,1675 +scipy/fftpack/tests/test_import.py,sha256=dzyXQHtsdW2WL5ruVp_-MsqSQd_n-tuyq22okrzXlGw,1156 +scipy/fftpack/tests/test_pseudo_diffs.py,sha256=ZJU6AkkH6jKjebu_-Ant-dT6tUGwo1Jx9c5kou1floU,13733 +scipy/fftpack/tests/test_real_transforms.py,sha256=QgaxzmzF5FdUkt5iCtNq-tT5lDjTE_Tyz-BOG5s7RFM,24485 +scipy/integrate/__init__.py,sha256=CmPLfkF66jXhHsKyQPOsvFEc9nxicRYwl6WDAa7cfJk,4373 +scipy/integrate/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/__pycache__/_bvp.cpython-310.pyc,, +scipy/integrate/__pycache__/_cubature.cpython-310.pyc,, +scipy/integrate/__pycache__/_lebedev.cpython-310.pyc,, +scipy/integrate/__pycache__/_ode.cpython-310.pyc,, +scipy/integrate/__pycache__/_odepack_py.cpython-310.pyc,, +scipy/integrate/__pycache__/_quad_vec.cpython-310.pyc,, +scipy/integrate/__pycache__/_quadpack_py.cpython-310.pyc,, +scipy/integrate/__pycache__/_quadrature.cpython-310.pyc,, +scipy/integrate/__pycache__/_tanhsinh.cpython-310.pyc,, +scipy/integrate/__pycache__/dop.cpython-310.pyc,, +scipy/integrate/__pycache__/lsoda.cpython-310.pyc,, +scipy/integrate/__pycache__/odepack.cpython-310.pyc,, +scipy/integrate/__pycache__/quadpack.cpython-310.pyc,, +scipy/integrate/__pycache__/vode.cpython-310.pyc,, +scipy/integrate/_bvp.py,sha256=0EazRKECaOErYe_MAAbmgRrbkdOgSXpwkQfwPLxP30I,40897 +scipy/integrate/_cubature.py,sha256=DI7iFsEgT4LpuPzXKReXqCWCwhXlsMWvhBiH_tkAKTY,25671 +scipy/integrate/_dop.cpython-310-x86_64-linux-gnu.so,sha256=667po3Y1B8nwYSLqptTJCwP_ua-z0VpvJ3tqqBXk6Jk,116993 +scipy/integrate/_ivp/__init__.py,sha256=gKFR_pPjr8fRLgAGY5sOzYKGUFu2nGX8x1RrXT-GZZc,256 +scipy/integrate/_ivp/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/base.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/bdf.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/common.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/dop853_coefficients.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/ivp.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/lsoda.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/radau.cpython-310.pyc,, +scipy/integrate/_ivp/__pycache__/rk.cpython-310.pyc,, +scipy/integrate/_ivp/base.py,sha256=Mlef_dgmn0wzjFxZA3oBbtHrQgrfdZw_8k1mLYNZP4A,10295 +scipy/integrate/_ivp/bdf.py,sha256=tTN2OiFRjGlIT-PkrCLi-mBfUmcAZ8NEprFSjwR_K5U,17501 +scipy/integrate/_ivp/common.py,sha256=GVKTcx-QO7WPr2ejNAi94aEdMv03zFVOr24Q1w2rZ2I,15745 +scipy/integrate/_ivp/dop853_coefficients.py,sha256=OrYvW0Hu6X7sOh37FU58gNkgC77KVpYclewv_ARGMAE,7237 +scipy/integrate/_ivp/ivp.py,sha256=DGmLGk4TbhkGhBiJvnbeNScZzLdnm-6nJoWt83hrz-s,31743 +scipy/integrate/_ivp/lsoda.py,sha256=t5t2jZBgBPt0G20TOI4SVXuGFAZYAhfDlJZhfCzeeDo,9927 +scipy/integrate/_ivp/radau.py,sha256=0KpFk0Me857geCXbbvAyTkqbrO8OI_2kLTdzGLpqYlY,19676 +scipy/integrate/_ivp/rk.py,sha256=-l1jAJF_T5SeaZsRb1muFHFZ1cYUfVXZQNydMwOJEFY,22800 +scipy/integrate/_ivp/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/integrate/_ivp/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/_ivp/tests/__pycache__/test_ivp.cpython-310.pyc,, +scipy/integrate/_ivp/tests/__pycache__/test_rk.cpython-310.pyc,, +scipy/integrate/_ivp/tests/test_ivp.py,sha256=A0hw3AqENXeTFp1Rcb_4ayEsLYLuMVFz9s7UrglatLQ,42823 +scipy/integrate/_ivp/tests/test_rk.py,sha256=K9UxZghBzSL2BzmgLndPJcWOWV4Nr530TGKWakpsoeM,1326 +scipy/integrate/_lebedev.py,sha256=Tj3I_tnQ3_mfARK_scDsd9aM5dLe9To-GeaCda5OMKw,262024 +scipy/integrate/_lsoda.cpython-310-x86_64-linux-gnu.so,sha256=CDho69YW3FTtYyKdDBK6ZdEcQYNPAlVIlaXzdQ5ncSw,516881 +scipy/integrate/_ode.py,sha256=Wm6XtYfe11GZWpnTA71N02ib-niAg2ytyens3YPB2Co,48299 +scipy/integrate/_odepack.cpython-310-x86_64-linux-gnu.so,sha256=QF45O36LtC3pAsd9mv-n6IFjFBruoYUy8gvXfvxhB20,479121 +scipy/integrate/_odepack_py.py,sha256=DhHLB7rx0p6TrQQzQQlwzqcb8oMuFRDra0nIFryb0M8,11231 +scipy/integrate/_quad_vec.py,sha256=VKdZEaWLDNW0-2S3tcGKv386QIcUlwb-vpxPk0_NwGU,22024 +scipy/integrate/_quadpack.cpython-310-x86_64-linux-gnu.so,sha256=HdP6CaKBYe_0FDpRJubkfV-7Yn3I5cu649JJviwOgxc,112024 +scipy/integrate/_quadpack_py.py,sha256=jOeoUlpqTEOh7Qw7RJxwxt5ojsW9iVsF0CaQ_kk0esE,53250 +scipy/integrate/_quadrature.py,sha256=6u3t4hUh4_3CtdHmaXAtKxB2-IBVPNO37CeEjZyS7rM,47907 +scipy/integrate/_rules/__init__.py,sha256=JNlDLTPYR-FVDeWbm9BHOot47OA8tvOj22g2iJlEsBg,328 +scipy/integrate/_rules/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_base.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_gauss_kronrod.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_gauss_legendre.cpython-310.pyc,, +scipy/integrate/_rules/__pycache__/_genz_malik.cpython-310.pyc,, +scipy/integrate/_rules/_base.py,sha256=AWdkCdJTmI8m_jUGv7MAhuwKBySGzVwf0GP4b3qh7-s,17931 +scipy/integrate/_rules/_gauss_kronrod.py,sha256=ULpHMJRd0J99IFwNufur9BYG8EQhxlGj-OdCBgnE8yk,8473 +scipy/integrate/_rules/_gauss_legendre.py,sha256=KJSMmztXRqTvpmkB-ky-WSVIqAMg_GcWoewTcRxJ1Cw,1733 +scipy/integrate/_rules/_genz_malik.py,sha256=104fosqAnmCI992oY-Z9V_QiuG2ruWLmGS2U_EdshEw,7308 +scipy/integrate/_tanhsinh.py,sha256=QMNW0HaxhR3gP_LqxYOZBsypDDpuCjuyFPv7SJaMj9g,61340 +scipy/integrate/_test_multivariate.cpython-310-x86_64-linux-gnu.so,sha256=oCO9DKyKPy4ERYj4rP5sVzsJ2V1Goc521tLC5k-WlzE,16896 +scipy/integrate/_test_odeint_banded.cpython-310-x86_64-linux-gnu.so,sha256=BS-7nShGNkmHz5LhkRFLM5-LIHTmq3nQVIobRKRNbeA,516585 +scipy/integrate/_vode.cpython-310-x86_64-linux-gnu.so,sha256=0IeD-Fu0oW3Nc-uTFwALl86BGpjgxD1EVmkSshQnv4I,565985 +scipy/integrate/dop.py,sha256=Kx5Ed_Te81X09bvGmBUq3-_kQNdTIsOdO7ykjEpEG9c,422 +scipy/integrate/lsoda.py,sha256=hUg4-tJcW3MjhLjLBsD88kzP7qGp_zLGw1AH2ZClHmw,436 +scipy/integrate/odepack.py,sha256=G5KiKninKFyYgF756_LtDGB68BGk7IwPidUOywFpLQo,545 +scipy/integrate/quadpack.py,sha256=vQNE5jQ-dFpH26er1i8LJSkylFVbeSgVGLwSRQawfYg,604 +scipy/integrate/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/integrate/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test__quad_vec.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_banded_ode_solvers.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_bvp.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_cubature.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_integrate.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_odeint_jac.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_quadpack.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_quadrature.cpython-310.pyc,, +scipy/integrate/tests/__pycache__/test_tanhsinh.cpython-310.pyc,, +scipy/integrate/tests/test__quad_vec.py,sha256=jkVVrf-7sF_kC3VUIfgBY2LuCeNtFff5G7o7bN3Jedk,6516 +scipy/integrate/tests/test_banded_ode_solvers.py,sha256=w_nO9OxOC9HtT-QpBlfumrzDUsrBAqxa9cWpm5b7ZjE,6728 +scipy/integrate/tests/test_bvp.py,sha256=tNSp-4YyIQNyLVykDU77i0-4zzkY0sEwVVaT2uoOvz4,20223 +scipy/integrate/tests/test_cubature.py,sha256=_qdTrc718vyv6pCh-nG6X4dcSWffJZsKZ7O9aPBrObA,37018 +scipy/integrate/tests/test_integrate.py,sha256=KiyXeJ7ThQUpL8_XQKfOTZ8i_LBVwgC7ykzF6Yg574I,24611 +scipy/integrate/tests/test_odeint_jac.py,sha256=enXGyQQ4m-9kMPDaWvipIt3buYZ5jNjaxITP8GoS86s,1816 +scipy/integrate/tests/test_quadpack.py,sha256=8EM7IsCLJxswnWAd8S5xyvWX9dWjudycdvDDq1ci7v4,28066 +scipy/integrate/tests/test_quadrature.py,sha256=B4DYgR-tbtWzJKsw05VaJ9aknXpO-N9oZ5--hsE6cyw,28248 +scipy/integrate/tests/test_tanhsinh.py,sha256=7eExO_tYFhDVNMdOLplSO9mx6B1PdB4vydDqFNmAWG0,44800 +scipy/integrate/vode.py,sha256=DPRqm2oBQx6KKi5tl9dDVpXEdAO--W0WpRQEyLeQpf4,424 +scipy/interpolate/__init__.py,sha256=QlL_nJvEkGbheWI4k2AgPf_FZ9QQdwKv807y1eFiLp4,3817 +scipy/interpolate/__pycache__/__init__.cpython-310.pyc,, +scipy/interpolate/__pycache__/_bary_rational.cpython-310.pyc,, +scipy/interpolate/__pycache__/_bsplines.cpython-310.pyc,, +scipy/interpolate/__pycache__/_cubic.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack2.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_impl.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_py.cpython-310.pyc,, +scipy/interpolate/__pycache__/_fitpack_repro.cpython-310.pyc,, +scipy/interpolate/__pycache__/_interpolate.cpython-310.pyc,, +scipy/interpolate/__pycache__/_ndbspline.cpython-310.pyc,, +scipy/interpolate/__pycache__/_ndgriddata.cpython-310.pyc,, +scipy/interpolate/__pycache__/_pade.cpython-310.pyc,, +scipy/interpolate/__pycache__/_polyint.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rbf.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rbfinterp.cpython-310.pyc,, +scipy/interpolate/__pycache__/_rgi.cpython-310.pyc,, +scipy/interpolate/__pycache__/dfitpack.cpython-310.pyc,, +scipy/interpolate/__pycache__/fitpack.cpython-310.pyc,, +scipy/interpolate/__pycache__/fitpack2.cpython-310.pyc,, +scipy/interpolate/__pycache__/interpnd.cpython-310.pyc,, +scipy/interpolate/__pycache__/interpolate.cpython-310.pyc,, +scipy/interpolate/__pycache__/ndgriddata.cpython-310.pyc,, +scipy/interpolate/__pycache__/polyint.cpython-310.pyc,, +scipy/interpolate/__pycache__/rbf.cpython-310.pyc,, +scipy/interpolate/_bary_rational.py,sha256=0iyVrHJy5GDXpIw7cn5TjE78xnAFsbImKOSReqD4zcg,27865 +scipy/interpolate/_bspl.cpython-310-x86_64-linux-gnu.so,sha256=d0lMRdmlQ309Th3a94LqlV_43BJ3epZPYcoLCPfWLt8,406513 +scipy/interpolate/_bsplines.py,sha256=iISJYDQooeLPFKTyuqtW_RMPPqxNmBFQMdoDvUEYLd0,82693 +scipy/interpolate/_cubic.py,sha256=boYHRQjLhs9PlIR5WOFoky8MoH2xEwNUcIHxK3t9J-Q,37727 +scipy/interpolate/_dfitpack.cpython-310-x86_64-linux-gnu.so,sha256=Va4mHz0nwj80-dTBMoViv19BGaD2LZqtQLN9sCgA6pI,346377 +scipy/interpolate/_dierckx.cpython-310-x86_64-linux-gnu.so,sha256=tiVk1xQUh8KaMC5P7OJQuu8q9vtFnNET4k24d-s8GOQ,141921 +scipy/interpolate/_fitpack.cpython-310-x86_64-linux-gnu.so,sha256=D45mQB8AcwB5x9jnD3fXtnqHalYXELoZXbcVJV4la78,91409 +scipy/interpolate/_fitpack2.py,sha256=DS3mjEptn2DJEqQ3NQ5WZUZWNYMLdCK_YBffwDUo5dQ,89728 +scipy/interpolate/_fitpack_impl.py,sha256=hSnz9q_sibFKhgPlrhlb4a0VvanoIh8sWJjxYooibmY,28678 +scipy/interpolate/_fitpack_py.py,sha256=sCzWA-X8ulb0bn-YcaBq9Zo1fpHD0nAoKmURIMbqGek,32157 +scipy/interpolate/_fitpack_repro.py,sha256=RWdm7I9LBGm5_CBWcgJZYD7MXhppnrj0GZx4-6IAAcI,36710 +scipy/interpolate/_interpnd.cpython-310-x86_64-linux-gnu.so,sha256=AHxyc_T01ifDGb0Zp74efCkb894h5i6eoUyxWdjdoAM,458840 +scipy/interpolate/_interpolate.py,sha256=Yqk9e3zK42-2emJcWDRzTC80tErXnyMkPkKyAs4-TYY,79656 +scipy/interpolate/_ndbspline.py,sha256=RdwKfjW87UC_oJASnDcbiiFl22DJo3Z9y1zRlMFqzVc,14900 +scipy/interpolate/_ndgriddata.py,sha256=AZk11XftWehCBhiQv7WRqUV0sLS5ltU1IUbOuHRJJN8,12093 +scipy/interpolate/_pade.py,sha256=OBorKWc3vCSGlsWrajoF1_7WeNd9QtdbX0wOHLdRI2A,1827 +scipy/interpolate/_polyint.py,sha256=jnfDD6IpNvu2OeL4x7bVL1icdKNW1-EPKLDTdTBbHwA,36366 +scipy/interpolate/_ppoly.cpython-310-x86_64-linux-gnu.so,sha256=shuuOAS0BmtNHlD0h2EmwB1p5ySGaJpybEEiR2t3TjM,466256 +scipy/interpolate/_rbf.py,sha256=tBeBsMEe_NO1yxEv8PsX8ngVearEn1VfOyrCqEfr_Uc,11674 +scipy/interpolate/_rbfinterp.py,sha256=bzuAuZpojP-cKCukD3jVekbQzZfHnrUT13Sex5pkKOI,19723 +scipy/interpolate/_rbfinterp_pythran.cpython-310-x86_64-linux-gnu.so,sha256=PKEUjybwiG29xTCK_DHxEPCRfE6-9ueB-uVknweteOU,256728 +scipy/interpolate/_rgi.py,sha256=M1RJ3ftZ4xfM3teo_UWt-ga7gn47yfJNm4BWmmqNqBU,31001 +scipy/interpolate/_rgi_cython.cpython-310-x86_64-linux-gnu.so,sha256=wV1sfCKeSgqFSNR_6fFIOZK5WWzd1TaABGJboEjW4VI,295704 +scipy/interpolate/dfitpack.py,sha256=z3AS0QKeTqVA-yV2RpSdmYAhL5g5sKud3c-0BcXLexA,915 +scipy/interpolate/fitpack.py,sha256=aCH6A3dRouuXW47tK5lEdd2pJa39LCkewY-1zTlI8Hc,702 +scipy/interpolate/fitpack2.py,sha256=P15_3gM5eZQYb_-K3c70xKdeIGM81u5WAkVhY8ei4N0,817 +scipy/interpolate/interpnd.py,sha256=FDGYwstwT7H3KxD0YcQdbRLti8QkuuMlT7MUdgYRixQ,683 +scipy/interpolate/interpolate.py,sha256=Aiu_dJ_oxq-Y1VXns5N5u5K1Wng2hzCgRgRiDhTAiVI,754 +scipy/interpolate/ndgriddata.py,sha256=VbvvoDPdWmrk8871y5olPS9StX0S_B27j_oGMAyj8QQ,636 +scipy/interpolate/polyint.py,sha256=ek1EtbIbLLwehb8XDSKeNvIdjTfDQoQ9CSu4TbY8Vbo,672 +scipy/interpolate/rbf.py,sha256=6oBxdpsKY8bH36nQnRNiLB9C1bNri8b2PHz9IsUIr-w,519 +scipy/interpolate/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/interpolate/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_bary_rational.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_bsplines.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_fitpack.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_fitpack2.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_gil.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_interpnd.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_interpolate.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_ndgriddata.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_pade.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_polyint.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_rbf.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_rbfinterp.cpython-310.pyc,, +scipy/interpolate/tests/__pycache__/test_rgi.cpython-310.pyc,, +scipy/interpolate/tests/data/bug-1310.npz,sha256=jWgDwLOY8nBMI28dG56OXt4GvRZaCrsPIoKBq71FWuk,2648 +scipy/interpolate/tests/data/estimate_gradients_hang.npy,sha256=QGwQhXQX_16pjYzSiUXJ0OT1wk-SpIrQ6Pq5Vb8kd_E,35680 +scipy/interpolate/tests/data/gcvspl.npz,sha256=A86BVabLoMG_CiRBoQwigZH5Ft7DbLggcjQpgRKWu6g,3138 +scipy/interpolate/tests/test_bary_rational.py,sha256=z8_gaM6Ia2GI291aBeOUSsmU9eg8kJ-_HzhXAmtnwpI,15448 +scipy/interpolate/tests/test_bsplines.py,sha256=VGX9nui-lJUWHP_jXJd3_4OlqFj_BkA4-xHEXxD_KpU,128301 +scipy/interpolate/tests/test_fitpack.py,sha256=cFJmwsWhdysO-BEpZ5pMHo6sXSGO1TYWWg_12omcvvk,16589 +scipy/interpolate/tests/test_fitpack2.py,sha256=jtk_OvC11z9Pifp5cngWRrkauFzRKS2liMGxAt6sjiQ,59819 +scipy/interpolate/tests/test_gil.py,sha256=BPC_Ig9lRg28mVHIqdSqWnwBKLukTXFkbrdqUYuskq4,1831 +scipy/interpolate/tests/test_interpnd.py,sha256=IF5nWlRte8ZSPY0Y8eMGya7fKxPQYuoN4OCseGfyens,15545 +scipy/interpolate/tests/test_interpolate.py,sha256=ZoAyhXV6TAFChCN9wSxe3clAmmm6hXyK_BP5OQggjFc,97777 +scipy/interpolate/tests/test_ndgriddata.py,sha256=b_AMpiIj3mlslZXHMnwOqDdI6ORXnO4McbpjGh51dL0,11025 +scipy/interpolate/tests/test_pade.py,sha256=5gmdgTBoJGsY-d813it9JP5Uh8Wc88dz3vPQ2pRZdNk,3868 +scipy/interpolate/tests/test_polyint.py,sha256=wUZqVdoSRbXm_n7rfcLQ3C_dGCkPxEG-MdpjmBPR7vQ,37296 +scipy/interpolate/tests/test_rbf.py,sha256=eoFUrp861RWX4SDbe6VJfDd9_vh9a-f6xwoOrfn7JtA,7021 +scipy/interpolate/tests/test_rbfinterp.py,sha256=Sk_e-H18y97dZ1dgCjMxr9bywAUseLBbou7PwlWQ16k,19094 +scipy/interpolate/tests/test_rgi.py,sha256=83PyPkDNhE-2Bb42pfpi8yTpjwRmnBuDdYRAp2INXfY,46277 +scipy/io/__init__.py,sha256=XegFIpTjKz9NXsHPLcvnYXT-mzUrMqPJUD7a8dhUK_0,2735 +scipy/io/__pycache__/__init__.cpython-310.pyc,, +scipy/io/__pycache__/_fortran.cpython-310.pyc,, +scipy/io/__pycache__/_idl.cpython-310.pyc,, +scipy/io/__pycache__/_mmio.cpython-310.pyc,, +scipy/io/__pycache__/_netcdf.cpython-310.pyc,, +scipy/io/__pycache__/harwell_boeing.cpython-310.pyc,, +scipy/io/__pycache__/idl.cpython-310.pyc,, +scipy/io/__pycache__/mmio.cpython-310.pyc,, +scipy/io/__pycache__/netcdf.cpython-310.pyc,, +scipy/io/__pycache__/wavfile.cpython-310.pyc,, +scipy/io/_fast_matrix_market/__init__.py,sha256=EmT5UuApydDttAWNYvZw3lbBuJMkw73dloawtX0o3uQ,17123 +scipy/io/_fast_matrix_market/__pycache__/__init__.cpython-310.pyc,, +scipy/io/_fast_matrix_market/_fmm_core.cpython-310-x86_64-linux-gnu.so,sha256=p92cyg4if3aX5yB82YBysrIS8KQbtbm-pN8ROZaQQa8,3845792 +scipy/io/_fortran.py,sha256=pgbB0LbOKEfPk07y-9IQXUyT7Kx_wHP0AyGPLtC53yM,10893 +scipy/io/_harwell_boeing/__init__.py,sha256=90qYbBzDEoTMG8ouVLGnTU2GMsY4BYOOtwJdoKT3Zz8,164 +scipy/io/_harwell_boeing/__pycache__/__init__.cpython-310.pyc,, +scipy/io/_harwell_boeing/__pycache__/_fortran_format_parser.cpython-310.pyc,, +scipy/io/_harwell_boeing/__pycache__/hb.cpython-310.pyc,, +scipy/io/_harwell_boeing/_fortran_format_parser.py,sha256=beJJq2mckeU_Hu4ZM_WvrHCICJOvghI4R4bAvOnH48Q,9025 +scipy/io/_harwell_boeing/hb.py,sha256=e4FbmYCXO4omXFcMW2n6qk_Cdcwx1eKHyUD5H-B71fc,19515 +scipy/io/_harwell_boeing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/io/_harwell_boeing/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/io/_harwell_boeing/tests/__pycache__/test_fortran_format.cpython-310.pyc,, +scipy/io/_harwell_boeing/tests/__pycache__/test_hb.cpython-310.pyc,, +scipy/io/_harwell_boeing/tests/test_fortran_format.py,sha256=hPH4AmfUmyBrDU3C_Rx3j7yaGEjefQJOai4rfxMHuV0,2383 +scipy/io/_harwell_boeing/tests/test_hb.py,sha256=jYbRWktqO5bgXDh8i9O_u_KDTpYQcMx_blw7Pn66Nd0,2516 +scipy/io/_idl.py,sha256=-31PPsVEtNR8It3clEfZuGRCzeBrB9OSQdkeOwNpsu0,27075 +scipy/io/_mmio.py,sha256=Pk9Qmf4r-g7-ZQE9cCsu9_BaqiQJDRcnYlJL840WeQo,32094 +scipy/io/_netcdf.py,sha256=wSulfl-YWbyIxhwF4w5gDpINzUAsvOXRXa4rWHSz8p0,39223 +scipy/io/_test_fortran.cpython-310-x86_64-linux-gnu.so,sha256=K8ZDVMJIiEhBv8-0hp_rdJDv1nlG9bgxcE8EKmS6gCs,63529 +scipy/io/arff/__init__.py,sha256=czaV8hvY6JnmEn2qyU3_fzcy_P55aXVT09OzGnhJT9I,805 +scipy/io/arff/__pycache__/__init__.cpython-310.pyc,, +scipy/io/arff/__pycache__/_arffread.cpython-310.pyc,, +scipy/io/arff/__pycache__/arffread.cpython-310.pyc,, +scipy/io/arff/_arffread.py,sha256=uOomT89u1pVrDdGKujArTE_e6Xz3Cw2f2ACPTPS6DlY,25752 +scipy/io/arff/arffread.py,sha256=KW6mASZrW2J1wmC3GYucy1EO7y-rg5MgcGDMyMTpfw4,575 +scipy/io/arff/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/io/arff/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/io/arff/tests/__pycache__/test_arffread.cpython-310.pyc,, +scipy/io/arff/tests/data/iris.arff,sha256=fTS6VWSX6dwoM16mYoo30dvLoJChriDcLenHAy0ZkVM,7486 +scipy/io/arff/tests/data/missing.arff,sha256=ga__Te95i1Yf-yu2kmYDBVTz0xpSTemz7jS74_OfI4I,120 +scipy/io/arff/tests/data/nodata.arff,sha256=DBXdnIe28vrbf4C-ar7ZgeFIa0kGD4pDBJ4YP-z4QHQ,229 +scipy/io/arff/tests/data/quoted_nominal.arff,sha256=01mPSc-_OpcjXFy3EoIzKdHCmzWSag4oK1Ek2tUc6_U,286 +scipy/io/arff/tests/data/quoted_nominal_spaces.arff,sha256=bcMOl-E0I5uTT27E7bDTbW2mYOp9jS8Yrj0NfFjQdKU,292 +scipy/io/arff/tests/data/test1.arff,sha256=nUFDXUbV3sIkur55rL4qvvBdqUTbzSRrTiIPwmtmG8I,191 +scipy/io/arff/tests/data/test10.arff,sha256=va7cXiWX_AnHf-_yz25ychD8hOgf7-sEMJITGwQla30,199009 +scipy/io/arff/tests/data/test11.arff,sha256=G-cbOUUxuc3859vVkRDNjcLRSnUu8-T-Y8n0dSpvweo,241 +scipy/io/arff/tests/data/test2.arff,sha256=COGWCYV9peOGLqlYWhqG4ANT2UqlAtoVehbJLW6fxHw,300 +scipy/io/arff/tests/data/test3.arff,sha256=jUTWGaZbzoeGBneCmKu6V6RwsRPp9_0sJaSCdBg6tyI,72 +scipy/io/arff/tests/data/test4.arff,sha256=mtyuSFKUeiRR2o3mNlwvDCxWq4DsHEBHj_8IthNzp-M,238 +scipy/io/arff/tests/data/test5.arff,sha256=2Q_prOBCfM_ggsGRavlOaJ_qnWPFf2akFXJFz0NtTIE,365 +scipy/io/arff/tests/data/test6.arff,sha256=V8FNv-WUdurutFXKTOq8DADtNDrzfW65gyOlv-lquOU,195 +scipy/io/arff/tests/data/test7.arff,sha256=rxsqdev8WeqC_nKJNwetjVYXA1-qCzWmaHlMvSaVRGk,559 +scipy/io/arff/tests/data/test8.arff,sha256=c34srlkU8hkXYpdKXVozEutiPryR8bf_5qEmiGQBoG4,429 +scipy/io/arff/tests/data/test9.arff,sha256=ZuXQQzprgmTXxENW7we3wBJTpByBlpakrvRgG8n7fUk,311 +scipy/io/arff/tests/test_arffread.py,sha256=NMOdsNI8uL1FJ2RB1hpi8RtNwlnIFWL1ENnvHVQLC9s,13158 +scipy/io/harwell_boeing.py,sha256=BzISbfgVnrO3vYx-mP2xkLqh9r3oq64NNPbEY03P6v0,538 +scipy/io/idl.py,sha256=A1QV5h6xBa1cTIejjsc1NfjG0MqMbxqFqXicC2OLNrM,504 +scipy/io/matlab/__init__.py,sha256=z1F-sXRyay69RcZUHjWSFe0IVKNKQbbMwQMrGD8i4qI,2156 +scipy/io/matlab/__pycache__/__init__.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_byteordercodes.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio4.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio5.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_mio5_params.cpython-310.pyc,, +scipy/io/matlab/__pycache__/_miobase.cpython-310.pyc,, +scipy/io/matlab/__pycache__/byteordercodes.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio4.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5_params.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio5_utils.cpython-310.pyc,, +scipy/io/matlab/__pycache__/mio_utils.cpython-310.pyc,, +scipy/io/matlab/__pycache__/miobase.cpython-310.pyc,, +scipy/io/matlab/__pycache__/streams.cpython-310.pyc,, +scipy/io/matlab/_byteordercodes.py,sha256=AUMjfdIARtCGqyMgDDJBGa_EncP5ioYrEzyZqXOLRxU,1983 +scipy/io/matlab/_mio.py,sha256=Qa_FMP-Zid7tOFTNiNjnVrYi7YkK4hKtcGJiAv884Bw,13587 +scipy/io/matlab/_mio4.py,sha256=W9FaF7ryhbT10TEgHcuovZkm7w2zIU3tDtnb5gIlYlQ,20993 +scipy/io/matlab/_mio5.py,sha256=6wfD_hwa4KdY1-pLXgjIAQfYpZO_LCCsaVMYWaV6dUI,33637 +scipy/io/matlab/_mio5_params.py,sha256=skRcKG70vOlVMSb1TO67LB5312zuOUSrcOK7mOCcUss,8201 +scipy/io/matlab/_mio5_utils.cpython-310-x86_64-linux-gnu.so,sha256=0w6ZSwnZ8w9-_rbcgfoFq0p74ytOeMK0j8O2IoGXv7E,264600 +scipy/io/matlab/_mio_utils.cpython-310-x86_64-linux-gnu.so,sha256=0b2atzEfb2G13BotoWIRlcrlUHAPkxv1SKMlXIB0vio,73280 +scipy/io/matlab/_miobase.py,sha256=OpKCydtebY-dqQR6GjI_8K85Zi9ZSSNBFeyUcafTjRw,13004 +scipy/io/matlab/_streams.cpython-310-x86_64-linux-gnu.so,sha256=B0zgN0dPcuL8mUUZXtTBb7VGF2ApT8-HlyQlcJTuGBA,147488 +scipy/io/matlab/byteordercodes.py,sha256=fHZVESDgIeYzGYtRlknPQ2nUqscQQ_4FhQc_ClkjBvQ,528 +scipy/io/matlab/mio.py,sha256=2b0WwgQ0rBkoJ4X0hgPl889PpR7Q0i7ibSLtTQVuTto,539 +scipy/io/matlab/mio4.py,sha256=hkhpBa4p0euf2rUjJviBWJ4TJs1wkUads3mX1fgDYMc,508 +scipy/io/matlab/mio5.py,sha256=jEFeEEkXWOhziPreDt0SqfAtOo9JMauxoODAbbXHmoQ,638 +scipy/io/matlab/mio5_params.py,sha256=2RWROlfc8RmXmcXGyM-be107Tm55ibc_U7DztJ2b4fc,593 +scipy/io/matlab/mio5_utils.py,sha256=DYiQfx5BkyDVnK4nZ3xPa-5tbpZE7WRx4SIdBmPVfSI,520 +scipy/io/matlab/mio_utils.py,sha256=VZPx03BNFbrQjB1CNbDCvvXUuP0_VoNRFV1R0YoB2iw,518 +scipy/io/matlab/miobase.py,sha256=3qQoq8Y7ZQpHIufUCzg6RAeaLqU3qTAozmuYbaOd7BI,565 +scipy/io/matlab/streams.py,sha256=0Aww9GRGGnRmiAMBAzIAXsFGySu5YCUNG-cHP1omYjI,513 +scipy/io/matlab/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/io/matlab/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_byteordercodes.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_mio.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_mio5_utils.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_mio_funcs.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_mio_utils.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_miobase.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_pathological.cpython-310.pyc,, +scipy/io/matlab/tests/__pycache__/test_streams.cpython-310.pyc,, +scipy/io/matlab/tests/data/bad_miuint32.mat,sha256=CVkYHp_U4jxYKRRHSuZ5fREop4tJjnZcQ02DKfObkRA,272 +scipy/io/matlab/tests/data/bad_miutf8_array_name.mat,sha256=V-jfVMkYyy8qRGcOIsNGcoO0GCgTxchrsQUBGBnfWHE,208 +scipy/io/matlab/tests/data/big_endian.mat,sha256=2ttpiaH2B6nmHnq-gsFeMvZ2ZSLOlpzt0IJiqBTcc8M,273 +scipy/io/matlab/tests/data/broken_utf8.mat,sha256=nm8aotRl6NIxlM3IgPegKR3EeevYZoJCrYpV4Sa1T5I,216 +scipy/io/matlab/tests/data/corrupted_zlib_checksum.mat,sha256=X4dvE7K9DmGEF3D6I-48hC86W41jB54H7bD8KTXjtYA,276 +scipy/io/matlab/tests/data/corrupted_zlib_data.mat,sha256=DfE1YBH-pYw-dAaEeKA6wZcyKeo9GlEfrzZtql-fO_w,3451 +scipy/io/matlab/tests/data/debigged_m4.mat,sha256=8QbD-LzoYbKSfOYPRRw-oelDJscwufYp5cqLfZ1hB0c,1024 +scipy/io/matlab/tests/data/japanese_utf8.txt,sha256=rgxiBH7xmEKF91ZkB3oMLrqABBXINEMHPXDKdZXNBEY,270 +scipy/io/matlab/tests/data/little_endian.mat,sha256=FQP_2MNod-FFF-JefN7ZxovQ6QLCdHQ0DPL_qBCP44Y,265 +scipy/io/matlab/tests/data/logical_sparse.mat,sha256=qujUUpYewaNsFKAwGpYS05z7kdUv9TQZTHV5_lWhRrs,208 +scipy/io/matlab/tests/data/malformed1.mat,sha256=DTuTr1-IzpLMBf8u5DPb3HXmw9xJo1aWfayA5S_3zUI,2208 +scipy/io/matlab/tests/data/miuint32_for_miint32.mat,sha256=romrBP_BS46Sl2-pKWsUnxYDad2wehyjq4wwLaVqums,272 +scipy/io/matlab/tests/data/miutf8_array_name.mat,sha256=Vo8JptFr-Kg2f2cEoDg8LtELSjVNyccdJY74WP_kqtc,208 +scipy/io/matlab/tests/data/nasty_duplicate_fieldnames.mat,sha256=bvdmj6zDDUIpOfIP8J4Klo107RYCDd5VK5gtOYx3GsU,8168 +scipy/io/matlab/tests/data/one_by_zero_char.mat,sha256=Z3QdZjTlOojjUpS0cfBP4XfNQI3GTjqU0n_pnAzgQhU,184 +scipy/io/matlab/tests/data/parabola.mat,sha256=ENWuWX_uwo4Av16dIGOwnbMReAMrShDhalkq8QUI8Rg,729 +scipy/io/matlab/tests/data/single_empty_string.mat,sha256=4uTmX0oydTjmtnhxqi9SyPWCG2I24gj_5LarS80bPik,171 +scipy/io/matlab/tests/data/some_functions.mat,sha256=JA736oG3s8PPdKhdsYK-BndLUsGrJCJAIRBseSIEZtM,1397 +scipy/io/matlab/tests/data/sqr.mat,sha256=3DtGl_V4wABKCDQ0P3He5qfOzpUTC-mINdK73MKS7AM,679 +scipy/io/matlab/tests/data/test3dmatrix_6.1_SOL2.mat,sha256=-odiBIQAbOLERg0Vg682QHGfs7C8MaA_gY77OWR8x78,232 +scipy/io/matlab/tests/data/test3dmatrix_6.5.1_GLNX86.mat,sha256=G5siwvZ-7Uv5KJ6h7AA3OHL6eiFsd8Lnjx4IcoByzCU,232 +scipy/io/matlab/tests/data/test3dmatrix_7.1_GLNX86.mat,sha256=EVj1wPnoyWGIdTpkSj3YAwqzTAm27eqZNxCaJAs3pwU,213 +scipy/io/matlab/tests/data/test3dmatrix_7.4_GLNX86.mat,sha256=S_Sd3sxorDd8tZ5CxD5_J8vXbfcksLWzhUQY5b82L9g,213 +scipy/io/matlab/tests/data/test_empty_struct.mat,sha256=WoC7g7TyXqNr2T0d5xE3IUq5PRzatE0mxXjqoHX5Xec,173 +scipy/io/matlab/tests/data/test_mat4_le_floats.mat,sha256=2xvn3Cg4039shJl62T-bH-VeVP_bKtwdqvGfIxv8FJ4,38 +scipy/io/matlab/tests/data/test_skip_variable.mat,sha256=pJLVpdrdEb-9SMZxaDu-uryShlIi90l5LfXhvpVipJ0,20225 +scipy/io/matlab/tests/data/testbool_8_WIN64.mat,sha256=_xBw_2oZA7u9Xs6GJItUpSIEV4jVdfdcwzmLNFWM6ow,185 +scipy/io/matlab/tests/data/testcell_6.1_SOL2.mat,sha256=OWOBzNpWTyAHIcZABRytVMcABiRYgEoMyF9gDaIkFe4,536 +scipy/io/matlab/tests/data/testcell_6.5.1_GLNX86.mat,sha256=7111TN_sh1uMHmYx-bjd_v9uaAnWhJMhrQFAtAw6Nvk,536 +scipy/io/matlab/tests/data/testcell_7.1_GLNX86.mat,sha256=62p6LRW6PbM-Y16aUeGVhclTVqS5IxPUtsohe7MjrYo,283 +scipy/io/matlab/tests/data/testcell_7.4_GLNX86.mat,sha256=NkTA8UW98hIQ0t5hGx_leG-MzNroDelYwqx8MPnO63Q,283 +scipy/io/matlab/tests/data/testcellnest_6.1_SOL2.mat,sha256=AeNaog8HUDCVrIuGICAXYu9SGDsvV6qeGjgvWHrVQho,568 +scipy/io/matlab/tests/data/testcellnest_6.5.1_GLNX86.mat,sha256=Gl4QA0yYwGxjiajjgWS939WVAM-W2ahNIm9wwMaT5oc,568 +scipy/io/matlab/tests/data/testcellnest_7.1_GLNX86.mat,sha256=CUGtkwIU9CBa0Slx13mbaM67_ec0p-unZdu8Z4YYM3c,228 +scipy/io/matlab/tests/data/testcellnest_7.4_GLNX86.mat,sha256=TeTk5yjl5j_bcnmIkpzuYHxGGQXNu-rK6xOsN4t6lX8,228 +scipy/io/matlab/tests/data/testcomplex_4.2c_SOL2.mat,sha256=WOwauWInSVUFBuOJ1Bo3spmUQ3UWUIlsIe4tYGlrU7o,176 +scipy/io/matlab/tests/data/testcomplex_6.1_SOL2.mat,sha256=GpAEccizI8WvlrBPdvlKUv6uKbZOo_cjUK3WVVb2lo4,352 +scipy/io/matlab/tests/data/testcomplex_6.5.1_GLNX86.mat,sha256=3MEbf0zJdQGAO7x-pzFCup2QptfYJHQG59z0vVOdxl4,352 +scipy/io/matlab/tests/data/testcomplex_7.1_GLNX86.mat,sha256=VNHV2AIEkvPuhae1kKIqt5t8AMgUyr0L_CAp-ykLxt4,247 +scipy/io/matlab/tests/data/testcomplex_7.4_GLNX86.mat,sha256=8rWGf5bqY7_2mcd5w5gTYgMkXVePlLL8qT7lh8kApn0,247 +scipy/io/matlab/tests/data/testdouble_4.2c_SOL2.mat,sha256=MzT7OYPEUXHYNPBrVkyKEaG5Cas2aOA0xvrO7l4YTrQ,103 +scipy/io/matlab/tests/data/testdouble_6.1_SOL2.mat,sha256=DpB-mVKx1gsjl-3IbxfxHNuzU5dnuku-MDQCA8kALVI,272 +scipy/io/matlab/tests/data/testdouble_6.5.1_GLNX86.mat,sha256=4hY5VEubavNEv5KvcqQnd7MWWvFUzHXXpYIqUuUt-50,272 +scipy/io/matlab/tests/data/testdouble_7.1_GLNX86.mat,sha256=N2QOOIXPyy0zPZZ_qY7xIDaodMGrTq3oXNBEHZEscw0,232 +scipy/io/matlab/tests/data/testdouble_7.4_GLNX86.mat,sha256=TrkJ4Xx_dC9YrPdewlsOvYs_xag7gT3cN4HkDsJmT8I,232 +scipy/io/matlab/tests/data/testemptycell_5.3_SOL2.mat,sha256=g96Vh9FpNhkiWKsRm4U6KqeKd1hNAEyYSD7IVzdzwsU,472 +scipy/io/matlab/tests/data/testemptycell_6.5.1_GLNX86.mat,sha256=2Zw-cMv-Mjbs2HkSl0ubmh_htFUEpkn7XVHG8iM32o0,472 +scipy/io/matlab/tests/data/testemptycell_7.1_GLNX86.mat,sha256=t5Ar8EgjZ7fkTUHIVpdXg-yYWo_MBaigMDJUGWEIrmU,218 +scipy/io/matlab/tests/data/testemptycell_7.4_GLNX86.mat,sha256=5PPvfOoL-_Q5ou_2nIzIrHgeaOZGFXGxAFdYzCQuwEQ,218 +scipy/io/matlab/tests/data/testfunc_7.4_GLNX86.mat,sha256=ScTKftENe78imbMc0I5ouBlIMcEEmZgu8HVKWAMNr58,381 +scipy/io/matlab/tests/data/testhdf5_7.4_GLNX86.mat,sha256=ZoVbGk38_MCppZ0LRr6OE07HL8ZB4rHXgMj9LwUBgGg,4168 +scipy/io/matlab/tests/data/testmatrix_4.2c_SOL2.mat,sha256=14YMiKAN9JCPTqSDXxa58BK6Un7EM4hEoSGAUuwKWGQ,151 +scipy/io/matlab/tests/data/testmatrix_6.1_SOL2.mat,sha256=ZdjNbcIE75V5Aht5EVBvJX26aabvNqbUH0Q9VBnxBS4,216 +scipy/io/matlab/tests/data/testmatrix_6.5.1_GLNX86.mat,sha256=OB82QgB6SwtsxT4t453OVSj-B777XrHGEGOMgMD1XGc,216 +scipy/io/matlab/tests/data/testmatrix_7.1_GLNX86.mat,sha256=-TYB0kREY7i7gt5x15fOYjXi410pXuDWUFxPYuMwywI,193 +scipy/io/matlab/tests/data/testmatrix_7.4_GLNX86.mat,sha256=l9psDc5K1bpxNeuFlyYIYauswLnOB6dTX6-jvelW0kU,193 +scipy/io/matlab/tests/data/testminus_4.2c_SOL2.mat,sha256=2914WYQajPc9-Guy3jDOLU3YkuE4OXC_63FUSDzJzX0,38 +scipy/io/matlab/tests/data/testminus_6.1_SOL2.mat,sha256=2X2fZKomz0ktBvibj7jvHbEvt2HRA8D6hN9qA1IDicw,200 +scipy/io/matlab/tests/data/testminus_6.5.1_GLNX86.mat,sha256=i364SgUCLSYRjQsyygvY1ArjEaO5uLip3HyU-R7zaLo,200 +scipy/io/matlab/tests/data/testminus_7.1_GLNX86.mat,sha256=gtYNC9_TciYdq8X9IwyGEjiw2f1uCVTGgiOPFOiQbJc,184 +scipy/io/matlab/tests/data/testminus_7.4_GLNX86.mat,sha256=eXcoTM8vKuh4tQnl92lwdDaqssGB6G9boSHh3FOCkng,184 +scipy/io/matlab/tests/data/testmulti_4.2c_SOL2.mat,sha256=Zhyu2KCsseSJ5NARdS00uwddCs4wmjcWNP2LJFns2-Q,240 +scipy/io/matlab/tests/data/testmulti_7.1_GLNX86.mat,sha256=KI3H58BVj6k6MFsj8icSbjy_0Z-jOesWN5cafStLPG8,276 +scipy/io/matlab/tests/data/testmulti_7.4_GLNX86.mat,sha256=Yr4YKCP27yMWlK5UOK3BAEOAyMr-m0yYGcj8v1tCx-I,276 +scipy/io/matlab/tests/data/testobject_6.1_SOL2.mat,sha256=kzLxy_1o1HclPXWyA-SX5gl6LsG1ioHuN4eS6x5iZio,800 +scipy/io/matlab/tests/data/testobject_6.5.1_GLNX86.mat,sha256=dq_6_n0v7cUz9YziXn-gZFNc9xYtNxZ8exTsziWIM7s,672 +scipy/io/matlab/tests/data/testobject_7.1_GLNX86.mat,sha256=3z-boFw0SC5142YPOLo2JqdusPItVzjCFMhXAQNaQUQ,306 +scipy/io/matlab/tests/data/testobject_7.4_GLNX86.mat,sha256=5OwLTMgCBlxsDfiEUzlVjqcSbVQG-X5mIw5JfW3wQXA,306 +scipy/io/matlab/tests/data/testonechar_4.2c_SOL2.mat,sha256=BCvppGhO19-j-vxAvbdsORIiyuJqzCuQog9Ao8V1lvA,40 +scipy/io/matlab/tests/data/testonechar_6.1_SOL2.mat,sha256=ThppTHGJFrUfal5tewS70DL00dSwk1otazuVdJrTioE,200 +scipy/io/matlab/tests/data/testonechar_6.5.1_GLNX86.mat,sha256=SBfN6e7Vz1rAdi8HLguYXcHUHk1viaXTYccdEyhhob4,200 +scipy/io/matlab/tests/data/testonechar_7.1_GLNX86.mat,sha256=m8W9GqvflfAsizkhgAfT0lLcxuegZIWCLNuHVX69Jac,184 +scipy/io/matlab/tests/data/testonechar_7.4_GLNX86.mat,sha256=t9ObKZOLy3vufnER8TlvQcUkd_wmXbJSdQoG4f3rVKY,184 +scipy/io/matlab/tests/data/testscalarcell_7.4_GLNX86.mat,sha256=5LX9sLH7Y6h_N_a1XRN2GuMgp_P7ECpPsXGDOypAJg0,194 +scipy/io/matlab/tests/data/testsimplecell.mat,sha256=Aoeh0PX2yiLDTwkxMEyZ_CNX2mJHZvyfuFJl817pA1c,220 +scipy/io/matlab/tests/data/testsparse_4.2c_SOL2.mat,sha256=dFUcB1gunfWqexgR4YDZ_Ec0w0HffM1DUE1C5PVfDDc,223 +scipy/io/matlab/tests/data/testsparse_6.1_SOL2.mat,sha256=9Sgd_SPkGNim7ZL0xgD71qml3DK0yDHYC7VSNLNQEXA,280 +scipy/io/matlab/tests/data/testsparse_6.5.1_GLNX86.mat,sha256=jp1ILNxLyV6XmCCGxAz529XoZ9dhCqGEO-ExPH70_Pg,328 +scipy/io/matlab/tests/data/testsparse_7.1_GLNX86.mat,sha256=k8QuQ_4Zu7FWTzHjRnHCVZ9Yu5vwNP0WyNzu6TuiY-4,229 +scipy/io/matlab/tests/data/testsparse_7.4_GLNX86.mat,sha256=QbZOCqIvnaK0XOH3kaSXBe-m_1_Rb33psq8E-WMSBTU,229 +scipy/io/matlab/tests/data/testsparsecomplex_4.2c_SOL2.mat,sha256=QMVoBXVyl9RBGvAjLoiW85kAXYJ-hHprUMegEG69A5w,294 +scipy/io/matlab/tests/data/testsparsecomplex_6.1_SOL2.mat,sha256=WfEroAT5YF4HGAKq3jTJxlFrKaTCh3rwlSlKu__VjwA,304 +scipy/io/matlab/tests/data/testsparsecomplex_6.5.1_GLNX86.mat,sha256=e0s6cyoKJeYMArdceHpnKDvtCVcw7XuB44OBDHpoa6U,400 +scipy/io/matlab/tests/data/testsparsecomplex_7.1_GLNX86.mat,sha256=kgHcuq-deI2y8hfkGwlMOkW7lntexdPHfuz0ar6b3jo,241 +scipy/io/matlab/tests/data/testsparsecomplex_7.4_GLNX86.mat,sha256=rYCaWNLXK7f_jjMc6_UvZz6ZDuMCuVRmJV5RyeXiDm8,241 +scipy/io/matlab/tests/data/testsparsefloat_7.4_GLNX86.mat,sha256=hnNV6GZazEeqTXuA9vcOUo4xam_UnKRYGYH9PUGTLv8,219 +scipy/io/matlab/tests/data/teststring_4.2c_SOL2.mat,sha256=cAhec51DlqIYfDXXGaumOE3Hqb3cFWM1UsUK3K_lDP8,375 +scipy/io/matlab/tests/data/teststring_6.1_SOL2.mat,sha256=ciFzNGMO7gjYecony-E8vtOwBY4vXIUhyug6Euaz3Kg,288 +scipy/io/matlab/tests/data/teststring_6.5.1_GLNX86.mat,sha256=yrJrpLiwLvU_LI1D6rw1Pk1qJK1YlC7Cmw7lwyJVLtw,288 +scipy/io/matlab/tests/data/teststring_7.1_GLNX86.mat,sha256=zo7sh-8dMpGqhoNxLEnfz3Oc7RonxiY5j0B3lxk0e8o,224 +scipy/io/matlab/tests/data/teststring_7.4_GLNX86.mat,sha256=igL_CvtAcNEa1nxunDjQZY5wS0rJOlzsUkBiDreJssk,224 +scipy/io/matlab/tests/data/teststringarray_4.2c_SOL2.mat,sha256=pRldk-R0ig1k3ouvaR9oVtBwZsQcDW_b4RBEDYu1-Vk,156 +scipy/io/matlab/tests/data/teststringarray_6.1_SOL2.mat,sha256=B9IdaSsyb0wxjyYyHOj_GDO0laAeWDEJhoEhC9xdm1E,232 +scipy/io/matlab/tests/data/teststringarray_6.5.1_GLNX86.mat,sha256=t4tKGJg2NEg_Ar5MkOjCoQb2hVL8Q_Jdh9FF4TPL_4g,232 +scipy/io/matlab/tests/data/teststringarray_7.1_GLNX86.mat,sha256=lpYkBZX8K-c4FO5z0P9DMfYc7Y-yzyg11J6m-19uYTU,203 +scipy/io/matlab/tests/data/teststringarray_7.4_GLNX86.mat,sha256=lG-c7U-5Bo8j8xZLpd0JAsMYwewT6cAw4eJCZH5xf6E,203 +scipy/io/matlab/tests/data/teststruct_6.1_SOL2.mat,sha256=3GJbA4O7LP57J6IYzmJqTPeSJrEaiNSk-rg7h0ANR1w,608 +scipy/io/matlab/tests/data/teststruct_6.5.1_GLNX86.mat,sha256=fRbqAnzTeOU3dTQx7O24MfMVFr6pM5u594FRrPPkYJE,552 +scipy/io/matlab/tests/data/teststruct_7.1_GLNX86.mat,sha256=mCtI_Yot08NazvWHvehOZbTV4bW_I4-D5jBgJ6T9EbI,314 +scipy/io/matlab/tests/data/teststruct_7.4_GLNX86.mat,sha256=52qaF4HRCtPl1jE6ljbkEl2mofZVAPpmBxrm-J5OTTI,314 +scipy/io/matlab/tests/data/teststructarr_6.1_SOL2.mat,sha256=vneCpWBwApBGfeKzdZcybyajxjR-ZYf64j0l08_hU84,528 +scipy/io/matlab/tests/data/teststructarr_6.5.1_GLNX86.mat,sha256=gqhRpSfNNB5SR9sCp-wWrvokr5VV_heGnvco6dmfOvY,472 +scipy/io/matlab/tests/data/teststructarr_7.1_GLNX86.mat,sha256=6VDU0mtTBEG0bBHqKP1p8xq846eMhSZ_WvBZv8MzE7M,246 +scipy/io/matlab/tests/data/teststructarr_7.4_GLNX86.mat,sha256=ejtyxeeX_W1a2rNrEUUiG9txPW8_UtSgt8IaDOxE2pg,246 +scipy/io/matlab/tests/data/teststructnest_6.1_SOL2.mat,sha256=sbi0wUwOrbU-gBq3lyDwhAbvchdtOJkflOR_MU7uGKA,496 +scipy/io/matlab/tests/data/teststructnest_6.5.1_GLNX86.mat,sha256=uTkKtrYBTuz4kICVisEaG7V5C2nJDKjy92mPDswTLPE,416 +scipy/io/matlab/tests/data/teststructnest_7.1_GLNX86.mat,sha256=o4F2jOhYyNpJCo-BMg6v_ITZQvjenXfXHLq94e7iwRo,252 +scipy/io/matlab/tests/data/teststructnest_7.4_GLNX86.mat,sha256=CNXO12O6tedEuMG0jNma4qfbTgCswAbHwh49a3uE3Yk,252 +scipy/io/matlab/tests/data/testunicode_7.1_GLNX86.mat,sha256=KV97FCW-1XZiXrwXJoZPbgyAht79oIFHa917W1KFLwE,357 +scipy/io/matlab/tests/data/testunicode_7.4_GLNX86.mat,sha256=9-8xzACZleBkMjZnbr8t4Ncs9B6mbzrONDblPnteBPU,357 +scipy/io/matlab/tests/data/testvec_4_GLNX86.mat,sha256=GQzR3mBVS266_NBfrRC9X0dLgmeu8Jl4r4ZYMOrn1V0,93 +scipy/io/matlab/tests/test_byteordercodes.py,sha256=FCHBAxeQZlhvTXw-AO-ukwTWvpN7NzmncBEDJ1P4de4,938 +scipy/io/matlab/tests/test_mio.py,sha256=GNu2ffj4NOTWgWoA08CZ9_hSHhitcz6ffYZsp52WZKU,46207 +scipy/io/matlab/tests/test_mio5_utils.py,sha256=eacgGg0TaQXOkG7iaeYovtWyjPgYCY50mHPoPjnHMTI,5389 +scipy/io/matlab/tests/test_mio_funcs.py,sha256=fSDaeVPvCRBFzqjWtXR5xIv9UQ_yv6Y_Nl5D5u0HIGo,1392 +scipy/io/matlab/tests/test_mio_utils.py,sha256=GX85RuLqr2HxS5_f7ZgrxbhswJy2GPQQoQbiQYg0s14,1594 +scipy/io/matlab/tests/test_miobase.py,sha256=CGefrU6m_GpOwaKr_Q93Z5zKp5nuv791kjxcNNP8iiE,1460 +scipy/io/matlab/tests/test_pathological.py,sha256=-Efeq2x2yAaLK28EKpai1vh4HsZTCteF_hY_vEGWndA,1055 +scipy/io/matlab/tests/test_streams.py,sha256=dcirMJ5slCA3eIjB9VRcGG3U2htTtXL8BiYOLvHCfds,7406 +scipy/io/mmio.py,sha256=Dc5HqR8BXOD0wir63VTVczuZcLjSxEjbSbeZd4y27po,526 +scipy/io/netcdf.py,sha256=RKhmlybZwbFNKA4US6xLX6O2IUDCmdkToosPt4bAUX0,533 +scipy/io/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/io/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_fortran.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_idl.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_mmio.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_netcdf.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_paths.cpython-310.pyc,, +scipy/io/tests/__pycache__/test_wavfile.cpython-310.pyc,, +scipy/io/tests/data/Transparent Busy.ani,sha256=vwoK3ysYo87-TwzvjerHjFjSPIGpw83jjiMDXcHPWjA,4362 +scipy/io/tests/data/array_float32_1d.sav,sha256=A_xXWkfS1sQCxP4ONezeEZvlKEXwZ1TPG2rCCFdmBNM,2628 +scipy/io/tests/data/array_float32_2d.sav,sha256=qJmN94pywXznXMHzt-L6DJgaIq_FfruVKJl_LMaI8UU,3192 +scipy/io/tests/data/array_float32_3d.sav,sha256=U7P6As7Nw6LdBY1pTOaW9C-O_NlXLXZwSgbT3H8Z8uk,13752 +scipy/io/tests/data/array_float32_4d.sav,sha256=Tl6erEw_Zq3dwVbVyPXRWqB83u_o4wkIVFOe3wQrSro,6616 +scipy/io/tests/data/array_float32_5d.sav,sha256=VmaBgCD854swYyLouDMHJf4LL6iUNgajEOQf0pUjHjg,7896 +scipy/io/tests/data/array_float32_6d.sav,sha256=lb7modI0OQDweJWbDxEV2OddffKgMgq1tvCy5EK6sOU,19416 +scipy/io/tests/data/array_float32_7d.sav,sha256=pqLWIoxev9sLCs9LLwxFlM4RCFwxHC4Q0dEEz578mpI,3288 +scipy/io/tests/data/array_float32_8d.sav,sha256=R8A004f9XLWvF6eKMNEqIrC6PGP1vLZr9sFqawqM8ZA,13656 +scipy/io/tests/data/array_float32_pointer_1d.sav,sha256=sV7qFNwHK-prG5vODa7m5HYK7HlH_lqdfsI5Y1RWDyg,2692 +scipy/io/tests/data/array_float32_pointer_2d.sav,sha256=b0brvK6xQeezoRuujmEcJNw2v6bfASLM3FSY9u5dMSg,3256 +scipy/io/tests/data/array_float32_pointer_3d.sav,sha256=a_Iyg1YjPBRh6B-N_n_BGIVjFje4K-EPibKV-bPbF7E,13816 +scipy/io/tests/data/array_float32_pointer_4d.sav,sha256=cXrkHHlPyoYstDL_OJ15-55sZOOeDNW2OJ3KWhBv-Kk,6680 +scipy/io/tests/data/array_float32_pointer_5d.sav,sha256=gRVAZ6jeqFZyIQI9JVBHed9Y0sjS-W4bLseb01rIcGs,7960 +scipy/io/tests/data/array_float32_pointer_6d.sav,sha256=9yic-CQiS0YR_ow2yUA2Nix0Nb_YCKMUsIgPhgcJT1c,19480 +scipy/io/tests/data/array_float32_pointer_7d.sav,sha256=Rp1s8RbW8eoEIRTqxba4opAyY0uhTuyy3YkwRlNspQU,3352 +scipy/io/tests/data/array_float32_pointer_8d.sav,sha256=Wk3Dd2ClAwWprXLKZon3blY7aMvMrJqz_NXzK0J5MFY,13720 +scipy/io/tests/data/example_1.nc,sha256=EkfC57dWXeljgXy5sidrJHJG12D1gmQUyPDK18WzlT4,1736 +scipy/io/tests/data/example_2.nc,sha256=wywMDspJ2QT431_sJUr_5DHqG3pt9VTvDJzfR9jeWCk,272 +scipy/io/tests/data/example_3_maskedvals.nc,sha256=P9N92jCJgKJo9VmNd7FeeJSvl4yUUFwBy6JpR4MeuME,1424 +scipy/io/tests/data/fortran-3x3d-2i.dat,sha256=oYCXgtY6qqIqLAhoh_46ob_RVQRcV4uu333pOiLKgRM,451 +scipy/io/tests/data/fortran-mixed.dat,sha256=zTi7RLEnyAat_DdC3iSEcSbyDtAu0aTKwUT-tExjasw,40 +scipy/io/tests/data/fortran-sf8-11x1x10.dat,sha256=KwaOrZOAe-wRhuxvmHIK-Wr59us40MmiA9QyWtIAUaA,888 +scipy/io/tests/data/fortran-sf8-15x10x22.dat,sha256=5ohvjjOUcIsGimSqDhpUUKwflyhVsfwKL5ElQe_SU0I,26408 +scipy/io/tests/data/fortran-sf8-1x1x1.dat,sha256=Djmoip8zn-UcxWGUPKV5wzKOYOf7pbU5L7HaR3BYlec,16 +scipy/io/tests/data/fortran-sf8-1x1x5.dat,sha256=Btgavm3w3c9md_5yFfq6Veo_5IK9KtlLF1JEPeHhZoU,48 +scipy/io/tests/data/fortran-sf8-1x1x7.dat,sha256=L0r9yAEMbfMwYQytzYsS45COqaVk-o_hi6zRY3yIiO4,64 +scipy/io/tests/data/fortran-sf8-1x3x5.dat,sha256=c2LTocHclwTIeaR1Pm3mVMyf5Pl_imfjIFwi4Lpv0Xs,128 +scipy/io/tests/data/fortran-si4-11x1x10.dat,sha256=OesvSIGsZjpKZlZsV74PNwy0Co0KH8-3gxL9-DWoa08,448 +scipy/io/tests/data/fortran-si4-15x10x22.dat,sha256=OJcKyw-GZmhHb8REXMsHDn7W5VP5bhmxgVPIAYG-Fj4,13208 +scipy/io/tests/data/fortran-si4-1x1x1.dat,sha256=1Lbx01wZPCOJHwg99MBDuc6QZKdMnccxNgICt4omfFM,12 +scipy/io/tests/data/fortran-si4-1x1x5.dat,sha256=L1St4yiHTA3v91JjnndYfUrdKfT1bWxckwnnrscEZXc,28 +scipy/io/tests/data/fortran-si4-1x1x7.dat,sha256=Dmqt-tD1v2DiPZkghGGZ9Ss-nJGfei-3yFXPO5Acpk4,36 +scipy/io/tests/data/fortran-si4-1x3x5.dat,sha256=3vl6q93m25jEcZVKD0CuKNHmhZwZKp-rv0tfHoPVP88,68 +scipy/io/tests/data/invalid_pointer.sav,sha256=JmgoISXC4r5fSmI5FqyapvmzQ4qpYLf-9N7_Et1p1HQ,1280 +scipy/io/tests/data/null_pointer.sav,sha256=P_3a_sU614F3InwM82jSMtWycSZkvqRn1apwd8XxbtE,2180 +scipy/io/tests/data/scalar_byte.sav,sha256=dNJbcE5OVDY_wHwN_UBUtfIRd13Oqu-RBEO74g5SsBA,2076 +scipy/io/tests/data/scalar_byte_descr.sav,sha256=DNTmDgDWOuzlQnrceER6YJ0NutUUwZ9tozVMBWQmuuY,2124 +scipy/io/tests/data/scalar_complex32.sav,sha256=NGd-EvmFZgt8Ko5MP3T_TLwyby6yS0BXM_OW8197hpU,2076 +scipy/io/tests/data/scalar_complex64.sav,sha256=gFBWtxuAajazupGFSbvlWUPDYK-JdWgZcEWih2-7IYU,2084 +scipy/io/tests/data/scalar_float32.sav,sha256=EwWQw2JTwq99CHVpDAh4R20R0jWaynXABaE2aTRmXrs,2072 +scipy/io/tests/data/scalar_float64.sav,sha256=iPcDlgF1t0HoabvNLWCbSiTPIa9rvVEbOGGmE_3Ilsk,2076 +scipy/io/tests/data/scalar_heap_pointer.sav,sha256=JXZbPmntXILsNOuLIKL8qdu8gDJekYrlN9DQxAWve0E,2204 +scipy/io/tests/data/scalar_int16.sav,sha256=kDBLbPYGo2pzmZDhyl8rlDv0l6TMEWLIoLtmgJXDMkk,2072 +scipy/io/tests/data/scalar_int32.sav,sha256=IzJwLvEoqWLO5JRaHp8qChfptlauU-ll3rb0TfDDM8Y,2072 +scipy/io/tests/data/scalar_int64.sav,sha256=-aSHQRiaE3wjAxINwuLX33_8qmWl4GUkTH45elTkA-8,2076 +scipy/io/tests/data/scalar_string.sav,sha256=AQ7iZ8dKk9QfnLdP9idKv1ojz0M_SwpL7XAUmbHodDQ,2124 +scipy/io/tests/data/scalar_uint16.sav,sha256=928fmxLsQM83ue4eUS3IEnsLSEzmHBklDA59JAUvGK8,2072 +scipy/io/tests/data/scalar_uint32.sav,sha256=X3RbPhS6_e-u-1S1gMyF7s9ys7oV6ZNwPrJqJ6zIJsk,2072 +scipy/io/tests/data/scalar_uint64.sav,sha256=ffVyS2oKn9PDtWjJdOjSRT2KZzy6Mscgd4u540MPHC4,2076 +scipy/io/tests/data/struct_arrays.sav,sha256=TzH-Gf0JgbP_OgeKYbV8ZbJXvWt1VetdUr6C_ziUlzg,2580 +scipy/io/tests/data/struct_arrays_byte_idl80.sav,sha256=oOmhTnmKlE60-JMJRRMv_zfFs4zqioMN8QA0ldlgQZo,1388 +scipy/io/tests/data/struct_arrays_replicated.sav,sha256=kXU8j9QI2Q8D22DVboH9fwwDQSLVvuWMJl3iIOhUAH8,2936 +scipy/io/tests/data/struct_arrays_replicated_3d.sav,sha256=s3ZUwhT6TfiVfk4AGBSyxYR4FRzo4sZQkTxFCJbIQMI,4608 +scipy/io/tests/data/struct_inherit.sav,sha256=4YajBZcIjqMQ4CI0lRUjXpYDY3rI5vzJJzOYpjWqOJk,2404 +scipy/io/tests/data/struct_pointer_arrays.sav,sha256=fkldO6-RO2uAN_AI9hM6SEaBPrBf8TfiodFGJpViaqg,2408 +scipy/io/tests/data/struct_pointer_arrays_replicated.sav,sha256=eKVerR0LoD9CuNlpwoBcn7BIdj3-8x56VNg--Qn7Hgc,2492 +scipy/io/tests/data/struct_pointer_arrays_replicated_3d.sav,sha256=vsqhGpn3YkZEYjQuI-GoX8Jg5Dv8A2uRtP0kzQkq4lg,2872 +scipy/io/tests/data/struct_pointers.sav,sha256=Zq6d5V9ZijpocxJpimrdFTQG827GADBkMB_-6AweDYI,2268 +scipy/io/tests/data/struct_pointers_replicated.sav,sha256=aIXPBIXTfPmd4IaLpYD5W_HUoIOdL5Y3Hj7WOeRM2sA,2304 +scipy/io/tests/data/struct_pointers_replicated_3d.sav,sha256=t1jhVXmhW6VotQMNZ0fv0sDO2pkN4EutGsx5No4VJQs,2456 +scipy/io/tests/data/struct_scalars.sav,sha256=LYICjERzGJ_VvYgtwJ_Up2svQTv8wBzNcVD3nsd_OPg,2316 +scipy/io/tests/data/struct_scalars_replicated.sav,sha256=lw3fC4kppi6BUWAd4n81h8_KgoUdiJl5UIt3CvJIuBs,2480 +scipy/io/tests/data/struct_scalars_replicated_3d.sav,sha256=xVAup6f1dSV_IsSwBQC3KVs0eLEZ6-o5EaZT9yUoDZI,3240 +scipy/io/tests/data/test-1234Hz-le-1ch-10S-20bit-extra.wav,sha256=h8CXsW5_ShKR197t_d-TUTlgDqOZ-7wK_EcVGucR-aY,74 +scipy/io/tests/data/test-44100Hz-2ch-32bit-float-be.wav,sha256=gjv__ng9xH_sm34hyxCbCgO4AP--PZAfDOArH5omkjM,3586 +scipy/io/tests/data/test-44100Hz-2ch-32bit-float-le.wav,sha256=H0LLyv2lc2guzYGnx4DWXU6vB57JrRX-G9Dd4qGh0hM,3586 +scipy/io/tests/data/test-44100Hz-be-1ch-4bytes.wav,sha256=KKz9SXv_R3gX_AVeED2vyhYnj4BvD1uyDiKpCT3ulZ0,17720 +scipy/io/tests/data/test-44100Hz-le-1ch-4bytes-early-eof-no-data.wav,sha256=YX1g8qdCOAG16vX9G6q4SsfCj2ZVk199jzDQ8S0zWYI,72 +scipy/io/tests/data/test-44100Hz-le-1ch-4bytes-early-eof.wav,sha256=bFrsRqw0QXmsaDtjD6TFP8hZ5jEYMyaCmt-ka_C6GNk,1024 +scipy/io/tests/data/test-44100Hz-le-1ch-4bytes-incomplete-chunk.wav,sha256=zMnhvZvrP4kyOWKVKfbBneyv03xvzgqXYhHNxsAxDJ4,13 +scipy/io/tests/data/test-44100Hz-le-1ch-4bytes-rf64.wav,sha256=GSJpCuezlvHbhP3Cr4jNWmz4zG46XZ6jci2fWtiMN0k,17756 +scipy/io/tests/data/test-44100Hz-le-1ch-4bytes.wav,sha256=9qTCvpgdz3raecVN1ViggHPnQjBf47xmXod9iCDsEik,17720 +scipy/io/tests/data/test-48000Hz-2ch-64bit-float-le-wavex.wav,sha256=EqYBnEgTxTKvaTAtdA5HIl47CCFIje93y4hawR6Pyu0,7792 +scipy/io/tests/data/test-8000Hz-be-3ch-5S-24bit.wav,sha256=hGYchxQFjrtvZCBo0ULi-xdZ8krqXcKdTl3NSUfqe8k,90 +scipy/io/tests/data/test-8000Hz-le-1ch-1byte-ulaw.wav,sha256=BoUCDct3GiY_JJV_HoghF3mzAebT18j02c-MOn19KxU,70 +scipy/io/tests/data/test-8000Hz-le-2ch-1byteu.wav,sha256=R6EJshvQp5YVR4GB9u4Khn5HM1VMfJUj082i8tkBIJ8,1644 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-24bit-inconsistent.wav,sha256=t2Mgri3h6JLQDekrwIhDBOaG46OUzHynUz0pKbvOpNU,90 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-24bit-rf64.wav,sha256=iSGyqouX53NaEB33tzKXa11NRIY97GG40_pqWF_k5LQ,126 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-24bit.wav,sha256=yCv0uh-ux_skJsxeOjzog0YBk3ZQO_kw5HJHMqtVyI0,90 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-36bit.wav,sha256=oiMVsQV9-qGBz_ZwsfAkgA9BZXNjXbH4zxCGvvdT0RY,120 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-45bit.wav,sha256=e97XoPrPGJDIh8nO6mii__ViY5yVlmt4OnPQoDN1djs,134 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-53bit.wav,sha256=wbonKlzvzQ_bQYyBsj-GwnihZOhn0uxfKhL_nENCGNc,150 +scipy/io/tests/data/test-8000Hz-le-3ch-5S-64bit.wav,sha256=Uu5QPQcbtnFlnxOd4zFGxpiTC4wgdp6JOoYJ2VMZIU0,164 +scipy/io/tests/data/test-8000Hz-le-4ch-9S-12bit.wav,sha256=1F67h8tr2xz0C5K21T9y9gspcGA0qnSOzsl2vjArAMs,116 +scipy/io/tests/data/test-8000Hz-le-5ch-9S-5bit.wav,sha256=TJvGU7GpgXdCrdrjzMlDtpieDMnDK-lWMMqlWjT23BY,89 +scipy/io/tests/data/various_compressed.sav,sha256=H-7pc-RCQx5y6_IbHk1hB6OfnhvuPyW6EJq4EwI9iMc,1015 +scipy/io/tests/test_fortran.py,sha256=0cUeyIczUhtaRMFPTqHwH1U_Rm1djCaD1vDbi-6DRBo,8609 +scipy/io/tests/test_idl.py,sha256=2QpZGBWoSCwH5jchc9wvot2L03p0qqeqzjqux5KP-bM,20569 +scipy/io/tests/test_mmio.py,sha256=ZJR9mGlYDHOQv97lp_P0XuTSmEkruqD0UNXzH9IFQeo,29039 +scipy/io/tests/test_netcdf.py,sha256=0OR5kfTlx9SonwZPT9P8gRz7p0HEZy_6Jwr7PkfXrpY,19459 +scipy/io/tests/test_paths.py,sha256=3f12UO-N11JJjkw8jBgVAhz5KVrkokJbHrnvfklDhNA,3190 +scipy/io/tests/test_wavfile.py,sha256=1E9LMmsbEXMbzyLaqXtV_pTBa_wAX2PSaV3cJ0xamCw,16851 +scipy/io/wavfile.py,sha256=zISeQssvUbZ1kJTqrFX0x8N8QWuriM7F_KPQvaqXPQ4,28647 +scipy/linalg/__init__.pxd,sha256=0MlO-o_Kr8gg--_ipXEHFGtB8pZdHX8VX4wLYe_UzPg,53 +scipy/linalg/__init__.py,sha256=UOFZX4GCusrQjcaPB6NNNerhsVDe707BvlfE7XB8KzU,7517 +scipy/linalg/__pycache__/__init__.cpython-310.pyc,, +scipy/linalg/__pycache__/_basic.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_cholesky.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_cossin.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_ldl.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_lu.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_polar.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_qr.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_qz.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_schur.cpython-310.pyc,, +scipy/linalg/__pycache__/_decomp_svd.cpython-310.pyc,, +scipy/linalg/__pycache__/_expm_frechet.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs_inv_ssq.cpython-310.pyc,, +scipy/linalg/__pycache__/_matfuncs_sqrtm.cpython-310.pyc,, +scipy/linalg/__pycache__/_misc.cpython-310.pyc,, +scipy/linalg/__pycache__/_procrustes.cpython-310.pyc,, +scipy/linalg/__pycache__/_sketches.cpython-310.pyc,, +scipy/linalg/__pycache__/_solvers.cpython-310.pyc,, +scipy/linalg/__pycache__/_special_matrices.cpython-310.pyc,, +scipy/linalg/__pycache__/_testutils.cpython-310.pyc,, +scipy/linalg/__pycache__/basic.cpython-310.pyc,, +scipy/linalg/__pycache__/blas.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_cholesky.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_lu.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_qr.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_schur.cpython-310.pyc,, +scipy/linalg/__pycache__/decomp_svd.cpython-310.pyc,, +scipy/linalg/__pycache__/interpolative.cpython-310.pyc,, +scipy/linalg/__pycache__/lapack.cpython-310.pyc,, +scipy/linalg/__pycache__/matfuncs.cpython-310.pyc,, +scipy/linalg/__pycache__/misc.cpython-310.pyc,, +scipy/linalg/__pycache__/special_matrices.cpython-310.pyc,, +scipy/linalg/_basic.py,sha256=5LXUCE49zLfVNzU1V-0HrsHWkFsNe16wzZ9cu2LubW0,76085 +scipy/linalg/_blas_subroutines.h,sha256=v5j0yyW_pBFpkeccHLk4ZooAehksxRstV_A-ZlgGFy4,18190 +scipy/linalg/_cythonized_array_utils.cpython-310-x86_64-linux-gnu.so,sha256=joOfLEYaOsUVrqsckTNq4BKHi2sDI-iPg-j99SlmqJc,629104 +scipy/linalg/_cythonized_array_utils.pxd,sha256=OlWTbJt3gmdrfRFyx_Vz7GTmDTjr8dids5HA4TfC6R0,890 +scipy/linalg/_cythonized_array_utils.pyi,sha256=HZWXvJdpXGcydTEjkaL_kXIcxpcMqBBfFz7ZhscsRNo,340 +scipy/linalg/_decomp.py,sha256=D3WgtUo43h4Cjb-9vLepEVs_7BSXX1wYLWBtdmhRO_M,61881 +scipy/linalg/_decomp_cholesky.py,sha256=pk7_zuMkd-q-8AHyrNpm0wDof4-DeWiCFA3ESBkvLSQ,13721 +scipy/linalg/_decomp_cossin.py,sha256=rf2DFhaDmpXnWr1YpL3s8-hTOlR42HfSyWN7OoWzrec,8977 +scipy/linalg/_decomp_interpolative.cpython-310-x86_64-linux-gnu.so,sha256=3XlSAl9FNjr4YedoRyzcyUwXEjejCTtlUlE715iEbb8,1050264 +scipy/linalg/_decomp_ldl.py,sha256=HYzVUNZgEyuC2ZoFOGneas8ZkhhOFzUGcapL3Pos_cE,12535 +scipy/linalg/_decomp_lu.py,sha256=bCwCzMX_StEoLg1vScxglenyCzqMw3-BGJQmBcNEqNM,12941 +scipy/linalg/_decomp_lu_cython.cpython-310-x86_64-linux-gnu.so,sha256=65GCx_cYWtvY1uLIy6IlJlw5hBxR9c50jc9sqqa0_ak,270816 +scipy/linalg/_decomp_lu_cython.pyi,sha256=EASCkhrbJcBHo4zMYCUl1qRJDvPrvCqxd1TfqMWEd_U,291 +scipy/linalg/_decomp_polar.py,sha256=arzJ40FP1-TFsRvXPCP1qdNTsT60lkBcKBHfhB2JxxY,3578 +scipy/linalg/_decomp_qr.py,sha256=PbkwukMtzEH94uVjO9IEqSg4xmi0PV-UHXg9iM15rRE,15388 +scipy/linalg/_decomp_qz.py,sha256=uH93in1ikPR-Wgi1g49EPm2XXuhKOWBzPUJEahCotx8,16330 +scipy/linalg/_decomp_schur.py,sha256=OOzr2woTgWHBrJETNRCrzdviLTjiSDcxBgM6gTVkZMY,12059 +scipy/linalg/_decomp_svd.py,sha256=Epk7P6mmLLmYDiRETZAb3O2v3wKfbOjmGseWkAUlRPM,16809 +scipy/linalg/_decomp_update.cpython-310-x86_64-linux-gnu.so,sha256=bGRFGj3WHGa2IKoOCgcA3wQezbZAkHAXbqmd_QdevuM,368608 +scipy/linalg/_expm_frechet.py,sha256=Yc6J9HICUULvXcYBUaCyoOPFhXwjkIFi7TdrcNeVEmo,12326 +scipy/linalg/_fblas.cpython-310-x86_64-linux-gnu.so,sha256=0rwLgSerFM8d866P5CQoKYMO9UzADOr9cg5o735n9JU,1032545 +scipy/linalg/_flapack.cpython-310-x86_64-linux-gnu.so,sha256=EXQvPzCHMzIjad6O-Gevaq-MOwAdYQBWMt_zezpBYe4,2478641 +scipy/linalg/_lapack_subroutines.h,sha256=Wk88h_VA1tkF168pjCl8E8UVFbUTm8jWbI2hH8NZ12c,239333 +scipy/linalg/_linalg_pythran.cpython-310-x86_64-linux-gnu.so,sha256=W0Io5kRSjQGV2NJ4WXNje-XEFAgWbh4ulBeQKzRnIPc,140520 +scipy/linalg/_matfuncs.py,sha256=oOrSsB4tKtgGwFV2YJSUf0I3rTl9ZqCpF2WgHleDn70,25177 +scipy/linalg/_matfuncs_expm.cpython-310-x86_64-linux-gnu.so,sha256=mIhSUmPqv4w6ZzhugeldXAtJLtqNm3YP3Lm38R7vTg8,511433 +scipy/linalg/_matfuncs_expm.pyi,sha256=wZAZfVtEbB78ljXgQoiL0I4yaPhmHOqIpGBYGQPvS6k,178 +scipy/linalg/_matfuncs_inv_ssq.py,sha256=8dL7xD6DU8D4h2YyHcYjRhZQvv1pSOEzMuKlGP6zonw,28095 +scipy/linalg/_matfuncs_sqrtm.py,sha256=-qdBb42d2HvSkyVi-90N4Ai5vzwkqwGL00duzi_V1jM,6268 +scipy/linalg/_matfuncs_sqrtm_triu.cpython-310-x86_64-linux-gnu.so,sha256=L2JmNSlFlqzKBsNxoTEkYrEebNkZZ46z4kTWHU4KLbU,280968 +scipy/linalg/_misc.py,sha256=udhvxGfEHxhS3ecQBuwQ65W9ezVQIaVBw8JOmfqH_oE,6301 +scipy/linalg/_procrustes.py,sha256=uqPSMCxvqdbYMv3YEGUvwhnZnyIaApknfJcNAfyiTBQ,3520 +scipy/linalg/_sketches.py,sha256=6XwvmXh2zHjUFFsTYmoBYzhAUfZG2hwtdKR-YOzcDDQ,6117 +scipy/linalg/_solve_toeplitz.cpython-310-x86_64-linux-gnu.so,sha256=AKFt92yyul1kPCU2_6OmNzXaUozBAcv9P4fsVTrjfv4,300512 +scipy/linalg/_solvers.py,sha256=zwhrz0DbJ6wf9fY7B0pZMvMoC-cHo1VYXd3DyHk7pTg,28800 +scipy/linalg/_special_matrices.py,sha256=ZmOTcoJfbsR3baZgHWQ80extNyYJeSo8Tx81nUmzkyc,40697 +scipy/linalg/_testutils.py,sha256=IWA5vvdZ8yaHeXo2IxpQLqG9q54YIomHscYs85q9pd0,1807 +scipy/linalg/basic.py,sha256=AuNvDlH8mnAJScycj4mV-Iq1M0bXxidpY4Vud_lRJlM,753 +scipy/linalg/blas.py,sha256=-D-IH0bah8h2SmrdVA4xPfIqmKiPTkVC14GJ3czelLA,11685 +scipy/linalg/cython_blas.cpython-310-x86_64-linux-gnu.so,sha256=y-7hIQhUdUhJFjF5JQOFj3BxgTEAYdbWKYGEf7h94so,353945 +scipy/linalg/cython_blas.pxd,sha256=DCPBxNWP-BvdT_REj6_a4TjUrNaf6sCq_XoxU3pEbfc,15592 +scipy/linalg/cython_blas.pyx,sha256=9iUdRoyiHzu6mFbMUEQnhCqkpqD6bDo_QPnVwIOy-3g,65304 +scipy/linalg/cython_lapack.cpython-310-x86_64-linux-gnu.so,sha256=gQ8N_Du6tekhgNGUYZevW1HpVr2f8nok1aKIGwyrPtI,879585 +scipy/linalg/cython_lapack.pxd,sha256=Ld5hPwcYxpOPahFNsfNomsp0_DY8BfG-W8TmZxh-iYM,204556 +scipy/linalg/cython_lapack.pyx,sha256=odVC_GknEWmSo9tDA7wucppRlFV8fbO9KBaw94iD_2M,707012 +scipy/linalg/decomp.py,sha256=w9HTI1OxXpX_rL72qcmykc5dUWal7lTlAU8k-9Eq7Dg,708 +scipy/linalg/decomp_cholesky.py,sha256=1g45oc115ZZR3CfMW1bCPseF5ATz4Xf6Ih26NRqyjfs,649 +scipy/linalg/decomp_lu.py,sha256=FPo9NHe9wg1FhCaoVV1_4mdfNj0S4plT4dHr4vMl1U8,593 +scipy/linalg/decomp_qr.py,sha256=EJNpu6lSa36Eo-e4rbYu5kDlRTMse2mmGul_PLRFXHs,567 +scipy/linalg/decomp_schur.py,sha256=vkVK3y-055523Q__ptxVNatDebPBE1HD-DFBe7kEh3w,602 +scipy/linalg/decomp_svd.py,sha256=HrJqbmgde7d7EWxCsa9XkS9QuWgPYMFOHiF4NcAL_Qg,631 +scipy/linalg/interpolative.py,sha256=8kCZv1z3UtzBuPvompAUUjHToLta4ffvOjVVLSaRLeQ,32757 +scipy/linalg/lapack.py,sha256=0bytum8c_A1Xdt5NH5dcol7GjFtrkjuAnH_cnLWr07g,15805 +scipy/linalg/matfuncs.py,sha256=vYw39D2LukCRCFJpx0qx8tgHlRZEDZI2wZfZwhh-Ubo,744 +scipy/linalg/misc.py,sha256=uxpR80jJ5w5mslplWlL6tIathas8mEXvRIwDXYMcTOk,592 +scipy/linalg/special_matrices.py,sha256=OXkkDj-ypZHiC17RUerraAzO8dC9aDuVujzb3Ft3GDY,757 +scipy/linalg/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/linalg/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_basic.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_blas.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_cython_blas.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_cython_lapack.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_cythonized_array_utils.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_cholesky.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_cossin.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_ldl.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_lu.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_polar.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_decomp_update.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_extending.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_fblas.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_interpolative.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_lapack.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_matfuncs.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_matmul_toeplitz.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_procrustes.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_sketches.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_solve_toeplitz.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_solvers.cpython-310.pyc,, +scipy/linalg/tests/__pycache__/test_special_matrices.cpython-310.pyc,, +scipy/linalg/tests/_cython_examples/extending.pyx,sha256=scunPSonBTtsidhd2hLtg-DPWoFkvzWcXDMYEO9iygo,887 +scipy/linalg/tests/_cython_examples/meson.build,sha256=AoGSc8a6hX_ivvj4MgP_stTLu2ant4ALdknPMYQlaZ0,670 +scipy/linalg/tests/data/carex_15_data.npz,sha256=E_PhSRqHa79Z1-oQrSnB-bWZaiq5khbzHVv81lkBLB4,34462 +scipy/linalg/tests/data/carex_18_data.npz,sha256=Wfg5Rn8nUrffb7bUCUOW7dMqWSm3ZPf_oeZmZDHmysY,161487 +scipy/linalg/tests/data/carex_19_data.npz,sha256=OOj8ewQd8LI9flyhXq0aBl5kZ2Ee-ahIzH25P4Ct_Yc,34050 +scipy/linalg/tests/data/carex_20_data.npz,sha256=FOIi00pxGMcoShZ1xv7O7ne4TflRpca6Kl7p_zBU-h0,31231 +scipy/linalg/tests/data/carex_6_data.npz,sha256=GyoHNrVB6_XEubTADW2rKB5zyfuZE8biWBp4Gze2Avk,15878 +scipy/linalg/tests/data/gendare_20170120_data.npz,sha256=o9-rRR2dXCAkPg7YXNi2yWV2afuaD4O1vhZVhXg9VbU,2164 +scipy/linalg/tests/test_basic.py,sha256=ykpAEKYmPCxF0mrUQUHzJIahmXzzFqrU4thGEVRKdqE,78883 +scipy/linalg/tests/test_blas.py,sha256=8w_6r4CBrif9MH69v15Iil5rEcyRDlUhgbbZnC8_Bck,41729 +scipy/linalg/tests/test_cython_blas.py,sha256=0Y2w1Btw6iatfodZE7z0lisJJLVCr70DAW-62he_sz4,4087 +scipy/linalg/tests/test_cython_lapack.py,sha256=McSFDUU4kgCavU1u3-uqBGlzUZiLGxM5qPfBFgPTqdE,796 +scipy/linalg/tests/test_cythonized_array_utils.py,sha256=vZh0gT7cN7m5H5xox5ClQT_GxoBbadRtYDBNKBDnhZQ,4172 +scipy/linalg/tests/test_decomp.py,sha256=IlzcrZlmRNPcdf8yF0Dixoj3W7OB-RaycKZYq4S16Lc,118686 +scipy/linalg/tests/test_decomp_cholesky.py,sha256=5WxQbSxK6134NztaoNu-d4OmudQRfhgeyf2LmyJdx1w,9743 +scipy/linalg/tests/test_decomp_cossin.py,sha256=QCIIlzrhJR9K_4WLniwR7JuaYyA3_3jPtScBJx4NU3c,11982 +scipy/linalg/tests/test_decomp_ldl.py,sha256=f6rUwqOxRNr0C3lM0zX0PjAj4yLi3T_bmKdAUGpW2xg,4971 +scipy/linalg/tests/test_decomp_lu.py,sha256=spCYelU_CXmHAaKrJM4V5djLKq5MCeX4wN1SBCFkSOo,12629 +scipy/linalg/tests/test_decomp_polar.py,sha256=fGKl3Skqz6IpHBeFcq6bdqvS8M53rXx2Wh6Kx4f5T3Y,3287 +scipy/linalg/tests/test_decomp_update.py,sha256=MCSzhUD-bcCs1Ll5pHJqCdRTgEpimCglZ3lb8bzwZqs,68502 +scipy/linalg/tests/test_extending.py,sha256=eirY2TQ2IwWje-5hW_kqvS0SnA2xEzLeG5sE0P3zuvI,1751 +scipy/linalg/tests/test_fblas.py,sha256=Ykb7LKjbxPXAdJD-IkXMAsbUmXMAkku2FQCr-jlDTUE,18687 +scipy/linalg/tests/test_interpolative.py,sha256=EVmkopJjhzDOs6h6NoSkQ-d7qRZDsys58mt4sp8yOoE,8577 +scipy/linalg/tests/test_lapack.py,sha256=M5Q_VvWz-7LANoqK7l8yyslf18jNouG2gaX7QZVtaJ0,134781 +scipy/linalg/tests/test_matfuncs.py,sha256=yXWlWUswLo_pDbKmhY8OkBSPfCrRXlU2om2QbwTAHIU,41997 +scipy/linalg/tests/test_matmul_toeplitz.py,sha256=73Qe51lCXEWZGpxk8GYv0owDSlN0IpnLJPlI0nsCdhY,4088 +scipy/linalg/tests/test_procrustes.py,sha256=zOl2G-PENDtEZGk4AVdqIp_4zUWoHmhGrj2RyuZRPTk,7660 +scipy/linalg/tests/test_sketches.py,sha256=FLqc8wn9esU8LbSsWS7_OC0sZ-BcGPROqPurBM8BZXc,3954 +scipy/linalg/tests/test_solve_toeplitz.py,sha256=5dmvPEpOwHAucdoMhT1lCvEMIbMrgpZwj9nUL1WRb2g,5122 +scipy/linalg/tests/test_solvers.py,sha256=jIJ1YjC5epuQACS2h7GZZUuIbt89KPM8tnUlXTsPyjU,33951 +scipy/linalg/tests/test_special_matrices.py,sha256=CyWH9bbVGogK-ECymnhyxogMDEMeOC2BN9A2XDYg-eE,25074 +scipy/misc/__init__.py,sha256=dVfULY959nFwpl5NCxyCpiHyNcSNaR7HYOg7QU21a5s,135 +scipy/misc/__pycache__/__init__.cpython-310.pyc,, +scipy/misc/__pycache__/common.cpython-310.pyc,, +scipy/misc/__pycache__/doccer.cpython-310.pyc,, +scipy/misc/common.py,sha256=nAGQOVR9ZEAb703uhOVQZqf-z0iCM4EDhbHK4_h_Tdc,142 +scipy/misc/doccer.py,sha256=wHbpGV8todadz6MIzJHalDfRjiKI164qs6iMcHgsVu0,142 +scipy/ndimage/__init__.py,sha256=w4dCQCzsFmzAs7xF18MCTf5ld8HdIFfZjoRxuLQeqwg,5154 +scipy/ndimage/__pycache__/__init__.cpython-310.pyc,, +scipy/ndimage/__pycache__/_delegators.cpython-310.pyc,, +scipy/ndimage/__pycache__/_filters.cpython-310.pyc,, +scipy/ndimage/__pycache__/_fourier.cpython-310.pyc,, +scipy/ndimage/__pycache__/_interpolation.cpython-310.pyc,, +scipy/ndimage/__pycache__/_measurements.cpython-310.pyc,, +scipy/ndimage/__pycache__/_morphology.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ndimage_api.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ni_docstrings.cpython-310.pyc,, +scipy/ndimage/__pycache__/_ni_support.cpython-310.pyc,, +scipy/ndimage/__pycache__/_support_alternative_backends.cpython-310.pyc,, +scipy/ndimage/__pycache__/filters.cpython-310.pyc,, +scipy/ndimage/__pycache__/fourier.cpython-310.pyc,, +scipy/ndimage/__pycache__/interpolation.cpython-310.pyc,, +scipy/ndimage/__pycache__/measurements.cpython-310.pyc,, +scipy/ndimage/__pycache__/morphology.cpython-310.pyc,, +scipy/ndimage/_ctest.cpython-310-x86_64-linux-gnu.so,sha256=h98uh-F0_Ywmq7sQkE-zVgPCuj5JX3uZqeFVBgpYS0A,17008 +scipy/ndimage/_cytest.cpython-310-x86_64-linux-gnu.so,sha256=wB-D9XbVebncwjrfQS5e-HMWx7OcO7UwZM185epaM-0,90984 +scipy/ndimage/_delegators.py,sha256=NBf6hkZ7pyrELlhUpP-CvuvBPEgO77FgAfhD38KEk-Q,9256 +scipy/ndimage/_filters.py,sha256=6CH71a4VDcn9thauWiE1BJBOVBb-vN5CFznz_lJ2nAw,70982 +scipy/ndimage/_fourier.py,sha256=SoAYRx7ax7Tv51MyYzDlZ3fN682x4T6N8yReX2La4-I,11266 +scipy/ndimage/_interpolation.py,sha256=Zlb4ZRJbTOrf21dedO28GHTXA0Kh9hMCDWBdGvRbco4,36670 +scipy/ndimage/_measurements.py,sha256=eyBWnB0x1CxseFOMPXkfpuu48nhkMuK24hZPBla2wVs,56113 +scipy/ndimage/_morphology.py,sha256=LF91gKJcHIWoD9ath_9-Y7HgUwQbA0ELqgVYvm1YAWA,100762 +scipy/ndimage/_nd_image.cpython-310-x86_64-linux-gnu.so,sha256=VtZ0C4AFg_pUrbpllO-FYoMvhtFWQ1UnxKHKhYRlO74,147184 +scipy/ndimage/_ndimage_api.py,sha256=S8DBRWydSRfAz-ZlHSMeCSbjYGgCLioa9_Q2VXGeC_g,586 +scipy/ndimage/_ni_docstrings.py,sha256=TxAEkoC5ysA5JuK8IM2xoq60yddVWqOXsmxYXIr-4_E,8542 +scipy/ndimage/_ni_label.cpython-310-x86_64-linux-gnu.so,sha256=jPRwg_3Y0xv6TWkGcWCenb5ui-DR5vrWvNGaq_0cE5M,424104 +scipy/ndimage/_ni_support.py,sha256=weYLkgApaf0WG54dksxJnFEY2ToCT9O3XNP4d4pppFM,5308 +scipy/ndimage/_rank_filter_1d.cpython-310-x86_64-linux-gnu.so,sha256=4DPKpmKSwLiGd1zUqn9UTE7RoV25It7bFb_VApB142Q,27448 +scipy/ndimage/_support_alternative_backends.py,sha256=G9J6cBRmZ0VFkAQ72uGdsiQ9-4ZlqTZ4KsX8cs_QZXg,2603 +scipy/ndimage/filters.py,sha256=cAv2zezrTJEm9JzKPV_pmXzZcgczCK_VaYJ4mdNW3FM,976 +scipy/ndimage/fourier.py,sha256=gnifi4S_Epyu4DpNsebz4A5BKzBWoGf11FkXWeXsoqY,599 +scipy/ndimage/interpolation.py,sha256=GHYvxCyQsLfKtNUc8AUN_vqmBhmAPwNnxm2-VpFMayk,664 +scipy/ndimage/measurements.py,sha256=xdSs52Y5RjURLP710iGURXWQFeS3ok4WjoYufKh9OeA,788 +scipy/ndimage/morphology.py,sha256=yFWSo7o_7PuYq61WGQOCIgMppneNLxqhJocyN0bMsVA,965 +scipy/ndimage/tests/__init__.py,sha256=GbIXCsLtZxgmuisjxfFsd3pj6-RQhmauc6AVy6sybDc,314 +scipy/ndimage/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_c_api.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_datatypes.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_filters.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_fourier.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_interpolation.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_measurements.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_morphology.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_ni_support.cpython-310.pyc,, +scipy/ndimage/tests/__pycache__/test_splines.cpython-310.pyc,, +scipy/ndimage/tests/data/label_inputs.txt,sha256=JPbEnncwUyhlAAv6grN8ysQW9w9M7ZSIn_NPopqU7z4,294 +scipy/ndimage/tests/data/label_results.txt,sha256=Cf2_l7FCWNjIkyi-XU1MaGzmLnf2J7NK2SZ_10O-8d0,4309 +scipy/ndimage/tests/data/label_strels.txt,sha256=AU2FUAg0WghfvnPDW6lhMB1kpNdfv3coCR8blcRNBJ8,252 +scipy/ndimage/tests/dots.png,sha256=sgtW-tx0ccBpTT6BSNniioPXlnusFr-IUglK_qOVBBQ,2114 +scipy/ndimage/tests/test_c_api.py,sha256=7Gv-hR91MWpiGQ32yjXIBjFytuaYLqz3wYiCXcC8ZSk,3738 +scipy/ndimage/tests/test_datatypes.py,sha256=TYMiGyBcdOq3KVLzvjZPjerD1EXonyHFQYBLTWDwN7o,2819 +scipy/ndimage/tests/test_filters.py,sha256=_tOMm5NItaTEoY3hqEDpXYflp6bpkosalSa3evzhwjA,125491 +scipy/ndimage/tests/test_fourier.py,sha256=2PL6aLDczM65NwUk7YTXXdjskLJmDCgpVD-xTHr55bo,7766 +scipy/ndimage/tests/test_interpolation.py,sha256=g-58BrUxEaje9cOWWWRMQDSMcNFTrhWBFEUdTZxzAy0,60681 +scipy/ndimage/tests/test_measurements.py,sha256=JzF8phts7W0xQSRJTo59JSe0voOW5MIxqkbCCRTqkiE,58874 +scipy/ndimage/tests/test_morphology.py,sha256=bi-c1tjMCgqQagW0Izuv86KO7p1uuFPFjiDUfDM3nIU,128720 +scipy/ndimage/tests/test_ni_support.py,sha256=fcMPR9wmtOePd9eKg1ksGgolmKqVO2xboHsYOd4mC1I,2511 +scipy/ndimage/tests/test_splines.py,sha256=uAtDEgjNoaqfIk3QGfDfD33XK5_R0WyGgsCUCS3j7P4,2557 +scipy/odr/__init__.py,sha256=CErxMJ0yBfu_cvCoKJMu9WjqUaohLIqqf228Gm9XWJI,4325 +scipy/odr/__odrpack.cpython-310-x86_64-linux-gnu.so,sha256=W3IjGj754ZlWFdSyz1kl31i6b48Q0Lio6qaSJw-4yMQ,622553 +scipy/odr/__pycache__/__init__.cpython-310.pyc,, +scipy/odr/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/odr/__pycache__/_models.cpython-310.pyc,, +scipy/odr/__pycache__/_odrpack.cpython-310.pyc,, +scipy/odr/__pycache__/models.cpython-310.pyc,, +scipy/odr/__pycache__/odrpack.cpython-310.pyc,, +scipy/odr/_add_newdocs.py,sha256=GeWL4oIb2ydph_K3qCjiIbPCM3QvpwP5EZwEJVOzJrQ,1128 +scipy/odr/_models.py,sha256=tfOLgqnV4LR3VKi7NAg1g1Jp_Zw8lG_PA5BHwU_pTH0,7800 +scipy/odr/_odrpack.py,sha256=n30DVx78Oh0zDItjKdqDaJpiXSyVPqHYGk63a1-5NZg,42496 +scipy/odr/models.py,sha256=Fcdj-P9rJ_B-Ct8bh3RrusnapeHLysVaDsM26Q8fHFo,590 +scipy/odr/odrpack.py,sha256=OlRlBxKlzp5VDi2fnnA-Jdl6G0chDt95JNCvJYg2czs,632 +scipy/odr/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/odr/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/odr/tests/__pycache__/test_odr.cpython-310.pyc,, +scipy/odr/tests/test_odr.py,sha256=MkCfBdQvbCtiLgDFaIAp0jclwj2mIhwgL3J0Asvq31Q,22079 +scipy/optimize/__init__.pxd,sha256=kFYBK9tveJXql1KXuOkKGvj4Fu67GmuyRP5kMVkMbyk,39 +scipy/optimize/__init__.py,sha256=7ZzePqFF1X1377f_s3dpVdeg51I3YwManuh8Pl4M1mE,13279 +scipy/optimize/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/__pycache__/_basinhopping.cpython-310.pyc,, +scipy/optimize/__pycache__/_bracket.cpython-310.pyc,, +scipy/optimize/__pycache__/_chandrupatla.cpython-310.pyc,, +scipy/optimize/__pycache__/_cobyla_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_cobyqa_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_constraints.cpython-310.pyc,, +scipy/optimize/__pycache__/_dcsrch.cpython-310.pyc,, +scipy/optimize/__pycache__/_differentiable_functions.cpython-310.pyc,, +scipy/optimize/__pycache__/_differentialevolution.cpython-310.pyc,, +scipy/optimize/__pycache__/_direct_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_dual_annealing.cpython-310.pyc,, +scipy/optimize/__pycache__/_elementwise.cpython-310.pyc,, +scipy/optimize/__pycache__/_hessian_update_strategy.cpython-310.pyc,, +scipy/optimize/__pycache__/_isotonic.cpython-310.pyc,, +scipy/optimize/__pycache__/_lbfgsb_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_linesearch.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_doc.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_highs.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_ip.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_rs.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_simplex.cpython-310.pyc,, +scipy/optimize/__pycache__/_linprog_util.cpython-310.pyc,, +scipy/optimize/__pycache__/_milp.cpython-310.pyc,, +scipy/optimize/__pycache__/_minimize.cpython-310.pyc,, +scipy/optimize/__pycache__/_minpack_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_nnls.cpython-310.pyc,, +scipy/optimize/__pycache__/_nonlin.cpython-310.pyc,, +scipy/optimize/__pycache__/_numdiff.cpython-310.pyc,, +scipy/optimize/__pycache__/_optimize.cpython-310.pyc,, +scipy/optimize/__pycache__/_qap.cpython-310.pyc,, +scipy/optimize/__pycache__/_remove_redundancy.cpython-310.pyc,, +scipy/optimize/__pycache__/_root.cpython-310.pyc,, +scipy/optimize/__pycache__/_root_scalar.cpython-310.pyc,, +scipy/optimize/__pycache__/_shgo.cpython-310.pyc,, +scipy/optimize/__pycache__/_slsqp_py.cpython-310.pyc,, +scipy/optimize/__pycache__/_spectral.cpython-310.pyc,, +scipy/optimize/__pycache__/_tnc.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_dogleg.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_exact.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_krylov.cpython-310.pyc,, +scipy/optimize/__pycache__/_trustregion_ncg.cpython-310.pyc,, +scipy/optimize/__pycache__/_tstutils.cpython-310.pyc,, +scipy/optimize/__pycache__/_zeros_py.cpython-310.pyc,, +scipy/optimize/__pycache__/cobyla.cpython-310.pyc,, +scipy/optimize/__pycache__/elementwise.cpython-310.pyc,, +scipy/optimize/__pycache__/lbfgsb.cpython-310.pyc,, +scipy/optimize/__pycache__/linesearch.cpython-310.pyc,, +scipy/optimize/__pycache__/minpack.cpython-310.pyc,, +scipy/optimize/__pycache__/minpack2.cpython-310.pyc,, +scipy/optimize/__pycache__/moduleTNC.cpython-310.pyc,, +scipy/optimize/__pycache__/nonlin.cpython-310.pyc,, +scipy/optimize/__pycache__/optimize.cpython-310.pyc,, +scipy/optimize/__pycache__/slsqp.cpython-310.pyc,, +scipy/optimize/__pycache__/tnc.cpython-310.pyc,, +scipy/optimize/__pycache__/zeros.cpython-310.pyc,, +scipy/optimize/_basinhopping.py,sha256=Ug6gQH56vjrs-6RwGZKyCgVzjkT9rgqOPH-sJSaWtmM,29778 +scipy/optimize/_bglu_dense.cpython-310-x86_64-linux-gnu.so,sha256=Mp8ZHT3V5u1rxmQR0jvRP44vLVvJ2Yp_Wqd6r3QkmrA,364392 +scipy/optimize/_bracket.py,sha256=tVevTxrwC9YyCgDKDCNrsxZyY6Hj8yM2F44kqawMCgs,31308 +scipy/optimize/_chandrupatla.py,sha256=cmgXWc33PxEUUVn2Bh5Go4XPx_K7Hzihb2DyUAn8C80,24639 +scipy/optimize/_cobyla.cpython-310-x86_64-linux-gnu.so,sha256=8452mghEbdmbpM4f2l46D7XQAN0bLl9pzQ7xk5YP4nw,104657 +scipy/optimize/_cobyla_py.py,sha256=_HUCEYEEFxNBniaw56eZqmjsrwCOMbOTdFaYUv5UqUI,10867 +scipy/optimize/_cobyqa_py.py,sha256=_zejgs3XKkieGiMlRVn1x12cyWoulaPP2SpvxA4zK3k,2971 +scipy/optimize/_constraints.py,sha256=K37Le2W-pA7fsR39wXiC3L60QZGFN_-EUhtmGie-qn4,22895 +scipy/optimize/_cython_nnls.cpython-310-x86_64-linux-gnu.so,sha256=_ZrWp8MHR_wnEEiiJH31CH2Uv0b10Xus4ND3UHfyVmk,121024 +scipy/optimize/_dcsrch.py,sha256=D5I9G4oH5kFD2Rrb61gppXFMwwz6JiQBYPvW3vbR5Gs,25235 +scipy/optimize/_differentiable_functions.py,sha256=aYwpOvlHfQ7j-BO15VcL1v5XLR36tr_OPmf1eCWLuHY,24922 +scipy/optimize/_differentialevolution.py,sha256=UrTsxsTC1ddNoBsZ2tnNI0Lpz4HUC0QlmcaA1wCiQPc,86506 +scipy/optimize/_direct.cpython-310-x86_64-linux-gnu.so,sha256=XlQzuhCEy5MJeeNQQVyge8JRdPQiRjKS8Gak_tKkSSs,43480 +scipy/optimize/_direct_py.py,sha256=-tEx51_9jg63zmDcSmmqeMtTlxXpci8fSh9TR_dFD4M,11849 +scipy/optimize/_dual_annealing.py,sha256=Zr5O-Juk2lslIlneQ4J9sgmDlPKh6sRZ9ytZZ9i-x7U,31121 +scipy/optimize/_elementwise.py,sha256=2CYFgK7uYw0St-T5M-GAhh8zgB3yU0mHmjS1Q6YYrNA,33136 +scipy/optimize/_group_columns.cpython-310-x86_64-linux-gnu.so,sha256=P2875GIfqzh5MPrn-8G3wzwte-C8HuX7MBE9dllikGc,99840 +scipy/optimize/_hessian_update_strategy.py,sha256=xmtREKGlLgVvlBynjb5eCnPbsH-xbPcprS-ZoziG80M,18423 +scipy/optimize/_highspy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/_highspy/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_highspy/__pycache__/_highs_wrapper.cpython-310.pyc,, +scipy/optimize/_highspy/_core.cpython-310-x86_64-linux-gnu.so,sha256=8iaECAmfHG__kXbAf2t5bl_dTgAAFazz8yoBuWbb7ck,5775816 +scipy/optimize/_highspy/_highs_options.cpython-310-x86_64-linux-gnu.so,sha256=jdefOJSmsC_TUEyCKIm151RWjvAsuhizZJ-pJxlGKZc,407344 +scipy/optimize/_highspy/_highs_wrapper.py,sha256=wVqUOgmFv3FthLk3GdCy9XLmmDc2VasCWGFLSyq2cwM,11294 +scipy/optimize/_isotonic.py,sha256=WY-9jtT5VVafVALYIp6lJPQnBfYVNDP9oJpg-kErYYI,6077 +scipy/optimize/_lbfgsb.cpython-310-x86_64-linux-gnu.so,sha256=vglZDNvaMcnIz092PRaZ_b1vR4f7wHR9NNRn2x0KFfw,462225 +scipy/optimize/_lbfgsb_py.py,sha256=KgLYyR-UeQg8chw-ttdarm5blMuop5lY4KqI_Hqk-2c,21047 +scipy/optimize/_linesearch.py,sha256=sZ45z0K3l6LLURdAfzO5CI5DctDlXqD92PCaz9mKzYE,27215 +scipy/optimize/_linprog.py,sha256=TGl9k9Ioh-hgHYgtndN5BNcU4vqfpZm8whRK2f4ehQQ,30262 +scipy/optimize/_linprog_doc.py,sha256=AeDv_zu0iU_oV0vxSrdzzY5GkKzOVx-5nmBgFB_UXhA,61942 +scipy/optimize/_linprog_highs.py,sha256=yN9w71Hs6qFYBNg21L6gz61-szlmLPUafblZEryyzy0,17144 +scipy/optimize/_linprog_ip.py,sha256=dEaU1pqYXRxWvH91Zxm4tMQ7813QNhjIB8Yj8Nb3cPY,45784 +scipy/optimize/_linprog_rs.py,sha256=wRVGZxCSpo4ttw4CPpmXozSvM9WRXD179fGiGh8gOQ4,23146 +scipy/optimize/_linprog_simplex.py,sha256=9_nxcVl-ofHN9p_dDyC1C6jHlPttSfO9kp8WF1ST4JM,24748 +scipy/optimize/_linprog_util.py,sha256=W85k22zMLJMAEZs_UHMqR5OxHtykyKoyHQBUCa3YAw0,62799 +scipy/optimize/_lsap.cpython-310-x86_64-linux-gnu.so,sha256=IpuNFqa2w4GPgtsLTjIwphOO1AGD05YEleMBt_oPpgw,27072 +scipy/optimize/_lsq/__init__.py,sha256=Yk4FSVEqe1h-qPqVX7XSkQNBYDtZO2veTmMAebCxhIQ,172 +scipy/optimize/_lsq/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/bvls.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/common.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/dogbox.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/least_squares.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/lsq_linear.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/trf.cpython-310.pyc,, +scipy/optimize/_lsq/__pycache__/trf_linear.cpython-310.pyc,, +scipy/optimize/_lsq/bvls.py,sha256=7u5B8LfUbv3ZRZ8DAZKuDTSNRfDEBmTsn25VZtMMsKk,5195 +scipy/optimize/_lsq/common.py,sha256=kNsAyIAPFPTEJqQCKUwR8NEbYWtgINDoF76SBg-rU6Y,20476 +scipy/optimize/_lsq/dogbox.py,sha256=97htRlr-Yt-u4Ob3ks7avAMdnjJsO83uHUMjMYrhyjc,11682 +scipy/optimize/_lsq/givens_elimination.cpython-310-x86_64-linux-gnu.so,sha256=RJrGkIpf-mRnCxvdDBdCcp05braTI0QKU3YFoPGjlO8,231792 +scipy/optimize/_lsq/least_squares.py,sha256=M_bznCB4ueIt9hklMVu4mCXskIKkZe1AVBL5biaSvTY,39302 +scipy/optimize/_lsq/lsq_linear.py,sha256=JWhGY2GJmeQoi7ZU0dg-TFSRIGvdNAgHhIaPK9GNOUA,15037 +scipy/optimize/_lsq/trf.py,sha256=ElVHnB2Un3eaQ4jJ8KHHp-hwXfYHMypnSthfRO33P90,19477 +scipy/optimize/_lsq/trf_linear.py,sha256=jIs7WviOu_8Kpb7sTln8W7YLgkcndv0eGIP15g_mC4g,7642 +scipy/optimize/_milp.py,sha256=KYJlJ0NulFZoO6d1yactJmhryLuPzmiRS8GIxqWXxbU,15227 +scipy/optimize/_minimize.py,sha256=MGd3sP6LNwpElRiW85iHxBEinhaohly0gfOLxhtUs7s,50135 +scipy/optimize/_minpack.cpython-310-x86_64-linux-gnu.so,sha256=crBQfJtaez4Wnx81ryfxYOiwMHJHpoo1mnXtc_ZWBUE,98312 +scipy/optimize/_minpack_py.py,sha256=sjx90i41TQ9CzXtr2LVkxP-woc2L_8v7YHVXidSpRK0,45028 +scipy/optimize/_moduleTNC.cpython-310-x86_64-linux-gnu.so,sha256=9flNO26sg_nbrAR8rZEDclNUxE2SqaL948bn-KXW8PI,152168 +scipy/optimize/_nnls.py,sha256=td0FOAvUICeUTGrXqFmdV6UXGi_Cy0PrG8hQviDsqe4,3233 +scipy/optimize/_nonlin.py,sha256=BtDRlEwSlvOhxo04mXQHpzytoV-FI_K5yVs0RAX8eBI,50177 +scipy/optimize/_numdiff.py,sha256=CpeUGKWHTsAk-JnvtbDBjpXvlI8pch1oXIPj40CNY2c,28931 +scipy/optimize/_optimize.py,sha256=AzljBSSf7wAO_G9W8pkg-o3IdlHzMdp5JulhMGcoORM,147685 +scipy/optimize/_pava_pybind.cpython-310-x86_64-linux-gnu.so,sha256=hSavFSsnm9MiEdjK6nOgroF5kX6rbrZqJY6tpFoYWvQ,223984 +scipy/optimize/_qap.py,sha256=6bIzIiLwD4V2MCJrqQBOJ2h7uycy0qx01mkl-CR1U3I,29390 +scipy/optimize/_remove_redundancy.py,sha256=JqaQo5XclDpilSzc1BFv4Elxr8CXlFlgV45ypUwALyc,18769 +scipy/optimize/_root.py,sha256=Zh-WttrslloClCDg7VKhrVbRkDHBRkS4-ijJkI-_twg,28714 +scipy/optimize/_root_scalar.py,sha256=PIVT37WbcUwytG0WsU_t_pkUiluqZcJUan61ErBo_7I,20391 +scipy/optimize/_shgo.py,sha256=y5ET23yh6LS0yltoVaeM3CH7gundIfAfUhOEKq09ksw,62399 +scipy/optimize/_shgo_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/_shgo_lib/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_shgo_lib/__pycache__/_complex.cpython-310.pyc,, +scipy/optimize/_shgo_lib/__pycache__/_vertex.cpython-310.pyc,, +scipy/optimize/_shgo_lib/_complex.py,sha256=Ivs6HoVpIaVrS1wMiJC5FhV3N8VZKvoVSkcZ8YA191s,50224 +scipy/optimize/_shgo_lib/_vertex.py,sha256=I2TAqEEdTK66Km6UIkrDm2-tKpeJUuFX7DAfTk3XvUg,13996 +scipy/optimize/_slsqp.cpython-310-x86_64-linux-gnu.so,sha256=exN7FO1DV3_BFBEHVPHmdcA4jaGNAMZyIj7nuoKXRfc,86704 +scipy/optimize/_slsqp_py.py,sha256=8KNFRiJlhinsqSMIp3-lzjrrw4lcrV7CADf1N6k87LA,19066 +scipy/optimize/_spectral.py,sha256=cgBoHOh5FcTqQ0LD5rOx4K7ECc7sbnODvcrn15_QeTI,8132 +scipy/optimize/_tnc.py,sha256=hmnQHaS5FLoaLzPHLcIVU2NPeO_-EQuJCc1Z8RLqDVs,17009 +scipy/optimize/_trlib/__init__.py,sha256=cNGWE1VffijqhPtSaqwagtBJvjJK-XrJ6K80RURLd48,524 +scipy/optimize/_trlib/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_trlib/_trlib.cpython-310-x86_64-linux-gnu.so,sha256=c-TVzL4aIpzho2u8A5jKjI2S8kUNtjdGG0YMGLXbxN4,380985 +scipy/optimize/_trustregion.py,sha256=z3yOE3-PGbIviDYTqpPQqa5wQhTMqc-LvssbY9Eou0A,10801 +scipy/optimize/_trustregion_constr/__init__.py,sha256=c8J2wYGQZr9WpLIT4zE4MUgEj4YNbHEWYYYsFmxAeXI,180 +scipy/optimize/_trustregion_constr/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/canonical_constraint.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/equality_constrained_sqp.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/minimize_trustregion_constr.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/projections.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/qp_subproblem.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/report.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/__pycache__/tr_interior_point.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/canonical_constraint.py,sha256=lWdsJ7WNTDm17jD-Omf5lflSMfcvdZWpReCND2CyjI0,12549 +scipy/optimize/_trustregion_constr/equality_constrained_sqp.py,sha256=eJc1Y25WhSLC6OGNJSFw0uA0c6LSUgfTQzmyXsXqVog,9154 +scipy/optimize/_trustregion_constr/minimize_trustregion_constr.py,sha256=WpVDoMk7rFHJI2KSG2YWiBm6bli180KvLneK9TVfz9Y,26145 +scipy/optimize/_trustregion_constr/projections.py,sha256=EO0uHULrNw8pm99vY-gd3pOFQEqrqk_13lVde9iUjTA,13169 +scipy/optimize/_trustregion_constr/qp_subproblem.py,sha256=EtAhRcEtSnGsEeEZ2HGEzm-7r0pnXMCgl9NemKWvdzg,22592 +scipy/optimize/_trustregion_constr/report.py,sha256=_L-HrO5C1lzvKvaijgkOYD210dvM4PkrhBSEQrMhVlw,1782 +scipy/optimize/_trustregion_constr/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/_trustregion_constr/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/__pycache__/test_canonical_constraint.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/__pycache__/test_nested_minimize.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/__pycache__/test_projections.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/__pycache__/test_qp_subproblem.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/__pycache__/test_report.cpython-310.pyc,, +scipy/optimize/_trustregion_constr/tests/test_canonical_constraint.py,sha256=zVPxZDa0WkG_tw9Fm_eo_JzsQ8rQrUJyQicq4J12Nd4,9869 +scipy/optimize/_trustregion_constr/tests/test_nested_minimize.py,sha256=tgBVQe97RwVu_GJACARyg0s9zHiFGVHSPNrXLCjlX7w,1216 +scipy/optimize/_trustregion_constr/tests/test_projections.py,sha256=-UrTi0-lWm4hANoytCmyImSJUH9Ed4x3apHDyRdJg5o,8834 +scipy/optimize/_trustregion_constr/tests/test_qp_subproblem.py,sha256=bU_4_VHpQZpCnC733G-rakx3Mxdwt4QndCM31mUH4vA,27719 +scipy/optimize/_trustregion_constr/tests/test_report.py,sha256=hyRnUGBhDhKHR5SKD66ZME4zzCIViIh3_-700p0afXY,1104 +scipy/optimize/_trustregion_constr/tr_interior_point.py,sha256=rRly3wy-O-MQ0dF2lc7b1IwTYWYXE_k87MzYnAW7EJw,14400 +scipy/optimize/_trustregion_dogleg.py,sha256=HS783IZYHE-EEuF82c4rkFp9u3MNKUdCeynZ6ap8y8s,4389 +scipy/optimize/_trustregion_exact.py,sha256=zaMQc5wUhZSnpxyXWwcqIh0O9bctOU4R-isaeblvSNc,15558 +scipy/optimize/_trustregion_krylov.py,sha256=KGdudJsoXXROXAc82aZ8ACojD3rimvyx5PYitbo4UzQ,3030 +scipy/optimize/_trustregion_ncg.py,sha256=y7b7QjFBfnB1wDtbwnvKD9DYpz7y7NqVrJ9RhNPcipw,4580 +scipy/optimize/_tstutils.py,sha256=BBaThpZNuwIQBqtVMOEB4bUHk3QdG2NpuLJBum8P6ak,34047 +scipy/optimize/_zeros.cpython-310-x86_64-linux-gnu.so,sha256=8wEb0RyMpKpbOPVfUTFDfN-vRonegrTDLDToQjlta0Y,21648 +scipy/optimize/_zeros_py.py,sha256=pN0GMI_qHtor8BnY73B49bDZiiSYAxY1EtsQ3Kf0BJ0,52066 +scipy/optimize/cobyla.py,sha256=k2io8SM0vahYT5Zu4nS4yfa05_gyH0y-jVVxdWkC4dU,557 +scipy/optimize/cython_optimize.pxd,sha256=ecYJEpT0CXN-2vtaZfGCChD-oiIaJyRDIsTHE8eUG5M,442 +scipy/optimize/cython_optimize/__init__.py,sha256=eehEQNmLGy3e_XjNh6t5vQIC9l_OREeE4tYRRaFZdNs,4887 +scipy/optimize/cython_optimize/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/cython_optimize/_zeros.cpython-310-x86_64-linux-gnu.so,sha256=5okZmS_eJyVFGkImsyu0ZAEj7x--azO4uu63BoEHwv4,115552 +scipy/optimize/cython_optimize/_zeros.pxd,sha256=anyu-MgWhq24f1bywI4TlohvJjOnpNpkCtSzpKBJSSo,1239 +scipy/optimize/cython_optimize/c_zeros.pxd,sha256=6Gc0l1q-1nlCO9uKrYeXFiHsbimRZzU3t6EoTa8MVvA,1118 +scipy/optimize/elementwise.py,sha256=8eEQW_PeNkr49YBTROr5xWDLgeJd7rxtdQk3tVuEECQ,1190 +scipy/optimize/lbfgsb.py,sha256=XT7kclUTtom8JASPYyAScx-5irlBd9s9yEnZzRwFqu8,601 +scipy/optimize/linesearch.py,sha256=w5OhOofynUbz7IzHAGEc6huLKV_rMR5eUq77VcskA9o,535 +scipy/optimize/minpack.py,sha256=2S9tkmBI670qqeDN7k_1-ZLYsFZV1yXaDMkrCvMETiQ,664 +scipy/optimize/minpack2.py,sha256=IPIduBcu0LRo75GJ9SiMa_GjfdKCOYzsWUs61_d1HR8,514 +scipy/optimize/moduleTNC.py,sha256=qTEQ4IWtv_LT6fH3-iYmYNwrtrjG1gS4KFbZ73iDcd0,507 +scipy/optimize/nonlin.py,sha256=uoKIYAdmhwNrC6zFbUIBCNdM1a59nn7hb5jxSOuK3rs,710 +scipy/optimize/optimize.py,sha256=SivH06ZYrbIwJLTQj3ZShU4FXft7w2y1a2uYE9ILIMo,877 +scipy/optimize/slsqp.py,sha256=K7nXxF99sjaI3_eoOm9w0VnrbaQXgnHlvvgs8lNa0zA,582 +scipy/optimize/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/optimize/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__basinhopping.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__differential_evolution.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__dual_annealing.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__linprog_clean_inputs.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__numdiff.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__remove_redundancy.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__root.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__shgo.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test__spectral.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_bracket.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_chandrupatla.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_cobyla.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_cobyqa.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_constraint_conversion.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_constraints.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_cython_optimize.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_differentiable_functions.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_direct.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_extending.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_hessian_update_strategy.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_isotonic_regression.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_lbfgsb_hessinv.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_lbfgsb_setulb.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_least_squares.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_linear_assignment.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_linesearch.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_linprog.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_lsq_common.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_lsq_linear.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_milp.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_minimize_constrained.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_minpack.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_nnls.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_nonlin.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_optimize.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_quadratic_assignment.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_regression.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_slsqp.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_tnc.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_trustregion.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_trustregion_exact.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_trustregion_krylov.cpython-310.pyc,, +scipy/optimize/tests/__pycache__/test_zeros.cpython-310.pyc,, +scipy/optimize/tests/_cython_examples/extending.pyx,sha256=5TCYF9hvIYu8S9Y7PIql-xdJfcn_LI50yDrf4uh7i2M,1314 +scipy/optimize/tests/_cython_examples/meson.build,sha256=GCeweHtWXjvk73tZN3HqsMTw7F1St0JuIhGyxmEiPv0,703 +scipy/optimize/tests/test__basinhopping.py,sha256=t2JHeg0qy4gUbKuPog9BcwgYyvwcPoh0zbrThoasWnI,19210 +scipy/optimize/tests/test__differential_evolution.py,sha256=yUs5lEXkvpv-s-r7EDBNaPorE56xKcgBwKgXtbEoASQ,69522 +scipy/optimize/tests/test__dual_annealing.py,sha256=8qzPbCQwqmNRJ2GYk1X02qNvmF3TAgJxzUG_x0c07o4,16640 +scipy/optimize/tests/test__linprog_clean_inputs.py,sha256=9HFrqlU1OHGTHCgy_R9w2rJ5A5xlu_3QpGbnzQezqXM,11678 +scipy/optimize/tests/test__numdiff.py,sha256=QEkhiCcGHO2CJLaJHXcq4ILDedtIpleEs3AQdQ-ME5Y,32359 +scipy/optimize/tests/test__remove_redundancy.py,sha256=gwakPkJo8Y8aRL4son1bp8USfwc9uMrLLnZFrDmfvxY,6799 +scipy/optimize/tests/test__root.py,sha256=yBSibeODBJwOqjTJHWXP9qWqh_D9XBnMjn5hFuTVQpo,4230 +scipy/optimize/tests/test__shgo.py,sha256=Bi_0KCdDhnWUbh9KWwGoLkV4BTJ6Fh0FT8mQU41IUa8,39804 +scipy/optimize/tests/test__spectral.py,sha256=xh-4SMIAWkx_ND2nt7rGACy3ckfw_votfyfxMpQ8m2I,6664 +scipy/optimize/tests/test_bracket.py,sha256=V-f_GEBCqwNOjFoqKcTg5OhglGvKHMqDqZjthwR5VwM,37043 +scipy/optimize/tests/test_chandrupatla.py,sha256=zX1XDkfp11bB_krw0mKGb0_XgXjhNdIltpiFhuGKmMc,39020 +scipy/optimize/tests/test_cobyla.py,sha256=UXlHcEYwaJNWVtAr60t2UpGA9TdpPyTud_tx13LmIuI,5272 +scipy/optimize/tests/test_cobyqa.py,sha256=5sHRoBc4ZVfjZZAYMGObwSAtWq2A53L9KSwHuUUhQLk,8143 +scipy/optimize/tests/test_constraint_conversion.py,sha256=7uRZeOxVD6KFbyVi6h-PSts3BxBPFiFZPVczhiVd5b4,12563 +scipy/optimize/tests/test_constraints.py,sha256=03SN10ubXpgrNq9Z4DEpPSC6hTXznW-YUF-nxdaxSQ4,9408 +scipy/optimize/tests/test_cython_optimize.py,sha256=n-HccBWoUmmBWq_OsNrAVnt4QrdssIYm4PWG29Ocias,2638 +scipy/optimize/tests/test_differentiable_functions.py,sha256=Dh3JD1bbmhEgAA1w7tfQFV7HpkBahHHQYsMZII58DFg,28489 +scipy/optimize/tests/test_direct.py,sha256=_R4_VkYkIJcS7X9a7n9rxwnZClK5i9nXSiYYkX0aRiA,13267 +scipy/optimize/tests/test_extending.py,sha256=r9Phn1PUn0U3U6QJeMiPreKG6jKmnWFqwpf1Al7w7K0,1104 +scipy/optimize/tests/test_hessian_update_strategy.py,sha256=EiL5ImqkGFmUTjgZjv0FGpGBjTzWXqT3w6eCrzQtPmo,14337 +scipy/optimize/tests/test_isotonic_regression.py,sha256=aJakW5zYcILN3wa--CYFBoZ3MB6n5Rzwd4WfNs_SFQk,7113 +scipy/optimize/tests/test_lbfgsb_hessinv.py,sha256=rpJbiCUfgJrjp-xVe4JiXjVNe6-l8-s8uPqzKROgmJQ,1137 +scipy/optimize/tests/test_lbfgsb_setulb.py,sha256=6Aqn26aKUJp75unFqCAzesLq_tWPsQpp2rCftauSOS8,3582 +scipy/optimize/tests/test_least_squares.py,sha256=MG9-lpqEQHJBH9eoRgRjWFCp2gwGRdSfRTirV53Q3cY,34021 +scipy/optimize/tests/test_linear_assignment.py,sha256=84d4YHCf9RzjYDKUujQe2GbudkP8dtlSpZtMBwCf_Oc,4085 +scipy/optimize/tests/test_linesearch.py,sha256=xmK2zvgIbLMOWkb2B1ALBWiPHQyGGxzDG0MXaHjNlqA,11400 +scipy/optimize/tests/test_linprog.py,sha256=8yqKv4Gx7mwlnLGOhNwpDwCMuhpQurJ6CA1jONNeeX8,102678 +scipy/optimize/tests/test_lsq_common.py,sha256=alCLPPQB4mrxLIAo_rn7eg9xrCEH7DerNBozSimOQRA,9500 +scipy/optimize/tests/test_lsq_linear.py,sha256=5bVPsp26HdqQ9kF4CdkQEyrm8yjjLX1LB22nV83Muhk,10959 +scipy/optimize/tests/test_milp.py,sha256=V4KeW9Z3CfCvCk_NT88yqvw9E_t2r-aIq-yJFwVIaWY,18302 +scipy/optimize/tests/test_minimize_constrained.py,sha256=ulswdUxITmCsav69ghAI3SysmD1WnFYja3JFHVk_bYk,27942 +scipy/optimize/tests/test_minpack.py,sha256=sOCIbIGKursdT4EBc5T6U7LT7JevCsIIWK39PWOOAb8,44841 +scipy/optimize/tests/test_nnls.py,sha256=jr0xf8WA-tis91BC0kAKmKl3RiBFTr4deWat4d_iwAI,25763 +scipy/optimize/tests/test_nonlin.py,sha256=N5iZpgXu0Q7aNkznOtEGC28POBVJKniiGMgMDA2M_JM,18559 +scipy/optimize/tests/test_optimize.py,sha256=RdQOf5np2uLgZ5WnN-Ay5YOOtWfU_Mdcptur86xH3pU,127471 +scipy/optimize/tests/test_quadratic_assignment.py,sha256=4BKOjpEPgSi0YATody23JUjzZ749rh-F7sMWlpuvy4g,17598 +scipy/optimize/tests/test_regression.py,sha256=CSg8X-hq6-6jW8vki6aVfEFYRUGTWOg58silM1XNXbU,1077 +scipy/optimize/tests/test_slsqp.py,sha256=GZn35XMVZQ1ouzdgKseNRI9ruWP4vr1HOcLGK3a8g4E,23518 +scipy/optimize/tests/test_tnc.py,sha256=ahSwu8F1tUcPV09l1MsbacUXXi1avQHzQNniYhZRf4s,12700 +scipy/optimize/tests/test_trustregion.py,sha256=y49k3H03wdf21FFrUBJpJP7-sqvbxRdvk63cMHkKO3Y,4669 +scipy/optimize/tests/test_trustregion_exact.py,sha256=pPY_GRZZ0dwXqUboObatYMpRuwVSwRScCfuu4WkuSbw,12933 +scipy/optimize/tests/test_trustregion_krylov.py,sha256=otFMoHYcJZzPdyv7UKOgerehGJXpOB8YWP0-lYHYhUk,6616 +scipy/optimize/tests/test_zeros.py,sha256=jLxGJNc7N8qPbTpRtf23ZeRrg6lzlW53slD8yA6al9s,36760 +scipy/optimize/tnc.py,sha256=aEKhka8wryg4mVlbrGFwzTJF_KYB49joMkSxKgh1KnA,560 +scipy/optimize/zeros.py,sha256=Sc06-J8JUazdfR36UamHhPndJoPK0FkOzHR-unHWoBw,620 +scipy/signal/__init__.py,sha256=tcYF8m39SxVh_JUIRVh8BdupHM3Gz8V6aJ_Y1Xorptg,13479 +scipy/signal/__pycache__/__init__.cpython-310.pyc,, +scipy/signal/__pycache__/_arraytools.cpython-310.pyc,, +scipy/signal/__pycache__/_czt.cpython-310.pyc,, +scipy/signal/__pycache__/_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/_fir_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/_lti_conversion.cpython-310.pyc,, +scipy/signal/__pycache__/_ltisys.cpython-310.pyc,, +scipy/signal/__pycache__/_max_len_seq.cpython-310.pyc,, +scipy/signal/__pycache__/_peak_finding.cpython-310.pyc,, +scipy/signal/__pycache__/_savitzky_golay.cpython-310.pyc,, +scipy/signal/__pycache__/_short_time_fft.cpython-310.pyc,, +scipy/signal/__pycache__/_signaltools.cpython-310.pyc,, +scipy/signal/__pycache__/_spectral_py.cpython-310.pyc,, +scipy/signal/__pycache__/_spline_filters.cpython-310.pyc,, +scipy/signal/__pycache__/_upfirdn.cpython-310.pyc,, +scipy/signal/__pycache__/_waveforms.cpython-310.pyc,, +scipy/signal/__pycache__/_wavelets.cpython-310.pyc,, +scipy/signal/__pycache__/bsplines.cpython-310.pyc,, +scipy/signal/__pycache__/filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/fir_filter_design.cpython-310.pyc,, +scipy/signal/__pycache__/lti_conversion.cpython-310.pyc,, +scipy/signal/__pycache__/ltisys.cpython-310.pyc,, +scipy/signal/__pycache__/signaltools.cpython-310.pyc,, +scipy/signal/__pycache__/spectral.cpython-310.pyc,, +scipy/signal/__pycache__/spline.cpython-310.pyc,, +scipy/signal/__pycache__/waveforms.cpython-310.pyc,, +scipy/signal/__pycache__/wavelets.cpython-310.pyc,, +scipy/signal/_arraytools.py,sha256=k3kHbl9RzcqsyftIYSFJZvJFL4zlcMAHyaRFUkFxOXY,8294 +scipy/signal/_czt.py,sha256=t5P1kRCM3iw3eCaL9hTgctMfQKezkqnjbghLjCkffQE,19445 +scipy/signal/_filter_design.py,sha256=4k8U0EV4ySo5c5NsvLkleFftDomEBRdl7gg1qdGBn4s,187997 +scipy/signal/_fir_filter_design.py,sha256=LEazCRvJAG9fyirZDqEnrgUpyv3ukl0r_SAOlUNQQw4,49741 +scipy/signal/_lti_conversion.py,sha256=ZLlxEy1TrxvSXvAeDDSxgvKHHv5_lXxxJUjwIgbfpQE,16057 +scipy/signal/_ltisys.py,sha256=sOxEME3e4217x6gFg7anY08p4CWoTS0jm6Np9IpsTM4,118051 +scipy/signal/_max_len_seq.py,sha256=8QkMWoYY3qy3bCKfsuXaS93Bnb2zd-ue6j5i5-3_hi0,5060 +scipy/signal/_max_len_seq_inner.cpython-310-x86_64-linux-gnu.so,sha256=y_rqTps6JMSgWUvo4jzcC7B2QWPEdW7-ZZJQ8ki4JE8,77496 +scipy/signal/_peak_finding.py,sha256=e9vpWL98OQ9Ik1f7gwLl4d5feTAiyLwPm_yarJq3T_8,48856 +scipy/signal/_peak_finding_utils.cpython-310-x86_64-linux-gnu.so,sha256=8eQ6z5f_v3WZjbZ8j14f2b4fBeBEXns6o_kcQsFPsnQ,305816 +scipy/signal/_savitzky_golay.py,sha256=AahANBsLy8d6FKmVgteGiAw1l_4wWWItZYSyOVnj_nk,13447 +scipy/signal/_short_time_fft.py,sha256=7rAcfvEUEoaS_KUZ8d7Esh7f--emMS3K9KB5HheJF1o,76306 +scipy/signal/_signaltools.py,sha256=HioDs7paXI1cUu9gWsRQ6ZkL6h_x28q4NS3WJ4OANvY,176450 +scipy/signal/_sigtools.cpython-310-x86_64-linux-gnu.so,sha256=z7jl1gVbI2P1K-KkAK8yGyNiH4t5u7yrgMcwePUHEXQ,99576 +scipy/signal/_sosfilt.cpython-310-x86_64-linux-gnu.so,sha256=p1LbI8WWFaDchArtZEH-7ECuofkiRAnqiPkEhoDBIMw,303408 +scipy/signal/_spectral_py.py,sha256=h0BILp8mj4Txrj7aNC3GWNviR8oKpxTNBHd-vgoGCqM,86897 +scipy/signal/_spline.cpython-310-x86_64-linux-gnu.so,sha256=M5hEE6CwKVJlJhuh-yXHXZn40DtRw6Ehs3GnuAb7pk4,55864 +scipy/signal/_spline.pyi,sha256=9tWZQCI7D84ONLwICZG6psBGtwKxAvLF7JaZ1tQUKoY,948 +scipy/signal/_spline_filters.py,sha256=t1HWc3YEhDu6AtXo8z1CLTkFYpcbYvpOIRIMPiRMEGM,24487 +scipy/signal/_upfirdn.py,sha256=ODSw2x1KHXN0vdKHm4vnovZxkoafcwIdUek0N8Edu5g,7882 +scipy/signal/_upfirdn_apply.cpython-310-x86_64-linux-gnu.so,sha256=n6gCvUZWSEbuHSLl7nr6VdpB13S1XOPYV1kXH4128r0,395696 +scipy/signal/_waveforms.py,sha256=Ca551WqyDWTrQrQ4hOwHl2dpHS1FSWg_SKyz1XObQrU,23089 +scipy/signal/_wavelets.py,sha256=QTjAp83C2V2sxIkUsITWLw3ceIRkmBJ5CYtwW_3szCU,873 +scipy/signal/bsplines.py,sha256=G1sa6en1z_41sU7ckRY8-flJjUKSqJJihaxBlwzUd3s,651 +scipy/signal/filter_design.py,sha256=EyHs8OX4mdeUi6e3Zf7IWuz6r5Re2eR_t0Bi10JuntM,1112 +scipy/signal/fir_filter_design.py,sha256=0BxZF7tqewVQ4J1u-Ls-DZfC25rIcizwr9v6WaxkS0k,640 +scipy/signal/lti_conversion.py,sha256=6uQ1qaT7XI75DoFmtRqRS94Hkpm-Qvy66CRNhmQ-Lbw,639 +scipy/signal/ltisys.py,sha256=TFul9jyL0ujEIchiOnDdIiJKIXZ8SSgOV066DvmX_QA,869 +scipy/signal/signaltools.py,sha256=I7U_hMuMf02zpdNi0LcPogucTDf0nUVUSkMZ1eAoq3E,1038 +scipy/signal/spectral.py,sha256=RA3jj6AWV6ptNwXfpVrbuyxxed8P7nWw8bLsD0iZIgw,662 +scipy/signal/spline.py,sha256=S54RVqPeA7nnGzLgICi-2rl3Ei3roPaDAJ6ihTeZSwk,747 +scipy/signal/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/signal/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/signal/tests/__pycache__/_scipy_spectral_test_shim.cpython-310.pyc,, +scipy/signal/tests/__pycache__/mpsig.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_array_tools.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_bsplines.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_cont2discrete.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_czt.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_dltisys.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_filter_design.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_fir_filter_design.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_ltisys.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_max_len_seq.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_peak_finding.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_result_type.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_savitzky_golay.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_short_time_fft.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_signaltools.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_spectral.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_splines.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_upfirdn.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_waveforms.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_wavelets.cpython-310.pyc,, +scipy/signal/tests/__pycache__/test_windows.cpython-310.pyc,, +scipy/signal/tests/_scipy_spectral_test_shim.py,sha256=WalP9LfyXaXTQUW3mG8yhA3wJQ8E6edOjUsH4kSC2us,19640 +scipy/signal/tests/mpsig.py,sha256=DHB3eHB0KYA-E0SBebKG36YLk-T5egbwwryne3RwIHM,3308 +scipy/signal/tests/test_array_tools.py,sha256=QN4SGbtxDSP2MFvyYl00RasYYyNF4A1g8Y6_1Sij7YQ,3589 +scipy/signal/tests/test_bsplines.py,sha256=_BZQE4CMyBfe0xG5QlWM8ckD5LNUADTY6CsGW1_0nxo,15926 +scipy/signal/tests/test_cont2discrete.py,sha256=0GgOVxKDnQRSN935P5N5A7qu3bm4iyp0Iz7qMs6vxTY,14672 +scipy/signal/tests/test_czt.py,sha256=2-kcWyadICVl_mF0vbq1KYii-rYMtZiuiOSb6HkYn7w,7156 +scipy/signal/tests/test_dltisys.py,sha256=WEs5DsDSKQDm4H7deYr6lCUvm8TkiFd9S4SJIluRWfg,21483 +scipy/signal/tests/test_filter_design.py,sha256=E_N744-VArOKv_gm4b6rdUmY5G4KB8jFPyVi7qDKhA8,198209 +scipy/signal/tests/test_fir_filter_design.py,sha256=BTl7u38PhxS-j3DZwZ0hv7c_LsUKPfNuN8-KlPgV_yc,27732 +scipy/signal/tests/test_ltisys.py,sha256=wU2ZC7E-lKDQ23_1Uvbem3PA_oNayRvzyccIaUqJbnc,45070 +scipy/signal/tests/test_max_len_seq.py,sha256=JzfWWN4n6FO9Axw6H6xWrWyc21LlkqMwkGl23f-V664,3318 +scipy/signal/tests/test_peak_finding.py,sha256=ZSybjXxgtO3Go-l9S8d3NMdCR_wgKMllEivr8NDjyRo,36076 +scipy/signal/tests/test_result_type.py,sha256=F48EQGbFfQfMwcnt-sMofHGNHVTbHntbMlgoeS2vYcY,1573 +scipy/signal/tests/test_savitzky_golay.py,sha256=Tq17JiZJu2_nL9Q2T-7jql_MuDinKeAKqvtTiqBx87U,12503 +scipy/signal/tests/test_short_time_fft.py,sha256=SmRhz7J_mwT0ophsVZxWgfm9fYmRr_4cRaXpRJZixC4,36362 +scipy/signal/tests/test_signaltools.py,sha256=F535YvwUJtCzLe4j7FpztU0zsVSP-rnoeAU0zvid0Es,153468 +scipy/signal/tests/test_spectral.py,sha256=W-x8s27sIrMd5jdLlUI1WfqGauYpeZSzWJGtV1ty_wY,78699 +scipy/signal/tests/test_splines.py,sha256=mSCnwez3Qj3RBRYmyIBX7KGOf-tItiz0pU29GaVTsOA,14705 +scipy/signal/tests/test_upfirdn.py,sha256=B90gfpfFCe4EqsGm9hViKM2NtneNYfsxZR2PG8johHo,11323 +scipy/signal/tests/test_waveforms.py,sha256=XEQVDE7FRDH-wPOyBv7LQhSbmvXR45gnBNbpWr0925I,12985 +scipy/signal/tests/test_wavelets.py,sha256=42yMux80J-K7Ue9QLnzN84U9K3j2GRdywMxGpbLldeM,2145 +scipy/signal/tests/test_windows.py,sha256=7KGQsexeNiI50RFjFnw4kr1tqigP-WFoGLFHK1Ygt5o,40990 +scipy/signal/waveforms.py,sha256=jfOXW7kgtGdh1nrMo1YLAh79W_Ln3WgzEN2esrp70wE,599 +scipy/signal/wavelets.py,sha256=7pA7HVMiXwG4fZZ0Q4nzz47hWWALMTYtxwGrIqV3bNE,510 +scipy/signal/windows/__init__.py,sha256=BUSXzc_D5Agp59RacDdG6EE9QjkXXtlcfQrTop_IJwo,2119 +scipy/signal/windows/__pycache__/__init__.cpython-310.pyc,, +scipy/signal/windows/__pycache__/_windows.cpython-310.pyc,, +scipy/signal/windows/__pycache__/windows.cpython-310.pyc,, +scipy/signal/windows/_windows.py,sha256=Scga4uJiDNUrH-p3ddILShNzXPmSOaA0Zvc6GOVyy6w,83594 +scipy/signal/windows/windows.py,sha256=FI6w8mt0V1221Rqv3Do3LuWRWrtKo3hYYTvpB_5UB1c,839 +scipy/sparse/__init__.py,sha256=OShVd94qpqQr4HMNPAvbMRQKf0Z6cL7bfRSbxcx99YQ,9361 +scipy/sparse/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/__pycache__/_base.cpython-310.pyc,, +scipy/sparse/__pycache__/_bsr.cpython-310.pyc,, +scipy/sparse/__pycache__/_compressed.cpython-310.pyc,, +scipy/sparse/__pycache__/_construct.cpython-310.pyc,, +scipy/sparse/__pycache__/_coo.cpython-310.pyc,, +scipy/sparse/__pycache__/_csc.cpython-310.pyc,, +scipy/sparse/__pycache__/_csr.cpython-310.pyc,, +scipy/sparse/__pycache__/_data.cpython-310.pyc,, +scipy/sparse/__pycache__/_dia.cpython-310.pyc,, +scipy/sparse/__pycache__/_dok.cpython-310.pyc,, +scipy/sparse/__pycache__/_extract.cpython-310.pyc,, +scipy/sparse/__pycache__/_index.cpython-310.pyc,, +scipy/sparse/__pycache__/_lil.cpython-310.pyc,, +scipy/sparse/__pycache__/_matrix.cpython-310.pyc,, +scipy/sparse/__pycache__/_matrix_io.cpython-310.pyc,, +scipy/sparse/__pycache__/_spfuncs.cpython-310.pyc,, +scipy/sparse/__pycache__/_sputils.cpython-310.pyc,, +scipy/sparse/__pycache__/base.cpython-310.pyc,, +scipy/sparse/__pycache__/bsr.cpython-310.pyc,, +scipy/sparse/__pycache__/compressed.cpython-310.pyc,, +scipy/sparse/__pycache__/construct.cpython-310.pyc,, +scipy/sparse/__pycache__/coo.cpython-310.pyc,, +scipy/sparse/__pycache__/csc.cpython-310.pyc,, +scipy/sparse/__pycache__/csr.cpython-310.pyc,, +scipy/sparse/__pycache__/data.cpython-310.pyc,, +scipy/sparse/__pycache__/dia.cpython-310.pyc,, +scipy/sparse/__pycache__/dok.cpython-310.pyc,, +scipy/sparse/__pycache__/extract.cpython-310.pyc,, +scipy/sparse/__pycache__/lil.cpython-310.pyc,, +scipy/sparse/__pycache__/sparsetools.cpython-310.pyc,, +scipy/sparse/__pycache__/spfuncs.cpython-310.pyc,, +scipy/sparse/__pycache__/sputils.cpython-310.pyc,, +scipy/sparse/_base.py,sha256=zVk_n3nwri0SW2dC4SPiq2TXE9EstsFZsIkTpV4uQeU,48805 +scipy/sparse/_bsr.py,sha256=7qZwcg8KeP-E-zYJn8uTcd9UqjP2NyyQ0CaqPcieWQA,30934 +scipy/sparse/_compressed.py,sha256=7fPtGYHLQ8hWbrSqqsaEXAf4DdK9IhBJibI6_ouE-gU,58863 +scipy/sparse/_construct.py,sha256=d044HGf_0-UqzsmifsAKCw2bPbQLTD1-vIFJOhxbTks,47960 +scipy/sparse/_coo.py,sha256=Oiyq04Pe0CPnEvYK-6Mtdo7XuQT8b1mkL7dx6Mze3To,64224 +scipy/sparse/_csc.py,sha256=KKVzIuWFCRlWGNCQMZpZp-_es0RefHimb-DW2AhNj6U,11142 +scipy/sparse/_csparsetools.cpython-310-x86_64-linux-gnu.so,sha256=gJv-44-C_mMZzVtXqOrEjhhyviQsNpdvQOaNSOoCypg,839504 +scipy/sparse/_csr.py,sha256=HbHai24yw-JPg9PZrgcFLEdfqQfj1BjmvNF_01qj-os,18156 +scipy/sparse/_data.py,sha256=NpxPIjJbmJDM_3AbRndYN55ffhz4j2aYV2ABgL3yM0c,20488 +scipy/sparse/_dia.py,sha256=2L51l7l9BKKVGtLvHvYz3u0RPJOcXhZ2L0-aV1FQaFM,20067 +scipy/sparse/_dok.py,sha256=tbmVoRu0-ECKB12hXW61qU82-kA6rcQhYQRJ3zzqoU4,23011 +scipy/sparse/_extract.py,sha256=0NWW00hxjk5gl4CjNRHtvcqsx54yNei2VVbqARMOlAo,5058 +scipy/sparse/_index.py,sha256=Mu4nOO8s0bq0O0l7NXUBuNMhdaal9tXYcxlRzqotYb0,16376 +scipy/sparse/_lil.py,sha256=uS3i5M_yhLjTDk9xySG_4COGgJA2QcwIpKphuwhcCV4,21125 +scipy/sparse/_matrix.py,sha256=-iZoYGC2dchFI3QKhmOpOCZgousk6vTO95jKgNDorg4,4427 +scipy/sparse/_matrix_io.py,sha256=0ZEoczSQq59zOGd_eWk6sfACt62vdQmth3ia7uqWFTM,5960 +scipy/sparse/_sparsetools.cpython-310-x86_64-linux-gnu.so,sha256=3eTvyXYx_7OzSCYKYrVgHkjFYlfMO2pDhBxcpdePi9c,4560912 +scipy/sparse/_spfuncs.py,sha256=lDVTp6CiQIuMxTfSzOi3-k6p97ayXJxdKPTf7j_4GWc,1987 +scipy/sparse/_sputils.py,sha256=xTe_MUII85GErqsA-DbOMdUQ1UFuOWxyyWB82xS_rBg,20750 +scipy/sparse/base.py,sha256=8Yx-QLKSRu9LJjgG-y8VqsRnsjImB2iKoJFxTgKGFsI,791 +scipy/sparse/bsr.py,sha256=CsYirxoLqHwBiEyNbOgGdZMx4Lt3adKZ-7uVv1gpzCY,811 +scipy/sparse/compressed.py,sha256=rbaz4AoTJvNnfnwEx4ocDXlkHJPOxe9DzqxCcJoHY2g,1009 +scipy/sparse/construct.py,sha256=i9lHBSRsDkvoNCbF9b7mZ0C2fHCjKU5CKCE30c-CxMc,925 +scipy/sparse/coo.py,sha256=VRF6kaYsVtyprwYrEuy1gRcCU5G7xsKyY0L1zJ_9JiQ,844 +scipy/sparse/csc.py,sha256=EV_LxYjPiRsTV6-J8kUefNna-R0tdI5uBt9Fj_XWlwc,609 +scipy/sparse/csgraph/__init__.py,sha256=znrEd48JFLdlcevl8IFDSM104Yl1YvXC0O_f8OUWATs,7842 +scipy/sparse/csgraph/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/csgraph/__pycache__/_laplacian.cpython-310.pyc,, +scipy/sparse/csgraph/__pycache__/_validation.cpython-310.pyc,, +scipy/sparse/csgraph/_flow.cpython-310-x86_64-linux-gnu.so,sha256=Qjnelnc6N-KHT9OL7K_ugZCXAuWPBuuckypoPUemlGo,354320 +scipy/sparse/csgraph/_laplacian.py,sha256=bpCduRWjIhcDpclvPbftx74PExTiW0P3EE6_Ztiop1Y,18273 +scipy/sparse/csgraph/_matching.cpython-310-x86_64-linux-gnu.so,sha256=E9ANn8NsiYuKBW6i_FNwqxDtxfGIKK7u4qJItyitT0s,357288 +scipy/sparse/csgraph/_min_spanning_tree.cpython-310-x86_64-linux-gnu.so,sha256=vhhU2Xq94ok9mMgbXVvc5RCxJDb2YC_oAoEo9oj0JvE,268568 +scipy/sparse/csgraph/_reordering.cpython-310-x86_64-linux-gnu.so,sha256=c_tqMa2-ngtNY88Jgfv7x85TsDB1HPR9Ejw0-fk214c,331928 +scipy/sparse/csgraph/_shortest_path.cpython-310-x86_64-linux-gnu.so,sha256=ntS0Nz3OY26tAnrR20U7pHiNTbDmq3848Ymz6r5oELw,576328 +scipy/sparse/csgraph/_tools.cpython-310-x86_64-linux-gnu.so,sha256=sxsvyTyWiKayt6NQcyXCBjX4K4TMvowtv39HTd8PA2I,218744 +scipy/sparse/csgraph/_traversal.cpython-310-x86_64-linux-gnu.so,sha256=I992Lwpsbg_vHk-_97SOcW19qRLbNl3cqTxek6ou4vg,658864 +scipy/sparse/csgraph/_validation.py,sha256=SxINtd4jYyH0YWdzspr8JR0syZfO3nMj7C60GWBUr6k,2629 +scipy/sparse/csgraph/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/csgraph/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_connected_components.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_conversions.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_flow.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_graph_laplacian.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_matching.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_pydata_sparse.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_reordering.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_shortest_path.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_spanning_tree.cpython-310.pyc,, +scipy/sparse/csgraph/tests/__pycache__/test_traversal.cpython-310.pyc,, +scipy/sparse/csgraph/tests/test_connected_components.py,sha256=a2HZjm7HsC0STqiDnhN6OJL4yIMcM28VNVtMXDI2BqE,3948 +scipy/sparse/csgraph/tests/test_conversions.py,sha256=3n2UJ_rwdcTkD8NfwDrk-8UBplJkqMFw12yPIwX9-R8,1854 +scipy/sparse/csgraph/tests/test_flow.py,sha256=I7csygtef5f6Uv67t2y3UZsln8Gg4eS1RE5zr7Xm-Eg,7718 +scipy/sparse/csgraph/tests/test_graph_laplacian.py,sha256=9nQDRj5_oVK0CXM-DW2Xb2jofW3YCiI0QBezdBUl_60,10936 +scipy/sparse/csgraph/tests/test_matching.py,sha256=wX0Pml9DHokv5_ve0L0t6Rse-JsBWU6Jr6LZ1I8HmTE,11871 +scipy/sparse/csgraph/tests/test_pydata_sparse.py,sha256=DThJQ9OwZMvTQnoPKfGZ5sCdXtBWLqfMFFeuHGOuiOs,4869 +scipy/sparse/csgraph/tests/test_reordering.py,sha256=_WNqdGcU-WNhQRpjCq4Nhp8YY6cmVKb13au5sJPpzig,2569 +scipy/sparse/csgraph/tests/test_shortest_path.py,sha256=OP4td7B9TLM79zTPQAi5LLLGvW81D1iNuR27HOlZcA8,16575 +scipy/sparse/csgraph/tests/test_spanning_tree.py,sha256=q4LYiXxfwWUc1io4vRVBr9uxMacfdefPvcRlb3TOEnw,2164 +scipy/sparse/csgraph/tests/test_traversal.py,sha256=PD1EJ8XD3xyCWU7SF9-Qw-skhEAI3tiNDxrabsXgU2I,6149 +scipy/sparse/csr.py,sha256=9UrWUoq5-hSl9bcaVeWxN4tmPJisTQ_6JiISCyrlMCw,658 +scipy/sparse/data.py,sha256=qGDAuAvTASgQ7wXXZ9t2JPp0rNBNVxObTTzXNHDRSEo,573 +scipy/sparse/dia.py,sha256=0y5_QfvVeU5doVbngvf8G36qVGU-FlnUxRChQ43e1aU,689 +scipy/sparse/dok.py,sha256=LMnaLFd266EZ3p4D1ZgOICGRZkY6s7YM0Wvlr6ylRn0,733 +scipy/sparse/extract.py,sha256=6qT2PNOilsEhDWl6MhmgpveIuQr4QCs3LATwIrBroOQ,567 +scipy/sparse/lil.py,sha256=Gve3XHYPYZavcUPJz1TSOhjv6AtPpkKBHTzCK6FG8ek,562 +scipy/sparse/linalg/__init__.py,sha256=KL54k4eDwEf7mHbL21uZe87S2rnSPIFcEI-pT3UpLIw,4111 +scipy/sparse/linalg/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_expm_multiply.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_interface.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_matfuncs.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_norm.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_onenormest.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_special_sparse_arrays.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/_svdp.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/dsolve.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/eigen.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/interface.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/isolve.cpython-310.pyc,, +scipy/sparse/linalg/__pycache__/matfuncs.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__init__.py,sha256=PIX7n_d0LOMZZZ65Dz4Mgz9trjKGB2kLaF16PQLkAIs,2039 +scipy/sparse/linalg/_dsolve/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/__pycache__/linsolve.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/_add_newdocs.py,sha256=4Nm6RAKQlKI4lQt4z20v0D6m0Vk8eqp0mIzEk5gfztA,3743 +scipy/sparse/linalg/_dsolve/_superlu.cpython-310-x86_64-linux-gnu.so,sha256=Q1g3TFmUJ7CHRiQcYpSjNmHa8PxcQhpsr7adDEt7M1Q,811113 +scipy/sparse/linalg/_dsolve/linsolve.py,sha256=F-KfpTKnlUl-ZXoDPnQ_2jY9NmsgByAiDsMaPHnHRFg,30697 +scipy/sparse/linalg/_dsolve/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/_dsolve/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/tests/__pycache__/test_linsolve.cpython-310.pyc,, +scipy/sparse/linalg/_dsolve/tests/test_linsolve.py,sha256=CDsPCMpry6XBFOqMcRFUiY6QkkzOdKl7avm6enGrHgc,32893 +scipy/sparse/linalg/_eigen/__init__.py,sha256=SwNho3iWZu_lJvcdSomA5cQdcDU8gocKbmRnm6Bf9-0,460 +scipy/sparse/linalg/_eigen/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/__pycache__/_svds.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/__pycache__/_svds_doc.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/_svds.py,sha256=niV8PR0Aonw85rbiSPpL-RswAn9TltpUwcni3Qu_kl8,19908 +scipy/sparse/linalg/_eigen/_svds_doc.py,sha256=0_sC8kKbu3b5BYpGl16sPLrZu6mDxiFhj8xkbG2w5-U,15003 +scipy/sparse/linalg/_eigen/arpack/COPYING,sha256=CSZWb59AYXjRIU-Mx5bhZrEhPdfAXgxbRhqLisnlC74,1892 +scipy/sparse/linalg/_eigen/arpack/__init__.py,sha256=zDxf9LokyPitn3_0d-PUXoBCh6tWK0eUSvsAj6nkXI0,562 +scipy/sparse/linalg/_eigen/arpack/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/__pycache__/arpack.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/_arpack.cpython-310-x86_64-linux-gnu.so,sha256=Jos5YvNDLyn3FK-6Z8LBQngnJgElme59UuLkTPDaqkA,877177 +scipy/sparse/linalg/_eigen/arpack/arpack.py,sha256=hajyf3Ri9i9GHrqsRMFiDkfQL4jB5ZYQTNLVT7ZY9CY,67169 +scipy/sparse/linalg/_eigen/arpack/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/_eigen/arpack/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/tests/__pycache__/test_arpack.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/arpack/tests/test_arpack.py,sha256=yiL2zpB7ti0rwEP5DYXRZD-7JE3m6Wer07MJ4O65e5s,23735 +scipy/sparse/linalg/_eigen/lobpcg/__init__.py,sha256=E5JEPRoVz-TaLrj_rPm5LP3jCwei4XD-RxbcxYwf5lM,420 +scipy/sparse/linalg/_eigen/lobpcg/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/__pycache__/lobpcg.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py,sha256=vMsZlXCgKzn8l0PzHQFFadrAGfG9Fp0aTxwihATTqKM,41951 +scipy/sparse/linalg/_eigen/lobpcg/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/_eigen/lobpcg/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/tests/__pycache__/test_lobpcg.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py,sha256=15uXmcxi0BwPYtuD5kaoddsLE9-bN7QvHJimqFGmtOE,27421 +scipy/sparse/linalg/_eigen/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/_eigen/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/tests/__pycache__/test_svds.cpython-310.pyc,, +scipy/sparse/linalg/_eigen/tests/test_svds.py,sha256=3rQz_qRbkEpu9tFNK98MfRDYMDVv5ZyPaALTzWhBW54,36794 +scipy/sparse/linalg/_expm_multiply.py,sha256=KOSuV2qF4OSKrLGSwUAFT1ibnv4bhU9JBFJkvy9AVXY,26491 +scipy/sparse/linalg/_interface.py,sha256=i59d_y4vcKyIRXBmqbjj0YUY30CNo27dQvkEZC9Q2UQ,29420 +scipy/sparse/linalg/_isolve/__init__.py,sha256=Z_eQUYbe6RWMSNi09T9TfPEWm8RsVxcIKYAlihM-U-c,479 +scipy/sparse/linalg/_isolve/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/_gcrotmk.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/iterative.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lgmres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lsmr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/lsqr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/minres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/tfqmr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/__pycache__/utils.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/_gcrotmk.py,sha256=du3X8-NokWITHFi6DbzYnMO4uba5uHSqWIZ4Zm_9HhA,15788 +scipy/sparse/linalg/_isolve/iterative.py,sha256=Vhk3_ozYf8Pscte_Vkl_u9AAlFyJxVNpe8jAqviHlF4,33861 +scipy/sparse/linalg/_isolve/lgmres.py,sha256=A-mgYLEvzt5n10yMDoo3ZPNweULpp52aVAMhpTrbOe0,8695 +scipy/sparse/linalg/_isolve/lsmr.py,sha256=8MRtv-FJa7nOHlJ7MZ4TsQiWAkZwntD0r55SOQuRqvI,15650 +scipy/sparse/linalg/_isolve/lsqr.py,sha256=Ca2SjyNwMFXSckUTW_LqYFkFc5CWOaZ1yiYB0tK2uB8,21322 +scipy/sparse/linalg/_isolve/minres.py,sha256=3heKvLLuULWhdCrhbhaanZvu5J6-EbQEtwOIzI6uEFs,10887 +scipy/sparse/linalg/_isolve/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/_isolve/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_gcrotmk.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_iterative.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_lgmres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_lsmr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_lsqr.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_minres.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/__pycache__/test_utils.cpython-310.pyc,, +scipy/sparse/linalg/_isolve/tests/test_gcrotmk.py,sha256=QiLhe-Z9KRv1TMfe5cbCLO9Nm4vhpNtJEXPChaP_4Lg,5861 +scipy/sparse/linalg/_isolve/tests/test_iterative.py,sha256=cDCvcVc_a3aPzDNWKX_3CHUADQ0SpAFeyNsejbQEdE8,26181 +scipy/sparse/linalg/_isolve/tests/test_lgmres.py,sha256=9J0oq4KEg4UkIOwPQnp7z7U9bJMpCV9NslHCDANCccI,7448 +scipy/sparse/linalg/_isolve/tests/test_lsmr.py,sha256=6D3aZELcgJrp3Qf_HisAIowcwxnCzAiCfTf77YNsbrg,6362 +scipy/sparse/linalg/_isolve/tests/test_lsqr.py,sha256=tYKtlTuXMYYHvfpmrhdCqlzk0BIyohl2b-4b0SA6nBg,3759 +scipy/sparse/linalg/_isolve/tests/test_minres.py,sha256=d_rLkqdObBDD4FBpTOYgzwysTqBtYjgV5v1IDLhyr-8,2434 +scipy/sparse/linalg/_isolve/tests/test_utils.py,sha256=VlmvctRaQtjuYvQuoe2t2ufib74Tua_7qsiVrs3j-p0,265 +scipy/sparse/linalg/_isolve/tfqmr.py,sha256=_Uyy3skUaIHpqBD18H-poX8Tot1IfqkMmnF6h0iU6TY,6240 +scipy/sparse/linalg/_isolve/utils.py,sha256=I-Fjco_b83YKUtZPVdobTjPyY41-2SHruVvKZVOIXaU,3598 +scipy/sparse/linalg/_matfuncs.py,sha256=JaiiIwtP6Uzk6Jal8D9Ep9jTCxSyJZIdKamfzJN8wlA,29338 +scipy/sparse/linalg/_norm.py,sha256=MizhY4JL8pqcuP2suUlP1hMkwL1fIoyYHkiaEKuKqTQ,6163 +scipy/sparse/linalg/_onenormest.py,sha256=BkWu89ffmifkBdLH--IQ7DiW0hvDkVEiudUx4HRVmcI,15480 +scipy/sparse/linalg/_propack/_cpropack.cpython-310-x86_64-linux-gnu.so,sha256=uqohzs8yZb81jZg6AG074r_HWAWSXMzIFCamcoankk4,566049 +scipy/sparse/linalg/_propack/_dpropack.cpython-310-x86_64-linux-gnu.so,sha256=2Q5LnSCbpO8VurKqYJrKaS8eGG1pkn3p7yhnwDtn4tc,533201 +scipy/sparse/linalg/_propack/_spropack.cpython-310-x86_64-linux-gnu.so,sha256=POE_iknO4juFIh1iAsBPKsx_Xr9BS4rttLcCpGHaGGo,533201 +scipy/sparse/linalg/_propack/_zpropack.cpython-310-x86_64-linux-gnu.so,sha256=-cnbmbD3RxVqa4ScEpdv_kD2oUIPNWZPE5ByPNfpznA,557857 +scipy/sparse/linalg/_special_sparse_arrays.py,sha256=e7Y4OOurKa3eMyOnWaN-e6YQOM17onTESURjDpWUYS4,34225 +scipy/sparse/linalg/_svdp.py,sha256=dUr5v53cR5S40r71QCAVy0qUdKMxOviaWAT0ptrcjTQ,11200 +scipy/sparse/linalg/dsolve.py,sha256=fvCzVUda-h-WzwGWDss4FVuv6TVE-OKHzARBlUCDIJw,654 +scipy/sparse/linalg/eigen.py,sha256=4BTo8Tc9SNQaruyrF4gRdFE5NstiA0XH9I44IyikZQ4,626 +scipy/sparse/linalg/interface.py,sha256=_KXBkGhZWvY_ZmGixqWMZe6J64bCPdjtrqr63HvicUI,573 +scipy/sparse/linalg/isolve.py,sha256=diSAxpbYg8PeH75QOEE-CREO8p39f4BZK2dGynJDKIc,649 +scipy/sparse/linalg/matfuncs.py,sha256=H2qJl4ZZqZ4bI-E9NCbu1oFfto0EdFxCTKTugMPHRHg,570 +scipy/sparse/linalg/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/linalg/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_expm_multiply.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_interface.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_matfuncs.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_norm.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_onenormest.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_propack.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_pydata_sparse.cpython-310.pyc,, +scipy/sparse/linalg/tests/__pycache__/test_special_sparse_arrays.cpython-310.pyc,, +scipy/sparse/linalg/tests/propack_test_data.npz,sha256=v-NNmpI1Pgj0APODcTblU6jpHUQRhpE9ObWb-KYnu6M,600350 +scipy/sparse/linalg/tests/test_expm_multiply.py,sha256=K7tSwySHF0sMxq06391fhzBwn-eRskwVn74QussqerE,14845 +scipy/sparse/linalg/tests/test_interface.py,sha256=q5rZwUzJBIwiW__n-IzztR6HkZEkv8oePzfG0f1j8K8,21086 +scipy/sparse/linalg/tests/test_matfuncs.py,sha256=TqDnJFHiKdiwXP0Gb6yaXNAeiReV6TdBe4wMQXmXTI4,21740 +scipy/sparse/linalg/tests/test_norm.py,sha256=dJp4VNGpnL5xET60-b1epJqIBZ4g-zDALZWS5Wg60cQ,6716 +scipy/sparse/linalg/tests/test_onenormest.py,sha256=Tzn0FcVcKmbjYoseUkkxjq4mCOhG2cPfDyo9fQCYVPI,9252 +scipy/sparse/linalg/tests/test_propack.py,sha256=--SIFSXDGzyBOTdGwOhgrYhSkbVy1RiyL_Dt_Yonp_4,5567 +scipy/sparse/linalg/tests/test_pydata_sparse.py,sha256=vXYRhevEwvMSmdwwn1hJfAuaKuS-taRx0BpcUzI6t8g,6809 +scipy/sparse/linalg/tests/test_special_sparse_arrays.py,sha256=2Z7r1LPx7QTekuXNTLcspGOdJ9riRwioGIpxzIa0Kh4,12854 +scipy/sparse/sparsetools.py,sha256=pCcuyQYvIahrvr43V398XHyqwcGtWCPLFH6n1uSYmB8,516 +scipy/sparse/spfuncs.py,sha256=TWpfkZk3JErNajVFUH5B85d3r6UuSv0Rsx0lMtUSac0,508 +scipy/sparse/sputils.py,sha256=PsqT7RUmiO8ph5jG8GHYmPbacDQFljjc0SL7RMxweJU,508 +scipy/sparse/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/sparse/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_arithmetic1d.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_array_api.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_base.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_common1d.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_construct.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_coo.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_csc.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_csr.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_dok.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_extract.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_indexing1d.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_matrix_io.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_minmax1d.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_sparsetools.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_spfuncs.cpython-310.pyc,, +scipy/sparse/tests/__pycache__/test_sputils.cpython-310.pyc,, +scipy/sparse/tests/data/csc_py2.npz,sha256=usJ_Gj6x_dEC2uObfdYc6D6C8JY4jjROFChQcZhNAfo,846 +scipy/sparse/tests/data/csc_py3.npz,sha256=axuEMVxwd0F-cgUS0IalpiF8KHW4GNJ3BK6bcjfGnf4,851 +scipy/sparse/tests/test_arithmetic1d.py,sha256=EHAimtdcEPpGpyCluJ8DC-WWbkFwlR3266vEVU1Vdss,11875 +scipy/sparse/tests/test_array_api.py,sha256=U8TBj4ZJ5Bc6sOsJ6Q8HgnGBhGJK-sLXS1QD_9pK-4c,14201 +scipy/sparse/tests/test_base.py,sha256=GzHpsVclH2ekn1GmboJq771n2akLou8680qLdRhoDZE,213738 +scipy/sparse/tests/test_common1d.py,sha256=ys6uPq1Uu5dluDBAcqROTOrFbgZVpV1G4MUU1QnEpb4,15504 +scipy/sparse/tests/test_construct.py,sha256=AayVXyTauNJPY4SpDLy9WNDs8k30J8aNz3-T05hwYfo,38433 +scipy/sparse/tests/test_coo.py,sha256=TPcHyD3b3qA37ZU94h5WLM2stFSfZ-8PoVqEZMcQoz8,29134 +scipy/sparse/tests/test_csc.py,sha256=rB2cBXznxPdQbMZpdQyQitUdCdEeO6bWt7tQ_LBGGDw,2958 +scipy/sparse/tests/test_csr.py,sha256=J8q7e22jt0mGv0OdhdRX5xxcAkVWRclHAOmWwWMeauA,7623 +scipy/sparse/tests/test_dok.py,sha256=25jxMgYsQ_q-aN5uDvALRX6PuV83LVktQeEF3gVINm8,5959 +scipy/sparse/tests/test_extract.py,sha256=4qUPrtCv9H7xd-c9Xs51seQCiIlK45n-9ZEVTDuPiv8,1685 +scipy/sparse/tests/test_indexing1d.py,sha256=r6G8k9GNGfMcVgDg13N2kvmaDkl9FL2CzYYfbLfKXQU,20754 +scipy/sparse/tests/test_matrix_io.py,sha256=sLyFQeZ8QpiSoTM1A735j-LK4K0MV-L7VnWtNaBJhw4,3305 +scipy/sparse/tests/test_minmax1d.py,sha256=UBeHcN4Pw_VAPXtgsyDev5pK3eXvisiiLjibeaiA8S0,4269 +scipy/sparse/tests/test_sparsetools.py,sha256=zKeUESux895mYLdhhW_uM5V1c-djdEKnZ-xURx5fNrw,10543 +scipy/sparse/tests/test_spfuncs.py,sha256=ECs34sgYYhTBWe4hIkx357obH2lLsnJWkh7TfacjThw,3258 +scipy/sparse/tests/test_sputils.py,sha256=fEPvwo6sjwZ9ytdnufFIUE-gEkIe10DbdsX51v3ljyo,15083 +scipy/spatial/__init__.py,sha256=-FVg_WjbK0J0U2kyei6Fz6NgqEso5cipWZ5gHnqjErs,3731 +scipy/spatial/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/__pycache__/_geometric_slerp.cpython-310.pyc,, +scipy/spatial/__pycache__/_kdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/_plotutils.cpython-310.pyc,, +scipy/spatial/__pycache__/_procrustes.cpython-310.pyc,, +scipy/spatial/__pycache__/_spherical_voronoi.cpython-310.pyc,, +scipy/spatial/__pycache__/ckdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/distance.cpython-310.pyc,, +scipy/spatial/__pycache__/kdtree.cpython-310.pyc,, +scipy/spatial/__pycache__/qhull.cpython-310.pyc,, +scipy/spatial/_ckdtree.cpython-310-x86_64-linux-gnu.so,sha256=vZT7vT2rapv5w-MAUr0j_OzZvJEB0x8skusrpMIRXfU,1023728 +scipy/spatial/_distance_pybind.cpython-310-x86_64-linux-gnu.so,sha256=I60VxWMIjhnCGN08uOTCJUjiBCJIC1CM7eiwrMp7wic,641424 +scipy/spatial/_distance_wrap.cpython-310-x86_64-linux-gnu.so,sha256=zFDOr_48j82zBmMQRWpMd2VfgJ-icgPe3VH8AS4igqU,113256 +scipy/spatial/_geometric_slerp.py,sha256=d3pavtaMuIIKjupWLwFLt7WrfqvtT18u7wcsBdnuOTs,7951 +scipy/spatial/_hausdorff.cpython-310-x86_64-linux-gnu.so,sha256=5EQATVqxmje9E00238hn2ekefZ2sF21BXyalFYT-ViQ,250248 +scipy/spatial/_kdtree.py,sha256=ImDiR14DOAhwK--x9VhMjAlH_uhumsKuMin1Np63O7Q,33479 +scipy/spatial/_plotutils.py,sha256=cp94kSvt1QzWV6YWjeTrLh0lbWoVQu_0-iagVpoIgMo,7557 +scipy/spatial/_procrustes.py,sha256=qvhHPHt_OIKo-ge_k19S4VWqbP6ZgMXLVnNey0JxTb8,4427 +scipy/spatial/_qhull.cpython-310-x86_64-linux-gnu.so,sha256=Tev5kfcsAb93_xxBNaH7XvwYx0NTjcZJdNObkc1bmzo,1189720 +scipy/spatial/_qhull.pyi,sha256=dmvze3QcaoA_Be6H8zswajVatOPwtJFIFxoZFE9qR-A,5969 +scipy/spatial/_spherical_voronoi.py,sha256=v1XkbWI7yoXQ6EJmJHs185vl0qHV8yfRrm3c_gBGyzg,13577 +scipy/spatial/_voronoi.cpython-310-x86_64-linux-gnu.so,sha256=IMu9RJX52Qr7G-vdRMxQ4zGcEGgSTjjeTiiBzCBvVj8,240968 +scipy/spatial/_voronoi.pyi,sha256=aAOiF4fvHz18hmuSjieKkRItssD443p2_w1ggXOIs1g,126 +scipy/spatial/ckdtree.py,sha256=0IssUT415ieBOJuvfZJxIra-TeYyd0KxDGLrXDZ_GGw,523 +scipy/spatial/distance.py,sha256=h_8YsmV28ycxIm3k9-o3EYeiHBrRc7uoUj5hMg_jC6s,98001 +scipy/spatial/distance.pyi,sha256=rVZpbHbTPWeqYN7aBSDBDIt3MLQWbUIYmgwzWJiODjE,5238 +scipy/spatial/kdtree.py,sha256=ZYJL8A_WpLyEH29aFQGLbxd9ttFdGBgdglbgAfsvhz8,636 +scipy/spatial/qhull.py,sha256=aFE-KscuINt6QIhFC2dqhwFCYu3HSBkVXDH5exHH71s,622 +scipy/spatial/qhull_src/COPYING.txt,sha256=NNsMDE-TGGHXIFVcnNei4ijRKQuimvDy7oDEG7IDivs,1635 +scipy/spatial/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/spatial/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test__plotutils.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test__procrustes.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_distance.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_hausdorff.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_kdtree.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_qhull.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_slerp.cpython-310.pyc,, +scipy/spatial/tests/__pycache__/test_spherical_voronoi.cpython-310.pyc,, +scipy/spatial/tests/data/cdist-X1.txt,sha256=ULnYAgX2_AwOVF-VE7XfnW5S0pzhx7UAoocxSnXMaWs,5750 +scipy/spatial/tests/data/cdist-X2.txt,sha256=_IJVjXsp3pvd8NNPNTLmVbHOrzl_RiEXz7cb86NfvZ4,11500 +scipy/spatial/tests/data/degenerate_pointset.npz,sha256=BIq8Hd2SS_LU0fIWAVVS7ZQx-emVRvvzgnaO2lh4gXU,22548 +scipy/spatial/tests/data/iris.txt,sha256=k19QSfkqhMmByqNMzwWDmM6wf5dt6whdGyfAyUO3AW0,15000 +scipy/spatial/tests/data/pdist-boolean-inp.txt,sha256=5Z9SMsXrtmzeUwJlVmGkrPDC_Km7nVpZIbBl7p3Hdc0,50000 +scipy/spatial/tests/data/pdist-chebyshev-ml-iris.txt,sha256=Yerj1wqIzcdyULlha-q02WBNGyS2Q5o2wAr0XVEkzis,178801 +scipy/spatial/tests/data/pdist-chebyshev-ml.txt,sha256=NEd2b-DONqUMV9f8gJ2yod17C_5fXGHHZ38PeFsXkyw,3041 +scipy/spatial/tests/data/pdist-cityblock-ml-iris.txt,sha256=UCWZJeMkMajbpjeG0FW60b0q-4r1geAyguNY6Chx5bM,178801 +scipy/spatial/tests/data/pdist-cityblock-ml.txt,sha256=8Iq7cF8oMJjpqd6qsDt_mKPQK0T8Ldot2P8C5rgbGIU,3041 +scipy/spatial/tests/data/pdist-correlation-ml-iris.txt,sha256=l2kEAu0Pm3OsFJsQtHf9Qdy5jnnoOu1v3MooBISnjP0,178801 +scipy/spatial/tests/data/pdist-correlation-ml.txt,sha256=S4GY3z-rf_BGuHmsnColMvR8KwYDyE9lqEbYT_a3Qag,3041 +scipy/spatial/tests/data/pdist-cosine-ml-iris.txt,sha256=hQzzoZrmw9OXAbqkxC8eTFXtJZrbFzMgcWMLbJlOv7U,178801 +scipy/spatial/tests/data/pdist-cosine-ml.txt,sha256=P92Tm6Ie8xg4jGSP7k7bmFRAP5MfxtVR_KacS73a6PI,3041 +scipy/spatial/tests/data/pdist-double-inp.txt,sha256=0Sx5yL8D8pyYDXTIBZAoTiSsRpG_eJz8uD2ttVrklhU,50000 +scipy/spatial/tests/data/pdist-euclidean-ml-iris.txt,sha256=3-UwBM7WZa4aCgmW_ZAdRSq8KYMq2gnkIUqU73Z0OLI,178801 +scipy/spatial/tests/data/pdist-euclidean-ml.txt,sha256=rkQA2-_d7uByKmw003lFXbXNDjHrUGBplZ8nB_TU5pk,3041 +scipy/spatial/tests/data/pdist-hamming-ml.txt,sha256=IAYroplsdz6n7PZ-vIMIJ4FjG9jC1OSxc3-oVJdSFDM,3041 +scipy/spatial/tests/data/pdist-jaccard-ml.txt,sha256=Zb42SoVEnlTj_N_ndnym3_d4RNZWeHm290hTtpp_zO8,3041 +scipy/spatial/tests/data/pdist-jensenshannon-ml-iris.txt,sha256=L7STTmlRX-z-YvksmiAxEe1UoTmDnQ_lnAjZH53Szp0,172738 +scipy/spatial/tests/data/pdist-jensenshannon-ml.txt,sha256=-sZUikGMWskONojs6fJIMX8VEWpviYYg4u1vipY6Bak,2818 +scipy/spatial/tests/data/pdist-minkowski-3.2-ml-iris.txt,sha256=N5L5CxRT5yf_vq6pFjorJ09Sr-RcnrAlH-_F3kEsyUU,178801 +scipy/spatial/tests/data/pdist-minkowski-3.2-ml.txt,sha256=DRgzqxRtvQVzFnpFAjNC9TDNgRtk2ZRkWPyAaeOx3q4,3041 +scipy/spatial/tests/data/pdist-minkowski-5.8-ml-iris.txt,sha256=jz7SGKU8GuJWASH2u428QL9c-G_-8nZvOFSOUlMdCyA,178801 +scipy/spatial/tests/data/pdist-seuclidean-ml-iris.txt,sha256=37H01o6GibccR_hKIwwbWxGX0Tuxnb-4Qc6rmDxwwUI,178801 +scipy/spatial/tests/data/pdist-seuclidean-ml.txt,sha256=YmcI7LZ6i-Wg1wjAkLVX7fmxzCj621Pc5itO3PvCm_k,3041 +scipy/spatial/tests/data/pdist-spearman-ml.txt,sha256=IrtJmDQliv4lDZ_UUjkZNso3EZyu7pMACxMB-rvHUj0,3041 +scipy/spatial/tests/data/random-bool-data.txt,sha256=MHAQdE4hPVzgu-csVVbm1DNJ80dP7XthJ1kb2In8ImM,6000 +scipy/spatial/tests/data/random-double-data.txt,sha256=GA8hYrHsTBeS864GJf0X6JRTvGlbpM8P8sJairmfnBU,75000 +scipy/spatial/tests/data/random-int-data.txt,sha256=xTUbCgoT4X8nll3kXu7S9lv-eJzZtwewwm5lFepxkdQ,10266 +scipy/spatial/tests/data/random-uint-data.txt,sha256=8IPpXhwglxzinL5PcK-PEqleZRlNKdx3zCVMoDklyrY,8711 +scipy/spatial/tests/data/selfdual-4d-polytope.txt,sha256=rkVhIL1mupGuqDrw1a5QFaODzZkdoaLMbGI_DbLLTzM,480 +scipy/spatial/tests/test__plotutils.py,sha256=fASbg0i7iLiJIEj5vIkiDuTq3wU0z3mKJY019kzKrFk,3814 +scipy/spatial/tests/test__procrustes.py,sha256=wmmnUHRdw_oID0YLi404IEWPH6vEGhvHXSeGPY_idHo,4974 +scipy/spatial/tests/test_distance.py,sha256=793ubGYbWj74ICe9khubsDoxzrjE32-HxFJllgXGptU,87892 +scipy/spatial/tests/test_hausdorff.py,sha256=XcDEzwFuOR9BaLegIj-DPp5GrAi_RsvcW8oGqJf0xkg,8217 +scipy/spatial/tests/test_kdtree.py,sha256=gIkFKF8ek0xuMjhUu9uWJGsQ0GmED4FtqNiasNCKzho,49314 +scipy/spatial/tests/test_qhull.py,sha256=ThiPSBGFYEVW3kxfOzz2BSOEijRNbMX1sobYNBZ4g5M,49954 +scipy/spatial/tests/test_slerp.py,sha256=gjBdGVUbaPctmw05Z297dUjq5a1lH3erm1meMQoVzeo,16427 +scipy/spatial/tests/test_spherical_voronoi.py,sha256=YCVSpO7-RrmKaAivwrLh5rZJ6CTTNKuIJ9iyhXsi178,14500 +scipy/spatial/transform/__init__.py,sha256=vkvtowJUcu-FrMMXjEiyfnG94Cqwl000z5Nwx2F8OX0,700 +scipy/spatial/transform/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/_rotation_groups.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/_rotation_spline.cpython-310.pyc,, +scipy/spatial/transform/__pycache__/rotation.cpython-310.pyc,, +scipy/spatial/transform/_rotation.cpython-310-x86_64-linux-gnu.so,sha256=JShHnvEDBbu2SyY91E-FUwAoVUxBRR1TIxg54vCpq0w,1043312 +scipy/spatial/transform/_rotation_groups.py,sha256=XS-9K6xYnnwWywMMYMVznBYc1-0DPhADHQp_FIT3_f8,4422 +scipy/spatial/transform/_rotation_spline.py,sha256=B1wmFTqR34W-CMAggNFvFgZwVrgP2v2iFVIzjnAxnA8,14076 +scipy/spatial/transform/rotation.py,sha256=co5Bpny89EfCywilEeeLDvJPESBLrSXTCCJqRlfdYzg,556 +scipy/spatial/transform/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/spatial/transform/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/spatial/transform/tests/__pycache__/test_rotation.cpython-310.pyc,, +scipy/spatial/transform/tests/__pycache__/test_rotation_groups.cpython-310.pyc,, +scipy/spatial/transform/tests/__pycache__/test_rotation_spline.cpython-310.pyc,, +scipy/spatial/transform/tests/test_rotation.py,sha256=Aiyb9c3hunWc6bk6TtjQzjPDJCvgepZcgWhgH4NG1jw,69979 +scipy/spatial/transform/tests/test_rotation_groups.py,sha256=V6DiLWvJsrdklhS-GlzcA9qEy0cTQpwaNR-7vkhBt1M,5560 +scipy/spatial/transform/tests/test_rotation_spline.py,sha256=g3prW5afu_yJxevIz2LMdRFYLfe8zq-3b6TMGw06Ads,5105 +scipy/special/__init__.pxd,sha256=l9Y21wnx5fZLvrxCeCMUWQvBI5gHx7LBhimDWptxke8,42 +scipy/special/__init__.py,sha256=DoBkidFI8n9vihdtuv6XB_VBiz750909thSvHTOAXVs,33726 +scipy/special/__pycache__/__init__.cpython-310.pyc,, +scipy/special/__pycache__/_add_newdocs.cpython-310.pyc,, +scipy/special/__pycache__/_basic.cpython-310.pyc,, +scipy/special/__pycache__/_ellip_harm.cpython-310.pyc,, +scipy/special/__pycache__/_input_validation.cpython-310.pyc,, +scipy/special/__pycache__/_lambertw.cpython-310.pyc,, +scipy/special/__pycache__/_logsumexp.cpython-310.pyc,, +scipy/special/__pycache__/_mptestutils.cpython-310.pyc,, +scipy/special/__pycache__/_multiufuncs.cpython-310.pyc,, +scipy/special/__pycache__/_orthogonal.cpython-310.pyc,, +scipy/special/__pycache__/_sf_error.cpython-310.pyc,, +scipy/special/__pycache__/_spfun_stats.cpython-310.pyc,, +scipy/special/__pycache__/_spherical_bessel.cpython-310.pyc,, +scipy/special/__pycache__/_support_alternative_backends.cpython-310.pyc,, +scipy/special/__pycache__/_testutils.cpython-310.pyc,, +scipy/special/__pycache__/add_newdocs.cpython-310.pyc,, +scipy/special/__pycache__/basic.cpython-310.pyc,, +scipy/special/__pycache__/orthogonal.cpython-310.pyc,, +scipy/special/__pycache__/sf_error.cpython-310.pyc,, +scipy/special/__pycache__/specfun.cpython-310.pyc,, +scipy/special/__pycache__/spfun_stats.cpython-310.pyc,, +scipy/special/_add_newdocs.py,sha256=ZGPOb0r2gI8MIG9SA7_dEleWl8CHFprVyt422UabbQ8,290517 +scipy/special/_basic.py,sha256=8AwohnlJ1Z_396QgTh4L1Ba5iiVL_iewk_tg4CukAjU,112015 +scipy/special/_comb.cpython-310-x86_64-linux-gnu.so,sha256=jNnZfbktymXBJhXVh_en7V-J07S1uuPVqNQmOq01lR4,63456 +scipy/special/_ellip_harm.py,sha256=YHHFZXMtzdJxyjZXKsy3ocIsV-eg6ne3Up79BuFl9P8,5382 +scipy/special/_ellip_harm_2.cpython-310-x86_64-linux-gnu.so,sha256=1sWe-uit0fuqQGoQB-ygPfWKw55lexddfLgmgs43Np8,138273 +scipy/special/_gufuncs.cpython-310-x86_64-linux-gnu.so,sha256=sNy4XBfh81pGARjeM-4DXtKIiZ8qmC-extWpyWUl_UA,753248 +scipy/special/_input_validation.py,sha256=ZEwg_sZaesaqzaVA_btZQAi_uPXtIViL_u3Zms6UnyQ,474 +scipy/special/_lambertw.py,sha256=-oSEnHFQWZiUZXMamxPWjfntWq5tt0rzHmI13DxGHBY,3962 +scipy/special/_logsumexp.py,sha256=EsqkmAtuVAtxZI1koHPGhvxChP9ar7pYlwVZjDTjI5s,13961 +scipy/special/_mptestutils.py,sha256=ocy_wBXqHGIg311jfjATEA8O29ICl4qPnvTgsmTm5qg,14441 +scipy/special/_multiufuncs.py,sha256=z9UQsy0fwHF-f6tUZOFAjmhw6EXx3njzA2mkyRk-Zho,18522 +scipy/special/_orthogonal.py,sha256=9RcRoMBby-UMRN8bBqK_m34b9gcAhvP3i630SzAnKJk,74230 +scipy/special/_orthogonal.pyi,sha256=a0iJfx1CdwZQjf2o8RfM7jiS2daOfXSwQ4a2hpoFhVs,8242 +scipy/special/_precompute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/special/_precompute/__pycache__/__init__.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/cosine_cdf.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/expn_asy.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/gammainc_asy.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/gammainc_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/hyp2f1_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/lambertw.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/loggamma.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/struve_convergence.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/utils.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wright_bessel.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wright_bessel_data.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/wrightomega.cpython-310.pyc,, +scipy/special/_precompute/__pycache__/zetac.cpython-310.pyc,, +scipy/special/_precompute/cosine_cdf.py,sha256=ZGSeDDpLRsapyx2GbIrqqYR98fvaEQrLn7IE-fuodhE,354 +scipy/special/_precompute/expn_asy.py,sha256=JAz0hY1gBJu3Q_dvscQrSJdgKuwpjqFZVwz-sOQQ21w,1265 +scipy/special/_precompute/gammainc_asy.py,sha256=P5OFRcPkkpjGQeYCaMZ8SFSUmZG_CjrEHv8OLwgcGFc,2502 +scipy/special/_precompute/gammainc_data.py,sha256=jogxBuXLr3uEpMBvpqScDz5TzEEalksH8f-cRGzasck,4077 +scipy/special/_precompute/hyp2f1_data.py,sha256=STSBybQ2pCAu6sh8c9tiHsoDOgnisnSp4tkP2cK4MuI,14707 +scipy/special/_precompute/lambertw.py,sha256=7f4F3ivouVNZwuvVX8TAi2lPB7LirPS8IfN5lEw9zI0,1961 +scipy/special/_precompute/loggamma.py,sha256=iq7ZBrUmk8pXYZwO_wINI4u8ENsLbL9VUShGjGO0Pt0,1094 +scipy/special/_precompute/struve_convergence.py,sha256=z7R0Q5_Ye-EqLI9g-yARdl_j5FooofXMRXPLVrIFJQQ,3624 +scipy/special/_precompute/utils.py,sha256=JXJuI07Jlm4bDHJFVtj0jHq05p-V1ofeXZB16Y05kzI,887 +scipy/special/_precompute/wright_bessel.py,sha256=7z2W3spGANZO31r_xauMA6hIQ0eseRlXx-zJW6du5tU,12868 +scipy/special/_precompute/wright_bessel_data.py,sha256=f1id2Gk5TPyUmSt-Evhoq2_hfRgLUU7Qu_mELKtaXGg,5647 +scipy/special/_precompute/wrightomega.py,sha256=YpmLwtGJ4qazMDY0RXjhnQiuRAISI-Pr9MwKc7pZlhc,955 +scipy/special/_precompute/zetac.py,sha256=LmhJP7JFg7XktHvfm-DgzuiWZFtVdpvYzzLOB1ePG1Q,591 +scipy/special/_sf_error.py,sha256=q_Rbfkws1ttgTQKYLt6zFTdY6DFX2HajJe_lXiNWC0c,375 +scipy/special/_specfun.cpython-310-x86_64-linux-gnu.so,sha256=XdRJHGyOiropEKd57VOma1Lj3NUnMHgzuOB8fP_2Nxs,296576 +scipy/special/_special_ufuncs.cpython-310-x86_64-linux-gnu.so,sha256=1Fvmv1ksiKbNJJELsuo_JUu9rFHp7c_Ro-OkM3DYs8U,1464544 +scipy/special/_spfun_stats.py,sha256=IjK325nhaTa7koQyvlVaeCo01TN9QWRpK6mDzkuuAq0,3779 +scipy/special/_spherical_bessel.py,sha256=E6aFHez6Ev8sUlJNLKWk5pZ0bwIp3vrafZr8Bh2Vws8,12446 +scipy/special/_support_alternative_backends.py,sha256=3Qlio4pv6iJoZvPhilpx5YZifX3R4a39k5uHbo_Vyd8,6315 +scipy/special/_test_internal.cpython-310-x86_64-linux-gnu.so,sha256=DnmYHXbQ2Klvk4xJeg7Fy3nbonh4vvmiHtxuwNGdIPc,259384 +scipy/special/_test_internal.pyi,sha256=cye6-VI7Jxvb4JDfa1R_f7slEDjYUUfM4edFZ_e0XiE,394 +scipy/special/_testutils.py,sha256=o_h6MBVRhEubUC7flB-1LLr1GF5GJgVw9iol46H2lPs,11975 +scipy/special/_ufuncs.cpython-310-x86_64-linux-gnu.so,sha256=0e_iM71NN3tiKtCfZQNb-J4t_L7FoLBETC4jXFdzrNc,1626249 +scipy/special/_ufuncs.pyi,sha256=AIHP4TNIs1CeqhIgObHyY0S2nNGBo6cICL_3hpRzj9o,8839 +scipy/special/_ufuncs.pyx,sha256=O98FaNvASL6ooj4ymS-Re2-1tZlzA6hyKwpUEdKWbEk,605812 +scipy/special/_ufuncs_cxx.cpython-310-x86_64-linux-gnu.so,sha256=244b7ML2tftjlcUgHpZYgzQFyj59oTRR9Z4CTZU7qGk,1855408 +scipy/special/_ufuncs_cxx.pxd,sha256=Ltt2eonXvAbhRTKQj74VH299NBK9mCx4XYCdyUXLQ4U,5644 +scipy/special/_ufuncs_cxx.pyx,sha256=Py0yENPlxWqfc700rtXPv2ZTrL8tnh1HR-K_vWlbCKU,31470 +scipy/special/_ufuncs_cxx_defs.h,sha256=X8HIX3AK-7HXPIAPN1KGw5KOdF5GTvMmlR4Sl9nLwFU,9609 +scipy/special/_ufuncs_defs.h,sha256=h0MFUp-u8riZ6vm7y7UhcCzw4_kuGWxVc7q5IAAW1Ns,3166 +scipy/special/add_newdocs.py,sha256=Wnd-5R0wQAVxSolD4QY2CamTSbe1k48Aie3XaBWRKKc,436 +scipy/special/basic.py,sha256=LRU8rIxXx42O4eVZv21nFwswAu7JFtQ42_4xT5BwYpE,1582 +scipy/special/cython_special.cpython-310-x86_64-linux-gnu.so,sha256=mMFaE5GPSrGRjaSA1nVnAo3G08fadOI8r97HBuVMnFc,3516056 +scipy/special/cython_special.pxd,sha256=6dBzCjT38uzfixyM49cTuB6zfUH69m2DGN2WBVVBk9I,16362 +scipy/special/cython_special.pyi,sha256=BQVUCzV8lCylnmLCtnN0Yz_ttlqyzcLc-BZx2KPXPzM,58 +scipy/special/libsf_error_state.so,sha256=fVQGVM3MTo7ZzqzfniAyEPgPkCzbLJj6uQI0-X-TisA,15840 +scipy/special/orthogonal.py,sha256=aLzv7PzJgsdLpyTrV6Cu-rpHNHWlUAEqOImiW4fuzuE,1724 +scipy/special/sf_error.py,sha256=wOZqzX7iipkH39hOHqBlkmretJRbYy-K7PsnZPyaJFU,573 +scipy/special/specfun.py,sha256=V1ZaKH1FFHPvzgkFa-UBVaVTLJRO4fodr7NAW_1jExo,588 +scipy/special/spfun_stats.py,sha256=ESJXGUwH7iijUk6aXZQVI1pnaWiVZ6_l0hVpC4bBSIw,535 +scipy/special/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/special/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_basic.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_bdtr.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_boost_ufuncs.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_boxcox.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_cdflib.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_cdft_asymptotic.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_cephes_intp_cast.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_cosine_distr.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_cython_special.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_data.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_dd.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_digamma.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_ellip_harm.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_erfinv.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_exponential_integrals.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_extending.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_faddeeva.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_gamma.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_gammainc.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_hyp2f1.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_hypergeometric.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_iv_ratio.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_kolmogorov.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_lambertw.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_legendre.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_log_softmax.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_loggamma.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_logit.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_logsumexp.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_mpmath.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_nan_inputs.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_ndtr.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_ndtri_exp.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_orthogonal.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_orthogonal_eval.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_owens_t.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_pcf.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_pdtr.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_powm1.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_precompute_expn_asy.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_precompute_gammainc.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_precompute_utils.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_round.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_sf_error.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_sici.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_specfun.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_spence.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_spfun_stats.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_sph_harm.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_spherical_bessel.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_support_alternative_backends.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_trig.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_ufunc_signatures.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_wright_bessel.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_wrightomega.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_xsf_cuda.cpython-310.pyc,, +scipy/special/tests/__pycache__/test_zeta.cpython-310.pyc,, +scipy/special/tests/_cython_examples/extending.pyx,sha256=0ISFhXHFnwuWXg5m9VIYdWGjP_W7hxUE8SwFNkvAM_s,292 +scipy/special/tests/_cython_examples/meson.build,sha256=pTPPwQXCFOd1qe3HpOXcT6lx3HjyUihzu9wTXJqVsnY,527 +scipy/special/tests/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/special/tests/data/__pycache__/__init__.cpython-310.pyc,, +scipy/special/tests/data/boost.npz,sha256=1z7Lu1FlRSI0K6BHCmJjqWhOYXwrg3RWX-OnlZP0sjE,1270643 +scipy/special/tests/data/gsl.npz,sha256=rKtwAgjLswHuUesfUSyxwn57TnUz_FpfXNXF1qoZfdg,51433 +scipy/special/tests/data/local.npz,sha256=ECuHbCfsTS-AQdWrL7bf78gUcCEzUWD1FUVeU-Bocf8,203438 +scipy/special/tests/test_basic.py,sha256=r_gC4JqRGW3jKi6LwVlGiuVwz5DEEdUOfm_Sew7uNUU,189822 +scipy/special/tests/test_bdtr.py,sha256=QwGyt0tnutuou25mS0u2LjRgDTYI6ohM2cbZ-He6Os4,3231 +scipy/special/tests/test_boost_ufuncs.py,sha256=I2miMp5IxgexHS6xsHyp9F0YozKr9mpWTpNCq0KI0CY,2245 +scipy/special/tests/test_boxcox.py,sha256=KK6Ti9TMWKbVaxPVfycrUnM09Th1J2ARhVnI7t7y098,3114 +scipy/special/tests/test_cdflib.py,sha256=wt3axXOqxSwgNYWMAPgQvXlzQIKbWV6kkKal57PobuY,23524 +scipy/special/tests/test_cdft_asymptotic.py,sha256=DBVVLaduZUHSWlKJ5aBXmxgdNm_YjLvWgyiTTcQq04c,1441 +scipy/special/tests/test_cephes_intp_cast.py,sha256=yllVoacRDDS_mH7E_pvDux_Jpf7_Fdt3F9Jsgj3_BaY,1129 +scipy/special/tests/test_cosine_distr.py,sha256=zL7aWLisIEy1oNKjcynqncgsCxcPKvPb9Odr-J5Xa1M,2690 +scipy/special/tests/test_cython_special.py,sha256=Y79hvQdFnT3w62Lhg8lFDN34hRpDf7vfV3DyNoCqNEY,19128 +scipy/special/tests/test_data.py,sha256=n6p4MFRXEejYCe_b0Q7CfIu3OXng4jn1nHnMPT9gCOA,30180 +scipy/special/tests/test_dd.py,sha256=I7xSqxTD-GYaO0ol25ZjsGZgqCVt13vbcQlUN7teeG4,1564 +scipy/special/tests/test_digamma.py,sha256=Bm7Hh_aETx6MTN3Wu7Sijy4rYGR_1haNGsi3xfzrAKM,1382 +scipy/special/tests/test_ellip_harm.py,sha256=0Kooy3pTFwWqmDT33sjxQZ1S8qjNe-MqO4gJhgcPrrI,9635 +scipy/special/tests/test_erfinv.py,sha256=fzdEHd6MxfSyzQDO93qndXukG2jWj-XNY2X4BJRIdBI,3059 +scipy/special/tests/test_exponential_integrals.py,sha256=hlzNhZEXjo5ioPteG0P85qXuMmVD-WVc67e049tvY8Q,3687 +scipy/special/tests/test_extending.py,sha256=7Q8NRxp-QBASTY9y0b8xOcAJmrMKhLaruE_MX7nmJ0M,1184 +scipy/special/tests/test_faddeeva.py,sha256=YLY3Ylp4u_8zxTGxOb5kxNfXXEW0ld_GP2ceOR2ev_Y,2568 +scipy/special/tests/test_gamma.py,sha256=hb-ZlA2ZNz6gUGvVtMBgXFl_w30HPmthuUEAmNcz0sw,258 +scipy/special/tests/test_gammainc.py,sha256=Avv52EDQ7M8kUpiVU1BVsW_Gj5HDCzAOojLtoFojKbw,3815 +scipy/special/tests/test_hyp2f1.py,sha256=r-PJvlIkf27Yo3giszrwwHIo6FWFO9D0jsSXifFuK3w,92259 +scipy/special/tests/test_hypergeometric.py,sha256=DUDe1YvIXt4IocGlJuqDO5swZ-QOyR2Etj2rwkF-NqQ,9996 +scipy/special/tests/test_iv_ratio.py,sha256=6Wa4PDSboT1srHiGUOR78_cTvStWgct31cGkLFvDT5A,10108 +scipy/special/tests/test_kolmogorov.py,sha256=-Ika_ORUwxDuaCXATLb489T9lDWoPkJR7r7PNRAE0mE,19280 +scipy/special/tests/test_lambertw.py,sha256=vd5G_70CQz3N_U15mcyE0-2KZ_8QYLKmrJ4ZL-RwFXY,4560 +scipy/special/tests/test_legendre.py,sha256=ndelP3mnTsONEs2TBKC_y1SBK9oCnYV2o8fTgRslFwU,57925 +scipy/special/tests/test_log_softmax.py,sha256=JdiC5C1Fm16rNdQHVWRu-FGMVOv24DPWRnguDDd1zEY,3415 +scipy/special/tests/test_loggamma.py,sha256=x6kuJf-bEnn5ECdkDSgvk3An_A-9UxVsZpqa49IwAq8,1992 +scipy/special/tests/test_logit.py,sha256=8tkUtuoxbu42WZ2LWMrHA2aW_IuB3M0Iqe9FZ0VrJbI,6503 +scipy/special/tests/test_logsumexp.py,sha256=HnnL-l7kD_pVrcnHihOuz_-gptrC-M1j00Gqugeyt8s,13157 +scipy/special/tests/test_mpmath.py,sha256=_3scYBHF0sVgGMYV9YP-bT__2EiTxTWnhqROkhk9dus,73771 +scipy/special/tests/test_nan_inputs.py,sha256=D85KHG-2K7dqWZDZcxY4n1JvhIxRlQcuCfVbeLogaFs,1858 +scipy/special/tests/test_ndtr.py,sha256=-UMxTIi4CaaLoJ5-SGW9THChPIM3e1_fTY0L877ioNA,2680 +scipy/special/tests/test_ndtri_exp.py,sha256=13eabgdbfcL37RReiUH7g9amT9XMsTLOfwxFJXR_2Ww,3708 +scipy/special/tests/test_orthogonal.py,sha256=yzZz0GltDQ2JIBQMUXqq8REx-ZuOlRYRa8HUBey0Tsc,32208 +scipy/special/tests/test_orthogonal_eval.py,sha256=OPW5OeQWVFHyY7SMG2tY8Ar85StXyz0zfsZe9y9ne14,9571 +scipy/special/tests/test_owens_t.py,sha256=zRbiKje7KrYJ25f1ZuIBfiFSyNtK_bnkIW7dRETIqME,1792 +scipy/special/tests/test_pcf.py,sha256=RNjEWZGFS99DOGZkkPJ8HNqLULko8UkX0nEWFYX26NE,664 +scipy/special/tests/test_pdtr.py,sha256=VmupC2ezUR3p5tgZx0rqXEHAtzsikBW2YgaIxuGwO5A,1284 +scipy/special/tests/test_powm1.py,sha256=9hZeiQVKqV63J5oguYXv_vqolpnJX2XRO1JN0ouLWAM,2276 +scipy/special/tests/test_precompute_expn_asy.py,sha256=bCQikPkWbxVUeimvo79ToVPgwaudzxGC7Av-hPBgIU4,583 +scipy/special/tests/test_precompute_gammainc.py,sha256=6XSz0LTbFRT-k0SlnPhYtpzrlxKHaL_CZbPyDhhfT5E,4459 +scipy/special/tests/test_precompute_utils.py,sha256=MOvdbLbzjN5Z1JQQgtIyjwjuIMPX4s2bTc_kxaX67wc,1165 +scipy/special/tests/test_round.py,sha256=Zv32kFQrDdOPawfGDeZo1PfBG4UsOyKfd3zjbCWLii0,511 +scipy/special/tests/test_sf_error.py,sha256=3nOa9ffbrVz2CxfMsHzGVOPaKW_LMV6LDKGfnjjsYXI,4204 +scipy/special/tests/test_sici.py,sha256=w4anBf8fiq2fmkwMSz3MX0uy35NLXVqfuW3Fwt2Nqek,1227 +scipy/special/tests/test_specfun.py,sha256=q2JYEnqmUq78rO8no9hXQZ3fc3RuxPrRCcpsLruovDg,1687 +scipy/special/tests/test_spence.py,sha256=fChPw7xncNCTPMUGb0C8BC-lDKHWoEXSz8Rb4Wv8vNo,1099 +scipy/special/tests/test_spfun_stats.py,sha256=mKJZ2-kLmVK3ZqX3UlDi9Mx4bRQZ9YoXQW2fxrW2kZs,1997 +scipy/special/tests/test_sph_harm.py,sha256=VEVx2-Rfm2se-n4YU6kafVI1Yml5eYXy1l_uPCJh5pE,3072 +scipy/special/tests/test_spherical_bessel.py,sha256=yvwnfjt-eCOChCOi48LsPOEhxCLppo1fA8Qcnp8Hzcg,15027 +scipy/special/tests/test_support_alternative_backends.py,sha256=xXl1ImMhcYLsx3s2UF5WIWm5thtNN2Iw3kaw8qEm7ww,4422 +scipy/special/tests/test_trig.py,sha256=ZlzoL1qKvw2ZCbIYTNYm6QkeKqYUSeE7kUghELXZwzU,2332 +scipy/special/tests/test_ufunc_signatures.py,sha256=5tsAbc-QwVe_7YbjbjjYNM1Phiwf51YYqqRx0Hk9EmE,1838 +scipy/special/tests/test_wright_bessel.py,sha256=6WHuXB97skPSsoMgXwRlO7bHydFLnl9iDfctEpZE0uE,7694 +scipy/special/tests/test_wrightomega.py,sha256=BW8TS_CuDjR7exA4l6ADnKhXwgFWUYaN1UIopMBJUZY,3560 +scipy/special/tests/test_xsf_cuda.py,sha256=yqSB6_ZkuFwFo__noNhKa4LzmzQEqPkxaQd4C9NEWjU,3393 +scipy/special/tests/test_zeta.py,sha256=IEPRUdSX5kerDYPmhLWYkYixmUg1ErqHSprQpfkZTP0,11549 +scipy/special/xsf/binom.h,sha256=IOVEKVugDUr9zqCLOk99Pj9LcMiGIZe4zzJCtWlYTZg,2471 +scipy/special/xsf/cdflib.h,sha256=1BrCII189UOWaBsII0H1kLgHfo8wdgaoysSbPojKIGU,4176 +scipy/special/xsf/cephes/airy.h,sha256=eTMfFrUgTjCEn0l8IiuKwBSDFHd5rZMrcTttNK0Akis,11089 +scipy/special/xsf/cephes/besselpoly.h,sha256=8MdB7tamsSebW9rpHS0TiVlq_YdkJTP1vDTrUx-i6io,1379 +scipy/special/xsf/cephes/beta.h,sha256=MDaX9iQorb6nYHKIjsS10qq0PmS-h8_f-MV3XHL35UQ,6981 +scipy/special/xsf/cephes/cbrt.h,sha256=bvmwllJjyMlgTUl9FqFXxhiGCXVan-BcrF3iF_UVEMg,3383 +scipy/special/xsf/cephes/chbevl.h,sha256=G6HJhFVbhKkXXBN_ZVborRWGBGO6PNAAQ5-zpOYoXBA,1906 +scipy/special/xsf/cephes/chdtr.h,sha256=eADp4we-EkfmgSRtjztWrkBhiad0LKfS4zCF5SLqth8,4047 +scipy/special/xsf/cephes/const.h,sha256=FfK7cYG3W8fCzBTe7M6Y8Ejfd_6OL1kzSswC9KyTNk4,3243 +scipy/special/xsf/cephes/ellie.h,sha256=ncKPlvJ2naCIouLawoGsiBlwp7hVNFMGwkLHq9Kljeg,9494 +scipy/special/xsf/cephes/ellik.h,sha256=0b40o6PlvzvUCbGnNJ-97BgE-8ZxLYjK9PuCjsoztzw,7601 +scipy/special/xsf/cephes/ellpe.h,sha256=XTCSsSMw8q1CZv19tAdzStjvZRaZ2ONEJNbccSqTiAk,3061 +scipy/special/xsf/cephes/ellpk.h,sha256=jI3WsxFmDAMsovrVyVkt_1voOsYRL2ZesgjuMKLlTpo,3392 +scipy/special/xsf/cephes/expn.h,sha256=IiyXzwtCkUT-TRz8TnMyvdoFi3g0Ri1BThEVydX3S7g,8942 +scipy/special/xsf/cephes/gamma.h,sha256=1ys_rqGE3dR_30YskFwfd8CpKXfCh7UIbZR3fxOtcPA,12004 +scipy/special/xsf/cephes/hyp2f1.h,sha256=kruh1lao3mygHmwVOfvu-MnFunbwNVdf5fZ9Gq5lydk,19986 +scipy/special/xsf/cephes/hyperg.h,sha256=q7BXWxVRmTwkHlJHqdep4CHWrYUWr1Ixv-as_xSKjBA,10458 +scipy/special/xsf/cephes/i0.h,sha256=rnsastkYnz7FPozLTZXE2NjLYjRtO2bqsCrNLmBS7V4,4548 +scipy/special/xsf/cephes/i1.h,sha256=WuxVJe6_M91pTmZgWFqqahu3slNwkDuzveUfGJlZUps,4740 +scipy/special/xsf/cephes/igam.h,sha256=w8_0jQmn-Lxtr-7NFeXKnqyo1jCRBBjup31kOJR0r0E,12877 +scipy/special/xsf/cephes/igam_asymp_coeff.h,sha256=ky3gnc7fifHIDRtamh4h5Ex2gKdBj6WPy4rmNtqD2nc,17893 +scipy/special/xsf/cephes/igami.h,sha256=B_PW8A2s1trORbnVDzKCtqdzslzWbzDsr9vKWey3pqY,12687 +scipy/special/xsf/cephes/j0.h,sha256=93xq6Budd0C4hNipx0maXQ_311NLxJMmVFzJe9jEnQk,6878 +scipy/special/xsf/cephes/j1.h,sha256=Qd9M25owFl3YOuAJ_Lr-hAh1m7bRxzFEEsOWDs6K68Y,6058 +scipy/special/xsf/cephes/jv.h,sha256=RpS_SWQlINWAr7vr7zCguo6V5zBt5o9ffBcdWLVKhzA,23130 +scipy/special/xsf/cephes/k0.h,sha256=ZeaVogEPyw0bGDFs4BFg1CR8I1WtIwqQGEPNv6M7B-w,4864 +scipy/special/xsf/cephes/k1.h,sha256=NYGMytXenLXSe2RZcRds3yGfHlvQwKmpegkDuKnDH8g,4626 +scipy/special/xsf/cephes/kn.h,sha256=SIUl7ePiFLVbXuTf2AC0VhoJkOPHTVQxkY0U5SCGYX8,6264 +scipy/special/xsf/cephes/lanczos.h,sha256=2Wp0n-MWPs2l0MtQ1RVaOvcLsC52zELOYPxYJoZK4OA,5494 +scipy/special/xsf/cephes/ndtr.h,sha256=y7RhtmvX0n61_Muy7awljyqTURnwtVLbL4Y3rwz9WCY,6681 +scipy/special/xsf/cephes/poch.h,sha256=jmJkxvIEnTcuaWPnmDH6lw5kPuE3AZGN1q7zmOaAL1s,2383 +scipy/special/xsf/cephes/polevl.h,sha256=7_WTjsgG9WKExZO0RSU8e0c_j6qvnWvDPYEa63Lq0Jk,4075 +scipy/special/xsf/cephes/psi.h,sha256=2GQCNBA4UHa-Y8bo9CE2Lm6q7HnOlOsxu1BPt9xfFdY,6291 +scipy/special/xsf/cephes/rgamma.h,sha256=zBqYhN1-xWE-Vpn2wvDsiDcGuO5qdIcsBEXCOrakwaU,3058 +scipy/special/xsf/cephes/scipy_iv.h,sha256=Tw2Ls0PAqBbZyfbcYuzNSX6NPiYQqfuwZAw2Taty2mY,25450 +scipy/special/xsf/cephes/shichi.h,sha256=wR_EwP7h-qwaqIjxb1Edn3RhhjPAEYQW5hFF1QzkMrQ,8513 +scipy/special/xsf/cephes/sici.h,sha256=7i2QVx2ij4ehnMTz4lcs3TeOInl-KPoDoQEetRtoPWI,7325 +scipy/special/xsf/cephes/sindg.h,sha256=SHZRnvwVhxjZUWNIjTd-cl4VFmZyZoG76nrUwkfyC9c,5634 +scipy/special/xsf/cephes/tandg.h,sha256=9Ko6moB_BLWq29XOWynKwp9XeTf6eQbotcKaIBPbrxQ,3391 +scipy/special/xsf/cephes/trig.h,sha256=vqygJpPKDlTylA29ejgX_cu58g76gzoWwyQvO05gwig,1340 +scipy/special/xsf/cephes/unity.h,sha256=vnNI6j6kpnkPkJuc-4gIiCOHPjPaz8TuChz7aqUzPKE,5053 +scipy/special/xsf/cephes/zeta.h,sha256=s21iDx7jlgHsOJdks6aXs2n-Z0K0A7C9Z2lLdpRtAUI,4381 +scipy/special/xsf/config.h,sha256=P5g5tNTQVAPx8P2bvxlEdT2shWQHXevshd5y91G7nt0,8438 +scipy/special/xsf/digamma.h,sha256=dt4JcA8YOwSLvJEMwglQHDjun5xH4cRZ3NU6RQU2pKk,7515 +scipy/special/xsf/error.h,sha256=UR9iGZFzuTeqAlNsqTKIRK9VaD-c70CAZLquyoAuDfA,1731 +scipy/special/xsf/evalpoly.h,sha256=JCz6KMNA4jDKenIfi0Z2KhVpVOb1bzzBltEz7oTOXlw,1119 +scipy/special/xsf/expint.h,sha256=iyJ6V4PHCOnRQRY4YWqifIF1Ri56LYNcbquMT_q5gBs,8345 +scipy/special/xsf/hyp2f1.h,sha256=r4T41QT5kxrx1Gysh8SZk-3CIiUiAEQBuBephEoUnEo,34738 +scipy/special/xsf/iv_ratio.h,sha256=nX7K3F8LV0zFNa3CoHC5dBMl5dAO5uH16lAskqZzARM,5674 +scipy/special/xsf/lambertw.h,sha256=Eon5lhh7L4n5ycalsiNfBjt3WiM1gd8-jR40F5g4u8Q,5411 +scipy/special/xsf/loggamma.h,sha256=GDJhdc7dldEiN7Xj2O5c91AgXCUkI4L_nFDO5FrAq-c,6209 +scipy/special/xsf/sici.h,sha256=mzu3DK3oGE7o7KMjmqfmdirWvpBuFejqQu1WKbir2vo,5854 +scipy/special/xsf/tools.h,sha256=x2ZqPsfRghqo7QJBmaCs8b7rJPDzB2VPUK92ExerRlM,16145 +scipy/special/xsf/trig.h,sha256=ZK6mxae-JxM9o8Cf4xytP5lXWhGgGQUgtm7vxsyxV2A,4362 +scipy/special/xsf/wright_bessel.h,sha256=eYkLjIiTx9iXHaAKdQXpGBWa4mmoZ0ZuQlSLGxSu53U,42619 +scipy/special/xsf/zlog1.h,sha256=tu6rdW4hOWkrEt00KTX3BWq5kD0ZPuiCIRT7G_M1pZE,965 +scipy/stats/__init__.py,sha256=CUo1rk_ClMcxEIobb_XxhRWZi1IZ--FkHazykYw8a6Q,18680 +scipy/stats/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/__pycache__/_axis_nan_policy.cpython-310.pyc,, +scipy/stats/__pycache__/_binned_statistic.cpython-310.pyc,, +scipy/stats/__pycache__/_binomtest.cpython-310.pyc,, +scipy/stats/__pycache__/_bws_test.cpython-310.pyc,, +scipy/stats/__pycache__/_censored_data.cpython-310.pyc,, +scipy/stats/__pycache__/_common.cpython-310.pyc,, +scipy/stats/__pycache__/_constants.cpython-310.pyc,, +scipy/stats/__pycache__/_continuous_distns.cpython-310.pyc,, +scipy/stats/__pycache__/_correlation.cpython-310.pyc,, +scipy/stats/__pycache__/_covariance.cpython-310.pyc,, +scipy/stats/__pycache__/_crosstab.cpython-310.pyc,, +scipy/stats/__pycache__/_discrete_distns.cpython-310.pyc,, +scipy/stats/__pycache__/_distn_infrastructure.cpython-310.pyc,, +scipy/stats/__pycache__/_distr_params.cpython-310.pyc,, +scipy/stats/__pycache__/_distribution_infrastructure.cpython-310.pyc,, +scipy/stats/__pycache__/_entropy.cpython-310.pyc,, +scipy/stats/__pycache__/_fit.cpython-310.pyc,, +scipy/stats/__pycache__/_hypotests.cpython-310.pyc,, +scipy/stats/__pycache__/_kde.cpython-310.pyc,, +scipy/stats/__pycache__/_ksstats.cpython-310.pyc,, +scipy/stats/__pycache__/_mannwhitneyu.cpython-310.pyc,, +scipy/stats/__pycache__/_mgc.cpython-310.pyc,, +scipy/stats/__pycache__/_morestats.cpython-310.pyc,, +scipy/stats/__pycache__/_mstats_basic.cpython-310.pyc,, +scipy/stats/__pycache__/_mstats_extras.cpython-310.pyc,, +scipy/stats/__pycache__/_multicomp.cpython-310.pyc,, +scipy/stats/__pycache__/_multivariate.cpython-310.pyc,, +scipy/stats/__pycache__/_new_distributions.cpython-310.pyc,, +scipy/stats/__pycache__/_odds_ratio.cpython-310.pyc,, +scipy/stats/__pycache__/_page_trend_test.cpython-310.pyc,, +scipy/stats/__pycache__/_probability_distribution.cpython-310.pyc,, +scipy/stats/__pycache__/_qmc.cpython-310.pyc,, +scipy/stats/__pycache__/_qmvnt.cpython-310.pyc,, +scipy/stats/__pycache__/_relative_risk.cpython-310.pyc,, +scipy/stats/__pycache__/_resampling.cpython-310.pyc,, +scipy/stats/__pycache__/_result_classes.cpython-310.pyc,, +scipy/stats/__pycache__/_sampling.cpython-310.pyc,, +scipy/stats/__pycache__/_sensitivity_analysis.cpython-310.pyc,, +scipy/stats/__pycache__/_stats_mstats_common.cpython-310.pyc,, +scipy/stats/__pycache__/_stats_py.cpython-310.pyc,, +scipy/stats/__pycache__/_survival.cpython-310.pyc,, +scipy/stats/__pycache__/_tukeylambda_stats.cpython-310.pyc,, +scipy/stats/__pycache__/_variation.cpython-310.pyc,, +scipy/stats/__pycache__/_warnings_errors.cpython-310.pyc,, +scipy/stats/__pycache__/_wilcoxon.cpython-310.pyc,, +scipy/stats/__pycache__/biasedurn.cpython-310.pyc,, +scipy/stats/__pycache__/contingency.cpython-310.pyc,, +scipy/stats/__pycache__/distributions.cpython-310.pyc,, +scipy/stats/__pycache__/kde.cpython-310.pyc,, +scipy/stats/__pycache__/morestats.cpython-310.pyc,, +scipy/stats/__pycache__/mstats.cpython-310.pyc,, +scipy/stats/__pycache__/mstats_basic.cpython-310.pyc,, +scipy/stats/__pycache__/mstats_extras.cpython-310.pyc,, +scipy/stats/__pycache__/mvn.cpython-310.pyc,, +scipy/stats/__pycache__/qmc.cpython-310.pyc,, +scipy/stats/__pycache__/sampling.cpython-310.pyc,, +scipy/stats/__pycache__/stats.cpython-310.pyc,, +scipy/stats/_ansari_swilk_statistics.cpython-310-x86_64-linux-gnu.so,sha256=PLoaVEfMtU6UMzZ25rSUEj94yFyovr56-0dgRCgd6ls,278232 +scipy/stats/_axis_nan_policy.py,sha256=vtqhfxpJUrpD9GETwnB1HN7fe2NLIPt8QkGXjr3VPa8,31788 +scipy/stats/_biasedurn.cpython-310-x86_64-linux-gnu.so,sha256=WAr4bXWYjow_osqrQ1IwTKgKJ22kFFiGxlR6fdkpqRA,323168 +scipy/stats/_biasedurn.pxd,sha256=bQC6xG4RH1E5h2jCKXRMADfgGctiO5TgNlJegKrR7DY,1046 +scipy/stats/_binned_statistic.py,sha256=ATvrikTtX6zW8FKbjpV7O7IvAKSCBBLQSH1JKFR9R7Q,32702 +scipy/stats/_binomtest.py,sha256=aW6p-vRkv3pSB8_0nTfT3kNAhV8Ip44A39EEPyl9Wlc,13118 +scipy/stats/_bws_test.py,sha256=XQMGiLMPKFN3b6O4nD5tkZdcI8D8vggSx8B7XLJ5EGs,7062 +scipy/stats/_censored_data.py,sha256=Ts7GSYYti2z-8yoOJTedj6aCLnGhugLlDRdxZc4rPxs,18306 +scipy/stats/_common.py,sha256=4RqXT04Knp1CoOJuSBV6Uy_XmcmtVr0bImAbSk_VHlQ,172 +scipy/stats/_constants.py,sha256=mBeJgvWcDZBmPFStDNEjlzeZY3aMDMCHWoj7dCmgugQ,1002 +scipy/stats/_continuous_distns.py,sha256=QBGjt-kwhjmzU3-XZtnkJSXy4KC6iIk7De_k3TZCFf4,407685 +scipy/stats/_correlation.py,sha256=TKenq2UmJ6gMligjczL1nTIXgUShprfYyBc23lhTCuo,7911 +scipy/stats/_covariance.py,sha256=g0oXQfcjugq9YpJhbmUECSOqYqPqsuDBD_69r_oGRDU,22524 +scipy/stats/_crosstab.py,sha256=djdU7xCQ-513VlxFEOvLN8oaY4QyUPHDJHWlilhyEVA,7351 +scipy/stats/_discrete_distns.py,sha256=nYPH9LKlqC0q_RFMitD4XEsP9F0pfnM-B1JnJtLwACw,65095 +scipy/stats/_distn_infrastructure.py,sha256=nfk3LYe26PjZzrTug-ZDKKCI-qsmTsQCfj99-fR9Tvw,151588 +scipy/stats/_distr_params.py,sha256=bD2Sdq0etEh0NYfi3-vFM-C7PevQfH0dRLbNnXeOtYY,9052 +scipy/stats/_distribution_infrastructure.py,sha256=yXlXMuwpT_MykLntuBKbNd4EmGjPe40e0HqC9Ia2PzI,203772 +scipy/stats/_entropy.py,sha256=hMlhLViQos20KYpBwmQf9fSfmbMzoCluF4uRg7yKxTc,15831 +scipy/stats/_fit.py,sha256=PmLg5oE25gnOIHVV-4U-nfUEsKdfgac4M9OaBSjKrow,59747 +scipy/stats/_hypotests.py,sha256=gDsPkfLiTH3oCeBL_FPOcC1Gvz53SHuda2a3YPE9hr4,79170 +scipy/stats/_kde.py,sha256=EAMQrO4MRwIcdOuQ1v-R6TP5IpAo_kZThwTEmRj8v7M,25089 +scipy/stats/_ksstats.py,sha256=JsUipfbLw0TMrmUpkvHY06Rk_eXT0l7WemK9xhVdLiA,20139 +scipy/stats/_levy_stable/__init__.py,sha256=J2Nw8Ye0e52Q9cC4o08H56QnLd1Frp_fB3WuxInP6II,45986 +scipy/stats/_levy_stable/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_levy_stable/levyst.cpython-310-x86_64-linux-gnu.so,sha256=SEGI0gwuUV9AFdksbORE8qKHFnPCkMBHUIpIgGBV6Bg,66512 +scipy/stats/_mannwhitneyu.py,sha256=LQII0f5CF4-OfWXqBuP4uPjNJ8IuVgPp04itqacy1EA,19330 +scipy/stats/_mgc.py,sha256=iImSUbFmYh_7Ouap70PFP6O6CVpUylf5y44z33j3obg,21359 +scipy/stats/_morestats.py,sha256=0Q1FJqhMJICguWL7HbrRKwwCyqfZUTLN7WxOeeKa2-0,170393 +scipy/stats/_mstats_basic.py,sha256=GXFCsZtbKg6kJuvXSCGRxhtme-dfzBLvl2r-g2UWGGM,122939 +scipy/stats/_mstats_extras.py,sha256=VMtwkTOFc3eBGFHiqO0cJjr98PC0fc2EIO_oKGIQJQo,16366 +scipy/stats/_multicomp.py,sha256=x9XBSCbTWl4V-hUZ_YaMYZ5smpE95qBCUic6yYygnpA,16836 +scipy/stats/_multivariate.py,sha256=V_ArfvakTKERdhchS5vob52fOnCPHqLMYcbS0FixhOY,249240 +scipy/stats/_mvn.cpython-310-x86_64-linux-gnu.so,sha256=xT3_oTQjlchCcXQvYLnBf8sDq2zWeSpugwVv5nKH-NQ,84992 +scipy/stats/_new_distributions.py,sha256=4QuIqw-_QwJeIPsLDzFNDZBIpD7mTx4dwvEwn_5uoJk,13239 +scipy/stats/_odds_ratio.py,sha256=zZvZsD7ftKeWUrypXeUapcNoq006XldVAkMMC3RLbWE,17005 +scipy/stats/_page_trend_test.py,sha256=OvisWd3E6CF7rdFRGv46HWOfJlyHalMITt5iJPzE8LI,18987 +scipy/stats/_probability_distribution.py,sha256=xcvEl_eux4p8SSRKbTpb3Ipmfs9XAx522RK1ebkKiks,61504 +scipy/stats/_qmc.py,sha256=sJfB3Jz8unPDBe_TPN5qm1YK4emQ7lJN7iQ2_vGBO9E,107502 +scipy/stats/_qmc_cy.cpython-310-x86_64-linux-gnu.so,sha256=T2KUSO37-wMwfz_DYXxdhKH46LmAtQ2VnVOYhHFoPTA,291104 +scipy/stats/_qmc_cy.pyi,sha256=xOpTSlaG_1YDZhkJjQQtukbcgOTAR9FpcRMkU5g9mXc,1134 +scipy/stats/_qmvnt.py,sha256=oKf0JU2bY9_oePM-sLMD_xowKjMdlXFYR5c1veeuWKw,18769 +scipy/stats/_rcont/__init__.py,sha256=dUzWdRuJNAxnGYVFjDqUB8DMYti3by1WziKEfBDOlB4,84 +scipy/stats/_rcont/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_rcont/rcont.cpython-310-x86_64-linux-gnu.so,sha256=zi6uEPl8-H55I74iLHJ9FeEZAOIDWrwG_00yeHBUQvc,263128 +scipy/stats/_relative_risk.py,sha256=5zeYBMshYwtomiLTkaXc1nmWYD0FsaQNjf0iuDadtSc,9571 +scipy/stats/_resampling.py,sha256=46DA0dE1CTlXR-vVBenghqptFL7wDadr2g0CKp4IMQs,104295 +scipy/stats/_result_classes.py,sha256=_ghuGdpFsCMuEmnfHg1AeorR-fASc77ACXYWEmQzXjI,1085 +scipy/stats/_sampling.py,sha256=YJ1mG2tkXW4Em-virElY-cNzMXn8lHbOxNxujqDsPY0,46408 +scipy/stats/_sensitivity_analysis.py,sha256=rSzMU4dmjN_zL-bt8tcxTTQbpRxNZuKrKn46zQtJyJc,25041 +scipy/stats/_sobol.cpython-310-x86_64-linux-gnu.so,sha256=To_aHUUI64LsSR2sRHLfsHFIfT-WZtVEhEgz61fWY-U,404048 +scipy/stats/_sobol.pyi,sha256=TAywylI75AF9th9QZY8TYfHvIQ1cyM5QZi7eBOAkrbg,971 +scipy/stats/_sobol_direction_numbers.npz,sha256=SFmTEUfULORluGBcsnf5V9mLg50DGU_fBleTV5BtGTs,589334 +scipy/stats/_stats.cpython-310-x86_64-linux-gnu.so,sha256=Rwwu5dHnIZ-QgZPQNpP2QvrkwB0z88fhwRSRugk9mMg,766544 +scipy/stats/_stats.pxd,sha256=T_7IrDqgIahKMECV5WAtxtsoV91XBVRM359kAXPIhww,709 +scipy/stats/_stats_mstats_common.py,sha256=9SFbzUBOf6QpTwCiRkyXIlKAlm6B9uC8lv_VXSsiPzo,11557 +scipy/stats/_stats_py.py,sha256=AbZl_rpQP9U2hNAMpvMiVQ-kHUFOCdpIKrl_SNZLils,417517 +scipy/stats/_stats_pythran.cpython-310-x86_64-linux-gnu.so,sha256=Qz3zaZ08pWEGl-7qPS_Lrda3DMxvIwnmW__J4zuXqlY,182128 +scipy/stats/_survival.py,sha256=JexV_eUz0H_2QSwpido_M_LJr4mkODmhHVwjzFXjgj8,25939 +scipy/stats/_tukeylambda_stats.py,sha256=eodvo09rCVfcYa1Uh6BKHKvXyY8K5Zg2uGQX1phQ6Ew,6871 +scipy/stats/_unuran/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/stats/_unuran/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/_unuran/unuran_wrapper.cpython-310-x86_64-linux-gnu.so,sha256=AbIMI-nKfIaLhra9EJ4VP04hvWruyIUi75OXbG9MLn0,1589832 +scipy/stats/_unuran/unuran_wrapper.pyi,sha256=TT9P08hsVQu6W7giss8kweV-FKcLffwZO9gyxmbpi2c,5588 +scipy/stats/_variation.py,sha256=2DfKIrosnZ68BzG7BLJNAAR692BN0SvZhlBs6M86l5U,4652 +scipy/stats/_warnings_errors.py,sha256=MpucxNFYEDytXh7vrZCMqTkRfuXTvvMpQ2W_Ak2OnPk,1196 +scipy/stats/_wilcoxon.py,sha256=wq_2sPwuiVA1kAFWJw3yegFp0TP5WVACPkYiTMrDs9U,9382 +scipy/stats/biasedurn.py,sha256=ECfilE4KrIhU2sK-KWtr8yxqthfVsyz_-o4F2TnMXU4,431 +scipy/stats/contingency.py,sha256=psNLzIB1A00rE4U9LwdYyt6XpYZlPRBCqQSMOEjHH04,18649 +scipy/stats/distributions.py,sha256=9Kt2fyTohorJcf6a7M9DYH8Nu4jEU66nKP01cRhKmuE,859 +scipy/stats/kde.py,sha256=8ZThSc3lz-l1Gb2jzIvy1J87_HTd7eXzxuPLClVpo7c,516 +scipy/stats/morestats.py,sha256=GdMXz4MSuPp7hsff_DoijVtFsCEyy6J3_M7BITKGiP4,973 +scipy/stats/mstats.py,sha256=aRbrykjrvl-qOBkmGjlFMH4rbWYSqBBQHReanSAomFg,2466 +scipy/stats/mstats_basic.py,sha256=PjgL37PCPwiDx_ptqnmKXc1W3QGlRjjPrG0nI5FA4So,1394 +scipy/stats/mstats_extras.py,sha256=925lNnnf_NTRoyAnXql-k9syzhv7MF6T2kPGsdE2FHc,721 +scipy/stats/mvn.py,sha256=pOcB_Dd_DHpfbYnuJKq-wqmNNGCun1M0294xK1bX0KQ,498 +scipy/stats/qmc.py,sha256=b6gLkc_FSm11Ssb9uIai4XxLk4XL_qqK6Jc2k4RSeN0,11703 +scipy/stats/sampling.py,sha256=VYwxxGosFs-T3qdCmdw4tJYEFLlegwj-JgDin7iwndE,1939 +scipy/stats/stats.py,sha256=EgWjDdnlfCRKJymUcBDvMvPn0ZLO3G_ml1XJ7wvMbCI,1512 +scipy/stats/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scipy/stats/tests/__pycache__/__init__.cpython-310.pyc,, +scipy/stats/tests/__pycache__/common_tests.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_axis_nan_policy.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_binned_statistic.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_censored_data.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_contingency.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_continuous.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_continuous_basic.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_continuous_fit_censored.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_correlation.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_crosstab.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_discrete_basic.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_discrete_distns.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_distributions.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_entropy.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_fast_gen_inversion.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_fit.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_hypotests.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_kdeoth.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_mgc.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_morestats.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_mstats_basic.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_mstats_extras.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_multicomp.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_multivariate.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_odds_ratio.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_qmc.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_rank.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_relative_risk.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_resampling.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_sampling.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_sensitivity_analysis.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_stats.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_survival.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_tukeylambda_stats.cpython-310.pyc,, +scipy/stats/tests/__pycache__/test_variation.cpython-310.pyc,, +scipy/stats/tests/common_tests.py,sha256=YN4v0L134k9B-QphMZECDUv5COfjGILIaQ9Su5qV8Zs,12434 +scipy/stats/tests/data/__pycache__/_mvt.cpython-310.pyc,, +scipy/stats/tests/data/__pycache__/fisher_exact_results_from_r.cpython-310.pyc,, +scipy/stats/tests/data/_mvt.py,sha256=OvFCmMqI74DWIgo32UV55dP1nzvFvYBSyYcmKJes9pI,6905 +scipy/stats/tests/data/fisher_exact_results_from_r.py,sha256=BKxPAi4h3IOebcZYGxCbutYuAX0tlb40P0DEkfEi918,27349 +scipy/stats/tests/data/jf_skew_t_gamlss_pdf_data.npy,sha256=JU0t7kpNVHuTMcYCQ8b8_K_9JsixBNCNT2BFp2RbO7o,4064 +scipy/stats/tests/data/levy_stable/stable-Z1-cdf-sample-data.npy,sha256=zxjB8tZaIyvyxxISgt8xvyqL6Cevr8TtgQ7TdFfuiYo,183728 +scipy/stats/tests/data/levy_stable/stable-Z1-pdf-sample-data.npy,sha256=_umVErq0zMZWm0e5JOSwNOHNurViT6_H4SBki9X3oSg,183688 +scipy/stats/tests/data/levy_stable/stable-loc-scale-sample-data.npy,sha256=88cZ7dVDH7nnuey20Z48p6kJUpi9GfImaFsPykDwwHM,9328 +scipy/stats/tests/data/nist_anova/AtmWtAg.dat,sha256=Qdd0i7H4cNhAABfFOZPuplhi_9SCquFpO-hNkyRcMD8,3063 +scipy/stats/tests/data/nist_anova/SiRstv.dat,sha256=x9wJ2g1qnzf4DK_w9F_WiOiDMDEg4td2z6uU77G07xM,1947 +scipy/stats/tests/data/nist_anova/SmLs01.dat,sha256=KdnJedRthF7XLA-w7XkIPIMTgzu89yBAMmZA2H4uQOQ,6055 +scipy/stats/tests/data/nist_anova/SmLs02.dat,sha256=nCPyxRk1dAoSPWiC7kG4dLaXs2GL3-KRXRt2NwgXoIA,46561 +scipy/stats/tests/data/nist_anova/SmLs03.dat,sha256=6yPHiQSk0KI4oURQOk99t-uEm-IZN-8eIPHb_y0mQ1U,451566 +scipy/stats/tests/data/nist_anova/SmLs04.dat,sha256=fI-HpgJF9cdGdBinclhVzOcWCCc5ZJZuXalUwirV-lc,6815 +scipy/stats/tests/data/nist_anova/SmLs05.dat,sha256=iJTaAWUFn7DPLTd9bQh_EMKEK1DPG0fnN8xk7BQlPRE,53799 +scipy/stats/tests/data/nist_anova/SmLs06.dat,sha256=riOkYT-LRgmJhPpCK32x7xYnD38gwnh_Eo1X8OK3eN8,523605 +scipy/stats/tests/data/nist_anova/SmLs07.dat,sha256=QtSS11d-vkVvqaIEeJ6oNwyET1CKoyQqjlfBl2sTOJA,7381 +scipy/stats/tests/data/nist_anova/SmLs08.dat,sha256=qrxQQ0I6gnhrefygKwT48x-bz-8laD8Vpn7c81nITRg,59228 +scipy/stats/tests/data/nist_anova/SmLs09.dat,sha256=qmELOQyNlH7CWOMt8PQ0Z_yxgg9Hxc4lqZOuHZxxWuc,577633 +scipy/stats/tests/data/nist_linregress/Norris.dat,sha256=zD_RTRxfqJHVZTAAyddzLDDbhCzKSfwFGr3hwZ1nq30,2591 +scipy/stats/tests/data/rel_breitwigner_pdf_sample_data_ROOT.npy,sha256=7vTccC3YxuMcGMdOH4EoTD6coqtQKC3jnJrTC3u4520,38624 +scipy/stats/tests/data/studentized_range_mpmath_ref.json,sha256=icZGNBodwmJNzOyEki9MreI2lS6nQJNWfnVJiHRNRNM,29239 +scipy/stats/tests/test_axis_nan_policy.py,sha256=gY4fbPZ5CQcLh6ThXVKBPIkhODT_9YobZ29x5SDREps,58567 +scipy/stats/tests/test_binned_statistic.py,sha256=WE5KdJq4zJxZ1LuYp8lv-RMcTEyjuSkjvFHWsGMujkM,18814 +scipy/stats/tests/test_censored_data.py,sha256=pAQfSHhmcetcxoS1ZgIHVm1pEbapW7az7I-y_8phb5w,6935 +scipy/stats/tests/test_contingency.py,sha256=00QIN99yybM_HhrLf8kck85gWPUAQmYIKI7XnVzPF94,10937 +scipy/stats/tests/test_continuous.py,sha256=xqtMvLk_0evu7JXfD3m99XB4aGOb86xfZ_vt0LRTo90,79370 +scipy/stats/tests/test_continuous_basic.py,sha256=DUoZd6JkrtNUdOw73pO7BZRPUQUlxXRV9nGag-HzDh8,42878 +scipy/stats/tests/test_continuous_fit_censored.py,sha256=7hu1sSo9hhh0g9pmPMmjj2BI2rkxvA1h20XdMYZeyog,24188 +scipy/stats/tests/test_correlation.py,sha256=I_iO0q5jqRa7yWMexR5hDdoeSuJS73HIUjOzzZUpBxE,3507 +scipy/stats/tests/test_crosstab.py,sha256=2zqnoWW70MkvFjxAQlpW4vzWI624rcYLAlAVf7vZ9DU,3906 +scipy/stats/tests/test_discrete_basic.py,sha256=8STriXyCJE6f0CevuI4PYbfISory6pi1KQdqpMShtzg,21022 +scipy/stats/tests/test_discrete_distns.py,sha256=OZcCMkh7FgabSKw_N0G3ZT_dYolSqnq3DRXjvHpFKso,25261 +scipy/stats/tests/test_distributions.py,sha256=w9LvKcRjZq-ezZS4g6TL2VxlqJOOBL2k8jDWXuZKwac,412126 +scipy/stats/tests/test_entropy.py,sha256=bQ2Rj43zrILlrWDw7tAzDntQNC-t8RhDemXt2HAdfS4,13953 +scipy/stats/tests/test_fast_gen_inversion.py,sha256=AD3Ae0tiT9mn2rljErvfCEfEG0TlAZfL_nufQuhnDBc,15935 +scipy/stats/tests/test_fit.py,sha256=XN7xEz1RbTNqWhStlOGXJEn4wITaTS5Fe0vHvyHhCVk,48875 +scipy/stats/tests/test_hypotests.py,sha256=VTxuKnCwFCd3jPzkPJEjSk_v0Gd9yDA1skGXm2fCeIc,79978 +scipy/stats/tests/test_kdeoth.py,sha256=3SqPL5iUxqFx-GgI0g9TYVdUhnTSX3sCnJZirUrol5E,20473 +scipy/stats/tests/test_mgc.py,sha256=x8e8Y1xmBeYZSc9IXoJVSJWudUD8CCbFPe5lmCghfrw,7961 +scipy/stats/tests/test_morestats.py,sha256=HtMQ_acaYaA_UH9RFutqwOuEspSKdi685IW4FQ8b9CE,141447 +scipy/stats/tests/test_mstats_basic.py,sha256=3CUi7mahUSPQCqYBZqnVKMy7CcQ_kaL2au6KGwVyWgc,87293 +scipy/stats/tests/test_mstats_extras.py,sha256=CCexzT1lksTG_WvGvHn6-CuWd_ZXoFviNGnBZd_hE7Y,7297 +scipy/stats/tests/test_multicomp.py,sha256=s5mL9NQMvD4khQ12n2_maXKX9Q5pI0HFjcaYMZyhcJ0,17826 +scipy/stats/tests/test_multivariate.py,sha256=-QIaPK97iADoy9mOKhOT7IZfp1Ap1Rzho6iBlObYcP4,160290 +scipy/stats/tests/test_odds_ratio.py,sha256=ZII-yvP_vhuaNa3qPB0Q5lh9yzRF-08ZcdkAwuu5E94,6727 +scipy/stats/tests/test_qmc.py,sha256=Y_X-H7dXX88Bl-YaxYLtvzOoNpLYuvl2k-4nNpsjRXU,57529 +scipy/stats/tests/test_rank.py,sha256=TL5pC9C5dULvoYOf4droiEmaSglSOlMZ4h88yzLRHy4,11793 +scipy/stats/tests/test_relative_risk.py,sha256=jzOGNQ2y9_YfFnXiGAiRDrgahy66qQkw6ZkHgygCJMA,3646 +scipy/stats/tests/test_resampling.py,sha256=OQQ31s1EviAaab7pcTc2jQS8rWCTg9-kdaxRapqRqVs,82429 +scipy/stats/tests/test_sampling.py,sha256=icj26ffwNkFRje6jpWQ2HnPr57nfWUSiP8bwU8mZIgo,54540 +scipy/stats/tests/test_sensitivity_analysis.py,sha256=nNF_B6Zl5YxmvppI8TEPOGroDsbgyLTF6jBmdJH2AUw,10678 +scipy/stats/tests/test_stats.py,sha256=g9zLhnOaYgJjeu84fdOFuWFRL8jwz5GBL_WJogVy8_A,413686 +scipy/stats/tests/test_survival.py,sha256=Wmig-n93Y2wCuye9btK4QqXwUAdzF0xR_MO9iYZARjU,21958 +scipy/stats/tests/test_tukeylambda_stats.py,sha256=6WUBNVoTseVjfrHfWXtU11gTgmRcdnwAPLQOI0y_5U8,3231 +scipy/stats/tests/test_variation.py,sha256=0kSCLGFi7sgEwLf6hf1LRSHCRgLNANQ5SMigh_zxv5s,9202 +scipy/version.py,sha256=onH968TxWuVmzTyMqWhrz1LhI91dIlSxYYrL2dQpLrw,318 diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/REQUESTED b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/scipy-1.15.3.dist-info/WHEEL b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..4e4c38ae320920b8f083b87f408214cdecd350d2 --- /dev/null +++ b/lib/python3.10/site-packages/scipy-1.15.3.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: meson +Root-Is-Purelib: false +Tag: cp310-cp310-manylinux_2_17_x86_64 +Tag: cp310-cp310-manylinux2014_x86_64 + diff --git a/lib/python3.10/site-packages/scipy/__config__.py b/lib/python3.10/site-packages/scipy/__config__.py new file mode 100644 index 0000000000000000000000000000000000000000..3c30a5796789325bd3ce38f3657210f8d319ea88 --- /dev/null +++ b/lib/python3.10/site-packages/scipy/__config__.py @@ -0,0 +1,161 @@ +# This file is generated by SciPy's build process +# It contains system_info results at the time of building this package. +from enum import Enum + +__all__ = ["show"] +_built_with_meson = True + + +class DisplayModes(Enum): + stdout = "stdout" + dicts = "dicts" + + +def _cleanup(d): + """ + Removes empty values in a `dict` recursively + This ensures we remove values that Meson could not provide to CONFIG + """ + if isinstance(d, dict): + return { k: _cleanup(v) for k, v in d.items() if v != '' and _cleanup(v) != '' } + else: + return d + + +CONFIG = _cleanup( + { + "Compilers": { + "c": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "13.3.0", + "commands": r"/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/_build_env/bin/x86_64-conda-linux-gnu-cc", + "args": r"-march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix, -DNDEBUG, -D_FORTIFY_SOURCE=2, -O2, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + "linker args": r"-Wl,-O2, -Wl,--sort-common, -Wl,--as-needed, -Wl,-z,relro, -Wl,-z,now, -Wl,--disable-new-dtags, -Wl,--gc-sections, -Wl,--allow-shlib-undefined, -Wl,-rpath,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -Wl,-rpath-link,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -L/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix, -DNDEBUG, -D_FORTIFY_SOURCE=2, -O2, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + }, + "cython": { + "name": r"cython", + "linker": r"cython", + "version": r"3.0.12", + "commands": r"cython", + "args": r"", + "linker args": r"", + }, + "c++": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "13.3.0", + "commands": r"/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/_build_env/bin/x86_64-conda-linux-gnu-c++", + "args": r"-fvisibility-inlines-hidden, -fmessage-length=0, -march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix, -DNDEBUG, -D_FORTIFY_SOURCE=2, -O2, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + "linker args": r"-Wl,-O2, -Wl,--sort-common, -Wl,--as-needed, -Wl,-z,relro, -Wl,-z,now, -Wl,--disable-new-dtags, -Wl,--gc-sections, -Wl,--allow-shlib-undefined, -Wl,-rpath,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -Wl,-rpath-link,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -L/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -fvisibility-inlines-hidden, -fmessage-length=0, -march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix, -DNDEBUG, -D_FORTIFY_SOURCE=2, -O2, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + }, + "fortran": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "13.3.0", + "commands": r"/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/_build_env/bin/x86_64-conda-linux-gnu-gfortran", + "args": r"-march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -I/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/_build_env/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix", + "linker args": r"-Wl,-O2, -Wl,--sort-common, -Wl,--as-needed, -Wl,-z,relro, -Wl,-z,now, -Wl,--disable-new-dtags, -Wl,--gc-sections, -Wl,--allow-shlib-undefined, -Wl,-rpath,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -Wl,-rpath-link,/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -L/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib, -march=nocona, -mtune=haswell, -ftree-vectorize, -fPIC, -fstack-protector-strong, -fno-plt, -O2, -ffunction-sections, -pipe, -isystem, /home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include, -I/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/_build_env/include, -fdebug-prefix-map=/home/conda/feedstock_root/build_artifacts/scipy-split_1739790642651/work=/usr/local/src/conda/scipy-split-1.15.2, -fdebug-prefix-map=/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe=/usr/local/src/conda-prefix", + }, + "pythran": { + "version": r"0.17.0", + "include directory": r"../../_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold/lib/python3.10/site-packages/pythran" + }, + }, + "Machine Information": { + "host": { + "cpu": r"x86_64", + "family": r"x86_64", + "endian": r"little", + "system": r"linux", + }, + "build": { + "cpu": r"x86_64", + "family": r"x86_64", + "endian": r"little", + "system": r"linux", + }, + "cross-compiled": bool("False".lower().replace('false', '')), + }, + "Build Dependencies": { + "blas": { + "name": "blas", + "found": bool("True".lower().replace('false', '')), + "version": "3.9.0", + "detection method": "pkgconfig", + "include directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + "lib directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib", + "openblas configuration": r"unknown", + "pc file directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib/pkgconfig", + }, + "lapack": { + "name": "lapack", + "found": bool("True".lower().replace('false', '')), + "version": "3.9.0", + "detection method": "pkgconfig", + "include directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + "lib directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib", + "openblas configuration": r"unknown", + "pc file directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/lib/pkgconfig", + }, + "pybind11": { + "name": "pybind11", + "version": "2.13.6", + "detection method": "pkgconfig", + "include directory": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/include", + }, + }, + "Python Information": { + "path": r"/home/aioz-nghiale/anaconda3/envs/testing_softzoo_pointe/bin/python", + "version": "3.10", + }, + } +) + + +def _check_pyyaml(): + import yaml + + return yaml + + +def show(mode=DisplayModes.stdout.value): + """ + Show libraries and system information on which SciPy was built + and is being used + + Parameters + ---------- + mode : {`'stdout'`, `'dicts'`}, optional. + Indicates how to display the config information. + `'stdout'` prints to console, `'dicts'` returns a dictionary + of the configuration. + + Returns + ------- + out : {`dict`, `None`} + If mode is `'dicts'`, a dict is returned, else None + + Notes + ----- + 1. The `'stdout'` mode will give more readable + output if ``pyyaml`` is installed + + """ + if mode == DisplayModes.stdout.value: + try: # Non-standard library, check import + yaml = _check_pyyaml() + + print(yaml.dump(CONFIG)) + except ModuleNotFoundError: + import warnings + import json + + warnings.warn("Install `pyyaml` for better output", stacklevel=1) + print(json.dumps(CONFIG, indent=2)) + elif mode == DisplayModes.dicts.value: + return CONFIG + else: + raise AttributeError( + f"Invalid `mode`, use one of: {', '.join([e.value for e in DisplayModes])}" + ) diff --git a/lib/python3.10/site-packages/scipy/__init__.py b/lib/python3.10/site-packages/scipy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d17c59d80a8e4ad7c8972a26b75cd3fb43a64ab5 --- /dev/null +++ b/lib/python3.10/site-packages/scipy/__init__.py @@ -0,0 +1,141 @@ +""" +SciPy: A scientific computing package for Python +================================================ + +Documentation is available in the docstrings and +online at https://docs.scipy.org. + +Subpackages +----------- +Using any of these subpackages requires an explicit import. For example, +``import scipy.cluster``. + +:: + + cluster --- Vector Quantization / Kmeans + constants --- Physical and mathematical constants and units + datasets --- Dataset methods + differentiate --- Finite difference differentiation tools + fft --- Discrete Fourier transforms + fftpack --- Legacy discrete Fourier transforms + integrate --- Integration routines + interpolate --- Interpolation Tools + io --- Data input and output + linalg --- Linear algebra routines + ndimage --- N-D image package + odr --- Orthogonal Distance Regression + optimize --- Optimization Tools + signal --- Signal Processing Tools + sparse --- Sparse Matrices + spatial --- Spatial data structures and algorithms + special --- Special functions + stats --- Statistical Functions + +Public API in the main SciPy namespace +-------------------------------------- +:: + + __version__ --- SciPy version string + LowLevelCallable --- Low-level callback function + show_config --- Show scipy build configuration + test --- Run scipy unittests + +""" + +import importlib as _importlib + +from numpy import __version__ as __numpy_version__ + + +try: + from scipy.__config__ import show as show_config +except ImportError as e: + msg = """Error importing SciPy: you cannot import SciPy while + being in scipy source directory; please exit the SciPy source + tree first and relaunch your Python interpreter.""" + raise ImportError(msg) from e + + +from scipy.version import version as __version__ + + +# Allow distributors to run custom init code +from . import _distributor_init +del _distributor_init + + +from scipy._lib import _pep440 +# In maintenance branch, change to np_maxversion N+3 if numpy is at N +np_minversion = '1.23.5' +np_maxversion = '2.5.0' +if (_pep440.parse(__numpy_version__) < _pep440.Version(np_minversion) or + _pep440.parse(__numpy_version__) >= _pep440.Version(np_maxversion)): + import warnings + warnings.warn(f"A NumPy version >={np_minversion} and <{np_maxversion}" + f" is required for this version of SciPy (detected " + f"version {__numpy_version__})", + UserWarning, stacklevel=2) +del _pep440 + + +# This is the first import of an extension module within SciPy. If there's +# a general issue with the install, such that extension modules are missing +# or cannot be imported, this is where we'll get a failure - so give an +# informative error message. +try: + from scipy._lib._ccallback import LowLevelCallable +except ImportError as e: + msg = "The `scipy` install you are using seems to be broken, " + \ + "(extension modules cannot be imported), " + \ + "please try reinstalling." + raise ImportError(msg) from e + + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester + + +submodules = [ + 'cluster', + 'constants', + 'datasets', + 'differentiate', + 'fft', + 'fftpack', + 'integrate', + 'interpolate', + 'io', + 'linalg', + 'ndimage', + 'odr', + 'optimize', + 'signal', + 'sparse', + 'spatial', + 'special', + 'stats' +] + +__all__ = submodules + [ + 'LowLevelCallable', + 'test', + 'show_config', + '__version__', +] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + if name in submodules: + return _importlib.import_module(f'scipy.{name}') + else: + try: + return globals()[name] + except KeyError: + raise AttributeError( + f"Module 'scipy' has no attribute '{name}'" + ) diff --git a/lib/python3.10/site-packages/scipy/_distributor_init.py b/lib/python3.10/site-packages/scipy/_distributor_init.py new file mode 100644 index 0000000000000000000000000000000000000000..5df134975aa27d31beaff74c3cbfd2d3fb0a55dd --- /dev/null +++ b/lib/python3.10/site-packages/scipy/_distributor_init.py @@ -0,0 +1,18 @@ +""" Distributor init file + +Distributors: you can replace the contents of this file with your own custom +code to support particular distributions of SciPy. + +For example, this is a good place to put any checks for hardware requirements +or BLAS/LAPACK library initialization. + +The SciPy standard source distribution will not put code in this file beyond +the try-except import of `_distributor_init_local` (which is not part of a +standard source distribution), so you can safely replace this file with your +own version. +""" + +try: + from . import _distributor_init_local # noqa: F401 +except ImportError: + pass diff --git a/lib/python3.10/site-packages/scipy/conftest.py b/lib/python3.10/site-packages/scipy/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..05129585b8f7041c202f1ae8a1d47d56336b4b67 --- /dev/null +++ b/lib/python3.10/site-packages/scipy/conftest.py @@ -0,0 +1,552 @@ +# Pytest customization +import json +import os +import warnings +import tempfile +from contextlib import contextmanager + +import numpy as np +import numpy.testing as npt +import pytest +import hypothesis + +from scipy._lib._fpumode import get_fpu_mode +from scipy._lib._testutils import FPUModeChangeWarning +from scipy._lib._array_api import SCIPY_ARRAY_API, SCIPY_DEVICE +from scipy._lib import _pep440 + +try: + from scipy_doctest.conftest import dt_config + HAVE_SCPDT = True +except ModuleNotFoundError: + HAVE_SCPDT = False + +try: + import pytest_run_parallel # noqa:F401 + PARALLEL_RUN_AVAILABLE = True +except Exception: + PARALLEL_RUN_AVAILABLE = False + + +def pytest_configure(config): + config.addinivalue_line("markers", + "slow: Tests that are very slow.") + config.addinivalue_line("markers", + "xslow: mark test as extremely slow (not run unless explicitly requested)") + config.addinivalue_line("markers", + "xfail_on_32bit: mark test as failing on 32-bit platforms") + try: + import pytest_timeout # noqa:F401 + except Exception: + config.addinivalue_line( + "markers", 'timeout: mark a test for a non-default timeout') + try: + # This is a more reliable test of whether pytest_fail_slow is installed + # When I uninstalled it, `import pytest_fail_slow` didn't fail! + from pytest_fail_slow import parse_duration # type: ignore[import-not-found] # noqa:F401,E501 + except Exception: + config.addinivalue_line( + "markers", 'fail_slow: mark a test for a non-default timeout failure') + config.addinivalue_line("markers", + "skip_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " + "exceptions=None): " + "mark the desired skip configuration for the `skip_xp_backends` fixture.") + config.addinivalue_line("markers", + "xfail_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " + "exceptions=None): " + "mark the desired xfail configuration for the `xfail_xp_backends` fixture.") + if not PARALLEL_RUN_AVAILABLE: + config.addinivalue_line( + 'markers', + 'parallel_threads(n): run the given test function in parallel ' + 'using `n` threads.') + config.addinivalue_line( + "markers", + "thread_unsafe: mark the test function as single-threaded", + ) + config.addinivalue_line( + "markers", + "iterations(n): run the given test function `n` times in each thread", + ) + + +def pytest_runtest_setup(item): + mark = item.get_closest_marker("xslow") + if mark is not None: + try: + v = int(os.environ.get('SCIPY_XSLOW', '0')) + except ValueError: + v = False + if not v: + pytest.skip("very slow test; " + "set environment variable SCIPY_XSLOW=1 to run it") + mark = item.get_closest_marker("xfail_on_32bit") + if mark is not None and np.intp(0).itemsize < 8: + pytest.xfail(f'Fails on our 32-bit test platform(s): {mark.args[0]}') + + # Older versions of threadpoolctl have an issue that may lead to this + # warning being emitted, see gh-14441 + with npt.suppress_warnings() as sup: + sup.filter(pytest.PytestUnraisableExceptionWarning) + + try: + from threadpoolctl import threadpool_limits + + HAS_THREADPOOLCTL = True + except Exception: # observed in gh-14441: (ImportError, AttributeError) + # Optional dependency only. All exceptions are caught, for robustness + HAS_THREADPOOLCTL = False + + if HAS_THREADPOOLCTL: + # Set the number of openmp threads based on the number of workers + # xdist is using to prevent oversubscription. Simplified version of what + # sklearn does (it can rely on threadpoolctl and its builtin OpenMP helper + # functions) + try: + xdist_worker_count = int(os.environ['PYTEST_XDIST_WORKER_COUNT']) + except KeyError: + # raises when pytest-xdist is not installed + return + + if not os.getenv('OMP_NUM_THREADS'): + max_openmp_threads = os.cpu_count() // 2 # use nr of physical cores + threads_per_worker = max(max_openmp_threads // xdist_worker_count, 1) + try: + threadpool_limits(threads_per_worker, user_api='blas') + except Exception: + # May raise AttributeError for older versions of OpenBLAS. + # Catch any error for robustness. + return + + +@pytest.fixture(scope="function", autouse=True) +def check_fpu_mode(request): + """ + Check FPU mode was not changed during the test. + """ + old_mode = get_fpu_mode() + yield + new_mode = get_fpu_mode() + + if old_mode != new_mode: + warnings.warn(f"FPU mode changed from {old_mode:#x} to {new_mode:#x} during " + "the test", + category=FPUModeChangeWarning, stacklevel=0) + + +if not PARALLEL_RUN_AVAILABLE: + @pytest.fixture + def num_parallel_threads(): + return 1 + + +# Array API backend handling +xp_available_backends = {'numpy': np} + +if SCIPY_ARRAY_API and isinstance(SCIPY_ARRAY_API, str): + # fill the dict of backends with available libraries + try: + import array_api_strict + xp_available_backends.update({'array_api_strict': array_api_strict}) + if _pep440.parse(array_api_strict.__version__) < _pep440.Version('2.0'): + raise ImportError("array-api-strict must be >= version 2.0") + array_api_strict.set_array_api_strict_flags( + api_version='2023.12' + ) + except ImportError: + pass + + try: + import torch # type: ignore[import-not-found] + xp_available_backends.update({'torch': torch}) + # can use `mps` or `cpu` + torch.set_default_device(SCIPY_DEVICE) + except ImportError: + pass + + try: + import cupy # type: ignore[import-not-found] + xp_available_backends.update({'cupy': cupy}) + except ImportError: + pass + + try: + import jax.numpy # type: ignore[import-not-found] + xp_available_backends.update({'jax.numpy': jax.numpy}) + jax.config.update("jax_enable_x64", True) + jax.config.update("jax_default_device", jax.devices(SCIPY_DEVICE)[0]) + except ImportError: + pass + + # by default, use all available backends + if SCIPY_ARRAY_API.lower() not in ("1", "true"): + SCIPY_ARRAY_API_ = json.loads(SCIPY_ARRAY_API) + + if 'all' in SCIPY_ARRAY_API_: + pass # same as True + else: + # only select a subset of backend by filtering out the dict + try: + xp_available_backends = { + backend: xp_available_backends[backend] + for backend in SCIPY_ARRAY_API_ + } + except KeyError: + msg = f"'--array-api-backend' must be in {xp_available_backends.keys()}" + raise ValueError(msg) + +if 'cupy' in xp_available_backends: + SCIPY_DEVICE = 'cuda' + +array_api_compatible = pytest.mark.parametrize("xp", xp_available_backends.values()) + +skip_xp_invalid_arg = pytest.mark.skipif(SCIPY_ARRAY_API, + reason = ('Test involves masked arrays, object arrays, or other types ' + 'that are not valid input when `SCIPY_ARRAY_API` is used.')) + + +def _backends_kwargs_from_request(request, skip_or_xfail): + """A helper for {skip,xfail}_xp_backends""" + # do not allow multiple backends + args_ = request.keywords[f'{skip_or_xfail}_xp_backends'].args + if len(args_) > 1: + # np_only / cpu_only has args=(), otherwise it's ('numpy',) + # and we do not allow ('numpy', 'cupy') + raise ValueError(f"multiple backends: {args_}") + + markers = list(request.node.iter_markers(f'{skip_or_xfail}_xp_backends')) + backends = [] + kwargs = {} + for marker in markers: + if marker.kwargs.get('np_only'): + kwargs['np_only'] = True + kwargs['exceptions'] = marker.kwargs.get('exceptions', []) + elif marker.kwargs.get('cpu_only'): + if not kwargs.get('np_only'): + # if np_only is given, it is certainly cpu only + kwargs['cpu_only'] = True + kwargs['exceptions'] = marker.kwargs.get('exceptions', []) + + # add backends, if any + if len(marker.args) > 0: + backend = marker.args[0] # was a tuple, ('numpy',) etc + backends.append(backend) + kwargs.update(**{backend: marker.kwargs}) + + return backends, kwargs + + +@pytest.fixture +def skip_xp_backends(xp, request): + """skip_xp_backends(backend=None, reason=None, np_only=False, cpu_only=False, exceptions=None) + + Skip a decorated test for the provided backend, or skip a category of backends. + + See ``skip_or_xfail_backends`` docstring for details. Note that, contrary to + ``skip_or_xfail_backends``, the ``backend`` and ``reason`` arguments are optional + single strings: this function only skips a single backend at a time. + To skip multiple backends, provide multiple decorators. + """ # noqa: E501 + if "skip_xp_backends" not in request.keywords: + return + + backends, kwargs = _backends_kwargs_from_request(request, skip_or_xfail='skip') + skip_or_xfail_xp_backends(xp, backends, kwargs, skip_or_xfail='skip') + + +@pytest.fixture +def xfail_xp_backends(xp, request): + """xfail_xp_backends(backend=None, reason=None, np_only=False, cpu_only=False, exceptions=None) + + xfail a decorated test for the provided backend, or xfail a category of backends. + + See ``skip_or_xfail_backends`` docstring for details. Note that, contrary to + ``skip_or_xfail_backends``, the ``backend`` and ``reason`` arguments are optional + single strings: this function only xfails a single backend at a time. + To xfail multiple backends, provide multiple decorators. + """ # noqa: E501 + if "xfail_xp_backends" not in request.keywords: + return + backends, kwargs = _backends_kwargs_from_request(request, skip_or_xfail='xfail') + skip_or_xfail_xp_backends(xp, backends, kwargs, skip_or_xfail='xfail') + + +def skip_or_xfail_xp_backends(xp, backends, kwargs, skip_or_xfail='skip'): + """ + Skip based on the ``skip_xp_backends`` or ``xfail_xp_backends`` marker. + + See the "Support for the array API standard" docs page for usage examples. + + Parameters + ---------- + backends : tuple + Backends to skip/xfail, e.g. ``("array_api_strict", "torch")``. + These are overriden when ``np_only`` is ``True``, and are not + necessary to provide for non-CPU backends when ``cpu_only`` is ``True``. + For a custom reason to apply, you should pass a dict ``{'reason': '...'}`` + to a keyword matching the name of the backend. + reason : str, optional + A reason for the skip/xfail in the case of ``np_only=True``. + If unprovided, a default reason is used. Note that it is not possible + to specify a custom reason with ``cpu_only``. + np_only : bool, optional + When ``True``, the test is skipped/xfailed for all backends other + than the default NumPy backend. There is no need to provide + any ``backends`` in this case. To specify a reason, pass a + value to ``reason``. Default: ``False``. + cpu_only : bool, optional + When ``True``, the test is skipped/xfailed on non-CPU devices. + There is no need to provide any ``backends`` in this case, + but any ``backends`` will also be skipped on the CPU. + Default: ``False``. + exceptions : list, optional + A list of exceptions for use with ``cpu_only`` or ``np_only``. + This should be provided when delegation is implemented for some, + but not all, non-CPU/non-NumPy backends. + skip_or_xfail : str + ``'skip'`` to skip, ``'xfail'`` to xfail. + """ + skip_or_xfail = getattr(pytest, skip_or_xfail) + np_only = kwargs.get("np_only", False) + cpu_only = kwargs.get("cpu_only", False) + exceptions = kwargs.get("exceptions", []) + + if reasons := kwargs.get("reasons"): + raise ValueError(f"provide a single `reason=` kwarg; got {reasons=} instead") + + # input validation + if np_only and cpu_only: + # np_only is a stricter subset of cpu_only + cpu_only = False + if exceptions and not (cpu_only or np_only): + raise ValueError("`exceptions` is only valid alongside `cpu_only` or `np_only`") + + if np_only: + reason = kwargs.get("reason", "do not run with non-NumPy backends.") + if not isinstance(reason, str) and len(reason) > 1: + raise ValueError("please provide a singleton `reason` " + "when using `np_only`") + if xp.__name__ != 'numpy' and xp.__name__ not in exceptions: + skip_or_xfail(reason=reason) + return + if cpu_only: + reason = ("no array-agnostic implementation or delegation available " + "for this backend and device") + exceptions = [] if exceptions is None else exceptions + if SCIPY_ARRAY_API and SCIPY_DEVICE != 'cpu': + if xp.__name__ == 'cupy' and 'cupy' not in exceptions: + skip_or_xfail(reason=reason) + elif xp.__name__ == 'torch' and 'torch' not in exceptions: + if 'cpu' not in xp.empty(0).device.type: + skip_or_xfail(reason=reason) + elif xp.__name__ == 'jax.numpy' and 'jax.numpy' not in exceptions: + for d in xp.empty(0).devices(): + if 'cpu' not in d.device_kind: + skip_or_xfail(reason=reason) + + if backends is not None: + for i, backend in enumerate(backends): + if xp.__name__ == backend: + reason = kwargs[backend].get('reason') + if not reason: + reason = f"do not run with array API backend: {backend}" + + skip_or_xfail(reason=reason) + + +# Following the approach of NumPy's conftest.py... +# Use a known and persistent tmpdir for hypothesis' caches, which +# can be automatically cleared by the OS or user. +hypothesis.configuration.set_hypothesis_home_dir( + os.path.join(tempfile.gettempdir(), ".hypothesis") +) + +# We register two custom profiles for SciPy - for details see +# https://hypothesis.readthedocs.io/en/latest/settings.html +# The first is designed for our own CI runs; the latter also +# forces determinism and is designed for use via scipy.test() +hypothesis.settings.register_profile( + name="nondeterministic", deadline=None, print_blob=True, +) +hypothesis.settings.register_profile( + name="deterministic", + deadline=None, print_blob=True, database=None, derandomize=True, + suppress_health_check=list(hypothesis.HealthCheck), +) + +# Profile is currently set by environment variable `SCIPY_HYPOTHESIS_PROFILE` +# In the future, it would be good to work the choice into dev.py. +SCIPY_HYPOTHESIS_PROFILE = os.environ.get("SCIPY_HYPOTHESIS_PROFILE", + "deterministic") +hypothesis.settings.load_profile(SCIPY_HYPOTHESIS_PROFILE) + + +############################################################################ +# doctesting stuff + +if HAVE_SCPDT: + + # FIXME: populate the dict once + @contextmanager + def warnings_errors_and_rng(test=None): + """Temporarily turn (almost) all warnings to errors. + + Filter out known warnings which we allow. + """ + known_warnings = dict() + + # these functions are known to emit "divide by zero" RuntimeWarnings + divide_by_zero = [ + 'scipy.linalg.norm', 'scipy.ndimage.center_of_mass', + ] + for name in divide_by_zero: + known_warnings[name] = dict(category=RuntimeWarning, + message='divide by zero') + + # Deprecated stuff in scipy.signal and elsewhere + deprecated = [ + 'scipy.signal.cwt', 'scipy.signal.morlet', 'scipy.signal.morlet2', + 'scipy.signal.ricker', + 'scipy.integrate.simpson', + 'scipy.interpolate.interp2d', + 'scipy.linalg.kron', + ] + for name in deprecated: + known_warnings[name] = dict(category=DeprecationWarning) + + from scipy import integrate + # the functions are known to emit IntegrationWarnings + integration_w = ['scipy.special.ellip_normal', + 'scipy.special.ellip_harm_2', + ] + for name in integration_w: + known_warnings[name] = dict(category=integrate.IntegrationWarning, + message='The occurrence of roundoff') + + # scipy.stats deliberately emits UserWarnings sometimes + user_w = ['scipy.stats.anderson_ksamp', 'scipy.stats.kurtosistest', + 'scipy.stats.normaltest', 'scipy.sparse.linalg.norm'] + for name in user_w: + known_warnings[name] = dict(category=UserWarning) + + # additional one-off warnings to filter + dct = { + 'scipy.sparse.linalg.norm': + dict(category=UserWarning, message="Exited at iteration"), + # tutorials + 'linalg.rst': + dict(message='the matrix subclass is not', + category=PendingDeprecationWarning), + 'stats.rst': + dict(message='The maximum number of subdivisions', + category=integrate.IntegrationWarning), + } + known_warnings.update(dct) + + # these legitimately emit warnings in examples + legit = set('scipy.signal.normalize') + + # Now, the meat of the matter: filter warnings, + # also control the random seed for each doctest. + + # XXX: this matches the refguide-check behavior, but is a tad strange: + # makes sure that the seed the old-fashioned np.random* methods is + # *NOT* reproducible but the new-style `default_rng()` *IS* repoducible. + # Should these two be either both repro or both not repro? + + from scipy._lib._util import _fixed_default_rng + import numpy as np + with _fixed_default_rng(): + np.random.seed(None) + with warnings.catch_warnings(): + if test and test.name in known_warnings: + warnings.filterwarnings('ignore', + **known_warnings[test.name]) + yield + elif test and test.name in legit: + yield + else: + warnings.simplefilter('error', Warning) + yield + + dt_config.user_context_mgr = warnings_errors_and_rng + dt_config.skiplist = set([ + 'scipy.linalg.LinAlgError', # comes from numpy + 'scipy.fftpack.fftshift', # fftpack stuff is also from numpy + 'scipy.fftpack.ifftshift', + 'scipy.fftpack.fftfreq', + 'scipy.special.sinc', # sinc is from numpy + 'scipy.optimize.show_options', # does not have much to doctest + 'scipy.signal.normalize', # manipulates warnings (XXX temp skip) + 'scipy.sparse.linalg.norm', # XXX temp skip + # these below test things which inherit from np.ndarray + # cross-ref https://github.com/numpy/numpy/issues/28019 + 'scipy.io.matlab.MatlabObject.strides', + 'scipy.io.matlab.MatlabObject.dtype', + 'scipy.io.matlab.MatlabOpaque.dtype', + 'scipy.io.matlab.MatlabOpaque.strides', + 'scipy.io.matlab.MatlabFunction.strides', + 'scipy.io.matlab.MatlabFunction.dtype' + ]) + + # these are affected by NumPy 2.0 scalar repr: rely on string comparison + if np.__version__ < "2": + dt_config.skiplist.update(set([ + 'scipy.io.hb_read', + 'scipy.io.hb_write', + 'scipy.sparse.csgraph.connected_components', + 'scipy.sparse.csgraph.depth_first_order', + 'scipy.sparse.csgraph.shortest_path', + 'scipy.sparse.csgraph.floyd_warshall', + 'scipy.sparse.csgraph.dijkstra', + 'scipy.sparse.csgraph.bellman_ford', + 'scipy.sparse.csgraph.johnson', + 'scipy.sparse.csgraph.yen', + 'scipy.sparse.csgraph.breadth_first_order', + 'scipy.sparse.csgraph.reverse_cuthill_mckee', + 'scipy.sparse.csgraph.structural_rank', + 'scipy.sparse.csgraph.construct_dist_matrix', + 'scipy.sparse.csgraph.reconstruct_path', + 'scipy.ndimage.value_indices', + 'scipy.stats.mstats.describe', + ])) + + # help pytest collection a bit: these names are either private + # (distributions), or just do not need doctesting. + dt_config.pytest_extra_ignore = [ + "scipy.stats.distributions", + "scipy.optimize.cython_optimize", + "scipy.test", + "scipy.show_config", + # equivalent to "pytest --ignore=path/to/file" + "scipy/special/_precompute", + "scipy/interpolate/_interpnd_info.py", + "scipy/_lib/array_api_compat", + "scipy/_lib/highs", + "scipy/_lib/unuran", + "scipy/_lib/_gcutils.py", + "scipy/_lib/doccer.py", + "scipy/_lib/_uarray", + ] + + dt_config.pytest_extra_xfail = { + # name: reason + "ND_regular_grid.rst": "ReST parser limitation", + "extrapolation_examples.rst": "ReST parser limitation", + "sampling_pinv.rst": "__cinit__ unexpected argument", + "sampling_srou.rst": "nan in scalar_power", + "probability_distributions.rst": "integration warning", + } + + # tutorials + dt_config.pseudocode = set(['integrate.nquad(func,']) + dt_config.local_resources = { + 'io.rst': [ + "octave_a.mat", + "octave_cells.mat", + "octave_struct.mat" + ] + } + + dt_config.strict_check = True +############################################################################ diff --git a/lib/python3.10/site-packages/scipy/version.py b/lib/python3.10/site-packages/scipy/version.py new file mode 100644 index 0000000000000000000000000000000000000000..b8340918dc7999743f881eaf7e94404bd2df067e --- /dev/null +++ b/lib/python3.10/site-packages/scipy/version.py @@ -0,0 +1,12 @@ + +""" +Module to expose more detailed version info for the installed `scipy` +""" +version = "1.15.2" +full_version = version +short_version = version.split('.dev')[0] +git_revision = "0f1fd4a7268b813fa2b844ca6038e4dfdf90084a" +release = 'dev' not in version and '+' not in version + +if not release: + version = full_version diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/INSTALLER b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/LICENSE.md b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..b5ebba626349f543886619b6007dee5430b3f345 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/LICENSE.md @@ -0,0 +1,27 @@ +Copyright (c) 2012-2021, Michael L. Waskom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the project nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/METADATA b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..04c5105de6216ebf14235d727949d9f920c514dc --- /dev/null +++ b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/METADATA @@ -0,0 +1,116 @@ +Metadata-Version: 2.1 +Name: seaborn +Version: 0.12.2 +Summary: Statistical data visualization +Author-email: Michael Waskom +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Classifier: Intended Audience :: Science/Research +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: License :: OSI Approved :: BSD License +Classifier: Topic :: Scientific/Engineering :: Visualization +Classifier: Topic :: Multimedia :: Graphics +Classifier: Operating System :: OS Independent +Classifier: Framework :: Matplotlib +Requires-Dist: numpy>=1.17,!=1.24.0 +Requires-Dist: pandas>=0.25 +Requires-Dist: matplotlib>=3.1,!=3.6.1 +Requires-Dist: typing_extensions; python_version < '3.8' +Requires-Dist: pytest ; extra == "dev" +Requires-Dist: pytest-cov ; extra == "dev" +Requires-Dist: pytest-xdist ; extra == "dev" +Requires-Dist: flake8 ; extra == "dev" +Requires-Dist: mypy ; extra == "dev" +Requires-Dist: pandas-stubs ; extra == "dev" +Requires-Dist: pre-commit ; extra == "dev" +Requires-Dist: flit ; extra == "dev" +Requires-Dist: numpydoc ; extra == "docs" +Requires-Dist: nbconvert ; extra == "docs" +Requires-Dist: ipykernel ; extra == "docs" +Requires-Dist: sphinx-copybutton ; extra == "docs" +Requires-Dist: sphinx-issues ; extra == "docs" +Requires-Dist: sphinx-design ; extra == "docs" +Requires-Dist: pyyaml ; extra == "docs" +Requires-Dist: pydata_sphinx_theme==0.10.0rc2 ; extra == "docs" +Requires-Dist: scipy>=1.3 ; extra == "stats" +Requires-Dist: statsmodels>=0.10 ; extra == "stats" +Project-URL: Docs, http://seaborn.pydata.org +Project-URL: Source, https://github.com/mwaskom/seaborn +Provides-Extra: dev +Provides-Extra: docs +Provides-Extra: stats + +
+ +-------------------------------------- + +seaborn: statistical data visualization +======================================= + +[![PyPI Version](https://img.shields.io/pypi/v/seaborn.svg)](https://pypi.org/project/seaborn/) +[![License](https://img.shields.io/pypi/l/seaborn.svg)](https://github.com/mwaskom/seaborn/blob/master/LICENSE) +[![DOI](https://joss.theoj.org/papers/10.21105/joss.03021/status.svg)](https://doi.org/10.21105/joss.03021) +[![Tests](https://github.com/mwaskom/seaborn/workflows/CI/badge.svg)](https://github.com/mwaskom/seaborn/actions) +[![Code Coverage](https://codecov.io/gh/mwaskom/seaborn/branch/master/graph/badge.svg)](https://codecov.io/gh/mwaskom/seaborn) + +Seaborn is a Python visualization library based on matplotlib. It provides a high-level interface for drawing attractive statistical graphics. + + +Documentation +------------- + +Online documentation is available at [seaborn.pydata.org](https://seaborn.pydata.org). + +The docs include a [tutorial](https://seaborn.pydata.org/tutorial.html), [example gallery](https://seaborn.pydata.org/examples/index.html), [API reference](https://seaborn.pydata.org/api.html), [FAQ](https://seaborn.pydata.org/faq), and other useful information. + +To build the documentation locally, please refer to [`doc/README.md`](doc/README.md). + +Dependencies +------------ + +Seaborn supports Python 3.7+ and no longer supports Python 2. + +Installation requires [numpy](https://numpy.org/), [pandas](https://pandas.pydata.org/), and [matplotlib](https://matplotlib.org/). Some advanced statistical functionality requires [scipy](https://www.scipy.org/) and/or [statsmodels](https://www.statsmodels.org/). + + +Installation +------------ + +The latest stable release (and required dependencies) can be installed from PyPI: + + pip install seaborn + +It is also possible to include optional statistical dependencies (only relevant for v0.12+): + + pip install seaborn[stats] + +Seaborn can also be installed with conda: + + conda install seaborn + +Note that the main anaconda repository lags PyPI in adding new releases, but conda-forge (`-c conda-forge`) typically updates quickly. + +Citing +------ + +A paper describing seaborn has been published in the [Journal of Open Source Software](https://joss.theoj.org/papers/10.21105/joss.03021). The paper provides an introduction to the key features of the library, and it can be used as a citation if seaborn proves integral to a scientific publication. + +Testing +------- + +Testing seaborn requires installing additional dependencies; they can be installed with the `dev` extra (e.g., `pip install .[dev]`). + +To test the code, run `make test` in the source directory. This will exercise the unit tests (using [pytest](https://docs.pytest.org/)) and generate a coverage report. + +Code style is enforced with `flake8` using the settings in the [`setup.cfg`](./setup.cfg) file. Run `make lint` to check. Alternately, you can use `pre-commit` to automatically run lint checks on any files you are committing: just run `pre-commit install` to set it up, and then commit as usual going forward. + +Development +----------- + +Seaborn development takes place on Github: https://github.com/mwaskom/seaborn + +Please submit bugs that you encounter to the [issue tracker](https://github.com/mwaskom/seaborn/issues) with a reproducible example demonstrating the problem. Questions about usage are more at home on StackOverflow, where there is a [seaborn tag](https://stackoverflow.com/questions/tagged/seaborn). + diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/RECORD b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..ea5757cbf1fed323f6d0210b7fbf1795a41ea09d --- /dev/null +++ b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/RECORD @@ -0,0 +1,116 @@ +seaborn-0.12.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +seaborn-0.12.2.dist-info/LICENSE.md,sha256=CHS_wBMIgzOFsvXKYiBVJXOfale4aIr-obB40PhF1tQ,1491 +seaborn-0.12.2.dist-info/METADATA,sha256=ikxlzTpyXq62jk-v6dmeInPgCYKf57fns2V0c_JIWqo,5397 +seaborn-0.12.2.dist-info/RECORD,, +seaborn-0.12.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +seaborn-0.12.2.dist-info/WHEEL,sha256=4TfKIB_xu-04bc2iKz6_zFt-gEFEEDU_31HGhqzOCE8,81 +seaborn/__init__.py,sha256=A81xWOuWpL5yCCVu4URKV9Sv5RzMovLoxTcG0cUTtAg,744 +seaborn/__pycache__/__init__.cpython-310.pyc,, +seaborn/__pycache__/_compat.cpython-310.pyc,, +seaborn/__pycache__/_decorators.cpython-310.pyc,, +seaborn/__pycache__/_docstrings.cpython-310.pyc,, +seaborn/__pycache__/_oldcore.cpython-310.pyc,, +seaborn/__pycache__/_statistics.cpython-310.pyc,, +seaborn/__pycache__/_testing.cpython-310.pyc,, +seaborn/__pycache__/algorithms.cpython-310.pyc,, +seaborn/__pycache__/axisgrid.cpython-310.pyc,, +seaborn/__pycache__/categorical.cpython-310.pyc,, +seaborn/__pycache__/cm.cpython-310.pyc,, +seaborn/__pycache__/distributions.cpython-310.pyc,, +seaborn/__pycache__/matrix.cpython-310.pyc,, +seaborn/__pycache__/miscplot.cpython-310.pyc,, +seaborn/__pycache__/objects.cpython-310.pyc,, +seaborn/__pycache__/palettes.cpython-310.pyc,, +seaborn/__pycache__/rcmod.cpython-310.pyc,, +seaborn/__pycache__/regression.cpython-310.pyc,, +seaborn/__pycache__/relational.cpython-310.pyc,, +seaborn/__pycache__/utils.cpython-310.pyc,, +seaborn/__pycache__/widgets.cpython-310.pyc,, +seaborn/_compat.py,sha256=Qhwhr9QCfcRyG95mlwYYGbDkPawlO-vJXlidiz8ZF8M,5742 +seaborn/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +seaborn/_core/__pycache__/__init__.cpython-310.pyc,, +seaborn/_core/__pycache__/data.cpython-310.pyc,, +seaborn/_core/__pycache__/exceptions.cpython-310.pyc,, +seaborn/_core/__pycache__/groupby.cpython-310.pyc,, +seaborn/_core/__pycache__/moves.cpython-310.pyc,, +seaborn/_core/__pycache__/plot.cpython-310.pyc,, +seaborn/_core/__pycache__/properties.cpython-310.pyc,, +seaborn/_core/__pycache__/rules.cpython-310.pyc,, +seaborn/_core/__pycache__/scales.cpython-310.pyc,, +seaborn/_core/__pycache__/subplots.cpython-310.pyc,, +seaborn/_core/__pycache__/typing.cpython-310.pyc,, +seaborn/_core/data.py,sha256=x1QeNYzws7FUCfqbNQm2Q5WjLHSdjUAXcStAsjkyRiQ,9319 +seaborn/_core/exceptions.py,sha256=Z2NlNt9J9p5N0CCGfSI06fmKMkhBs-7NPGsIVPV6P_w,1179 +seaborn/_core/groupby.py,sha256=wt7v38RN7RWoJLdbLI8i3K94r3Dxhx6675HT9cafeZQ,4710 +seaborn/_core/moves.py,sha256=YRlOaXaNFu-60XUZl_sm6q2Kg8H8kkTBx7pJI3gFEbM,7550 +seaborn/_core/plot.py,sha256=xbQdWe8_3ABP3nQgY9QZ6u-C_Fgh4m0Eiej7JcioQ4o,63175 +seaborn/_core/properties.py,sha256=N8fkWzpxzGaQ3r4jn49NIzD0UFzKCCy1TNfRvGrnphE,30630 +seaborn/_core/rules.py,sha256=XVUTc3r53ovc8YCDSx3jNE9bTTGyGP8u6oe_ahJ7TZ8,5326 +seaborn/_core/scales.py,sha256=Czqw6_ocQ4g35DPmnCgCjG_AP2LcqNjy0KtZNUqa3I8,35254 +seaborn/_core/subplots.py,sha256=K4Cxp9MRbF6znxsDED_kNDSQO59LCTDf4Ea0CxD6kas,10171 +seaborn/_core/typing.py,sha256=5_83zyxdHFB1xAE2zzviCYOIlSUeBc2wOk2m5fRNbAA,1261 +seaborn/_decorators.py,sha256=bkl0uqgjl5xVATuFxx7sY5huqFmNKB0pFR639XG3qb8,485 +seaborn/_docstrings.py,sha256=v7VHiUS4Cdh20wAfUK4KVQpIO0W7E0EL2j9BWrfnCV8,6464 +seaborn/_marks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +seaborn/_marks/__pycache__/__init__.cpython-310.pyc,, +seaborn/_marks/__pycache__/area.cpython-310.pyc,, +seaborn/_marks/__pycache__/bar.cpython-310.pyc,, +seaborn/_marks/__pycache__/base.cpython-310.pyc,, +seaborn/_marks/__pycache__/dot.cpython-310.pyc,, +seaborn/_marks/__pycache__/line.cpython-310.pyc,, +seaborn/_marks/__pycache__/text.cpython-310.pyc,, +seaborn/_marks/area.py,sha256=KDfk6AVcZKj75vQU8jeaxbjm9KZCgIfjKtOznvP5dKE,5222 +seaborn/_marks/bar.py,sha256=lo_pQoxx026bmE1FfOpFAGaMC6pgrQgmmZT1Pl2-oIo,9208 +seaborn/_marks/base.py,sha256=jnbGfipBFBQvnuNkx2vkZLPmwDV32qZ1Q5goKHi5Rac,10163 +seaborn/_marks/dot.py,sha256=xJDvCwb0hkII-TKsZPehj_SOiE6YFHxAiN0siTJUzF4,6623 +seaborn/_marks/line.py,sha256=2V3xvUD6oDvDlMZ2wP1q1CivvL9XlxffSV9InydCIC8,9244 +seaborn/_marks/text.py,sha256=ewGwU63QLbYXeRlTctNQOqai7z7Q3fTh0EfiK5DkjsA,2257 +seaborn/_oldcore.py,sha256=zraj6TAySoayvlfaixzfRzfw1WLZCuORF4FRSo7u2Cc,65320 +seaborn/_statistics.py,sha256=UB9M400p1Bzy5Gye_9xywWHgtGLPTLsiYv_GXuhvUEg,19390 +seaborn/_stats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +seaborn/_stats/__pycache__/__init__.cpython-310.pyc,, +seaborn/_stats/__pycache__/aggregation.cpython-310.pyc,, +seaborn/_stats/__pycache__/base.cpython-310.pyc,, +seaborn/_stats/__pycache__/counting.cpython-310.pyc,, +seaborn/_stats/__pycache__/density.cpython-310.pyc,, +seaborn/_stats/__pycache__/order.cpython-310.pyc,, +seaborn/_stats/__pycache__/regression.cpython-310.pyc,, +seaborn/_stats/aggregation.py,sha256=CxyKJJSRhDAiX4I8jeCy5uFmj8EI-n88sOv_VQr10zQ,3267 +seaborn/_stats/base.py,sha256=t4lPmDDnKPg3VU-6EwVYOlYgSIGtB9ziL2Q5y-T-TnU,2633 +seaborn/_stats/counting.py,sha256=DoqzUKUuXclkdSlORLDZmsnKnUNpEi2FvCa1rDq0AZY,8303 +seaborn/_stats/density.py,sha256=w5po5AMnvj216un8ZuDwPzJZHc_4c1YlNVKwUn3vwqc,8676 +seaborn/_stats/order.py,sha256=tr_xcY-9NoQFugluGpq-ajhVk1wBShUY5VWCdHJF0Kc,2274 +seaborn/_stats/regression.py,sha256=WnOWNGrn8SS32XvD0oo8cRL6RBAVkY8DloJ0tefeyLs,1283 +seaborn/_testing.py,sha256=xkuz58Zq68viVadWFsnJN9jsDzl3JqIANjG0Ggjbj_o,2320 +seaborn/algorithms.py,sha256=WH95Wp0NPlYm5NOtWF0iIyYrZOQNqqDH2TS809t0TDI,4970 +seaborn/axisgrid.py,sha256=evrJBNgy2ofsomWld2PiEA7Qtsh9ziDFDR_1U_IQs7g,87520 +seaborn/categorical.py,sha256=sqWI769c3ts1v5hwZ9Ady5VJqBjzG1t93ScE39rRzGE,130018 +seaborn/cm.py,sha256=8gm1RByEFyW0F7skr1Dt_UKP25urFCsB9yQY258MtMA,66038 +seaborn/colors/__init__.py,sha256=W2YdXBHlU6xKSevYGLABkA_CjbD_FApIjspNC2vgUs4,88 +seaborn/colors/__pycache__/__init__.cpython-310.pyc,, +seaborn/colors/__pycache__/crayons.cpython-310.pyc,, +seaborn/colors/__pycache__/xkcd_rgb.cpython-310.pyc,, +seaborn/colors/crayons.py,sha256=iqNxMx4G8JP3h2zeKVlTmf1nzdxsVlz3jkIb4193fSw,4330 +seaborn/colors/xkcd_rgb.py,sha256=-AeSrxwnrtDn-GWWGu8RQA51nXWgHwKmWoljVQFAPyo,35379 +seaborn/distributions.py,sha256=EjH7qVHmWfONflaZf3W9CJVl9Q4FJGzC9HiMY--zWWQ,87232 +seaborn/external/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +seaborn/external/__pycache__/__init__.cpython-310.pyc,, +seaborn/external/__pycache__/appdirs.cpython-310.pyc,, +seaborn/external/__pycache__/docscrape.cpython-310.pyc,, +seaborn/external/__pycache__/husl.cpython-310.pyc,, +seaborn/external/__pycache__/kde.cpython-310.pyc,, +seaborn/external/__pycache__/version.cpython-310.pyc,, +seaborn/external/appdirs.py,sha256=15Pwb8UaMbE-ncA0vhMpEudT2WzQrFKVLwwKCSyXV5c,8969 +seaborn/external/docscrape.py,sha256=nBrEN2UsndJatOtCnFHKIoKpyOkVaeXKUuI6JW9xbM8,23296 +seaborn/external/husl.py,sha256=499Cdb36Y_WNv3Y6y-qr5H3wDQwaZJr4pr9slFQy1c4,6666 +seaborn/external/kde.py,sha256=DIvBtRsCE2btnMewEdPFNcuPVq13Ou2WwI2A3YNpxBQ,13726 +seaborn/external/version.py,sha256=8jek9HJb2-ZEtYd8Y30yuZYzkiskpzsD6672Ynfk0ww,13401 +seaborn/matrix.py,sha256=3k6sl-wfO9jdqHDW7JyS6bi0k3pGKx0JDp0127HvsWE,47327 +seaborn/miscplot.py,sha256=uiyb3tBQD7XrijXCqx46Zvqq4kpDPdQAfNLRFSrpX2c,1407 +seaborn/objects.py,sha256=F0UIjvmEWg5xKYJpamSg_J6yOPH0wkpxn7X6OofELwU,2208 +seaborn/palettes.py,sha256=8ZeLHzabxVNeYGvRcEfHGf8EFaW3AyvsT26eXcD77nA,27892 +seaborn/rcmod.py,sha256=9KAnif_QIPnPO2awAz7LR9jE0PvTkTjPKGFiFO__okI,15929 +seaborn/regression.py,sha256=dHC3KgkWrqYrw5qLSmXlN7kshdjXqxEjvoIHgssQ_cg,33248 +seaborn/relational.py,sha256=Fa8cuLV7C7Z3Nj1ATp7oAbjLzTvtkLgwppAUvyzzOSY,37520 +seaborn/utils.py,sha256=w03K-ctmhm7E8RP0Q8s7s7J3wdF0-gJyEhPs0Oz0BL8,28482 +seaborn/widgets.py,sha256=uOZsl7bq7aAXJoCPw6U5gICTAW7F7rTD13rVcs53TsU,14564 diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/REQUESTED b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/WHEEL b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..668ba4d0151c5c76ed6e758061daa8c1b0bf5d21 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn-0.12.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.7.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/python3.10/site-packages/seaborn/__init__.py b/lib/python3.10/site-packages/seaborn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d22e2bf30f4d9a86e0964521df541b1ebdc1f837 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/__init__.py @@ -0,0 +1,21 @@ +# Import seaborn objects +from .rcmod import * # noqa: F401,F403 +from .utils import * # noqa: F401,F403 +from .palettes import * # noqa: F401,F403 +from .relational import * # noqa: F401,F403 +from .regression import * # noqa: F401,F403 +from .categorical import * # noqa: F401,F403 +from .distributions import * # noqa: F401,F403 +from .matrix import * # noqa: F401,F403 +from .miscplot import * # noqa: F401,F403 +from .axisgrid import * # noqa: F401,F403 +from .widgets import * # noqa: F401,F403 +from .colors import xkcd_rgb, crayons # noqa: F401 +from . import cm # noqa: F401 + +# Capture the original matplotlib rcParams +import matplotlib as mpl +_orig_rc_params = mpl.rcParams.copy() + +# Define the seaborn version +__version__ = "0.12.2" diff --git a/lib/python3.10/site-packages/seaborn/_compat.py b/lib/python3.10/site-packages/seaborn/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..5819454a4e5457c9d4819a8b1435528bd9def438 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_compat.py @@ -0,0 +1,164 @@ +import numpy as np +import matplotlib as mpl +from seaborn.external.version import Version + + +def MarkerStyle(marker=None, fillstyle=None): + """ + Allow MarkerStyle to accept a MarkerStyle object as parameter. + + Supports matplotlib < 3.3.0 + https://github.com/matplotlib/matplotlib/pull/16692 + + """ + if isinstance(marker, mpl.markers.MarkerStyle): + if fillstyle is None: + return marker + else: + marker = marker.get_marker() + return mpl.markers.MarkerStyle(marker, fillstyle) + + +def norm_from_scale(scale, norm): + """Produce a Normalize object given a Scale and min/max domain limits.""" + # This is an internal maplotlib function that simplifies things to access + # It is likely to become part of the matplotlib API at some point: + # https://github.com/matplotlib/matplotlib/issues/20329 + if isinstance(norm, mpl.colors.Normalize): + return norm + + if scale is None: + return None + + if norm is None: + vmin = vmax = None + else: + vmin, vmax = norm # TODO more helpful error if this fails? + + class ScaledNorm(mpl.colors.Normalize): + + def __call__(self, value, clip=None): + # From github.com/matplotlib/matplotlib/blob/v3.4.2/lib/matplotlib/colors.py + # See github.com/matplotlib/matplotlib/tree/v3.4.2/LICENSE + value, is_scalar = self.process_value(value) + self.autoscale_None(value) + if self.vmin > self.vmax: + raise ValueError("vmin must be less or equal to vmax") + if self.vmin == self.vmax: + return np.full_like(value, 0) + if clip is None: + clip = self.clip + if clip: + value = np.clip(value, self.vmin, self.vmax) + # ***** Seaborn changes start **** + t_value = self.transform(value).reshape(np.shape(value)) + t_vmin, t_vmax = self.transform([self.vmin, self.vmax]) + # ***** Seaborn changes end ***** + if not np.isfinite([t_vmin, t_vmax]).all(): + raise ValueError("Invalid vmin or vmax") + t_value -= t_vmin + t_value /= (t_vmax - t_vmin) + t_value = np.ma.masked_invalid(t_value, copy=False) + return t_value[0] if is_scalar else t_value + + new_norm = ScaledNorm(vmin, vmax) + new_norm.transform = scale.get_transform().transform + + return new_norm + + +def scale_factory(scale, axis, **kwargs): + """ + Backwards compatability for creation of independent scales. + + Matplotlib scales require an Axis object for instantiation on < 3.4. + But the axis is not used, aside from extraction of the axis_name in LogScale. + + """ + modify_transform = False + if Version(mpl.__version__) < Version("3.4"): + if axis[0] in "xy": + modify_transform = True + axis = axis[0] + base = kwargs.pop("base", None) + if base is not None: + kwargs[f"base{axis}"] = base + nonpos = kwargs.pop("nonpositive", None) + if nonpos is not None: + kwargs[f"nonpos{axis}"] = nonpos + + if isinstance(scale, str): + class Axis: + axis_name = axis + axis = Axis() + + scale = mpl.scale.scale_factory(scale, axis, **kwargs) + + if modify_transform: + transform = scale.get_transform() + transform.base = kwargs.get("base", 10) + if kwargs.get("nonpositive") == "mask": + # Setting a private attribute, but we only get here + # on an old matplotlib, so this won't break going forwards + transform._clip = False + + return scale + + +def set_scale_obj(ax, axis, scale): + """Handle backwards compatability with setting matplotlib scale.""" + if Version(mpl.__version__) < Version("3.4"): + # The ability to pass a BaseScale instance to Axes.set_{}scale was added + # to matplotlib in version 3.4.0: GH: matplotlib/matplotlib/pull/19089 + # Workaround: use the scale name, which is restrictive only if the user + # wants to define a custom scale; they'll need to update the registry too. + if scale.name is None: + # Hack to support our custom Formatter-less CatScale + return + method = getattr(ax, f"set_{axis}scale") + kws = {} + if scale.name == "function": + trans = scale.get_transform() + kws["functions"] = (trans._forward, trans._inverse) + method(scale.name, **kws) + axis_obj = getattr(ax, f"{axis}axis") + scale.set_default_locators_and_formatters(axis_obj) + else: + ax.set(**{f"{axis}scale": scale}) + + +def get_colormap(name): + """Handle changes to matplotlib colormap interface in 3.6.""" + try: + return mpl.colormaps[name] + except AttributeError: + return mpl.cm.get_cmap(name) + + +def register_colormap(name, cmap): + """Handle changes to matplotlib colormap interface in 3.6.""" + try: + if name not in mpl.colormaps: + mpl.colormaps.register(cmap, name=name) + except AttributeError: + mpl.cm.register_cmap(name, cmap) + + +def set_layout_engine(fig, engine): + """Handle changes to auto layout engine interface in 3.6""" + if hasattr(fig, "set_layout_engine"): + fig.set_layout_engine(engine) + else: + if engine == "tight": + fig.set_tight_layout(True) + elif engine == "constrained": + fig.set_constrained_layout(True) + + +def share_axis(ax0, ax1, which): + """Handle changes to post-hoc axis sharing.""" + if Version(mpl.__version__) < Version("3.5.0"): + group = getattr(ax0, f"get_shared_{which}_axes")() + group.join(ax1, ax0) + else: + getattr(ax1, f"share{which}")(ax0) diff --git a/lib/python3.10/site-packages/seaborn/_decorators.py b/lib/python3.10/site-packages/seaborn/_decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..6d7b2e9b49c810f1483f73d06f3cac8908b576b2 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_decorators.py @@ -0,0 +1,16 @@ +from inspect import signature + + +def share_init_params_with_map(cls): + """Make cls.map a classmethod with same signature as cls.__init__.""" + map_sig = signature(cls.map) + init_sig = signature(cls.__init__) + + new = [v for k, v in init_sig.parameters.items() if k != "self"] + new.insert(0, map_sig.parameters["cls"]) + cls.map.__signature__ = map_sig.replace(parameters=new) + cls.map.__doc__ = cls.__init__.__doc__ + + cls.map = classmethod(cls.map) + + return cls diff --git a/lib/python3.10/site-packages/seaborn/_docstrings.py b/lib/python3.10/site-packages/seaborn/_docstrings.py new file mode 100644 index 0000000000000000000000000000000000000000..2ab210b6ffbf63f21ebee9a4a3d59dcbc94fcb57 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_docstrings.py @@ -0,0 +1,198 @@ +import re +import pydoc +from .external.docscrape import NumpyDocString + + +class DocstringComponents: + + regexp = re.compile(r"\n((\n|.)+)\n\s*", re.MULTILINE) + + def __init__(self, comp_dict, strip_whitespace=True): + """Read entries from a dict, optionally stripping outer whitespace.""" + if strip_whitespace: + entries = {} + for key, val in comp_dict.items(): + m = re.match(self.regexp, val) + if m is None: + entries[key] = val + else: + entries[key] = m.group(1) + else: + entries = comp_dict.copy() + + self.entries = entries + + def __getattr__(self, attr): + """Provide dot access to entries for clean raw docstrings.""" + if attr in self.entries: + return self.entries[attr] + else: + try: + return self.__getattribute__(attr) + except AttributeError as err: + # If Python is run with -OO, it will strip docstrings and our lookup + # from self.entries will fail. We check for __debug__, which is actually + # set to False by -O (it is True for normal execution). + # But we only want to see an error when building the docs; + # not something users should see, so this slight inconsistency is fine. + if __debug__: + raise err + else: + pass + + @classmethod + def from_nested_components(cls, **kwargs): + """Add multiple sub-sets of components.""" + return cls(kwargs, strip_whitespace=False) + + @classmethod + def from_function_params(cls, func): + """Use the numpydoc parser to extract components from existing func.""" + params = NumpyDocString(pydoc.getdoc(func))["Parameters"] + comp_dict = {} + for p in params: + name = p.name + type = p.type + desc = "\n ".join(p.desc) + comp_dict[name] = f"{name} : {type}\n {desc}" + + return cls(comp_dict) + + +# TODO is "vector" the best term here? We mean to imply 1D data with a variety +# of types? + +# TODO now that we can parse numpydoc style strings, do we need to define dicts +# of docstring components, or just write out a docstring? + + +_core_params = dict( + data=""" +data : :class:`pandas.DataFrame`, :class:`numpy.ndarray`, mapping, or sequence + Input data structure. Either a long-form collection of vectors that can be + assigned to named variables or a wide-form dataset that will be internally + reshaped. + """, # TODO add link to user guide narrative when exists + xy=""" +x, y : vectors or keys in ``data`` + Variables that specify positions on the x and y axes. + """, + hue=""" +hue : vector or key in ``data`` + Semantic variable that is mapped to determine the color of plot elements. + """, + palette=""" +palette : string, list, dict, or :class:`matplotlib.colors.Colormap` + Method for choosing the colors to use when mapping the ``hue`` semantic. + String values are passed to :func:`color_palette`. List or dict values + imply categorical mapping, while a colormap object implies numeric mapping. + """, # noqa: E501 + hue_order=""" +hue_order : vector of strings + Specify the order of processing and plotting for categorical levels of the + ``hue`` semantic. + """, + hue_norm=""" +hue_norm : tuple or :class:`matplotlib.colors.Normalize` + Either a pair of values that set the normalization range in data units + or an object that will map from data units into a [0, 1] interval. Usage + implies numeric mapping. + """, + color=""" +color : :mod:`matplotlib color ` + Single color specification for when hue mapping is not used. Otherwise, the + plot will try to hook into the matplotlib property cycle. + """, + ax=""" +ax : :class:`matplotlib.axes.Axes` + Pre-existing axes for the plot. Otherwise, call :func:`matplotlib.pyplot.gca` + internally. + """, # noqa: E501 +) + + +_core_returns = dict( + ax=""" +:class:`matplotlib.axes.Axes` + The matplotlib axes containing the plot. + """, + facetgrid=""" +:class:`FacetGrid` + An object managing one or more subplots that correspond to conditional data + subsets with convenient methods for batch-setting of axes attributes. + """, + jointgrid=""" +:class:`JointGrid` + An object managing multiple subplots that correspond to joint and marginal axes + for plotting a bivariate relationship or distribution. + """, + pairgrid=""" +:class:`PairGrid` + An object managing multiple subplots that correspond to joint and marginal axes + for pairwise combinations of multiple variables in a dataset. + """, +) + + +_seealso_blurbs = dict( + + # Relational plots + scatterplot=""" +scatterplot : Plot data using points. + """, + lineplot=""" +lineplot : Plot data using lines. + """, + + # Distribution plots + displot=""" +displot : Figure-level interface to distribution plot functions. + """, + histplot=""" +histplot : Plot a histogram of binned counts with optional normalization or smoothing. + """, + kdeplot=""" +kdeplot : Plot univariate or bivariate distributions using kernel density estimation. + """, + ecdfplot=""" +ecdfplot : Plot empirical cumulative distribution functions. + """, + rugplot=""" +rugplot : Plot a tick at each observation value along the x and/or y axes. + """, + + # Categorical plots + stripplot=""" +stripplot : Plot a categorical scatter with jitter. + """, + swarmplot=""" +swarmplot : Plot a categorical scatter with non-overlapping points. + """, + violinplot=""" +violinplot : Draw an enhanced boxplot using kernel density estimation. + """, + pointplot=""" +pointplot : Plot point estimates and CIs using markers and lines. + """, + + # Multiples + jointplot=""" +jointplot : Draw a bivariate plot with univariate marginal distributions. + """, + pairplot=""" +jointplot : Draw multiple bivariate plots with univariate marginal distributions. + """, + jointgrid=""" +JointGrid : Set up a figure with joint and marginal views on bivariate data. + """, + pairgrid=""" +PairGrid : Set up a figure with joint and marginal views on multiple variables. + """, +) + + +_core_docs = dict( + params=DocstringComponents(_core_params), + returns=DocstringComponents(_core_returns), + seealso=DocstringComponents(_seealso_blurbs), +) diff --git a/lib/python3.10/site-packages/seaborn/_oldcore.py b/lib/python3.10/site-packages/seaborn/_oldcore.py new file mode 100644 index 0000000000000000000000000000000000000000..9f521a4776c154e7fdd44fbe2a278b0e409c4bd4 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_oldcore.py @@ -0,0 +1,1771 @@ +import warnings +import itertools +from copy import copy +from functools import partial +from collections import UserString +from collections.abc import Iterable, Sequence, Mapping +from numbers import Number +from datetime import datetime + +import numpy as np +import pandas as pd +import matplotlib as mpl + +from ._decorators import ( + share_init_params_with_map, +) +from .external.version import Version +from .palettes import ( + QUAL_PALETTES, + color_palette, +) +from .utils import ( + _check_argument, + get_color_cycle, + remove_na, +) + + +class SemanticMapping: + """Base class for mapping data values to plot attributes.""" + + # -- Default attributes that all SemanticMapping subclasses must set + + # Whether the mapping is numeric, categorical, or datetime + map_type = None + + # Ordered list of unique values in the input data + levels = None + + # A mapping from the data values to corresponding plot attributes + lookup_table = None + + def __init__(self, plotter): + + # TODO Putting this here so we can continue to use a lot of the + # logic that's built into the library, but the idea of this class + # is to move towards semantic mappings that are agnostic about the + # kind of plot they're going to be used to draw. + # Fully achieving that is going to take some thinking. + self.plotter = plotter + + def map(cls, plotter, *args, **kwargs): + # This method is assigned the __init__ docstring + method_name = f"_{cls.__name__[:-7].lower()}_map" + setattr(plotter, method_name, cls(plotter, *args, **kwargs)) + return plotter + + def _check_list_length(self, levels, values, variable): + """Input check when values are provided as a list.""" + # Copied from _core/properties; eventually will be replaced for that. + message = "" + if len(levels) > len(values): + message = " ".join([ + f"\nThe {variable} list has fewer values ({len(values)})", + f"than needed ({len(levels)}) and will cycle, which may", + "produce an uninterpretable plot." + ]) + values = [x for _, x in zip(levels, itertools.cycle(values))] + + elif len(values) > len(levels): + message = " ".join([ + f"The {variable} list has more values ({len(values)})", + f"than needed ({len(levels)}), which may not be intended.", + ]) + values = values[:len(levels)] + + if message: + warnings.warn(message, UserWarning, stacklevel=6) + + return values + + def _lookup_single(self, key): + """Apply the mapping to a single data value.""" + return self.lookup_table[key] + + def __call__(self, key, *args, **kwargs): + """Get the attribute(s) values for the data key.""" + if isinstance(key, (list, np.ndarray, pd.Series)): + return [self._lookup_single(k, *args, **kwargs) for k in key] + else: + return self._lookup_single(key, *args, **kwargs) + + +@share_init_params_with_map +class HueMapping(SemanticMapping): + """Mapping that sets artist colors according to data values.""" + # A specification of the colors that should appear in the plot + palette = None + + # An object that normalizes data values to [0, 1] range for color mapping + norm = None + + # A continuous colormap object for interpolating in a numeric context + cmap = None + + def __init__( + self, plotter, palette=None, order=None, norm=None, + ): + """Map the levels of the `hue` variable to distinct colors. + + Parameters + ---------- + # TODO add generic parameters + + """ + super().__init__(plotter) + + data = plotter.plot_data.get("hue", pd.Series(dtype=float)) + + if data.isna().all(): + if palette is not None: + msg = "Ignoring `palette` because no `hue` variable has been assigned." + warnings.warn(msg, stacklevel=4) + else: + + map_type = self.infer_map_type( + palette, norm, plotter.input_format, plotter.var_types["hue"] + ) + + # Our goal is to end up with a dictionary mapping every unique + # value in `data` to a color. We will also keep track of the + # metadata about this mapping we will need for, e.g., a legend + + # --- Option 1: numeric mapping with a matplotlib colormap + + if map_type == "numeric": + + data = pd.to_numeric(data) + levels, lookup_table, norm, cmap = self.numeric_mapping( + data, palette, norm, + ) + + # --- Option 2: categorical mapping using seaborn palette + + elif map_type == "categorical": + + cmap = norm = None + levels, lookup_table = self.categorical_mapping( + data, palette, order, + ) + + # --- Option 3: datetime mapping + + else: + # TODO this needs actual implementation + cmap = norm = None + levels, lookup_table = self.categorical_mapping( + # Casting data to list to handle differences in the way + # pandas and numpy represent datetime64 data + list(data), palette, order, + ) + + self.map_type = map_type + self.lookup_table = lookup_table + self.palette = palette + self.levels = levels + self.norm = norm + self.cmap = cmap + + def _lookup_single(self, key): + """Get the color for a single value, using colormap to interpolate.""" + try: + # Use a value that's in the original data vector + value = self.lookup_table[key] + except KeyError: + + if self.norm is None: + # Currently we only get here in scatterplot with hue_order, + # because scatterplot does not consider hue a grouping variable + # So unused hue levels are in the data, but not the lookup table + return (0, 0, 0, 0) + + # Use the colormap to interpolate between existing datapoints + # (e.g. in the context of making a continuous legend) + try: + normed = self.norm(key) + except TypeError as err: + if np.isnan(key): + value = (0, 0, 0, 0) + else: + raise err + else: + if np.ma.is_masked(normed): + normed = np.nan + value = self.cmap(normed) + return value + + def infer_map_type(self, palette, norm, input_format, var_type): + """Determine how to implement the mapping.""" + if palette in QUAL_PALETTES: + map_type = "categorical" + elif norm is not None: + map_type = "numeric" + elif isinstance(palette, (dict, list)): + map_type = "categorical" + elif input_format == "wide": + map_type = "categorical" + else: + map_type = var_type + + return map_type + + def categorical_mapping(self, data, palette, order): + """Determine colors when the hue mapping is categorical.""" + # -- Identify the order and name of the levels + + levels = categorical_order(data, order) + n_colors = len(levels) + + # -- Identify the set of colors to use + + if isinstance(palette, dict): + + missing = set(levels) - set(palette) + if any(missing): + err = "The palette dictionary is missing keys: {}" + raise ValueError(err.format(missing)) + + lookup_table = palette + + else: + + if palette is None: + if n_colors <= len(get_color_cycle()): + colors = color_palette(None, n_colors) + else: + colors = color_palette("husl", n_colors) + elif isinstance(palette, list): + colors = self._check_list_length(levels, palette, "palette") + else: + colors = color_palette(palette, n_colors) + + lookup_table = dict(zip(levels, colors)) + + return levels, lookup_table + + def numeric_mapping(self, data, palette, norm): + """Determine colors when the hue variable is quantitative.""" + if isinstance(palette, dict): + + # The presence of a norm object overrides a dictionary of hues + # in specifying a numeric mapping, so we need to process it here. + levels = list(sorted(palette)) + colors = [palette[k] for k in sorted(palette)] + cmap = mpl.colors.ListedColormap(colors) + lookup_table = palette.copy() + + else: + + # The levels are the sorted unique values in the data + levels = list(np.sort(remove_na(data.unique()))) + + # --- Sort out the colormap to use from the palette argument + + # Default numeric palette is our default cubehelix palette + # TODO do we want to do something complicated to ensure contrast? + palette = "ch:" if palette is None else palette + + if isinstance(palette, mpl.colors.Colormap): + cmap = palette + else: + cmap = color_palette(palette, as_cmap=True) + + # Now sort out the data normalization + if norm is None: + norm = mpl.colors.Normalize() + elif isinstance(norm, tuple): + norm = mpl.colors.Normalize(*norm) + elif not isinstance(norm, mpl.colors.Normalize): + err = "``hue_norm`` must be None, tuple, or Normalize object." + raise ValueError(err) + + if not norm.scaled(): + norm(np.asarray(data.dropna())) + + lookup_table = dict(zip(levels, cmap(norm(levels)))) + + return levels, lookup_table, norm, cmap + + +@share_init_params_with_map +class SizeMapping(SemanticMapping): + """Mapping that sets artist sizes according to data values.""" + # An object that normalizes data values to [0, 1] range + norm = None + + def __init__( + self, plotter, sizes=None, order=None, norm=None, + ): + """Map the levels of the `size` variable to distinct values. + + Parameters + ---------- + # TODO add generic parameters + + """ + super().__init__(plotter) + + data = plotter.plot_data.get("size", pd.Series(dtype=float)) + + if data.notna().any(): + + map_type = self.infer_map_type( + norm, sizes, plotter.var_types["size"] + ) + + # --- Option 1: numeric mapping + + if map_type == "numeric": + + levels, lookup_table, norm, size_range = self.numeric_mapping( + data, sizes, norm, + ) + + # --- Option 2: categorical mapping + + elif map_type == "categorical": + + levels, lookup_table = self.categorical_mapping( + data, sizes, order, + ) + size_range = None + + # --- Option 3: datetime mapping + + # TODO this needs an actual implementation + else: + + levels, lookup_table = self.categorical_mapping( + # Casting data to list to handle differences in the way + # pandas and numpy represent datetime64 data + list(data), sizes, order, + ) + size_range = None + + self.map_type = map_type + self.levels = levels + self.norm = norm + self.sizes = sizes + self.size_range = size_range + self.lookup_table = lookup_table + + def infer_map_type(self, norm, sizes, var_type): + + if norm is not None: + map_type = "numeric" + elif isinstance(sizes, (dict, list)): + map_type = "categorical" + else: + map_type = var_type + + return map_type + + def _lookup_single(self, key): + + try: + value = self.lookup_table[key] + except KeyError: + normed = self.norm(key) + if np.ma.is_masked(normed): + normed = np.nan + value = self.size_range[0] + normed * np.ptp(self.size_range) + return value + + def categorical_mapping(self, data, sizes, order): + + levels = categorical_order(data, order) + + if isinstance(sizes, dict): + + # Dict inputs map existing data values to the size attribute + missing = set(levels) - set(sizes) + if any(missing): + err = f"Missing sizes for the following levels: {missing}" + raise ValueError(err) + lookup_table = sizes.copy() + + elif isinstance(sizes, list): + + # List inputs give size values in the same order as the levels + sizes = self._check_list_length(levels, sizes, "sizes") + lookup_table = dict(zip(levels, sizes)) + + else: + + if isinstance(sizes, tuple): + + # Tuple input sets the min, max size values + if len(sizes) != 2: + err = "A `sizes` tuple must have only 2 values" + raise ValueError(err) + + elif sizes is not None: + + err = f"Value for `sizes` not understood: {sizes}" + raise ValueError(err) + + else: + + # Otherwise, we need to get the min, max size values from + # the plotter object we are attached to. + + # TODO this is going to cause us trouble later, because we + # want to restructure things so that the plotter is generic + # across the visual representation of the data. But at this + # point, we don't know the visual representation. Likely we + # want to change the logic of this Mapping so that it gives + # points on a normalized range that then gets un-normalized + # when we know what we're drawing. But given the way the + # package works now, this way is cleanest. + sizes = self.plotter._default_size_range + + # For categorical sizes, use regularly-spaced linear steps + # between the minimum and maximum sizes. Then reverse the + # ramp so that the largest value is used for the first entry + # in size_order, etc. This is because "ordered" categories + # are often though to go in decreasing priority. + sizes = np.linspace(*sizes, len(levels))[::-1] + lookup_table = dict(zip(levels, sizes)) + + return levels, lookup_table + + def numeric_mapping(self, data, sizes, norm): + + if isinstance(sizes, dict): + # The presence of a norm object overrides a dictionary of sizes + # in specifying a numeric mapping, so we need to process it + # dictionary here + levels = list(np.sort(list(sizes))) + size_values = sizes.values() + size_range = min(size_values), max(size_values) + + else: + + # The levels here will be the unique values in the data + levels = list(np.sort(remove_na(data.unique()))) + + if isinstance(sizes, tuple): + + # For numeric inputs, the size can be parametrized by + # the minimum and maximum artist values to map to. The + # norm object that gets set up next specifies how to + # do the mapping. + + if len(sizes) != 2: + err = "A `sizes` tuple must have only 2 values" + raise ValueError(err) + + size_range = sizes + + elif sizes is not None: + + err = f"Value for `sizes` not understood: {sizes}" + raise ValueError(err) + + else: + + # When not provided, we get the size range from the plotter + # object we are attached to. See the note in the categorical + # method about how this is suboptimal for future development. + size_range = self.plotter._default_size_range + + # Now that we know the minimum and maximum sizes that will get drawn, + # we need to map the data values that we have into that range. We will + # use a matplotlib Normalize class, which is typically used for numeric + # color mapping but works fine here too. It takes data values and maps + # them into a [0, 1] interval, potentially nonlinear-ly. + + if norm is None: + # Default is a linear function between the min and max data values + norm = mpl.colors.Normalize() + elif isinstance(norm, tuple): + # It is also possible to give different limits in data space + norm = mpl.colors.Normalize(*norm) + elif not isinstance(norm, mpl.colors.Normalize): + err = f"Value for size `norm` parameter not understood: {norm}" + raise ValueError(err) + else: + # If provided with Normalize object, copy it so we can modify + norm = copy(norm) + + # Set the mapping so all output values are in [0, 1] + norm.clip = True + + # If the input range is not set, use the full range of the data + if not norm.scaled(): + norm(levels) + + # Map from data values to [0, 1] range + sizes_scaled = norm(levels) + + # Now map from the scaled range into the artist units + if isinstance(sizes, dict): + lookup_table = sizes + else: + lo, hi = size_range + sizes = lo + sizes_scaled * (hi - lo) + lookup_table = dict(zip(levels, sizes)) + + return levels, lookup_table, norm, size_range + + +@share_init_params_with_map +class StyleMapping(SemanticMapping): + """Mapping that sets artist style according to data values.""" + + # Style mapping is always treated as categorical + map_type = "categorical" + + def __init__( + self, plotter, markers=None, dashes=None, order=None, + ): + """Map the levels of the `style` variable to distinct values. + + Parameters + ---------- + # TODO add generic parameters + + """ + super().__init__(plotter) + + data = plotter.plot_data.get("style", pd.Series(dtype=float)) + + if data.notna().any(): + + # Cast to list to handle numpy/pandas datetime quirks + if variable_type(data) == "datetime": + data = list(data) + + # Find ordered unique values + levels = categorical_order(data, order) + + markers = self._map_attributes( + markers, levels, unique_markers(len(levels)), "markers", + ) + dashes = self._map_attributes( + dashes, levels, unique_dashes(len(levels)), "dashes", + ) + + # Build the paths matplotlib will use to draw the markers + paths = {} + filled_markers = [] + for k, m in markers.items(): + if not isinstance(m, mpl.markers.MarkerStyle): + m = mpl.markers.MarkerStyle(m) + paths[k] = m.get_path().transformed(m.get_transform()) + filled_markers.append(m.is_filled()) + + # Mixture of filled and unfilled markers will show line art markers + # in the edge color, which defaults to white. This can be handled, + # but there would be additional complexity with specifying the + # weight of the line art markers without overwhelming the filled + # ones with the edges. So for now, we will disallow mixtures. + if any(filled_markers) and not all(filled_markers): + err = "Filled and line art markers cannot be mixed" + raise ValueError(err) + + lookup_table = {} + for key in levels: + lookup_table[key] = {} + if markers: + lookup_table[key]["marker"] = markers[key] + lookup_table[key]["path"] = paths[key] + if dashes: + lookup_table[key]["dashes"] = dashes[key] + + self.levels = levels + self.lookup_table = lookup_table + + def _lookup_single(self, key, attr=None): + """Get attribute(s) for a given data point.""" + if attr is None: + value = self.lookup_table[key] + else: + value = self.lookup_table[key][attr] + return value + + def _map_attributes(self, arg, levels, defaults, attr): + """Handle the specification for a given style attribute.""" + if arg is True: + lookup_table = dict(zip(levels, defaults)) + elif isinstance(arg, dict): + missing = set(levels) - set(arg) + if missing: + err = f"These `{attr}` levels are missing values: {missing}" + raise ValueError(err) + lookup_table = arg + elif isinstance(arg, Sequence): + arg = self._check_list_length(levels, arg, attr) + lookup_table = dict(zip(levels, arg)) + elif arg: + err = f"This `{attr}` argument was not understood: {arg}" + raise ValueError(err) + else: + lookup_table = {} + + return lookup_table + + +# =========================================================================== # + + +class VectorPlotter: + """Base class for objects underlying *plot functions.""" + + _semantic_mappings = { + "hue": HueMapping, + "size": SizeMapping, + "style": StyleMapping, + } + + # TODO units is another example of a non-mapping "semantic" + # we need a general name for this and separate handling + semantics = "x", "y", "hue", "size", "style", "units" + wide_structure = { + "x": "@index", "y": "@values", "hue": "@columns", "style": "@columns", + } + flat_structure = {"x": "@index", "y": "@values"} + + _default_size_range = 1, 2 # Unused but needed in tests, ugh + + def __init__(self, data=None, variables={}): + + self._var_levels = {} + # var_ordered is relevant only for categorical axis variables, and may + # be better handled by an internal axis information object that tracks + # such information and is set up by the scale_* methods. The analogous + # information for numeric axes would be information about log scales. + self._var_ordered = {"x": False, "y": False} # alt., used DefaultDict + self.assign_variables(data, variables) + + for var, cls in self._semantic_mappings.items(): + + # Create the mapping function + map_func = partial(cls.map, plotter=self) + setattr(self, f"map_{var}", map_func) + + # Call the mapping function to initialize with default values + getattr(self, f"map_{var}")() + + @classmethod + def get_semantics(cls, kwargs, semantics=None): + """Subset a dictionary arguments with known semantic variables.""" + # TODO this should be get_variables since we have included x and y + if semantics is None: + semantics = cls.semantics + variables = {} + for key, val in kwargs.items(): + if key in semantics and val is not None: + variables[key] = val + return variables + + @property + def has_xy_data(self): + """Return True at least one of x or y is defined.""" + return bool({"x", "y"} & set(self.variables)) + + @property + def var_levels(self): + """Property interface to ordered list of variables levels. + + Each time it's accessed, it updates the var_levels dictionary with the + list of levels in the current semantic mappers. But it also allows the + dictionary to persist, so it can be used to set levels by a key. This is + used to track the list of col/row levels using an attached FacetGrid + object, but it's kind of messy and ideally fixed by improving the + faceting logic so it interfaces better with the modern approach to + tracking plot variables. + + """ + for var in self.variables: + try: + map_obj = getattr(self, f"_{var}_map") + self._var_levels[var] = map_obj.levels + except AttributeError: + pass + return self._var_levels + + def assign_variables(self, data=None, variables={}): + """Define plot variables, optionally using lookup from `data`.""" + x = variables.get("x", None) + y = variables.get("y", None) + + if x is None and y is None: + self.input_format = "wide" + plot_data, variables = self._assign_variables_wideform( + data, **variables, + ) + else: + self.input_format = "long" + plot_data, variables = self._assign_variables_longform( + data, **variables, + ) + + self.plot_data = plot_data + self.variables = variables + self.var_types = { + v: variable_type( + plot_data[v], + boolean_type="numeric" if v in "xy" else "categorical" + ) + for v in variables + } + + return self + + def _assign_variables_wideform(self, data=None, **kwargs): + """Define plot variables given wide-form data. + + Parameters + ---------- + data : flat vector or collection of vectors + Data can be a vector or mapping that is coerceable to a Series + or a sequence- or mapping-based collection of such vectors, or a + rectangular numpy array, or a Pandas DataFrame. + kwargs : variable -> data mappings + Behavior with keyword arguments is currently undefined. + + Returns + ------- + plot_data : :class:`pandas.DataFrame` + Long-form data object mapping seaborn variables (x, y, hue, ...) + to data vectors. + variables : dict + Keys are defined seaborn variables; values are names inferred from + the inputs (or None when no name can be determined). + + """ + # Raise if semantic or other variables are assigned in wide-form mode + assigned = [k for k, v in kwargs.items() if v is not None] + if any(assigned): + s = "s" if len(assigned) > 1 else "" + err = f"The following variable{s} cannot be assigned with wide-form data: " + err += ", ".join(f"`{v}`" for v in assigned) + raise ValueError(err) + + # Determine if the data object actually has any data in it + empty = data is None or not len(data) + + # Then, determine if we have "flat" data (a single vector) + if isinstance(data, dict): + values = data.values() + else: + values = np.atleast_1d(np.asarray(data, dtype=object)) + flat = not any( + isinstance(v, Iterable) and not isinstance(v, (str, bytes)) + for v in values + ) + + if empty: + + # Make an object with the structure of plot_data, but empty + plot_data = pd.DataFrame() + variables = {} + + elif flat: + + # Handle flat data by converting to pandas Series and using the + # index and/or values to define x and/or y + # (Could be accomplished with a more general to_series() interface) + flat_data = pd.Series(data).copy() + names = { + "@values": flat_data.name, + "@index": flat_data.index.name + } + + plot_data = {} + variables = {} + + for var in ["x", "y"]: + if var in self.flat_structure: + attr = self.flat_structure[var] + plot_data[var] = getattr(flat_data, attr[1:]) + variables[var] = names[self.flat_structure[var]] + + plot_data = pd.DataFrame(plot_data) + + else: + + # Otherwise assume we have some collection of vectors. + + # Handle Python sequences such that entries end up in the columns, + # not in the rows, of the intermediate wide DataFrame. + # One way to accomplish this is to convert to a dict of Series. + if isinstance(data, Sequence): + data_dict = {} + for i, var in enumerate(data): + key = getattr(var, "name", i) + # TODO is there a safer/more generic way to ensure Series? + # sort of like np.asarray, but for pandas? + data_dict[key] = pd.Series(var) + + data = data_dict + + # Pandas requires that dict values either be Series objects + # or all have the same length, but we want to allow "ragged" inputs + if isinstance(data, Mapping): + data = {key: pd.Series(val) for key, val in data.items()} + + # Otherwise, delegate to the pandas DataFrame constructor + # This is where we'd prefer to use a general interface that says + # "give me this data as a pandas DataFrame", so we can accept + # DataFrame objects from other libraries + wide_data = pd.DataFrame(data, copy=True) + + # At this point we should reduce the dataframe to numeric cols + numeric_cols = [ + k for k, v in wide_data.items() if variable_type(v) == "numeric" + ] + wide_data = wide_data[numeric_cols] + + # Now melt the data to long form + melt_kws = {"var_name": "@columns", "value_name": "@values"} + use_index = "@index" in self.wide_structure.values() + if use_index: + melt_kws["id_vars"] = "@index" + try: + orig_categories = wide_data.columns.categories + orig_ordered = wide_data.columns.ordered + wide_data.columns = wide_data.columns.add_categories("@index") + except AttributeError: + category_columns = False + else: + category_columns = True + wide_data["@index"] = wide_data.index.to_series() + + plot_data = wide_data.melt(**melt_kws) + + if use_index and category_columns: + plot_data["@columns"] = pd.Categorical(plot_data["@columns"], + orig_categories, + orig_ordered) + + # Assign names corresponding to plot semantics + for var, attr in self.wide_structure.items(): + plot_data[var] = plot_data[attr] + + # Define the variable names + variables = {} + for var, attr in self.wide_structure.items(): + obj = getattr(wide_data, attr[1:]) + variables[var] = getattr(obj, "name", None) + + # Remove redundant columns from plot_data + plot_data = plot_data[list(variables)] + + return plot_data, variables + + def _assign_variables_longform(self, data=None, **kwargs): + """Define plot variables given long-form data and/or vector inputs. + + Parameters + ---------- + data : dict-like collection of vectors + Input data where variable names map to vector values. + kwargs : variable -> data mappings + Keys are seaborn variables (x, y, hue, ...) and values are vectors + in any format that can construct a :class:`pandas.DataFrame` or + names of columns or index levels in ``data``. + + Returns + ------- + plot_data : :class:`pandas.DataFrame` + Long-form data object mapping seaborn variables (x, y, hue, ...) + to data vectors. + variables : dict + Keys are defined seaborn variables; values are names inferred from + the inputs (or None when no name can be determined). + + Raises + ------ + ValueError + When variables are strings that don't appear in ``data``. + + """ + plot_data = {} + variables = {} + + # Data is optional; all variables can be defined as vectors + if data is None: + data = {} + + # TODO should we try a data.to_dict() or similar here to more + # generally accept objects with that interface? + # Note that dict(df) also works for pandas, and gives us what we + # want, whereas DataFrame.to_dict() gives a nested dict instead of + # a dict of series. + + # Variables can also be extracted from the index attribute + # TODO is this the most general way to enable it? + # There is no index.to_dict on multiindex, unfortunately + try: + index = data.index.to_frame() + except AttributeError: + index = {} + + # The caller will determine the order of variables in plot_data + for key, val in kwargs.items(): + + # First try to treat the argument as a key for the data collection. + # But be flexible about what can be used as a key. + # Usually it will be a string, but allow numbers or tuples too when + # taking from the main data object. Only allow strings to reference + # fields in the index, because otherwise there is too much ambiguity. + try: + val_as_data_key = ( + val in data + or (isinstance(val, (str, bytes)) and val in index) + ) + except (KeyError, TypeError): + val_as_data_key = False + + if val_as_data_key: + + # We know that __getitem__ will work + + if val in data: + plot_data[key] = data[val] + elif val in index: + plot_data[key] = index[val] + variables[key] = val + + elif isinstance(val, (str, bytes)): + + # This looks like a column name but we don't know what it means! + + err = f"Could not interpret value `{val}` for parameter `{key}`" + raise ValueError(err) + + else: + + # Otherwise, assume the value is itself data + + # Raise when data object is present and a vector can't matched + if isinstance(data, pd.DataFrame) and not isinstance(val, pd.Series): + if np.ndim(val) and len(data) != len(val): + val_cls = val.__class__.__name__ + err = ( + f"Length of {val_cls} vectors must match length of `data`" + f" when both are used, but `data` has length {len(data)}" + f" and the vector passed to `{key}` has length {len(val)}." + ) + raise ValueError(err) + + plot_data[key] = val + + # Try to infer the name of the variable + variables[key] = getattr(val, "name", None) + + # Construct a tidy plot DataFrame. This will convert a number of + # types automatically, aligning on index in case of pandas objects + plot_data = pd.DataFrame(plot_data) + + # Reduce the variables dictionary to fields with valid data + variables = { + var: name + for var, name in variables.items() + if plot_data[var].notnull().any() + } + + return plot_data, variables + + def iter_data( + self, grouping_vars=None, *, + reverse=False, from_comp_data=False, + by_facet=True, allow_empty=False, dropna=True, + ): + """Generator for getting subsets of data defined by semantic variables. + + Also injects "col" and "row" into grouping semantics. + + Parameters + ---------- + grouping_vars : string or list of strings + Semantic variables that define the subsets of data. + reverse : bool + If True, reverse the order of iteration. + from_comp_data : bool + If True, use self.comp_data rather than self.plot_data + by_facet : bool + If True, add faceting variables to the set of grouping variables. + allow_empty : bool + If True, yield an empty dataframe when no observations exist for + combinations of grouping variables. + dropna : bool + If True, remove rows with missing data. + + Yields + ------ + sub_vars : dict + Keys are semantic names, values are the level of that semantic. + sub_data : :class:`pandas.DataFrame` + Subset of ``plot_data`` for this combination of semantic values. + + """ + # TODO should this default to using all (non x/y?) semantics? + # or define grouping vars somewhere? + if grouping_vars is None: + grouping_vars = [] + elif isinstance(grouping_vars, str): + grouping_vars = [grouping_vars] + elif isinstance(grouping_vars, tuple): + grouping_vars = list(grouping_vars) + + # Always insert faceting variables + if by_facet: + facet_vars = {"col", "row"} + grouping_vars.extend( + facet_vars & set(self.variables) - set(grouping_vars) + ) + + # Reduce to the semantics used in this plot + grouping_vars = [ + var for var in grouping_vars if var in self.variables + ] + + if from_comp_data: + data = self.comp_data + else: + data = self.plot_data + + if dropna: + data = data.dropna() + + levels = self.var_levels.copy() + if from_comp_data: + for axis in {"x", "y"} & set(grouping_vars): + if self.var_types[axis] == "categorical": + if self._var_ordered[axis]: + # If the axis is ordered, then the axes in a possible + # facet grid are by definition "shared", or there is a + # single axis with a unique cat -> idx mapping. + # So we can just take the first converter object. + converter = self.converters[axis].iloc[0] + levels[axis] = converter.convert_units(levels[axis]) + else: + # Otherwise, the mappings may not be unique, but we can + # use the unique set of index values in comp_data. + levels[axis] = np.sort(data[axis].unique()) + elif self.var_types[axis] == "datetime": + levels[axis] = mpl.dates.date2num(levels[axis]) + elif self.var_types[axis] == "numeric" and self._log_scaled(axis): + levels[axis] = np.log10(levels[axis]) + + if grouping_vars: + + grouped_data = data.groupby( + grouping_vars, sort=False, as_index=False + ) + + grouping_keys = [] + for var in grouping_vars: + grouping_keys.append(levels.get(var, [])) + + iter_keys = itertools.product(*grouping_keys) + if reverse: + iter_keys = reversed(list(iter_keys)) + + for key in iter_keys: + + # Pandas fails with singleton tuple inputs + pd_key = key[0] if len(key) == 1 else key + + try: + data_subset = grouped_data.get_group(pd_key) + except KeyError: + # XXX we are adding this to allow backwards compatibility + # with the empty artists that old categorical plots would + # add (before 0.12), which we may decide to break, in which + # case this option could be removed + data_subset = data.loc[[]] + + if data_subset.empty and not allow_empty: + continue + + sub_vars = dict(zip(grouping_vars, key)) + + yield sub_vars, data_subset.copy() + + else: + + yield {}, data.copy() + + @property + def comp_data(self): + """Dataframe with numeric x and y, after unit conversion and log scaling.""" + if not hasattr(self, "ax"): + # Probably a good idea, but will need a bunch of tests updated + # Most of these tests should just use the external interface + # Then this can be re-enabled. + # raise AttributeError("No Axes attached to plotter") + return self.plot_data + + if not hasattr(self, "_comp_data"): + + comp_data = ( + self.plot_data + .copy(deep=False) + .drop(["x", "y"], axis=1, errors="ignore") + ) + + for var in "yx": + if var not in self.variables: + continue + + parts = [] + grouped = self.plot_data[var].groupby(self.converters[var], sort=False) + for converter, orig in grouped: + with pd.option_context('mode.use_inf_as_na', True): + orig = orig.dropna() + if var in self.var_levels: + # TODO this should happen in some centralized location + # it is similar to GH2419, but more complicated because + # supporting `order` in categorical plots is tricky + orig = orig[orig.isin(self.var_levels[var])] + comp = pd.to_numeric(converter.convert_units(orig)) + if converter.get_scale() == "log": + comp = np.log10(comp) + parts.append(pd.Series(comp, orig.index, name=orig.name)) + if parts: + comp_col = pd.concat(parts) + else: + comp_col = pd.Series(dtype=float, name=var) + comp_data.insert(0, var, comp_col) + + self._comp_data = comp_data + + return self._comp_data + + def _get_axes(self, sub_vars): + """Return an Axes object based on existence of row/col variables.""" + row = sub_vars.get("row", None) + col = sub_vars.get("col", None) + if row is not None and col is not None: + return self.facets.axes_dict[(row, col)] + elif row is not None: + return self.facets.axes_dict[row] + elif col is not None: + return self.facets.axes_dict[col] + elif self.ax is None: + return self.facets.ax + else: + return self.ax + + def _attach( + self, + obj, + allowed_types=None, + log_scale=None, + ): + """Associate the plotter with an Axes manager and initialize its units. + + Parameters + ---------- + obj : :class:`matplotlib.axes.Axes` or :class:'FacetGrid` + Structural object that we will eventually plot onto. + allowed_types : str or list of str + If provided, raise when either the x or y variable does not have + one of the declared seaborn types. + log_scale : bool, number, or pair of bools or numbers + If not False, set the axes to use log scaling, with the given + base or defaulting to 10. If a tuple, interpreted as separate + arguments for the x and y axes. + + """ + from .axisgrid import FacetGrid + if isinstance(obj, FacetGrid): + self.ax = None + self.facets = obj + ax_list = obj.axes.flatten() + if obj.col_names is not None: + self.var_levels["col"] = obj.col_names + if obj.row_names is not None: + self.var_levels["row"] = obj.row_names + else: + self.ax = obj + self.facets = None + ax_list = [obj] + + # Identify which "axis" variables we have defined + axis_variables = set("xy").intersection(self.variables) + + # -- Verify the types of our x and y variables here. + # This doesn't really make complete sense being here here, but it's a fine + # place for it, given the current system. + # (Note that for some plots, there might be more complicated restrictions) + # e.g. the categorical plots have their own check that as specific to the + # non-categorical axis. + if allowed_types is None: + allowed_types = ["numeric", "datetime", "categorical"] + elif isinstance(allowed_types, str): + allowed_types = [allowed_types] + + for var in axis_variables: + var_type = self.var_types[var] + if var_type not in allowed_types: + err = ( + f"The {var} variable is {var_type}, but one of " + f"{allowed_types} is required" + ) + raise TypeError(err) + + # -- Get axis objects for each row in plot_data for type conversions and scaling + + facet_dim = {"x": "col", "y": "row"} + + self.converters = {} + for var in axis_variables: + other_var = {"x": "y", "y": "x"}[var] + + converter = pd.Series(index=self.plot_data.index, name=var, dtype=object) + share_state = getattr(self.facets, f"_share{var}", True) + + # Simplest cases are that we have a single axes, all axes are shared, + # or sharing is only on the orthogonal facet dimension. In these cases, + # all datapoints get converted the same way, so use the first axis + if share_state is True or share_state == facet_dim[other_var]: + converter.loc[:] = getattr(ax_list[0], f"{var}axis") + + else: + + # Next simplest case is when no axes are shared, and we can + # use the axis objects within each facet + if share_state is False: + for axes_vars, axes_data in self.iter_data(): + ax = self._get_axes(axes_vars) + converter.loc[axes_data.index] = getattr(ax, f"{var}axis") + + # In the more complicated case, the axes are shared within each + # "file" of the facetgrid. In that case, we need to subset the data + # for that file and assign it the first axis in the slice of the grid + else: + + names = getattr(self.facets, f"{share_state}_names") + for i, level in enumerate(names): + idx = (i, 0) if share_state == "row" else (0, i) + axis = getattr(self.facets.axes[idx], f"{var}axis") + converter.loc[self.plot_data[share_state] == level] = axis + + # Store the converter vector, which we use elsewhere (e.g comp_data) + self.converters[var] = converter + + # Now actually update the matplotlib objects to do the conversion we want + grouped = self.plot_data[var].groupby(self.converters[var], sort=False) + for converter, seed_data in grouped: + if self.var_types[var] == "categorical": + if self._var_ordered[var]: + order = self.var_levels[var] + else: + order = None + seed_data = categorical_order(seed_data, order) + converter.update_units(seed_data) + + # -- Set numerical axis scales + + # First unpack the log_scale argument + if log_scale is None: + scalex = scaley = False + else: + # Allow single value or x, y tuple + try: + scalex, scaley = log_scale + except TypeError: + scalex = log_scale if "x" in self.variables else False + scaley = log_scale if "y" in self.variables else False + + # Now use it + for axis, scale in zip("xy", (scalex, scaley)): + if scale: + for ax in ax_list: + set_scale = getattr(ax, f"set_{axis}scale") + if scale is True: + set_scale("log") + else: + if Version(mpl.__version__) >= Version("3.3"): + set_scale("log", base=scale) + else: + set_scale("log", **{f"base{axis}": scale}) + + # For categorical y, we want the "first" level to be at the top of the axis + if self.var_types.get("y", None) == "categorical": + for ax in ax_list: + try: + ax.yaxis.set_inverted(True) + except AttributeError: # mpl < 3.1 + if not ax.yaxis_inverted(): + ax.invert_yaxis() + + # TODO -- Add axes labels + + def _log_scaled(self, axis): + """Return True if specified axis is log scaled on all attached axes.""" + if not hasattr(self, "ax"): + return False + + if self.ax is None: + axes_list = self.facets.axes.flatten() + else: + axes_list = [self.ax] + + log_scaled = [] + for ax in axes_list: + data_axis = getattr(ax, f"{axis}axis") + log_scaled.append(data_axis.get_scale() == "log") + + if any(log_scaled) and not all(log_scaled): + raise RuntimeError("Axis scaling is not consistent") + + return any(log_scaled) + + def _add_axis_labels(self, ax, default_x="", default_y=""): + """Add axis labels if not present, set visibility to match ticklabels.""" + # TODO ax could default to None and use attached axes if present + # but what to do about the case of facets? Currently using FacetGrid's + # set_axis_labels method, which doesn't add labels to the interior even + # when the axes are not shared. Maybe that makes sense? + if not ax.get_xlabel(): + x_visible = any(t.get_visible() for t in ax.get_xticklabels()) + ax.set_xlabel(self.variables.get("x", default_x), visible=x_visible) + if not ax.get_ylabel(): + y_visible = any(t.get_visible() for t in ax.get_yticklabels()) + ax.set_ylabel(self.variables.get("y", default_y), visible=y_visible) + + # XXX If the scale_* methods are going to modify the plot_data structure, they + # can't be called twice. That means that if they are called twice, they should + # raise. Alternatively, we could store an original version of plot_data and each + # time they are called they operate on the store, not the current state. + + def scale_native(self, axis, *args, **kwargs): + + # Default, defer to matplotlib + + raise NotImplementedError + + def scale_numeric(self, axis, *args, **kwargs): + + # Feels needed to completeness, what should it do? + # Perhaps handle log scaling? Set the ticker/formatter/limits? + + raise NotImplementedError + + def scale_datetime(self, axis, *args, **kwargs): + + # Use pd.to_datetime to convert strings or numbers to datetime objects + # Note, use day-resolution for numeric->datetime to match matplotlib + + raise NotImplementedError + + def scale_categorical(self, axis, order=None, formatter=None): + """ + Enforce categorical (fixed-scale) rules for the data on given axis. + + Parameters + ---------- + axis : "x" or "y" + Axis of the plot to operate on. + order : list + Order that unique values should appear in. + formatter : callable + Function mapping values to a string representation. + + Returns + ------- + self + + """ + # This method both modifies the internal representation of the data + # (converting it to string) and sets some attributes on self. It might be + # a good idea to have a separate object attached to self that contains the + # information in those attributes (i.e. whether to enforce variable order + # across facets, the order to use) similar to the SemanticMapping objects + # we have for semantic variables. That object could also hold the converter + # objects that get used, if we can decouple those from an existing axis + # (cf. https://github.com/matplotlib/matplotlib/issues/19229). + # There are some interactions with faceting information that would need + # to be thought through, since the converts to use depend on facets. + # If we go that route, these methods could become "borrowed" methods similar + # to what happens with the alternate semantic mapper constructors, although + # that approach is kind of fussy and confusing. + + # TODO this method could also set the grid state? Since we like to have no + # grid on the categorical axis by default. Again, a case where we'll need to + # store information until we use it, so best to have a way to collect the + # attributes that this method sets. + + # TODO if we are going to set visual properties of the axes with these methods, + # then we could do the steps currently in CategoricalPlotter._adjust_cat_axis + + # TODO another, and distinct idea, is to expose a cut= param here + + _check_argument("axis", ["x", "y"], axis) + + # Categorical plots can be "univariate" in which case they get an anonymous + # category label on the opposite axis. + if axis not in self.variables: + self.variables[axis] = None + self.var_types[axis] = "categorical" + self.plot_data[axis] = "" + + # If the "categorical" variable has a numeric type, sort the rows so that + # the default result from categorical_order has those values sorted after + # they have been coerced to strings. The reason for this is so that later + # we can get facet-wise orders that are correct. + # XXX Should this also sort datetimes? + # It feels more consistent, but technically will be a default change + # If so, should also change categorical_order to behave that way + if self.var_types[axis] == "numeric": + self.plot_data = self.plot_data.sort_values(axis, kind="mergesort") + + # Now get a reference to the categorical data vector + cat_data = self.plot_data[axis] + + # Get the initial categorical order, which we do before string + # conversion to respect the original types of the order list. + # Track whether the order is given explicitly so that we can know + # whether or not to use the order constructed here downstream + self._var_ordered[axis] = order is not None or cat_data.dtype.name == "category" + order = pd.Index(categorical_order(cat_data, order)) + + # Then convert data to strings. This is because in matplotlib, + # "categorical" data really mean "string" data, so doing this artists + # will be drawn on the categorical axis with a fixed scale. + # TODO implement formatter here; check that it returns strings? + if formatter is not None: + cat_data = cat_data.map(formatter) + order = order.map(formatter) + else: + cat_data = cat_data.astype(str) + order = order.astype(str) + + # Update the levels list with the type-converted order variable + self.var_levels[axis] = order + + # Now ensure that seaborn will use categorical rules internally + self.var_types[axis] = "categorical" + + # Put the string-typed categorical vector back into the plot_data structure + self.plot_data[axis] = cat_data + + return self + + +class VariableType(UserString): + """ + Prevent comparisons elsewhere in the library from using the wrong name. + + Errors are simple assertions because users should not be able to trigger + them. If that changes, they should be more verbose. + + """ + # TODO we can replace this with typing.Literal on Python 3.8+ + allowed = "numeric", "datetime", "categorical" + + def __init__(self, data): + assert data in self.allowed, data + super().__init__(data) + + def __eq__(self, other): + assert other in self.allowed, other + return self.data == other + + +def variable_type(vector, boolean_type="numeric"): + """ + Determine whether a vector contains numeric, categorical, or datetime data. + + This function differs from the pandas typing API in two ways: + + - Python sequences or object-typed PyData objects are considered numeric if + all of their entries are numeric. + - String or mixed-type data are considered categorical even if not + explicitly represented as a :class:`pandas.api.types.CategoricalDtype`. + + Parameters + ---------- + vector : :func:`pandas.Series`, :func:`numpy.ndarray`, or Python sequence + Input data to test. + boolean_type : 'numeric' or 'categorical' + Type to use for vectors containing only 0s and 1s (and NAs). + + Returns + ------- + var_type : 'numeric', 'categorical', or 'datetime' + Name identifying the type of data in the vector. + """ + + # If a categorical dtype is set, infer categorical + if pd.api.types.is_categorical_dtype(vector): + return VariableType("categorical") + + # Special-case all-na data, which is always "numeric" + if pd.isna(vector).all(): + return VariableType("numeric") + + # Special-case binary/boolean data, allow caller to determine + # This triggers a numpy warning when vector has strings/objects + # https://github.com/numpy/numpy/issues/6784 + # Because we reduce with .all(), we are agnostic about whether the + # comparison returns a scalar or vector, so we will ignore the warning. + # It triggers a separate DeprecationWarning when the vector has datetimes: + # https://github.com/numpy/numpy/issues/13548 + # This is considered a bug by numpy and will likely go away. + with warnings.catch_warnings(): + warnings.simplefilter( + action='ignore', category=(FutureWarning, DeprecationWarning) + ) + if np.isin(vector, [0, 1, np.nan]).all(): + return VariableType(boolean_type) + + # Defer to positive pandas tests + if pd.api.types.is_numeric_dtype(vector): + return VariableType("numeric") + + if pd.api.types.is_datetime64_dtype(vector): + return VariableType("datetime") + + # --- If we get to here, we need to check the entries + + # Check for a collection where everything is a number + + def all_numeric(x): + for x_i in x: + if not isinstance(x_i, Number): + return False + return True + + if all_numeric(vector): + return VariableType("numeric") + + # Check for a collection where everything is a datetime + + def all_datetime(x): + for x_i in x: + if not isinstance(x_i, (datetime, np.datetime64)): + return False + return True + + if all_datetime(vector): + return VariableType("datetime") + + # Otherwise, our final fallback is to consider things categorical + + return VariableType("categorical") + + +def infer_orient(x=None, y=None, orient=None, require_numeric=True): + """Determine how the plot should be oriented based on the data. + + For historical reasons, the convention is to call a plot "horizontally" + or "vertically" oriented based on the axis representing its dependent + variable. Practically, this is used when determining the axis for + numerical aggregation. + + Parameters + ---------- + x, y : Vector data or None + Positional data vectors for the plot. + orient : string or None + Specified orientation, which must start with "v" or "h" if not None. + require_numeric : bool + If set, raise when the implied dependent variable is not numeric. + + Returns + ------- + orient : "v" or "h" + + Raises + ------ + ValueError: When `orient` is not None and does not start with "h" or "v" + TypeError: When dependent variable is not numeric, with `require_numeric` + + """ + + x_type = None if x is None else variable_type(x) + y_type = None if y is None else variable_type(y) + + nonnumeric_dv_error = "{} orientation requires numeric `{}` variable." + single_var_warning = "{} orientation ignored with only `{}` specified." + + if x is None: + if str(orient).startswith("h"): + warnings.warn(single_var_warning.format("Horizontal", "y")) + if require_numeric and y_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Vertical", "y")) + return "v" + + elif y is None: + if str(orient).startswith("v"): + warnings.warn(single_var_warning.format("Vertical", "x")) + if require_numeric and x_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Horizontal", "x")) + return "h" + + elif str(orient).startswith("v"): + if require_numeric and y_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Vertical", "y")) + return "v" + + elif str(orient).startswith("h"): + if require_numeric and x_type != "numeric": + raise TypeError(nonnumeric_dv_error.format("Horizontal", "x")) + return "h" + + elif orient is not None: + err = ( + "`orient` must start with 'v' or 'h' or be None, " + f"but `{repr(orient)}` was passed." + ) + raise ValueError(err) + + elif x_type != "categorical" and y_type == "categorical": + return "h" + + elif x_type != "numeric" and y_type == "numeric": + return "v" + + elif x_type == "numeric" and y_type != "numeric": + return "h" + + elif require_numeric and "numeric" not in (x_type, y_type): + err = "Neither the `x` nor `y` variable appears to be numeric." + raise TypeError(err) + + else: + return "v" + + +def unique_dashes(n): + """Build an arbitrarily long list of unique dash styles for lines. + + Parameters + ---------- + n : int + Number of unique dash specs to generate. + + Returns + ------- + dashes : list of strings or tuples + Valid arguments for the ``dashes`` parameter on + :class:`matplotlib.lines.Line2D`. The first spec is a solid + line (``""``), the remainder are sequences of long and short + dashes. + + """ + # Start with dash specs that are well distinguishable + dashes = [ + "", + (4, 1.5), + (1, 1), + (3, 1.25, 1.5, 1.25), + (5, 1, 1, 1), + ] + + # Now programmatically build as many as we need + p = 3 + while len(dashes) < n: + + # Take combinations of long and short dashes + a = itertools.combinations_with_replacement([3, 1.25], p) + b = itertools.combinations_with_replacement([4, 1], p) + + # Interleave the combinations, reversing one of the streams + segment_list = itertools.chain(*zip( + list(a)[1:-1][::-1], + list(b)[1:-1] + )) + + # Now insert the gaps + for segments in segment_list: + gap = min(segments) + spec = tuple(itertools.chain(*((seg, gap) for seg in segments))) + dashes.append(spec) + + p += 1 + + return dashes[:n] + + +def unique_markers(n): + """Build an arbitrarily long list of unique marker styles for points. + + Parameters + ---------- + n : int + Number of unique marker specs to generate. + + Returns + ------- + markers : list of string or tuples + Values for defining :class:`matplotlib.markers.MarkerStyle` objects. + All markers will be filled. + + """ + # Start with marker specs that are well distinguishable + markers = [ + "o", + "X", + (4, 0, 45), + "P", + (4, 0, 0), + (4, 1, 0), + "^", + (4, 1, 45), + "v", + ] + + # Now generate more from regular polygons of increasing order + s = 5 + while len(markers) < n: + a = 360 / (s + 1) / 2 + markers.extend([ + (s + 1, 1, a), + (s + 1, 0, a), + (s, 1, 0), + (s, 0, 0), + ]) + s += 1 + + # Convert to MarkerStyle object, using only exactly what we need + # markers = [mpl.markers.MarkerStyle(m) for m in markers[:n]] + + return markers[:n] + + +def categorical_order(vector, order=None): + """Return a list of unique data values. + + Determine an ordered list of levels in ``values``. + + Parameters + ---------- + vector : list, array, Categorical, or Series + Vector of "categorical" values + order : list-like, optional + Desired order of category levels to override the order determined + from the ``values`` object. + + Returns + ------- + order : list + Ordered list of category levels not including null values. + + """ + if order is None: + if hasattr(vector, "categories"): + order = vector.categories + else: + try: + order = vector.cat.categories + except (TypeError, AttributeError): + + try: + order = vector.unique() + except AttributeError: + order = pd.unique(vector) + + if variable_type(vector) == "numeric": + order = np.sort(order) + + order = filter(pd.notnull, order) + return list(order) diff --git a/lib/python3.10/site-packages/seaborn/_statistics.py b/lib/python3.10/site-packages/seaborn/_statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..7fe4fbe7b2f1f821a53305a825e640124e2c3936 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_statistics.py @@ -0,0 +1,554 @@ +"""Statistical transformations for visualization. + +This module is currently private, but is being written to eventually form part +of the public API. + +The classes should behave roughly in the style of scikit-learn. + +- All data-independent parameters should be passed to the class constructor. +- Each class should implement a default transformation that is exposed through + __call__. These are currently written for vector arguments, but I think + consuming a whole `plot_data` DataFrame and return it with transformed + variables would make more sense. +- Some class have data-dependent preprocessing that should be cached and used + multiple times (think defining histogram bins off all data and then counting + observations within each bin multiple times per data subsets). These currently + have unique names, but it would be good to have a common name. Not quite + `fit`, but something similar. +- Alternatively, the transform interface could take some information about grouping + variables and do a groupby internally. +- Some classes should define alternate transforms that might make the most sense + with a different function. For example, KDE usually evaluates the distribution + on a regular grid, but it would be useful for it to transform at the actual + datapoints. Then again, this could be controlled by a parameter at the time of + class instantiation. + +""" +from numbers import Number +import numpy as np +import pandas as pd +try: + from scipy.stats import gaussian_kde + _no_scipy = False +except ImportError: + from .external.kde import gaussian_kde + _no_scipy = True + +from .algorithms import bootstrap +from .utils import _check_argument + + +class KDE: + """Univariate and bivariate kernel density estimator.""" + def __init__( + self, *, + bw_method=None, + bw_adjust=1, + gridsize=200, + cut=3, + clip=None, + cumulative=False, + ): + """Initialize the estimator with its parameters. + + Parameters + ---------- + bw_method : string, scalar, or callable, optional + Method for determining the smoothing bandwidth to use; passed to + :class:`scipy.stats.gaussian_kde`. + bw_adjust : number, optional + Factor that multiplicatively scales the value chosen using + ``bw_method``. Increasing will make the curve smoother. See Notes. + gridsize : int, optional + Number of points on each dimension of the evaluation grid. + cut : number, optional + Factor, multiplied by the smoothing bandwidth, that determines how + far the evaluation grid extends past the extreme datapoints. When + set to 0, truncate the curve at the data limits. + clip : pair of numbers or None, or a pair of such pairs + Do not evaluate the density outside of these limits. + cumulative : bool, optional + If True, estimate a cumulative distribution function. Requires scipy. + + """ + if clip is None: + clip = None, None + + self.bw_method = bw_method + self.bw_adjust = bw_adjust + self.gridsize = gridsize + self.cut = cut + self.clip = clip + self.cumulative = cumulative + + if cumulative and _no_scipy: + raise RuntimeError("Cumulative KDE evaluation requires scipy") + + self.support = None + + def _define_support_grid(self, x, bw, cut, clip, gridsize): + """Create the grid of evaluation points depending for vector x.""" + clip_lo = -np.inf if clip[0] is None else clip[0] + clip_hi = +np.inf if clip[1] is None else clip[1] + gridmin = max(x.min() - bw * cut, clip_lo) + gridmax = min(x.max() + bw * cut, clip_hi) + return np.linspace(gridmin, gridmax, gridsize) + + def _define_support_univariate(self, x, weights): + """Create a 1D grid of evaluation points.""" + kde = self._fit(x, weights) + bw = np.sqrt(kde.covariance.squeeze()) + grid = self._define_support_grid( + x, bw, self.cut, self.clip, self.gridsize + ) + return grid + + def _define_support_bivariate(self, x1, x2, weights): + """Create a 2D grid of evaluation points.""" + clip = self.clip + if clip[0] is None or np.isscalar(clip[0]): + clip = (clip, clip) + + kde = self._fit([x1, x2], weights) + bw = np.sqrt(np.diag(kde.covariance).squeeze()) + + grid1 = self._define_support_grid( + x1, bw[0], self.cut, clip[0], self.gridsize + ) + grid2 = self._define_support_grid( + x2, bw[1], self.cut, clip[1], self.gridsize + ) + + return grid1, grid2 + + def define_support(self, x1, x2=None, weights=None, cache=True): + """Create the evaluation grid for a given data set.""" + if x2 is None: + support = self._define_support_univariate(x1, weights) + else: + support = self._define_support_bivariate(x1, x2, weights) + + if cache: + self.support = support + + return support + + def _fit(self, fit_data, weights=None): + """Fit the scipy kde while adding bw_adjust logic and version check.""" + fit_kws = {"bw_method": self.bw_method} + if weights is not None: + fit_kws["weights"] = weights + + kde = gaussian_kde(fit_data, **fit_kws) + kde.set_bandwidth(kde.factor * self.bw_adjust) + + return kde + + def _eval_univariate(self, x, weights=None): + """Fit and evaluate a univariate on univariate data.""" + support = self.support + if support is None: + support = self.define_support(x, cache=False) + + kde = self._fit(x, weights) + + if self.cumulative: + s_0 = support[0] + density = np.array([ + kde.integrate_box_1d(s_0, s_i) for s_i in support + ]) + else: + density = kde(support) + + return density, support + + def _eval_bivariate(self, x1, x2, weights=None): + """Fit and evaluate a univariate on bivariate data.""" + support = self.support + if support is None: + support = self.define_support(x1, x2, cache=False) + + kde = self._fit([x1, x2], weights) + + if self.cumulative: + + grid1, grid2 = support + density = np.zeros((grid1.size, grid2.size)) + p0 = grid1.min(), grid2.min() + for i, xi in enumerate(grid1): + for j, xj in enumerate(grid2): + density[i, j] = kde.integrate_box(p0, (xi, xj)) + + else: + + xx1, xx2 = np.meshgrid(*support) + density = kde([xx1.ravel(), xx2.ravel()]).reshape(xx1.shape) + + return density, support + + def __call__(self, x1, x2=None, weights=None): + """Fit and evaluate on univariate or bivariate data.""" + if x2 is None: + return self._eval_univariate(x1, weights) + else: + return self._eval_bivariate(x1, x2, weights) + + +# Note: we no longer use this for univariate histograms in histplot, +# preferring _stats.Hist. We'll deprecate this once we have a bivariate Stat class. +class Histogram: + """Univariate and bivariate histogram estimator.""" + def __init__( + self, + stat="count", + bins="auto", + binwidth=None, + binrange=None, + discrete=False, + cumulative=False, + ): + """Initialize the estimator with its parameters. + + Parameters + ---------- + stat : str + Aggregate statistic to compute in each bin. + + - `count`: show the number of observations in each bin + - `frequency`: show the number of observations divided by the bin width + - `probability` or `proportion`: normalize such that bar heights sum to 1 + - `percent`: normalize such that bar heights sum to 100 + - `density`: normalize such that the total area of the histogram equals 1 + + bins : str, number, vector, or a pair of such values + Generic bin parameter that can be the name of a reference rule, + the number of bins, or the breaks of the bins. + Passed to :func:`numpy.histogram_bin_edges`. + binwidth : number or pair of numbers + Width of each bin, overrides ``bins`` but can be used with + ``binrange``. + binrange : pair of numbers or a pair of pairs + Lowest and highest value for bin edges; can be used either + with ``bins`` or ``binwidth``. Defaults to data extremes. + discrete : bool or pair of bools + If True, set ``binwidth`` and ``binrange`` such that bin + edges cover integer values in the dataset. + cumulative : bool + If True, return the cumulative statistic. + + """ + stat_choices = [ + "count", "frequency", "density", "probability", "proportion", "percent", + ] + _check_argument("stat", stat_choices, stat) + + self.stat = stat + self.bins = bins + self.binwidth = binwidth + self.binrange = binrange + self.discrete = discrete + self.cumulative = cumulative + + self.bin_kws = None + + def _define_bin_edges(self, x, weights, bins, binwidth, binrange, discrete): + """Inner function that takes bin parameters as arguments.""" + if binrange is None: + start, stop = x.min(), x.max() + else: + start, stop = binrange + + if discrete: + bin_edges = np.arange(start - .5, stop + 1.5) + elif binwidth is not None: + step = binwidth + bin_edges = np.arange(start, stop + step, step) + # Handle roundoff error (maybe there is a less clumsy way?) + if bin_edges.max() < stop or len(bin_edges) < 2: + bin_edges = np.append(bin_edges, bin_edges.max() + step) + else: + bin_edges = np.histogram_bin_edges( + x, bins, binrange, weights, + ) + return bin_edges + + def define_bin_params(self, x1, x2=None, weights=None, cache=True): + """Given data, return numpy.histogram parameters to define bins.""" + if x2 is None: + + bin_edges = self._define_bin_edges( + x1, weights, self.bins, self.binwidth, self.binrange, self.discrete, + ) + + if isinstance(self.bins, (str, Number)): + n_bins = len(bin_edges) - 1 + bin_range = bin_edges.min(), bin_edges.max() + bin_kws = dict(bins=n_bins, range=bin_range) + else: + bin_kws = dict(bins=bin_edges) + + else: + + bin_edges = [] + for i, x in enumerate([x1, x2]): + + # Resolve out whether bin parameters are shared + # or specific to each variable + + bins = self.bins + if not bins or isinstance(bins, (str, Number)): + pass + elif isinstance(bins[i], str): + bins = bins[i] + elif len(bins) == 2: + bins = bins[i] + + binwidth = self.binwidth + if binwidth is None: + pass + elif not isinstance(binwidth, Number): + binwidth = binwidth[i] + + binrange = self.binrange + if binrange is None: + pass + elif not isinstance(binrange[0], Number): + binrange = binrange[i] + + discrete = self.discrete + if not isinstance(discrete, bool): + discrete = discrete[i] + + # Define the bins for this variable + + bin_edges.append(self._define_bin_edges( + x, weights, bins, binwidth, binrange, discrete, + )) + + bin_kws = dict(bins=tuple(bin_edges)) + + if cache: + self.bin_kws = bin_kws + + return bin_kws + + def _eval_bivariate(self, x1, x2, weights): + """Inner function for histogram of two variables.""" + bin_kws = self.bin_kws + if bin_kws is None: + bin_kws = self.define_bin_params(x1, x2, cache=False) + + density = self.stat == "density" + + hist, *bin_edges = np.histogram2d( + x1, x2, **bin_kws, weights=weights, density=density + ) + + area = np.outer( + np.diff(bin_edges[0]), + np.diff(bin_edges[1]), + ) + + if self.stat == "probability" or self.stat == "proportion": + hist = hist.astype(float) / hist.sum() + elif self.stat == "percent": + hist = hist.astype(float) / hist.sum() * 100 + elif self.stat == "frequency": + hist = hist.astype(float) / area + + if self.cumulative: + if self.stat in ["density", "frequency"]: + hist = (hist * area).cumsum(axis=0).cumsum(axis=1) + else: + hist = hist.cumsum(axis=0).cumsum(axis=1) + + return hist, bin_edges + + def _eval_univariate(self, x, weights): + """Inner function for histogram of one variable.""" + bin_kws = self.bin_kws + if bin_kws is None: + bin_kws = self.define_bin_params(x, weights=weights, cache=False) + + density = self.stat == "density" + hist, bin_edges = np.histogram( + x, **bin_kws, weights=weights, density=density, + ) + + if self.stat == "probability" or self.stat == "proportion": + hist = hist.astype(float) / hist.sum() + elif self.stat == "percent": + hist = hist.astype(float) / hist.sum() * 100 + elif self.stat == "frequency": + hist = hist.astype(float) / np.diff(bin_edges) + + if self.cumulative: + if self.stat in ["density", "frequency"]: + hist = (hist * np.diff(bin_edges)).cumsum() + else: + hist = hist.cumsum() + + return hist, bin_edges + + def __call__(self, x1, x2=None, weights=None): + """Count the occurrences in each bin, maybe normalize.""" + if x2 is None: + return self._eval_univariate(x1, weights) + else: + return self._eval_bivariate(x1, x2, weights) + + +class ECDF: + """Univariate empirical cumulative distribution estimator.""" + def __init__(self, stat="proportion", complementary=False): + """Initialize the class with its parameters + + Parameters + ---------- + stat : {{"proportion", "count"}} + Distribution statistic to compute. + complementary : bool + If True, use the complementary CDF (1 - CDF) + + """ + _check_argument("stat", ["count", "proportion"], stat) + self.stat = stat + self.complementary = complementary + + def _eval_bivariate(self, x1, x2, weights): + """Inner function for ECDF of two variables.""" + raise NotImplementedError("Bivariate ECDF is not implemented") + + def _eval_univariate(self, x, weights): + """Inner function for ECDF of one variable.""" + sorter = x.argsort() + x = x[sorter] + weights = weights[sorter] + y = weights.cumsum() + + if self.stat == "proportion": + y = y / y.max() + + x = np.r_[-np.inf, x] + y = np.r_[0, y] + + if self.complementary: + y = y.max() - y + + return y, x + + def __call__(self, x1, x2=None, weights=None): + """Return proportion or count of observations below each sorted datapoint.""" + x1 = np.asarray(x1) + if weights is None: + weights = np.ones_like(x1) + else: + weights = np.asarray(weights) + + if x2 is None: + return self._eval_univariate(x1, weights) + else: + return self._eval_bivariate(x1, x2, weights) + + +class EstimateAggregator: + + def __init__(self, estimator, errorbar=None, **boot_kws): + """ + Data aggregator that produces an estimate and error bar interval. + + Parameters + ---------- + estimator : callable or string + Function (or method name) that maps a vector to a scalar. + errorbar : string, (string, number) tuple, or callable + Name of errorbar method (either "ci", "pi", "se", or "sd"), or a tuple + with a method name and a level parameter, or a function that maps from a + vector to a (min, max) interval. + boot_kws + Additional keywords are passed to bootstrap when error_method is "ci". + + """ + self.estimator = estimator + + method, level = _validate_errorbar_arg(errorbar) + self.error_method = method + self.error_level = level + + self.boot_kws = boot_kws + + def __call__(self, data, var): + """Aggregate over `var` column of `data` with estimate and error interval.""" + vals = data[var] + if callable(self.estimator): + # You would think we could pass to vals.agg, and yet: + # https://github.com/mwaskom/seaborn/issues/2943 + estimate = self.estimator(vals) + else: + estimate = vals.agg(self.estimator) + + # Options that produce no error bars + if self.error_method is None: + err_min = err_max = np.nan + elif len(data) <= 1: + err_min = err_max = np.nan + + # Generic errorbars from user-supplied function + elif callable(self.error_method): + err_min, err_max = self.error_method(vals) + + # Parametric options + elif self.error_method == "sd": + half_interval = vals.std() * self.error_level + err_min, err_max = estimate - half_interval, estimate + half_interval + elif self.error_method == "se": + half_interval = vals.sem() * self.error_level + err_min, err_max = estimate - half_interval, estimate + half_interval + + # Nonparametric options + elif self.error_method == "pi": + err_min, err_max = _percentile_interval(vals, self.error_level) + elif self.error_method == "ci": + units = data.get("units", None) + boots = bootstrap(vals, units=units, func=self.estimator, **self.boot_kws) + err_min, err_max = _percentile_interval(boots, self.error_level) + + return pd.Series({var: estimate, f"{var}min": err_min, f"{var}max": err_max}) + + +def _percentile_interval(data, width): + """Return a percentile interval from data of a given width.""" + edge = (100 - width) / 2 + percentiles = edge, 100 - edge + return np.nanpercentile(data, percentiles) + + +def _validate_errorbar_arg(arg): + """Check type and value of errorbar argument and assign default level.""" + DEFAULT_LEVELS = { + "ci": 95, + "pi": 95, + "se": 1, + "sd": 1, + } + + usage = "`errorbar` must be a callable, string, or (string, number) tuple" + + if arg is None: + return None, None + elif callable(arg): + return arg, None + elif isinstance(arg, str): + method = arg + level = DEFAULT_LEVELS.get(method, None) + else: + try: + method, level = arg + except (ValueError, TypeError) as err: + raise err.__class__(usage) from err + + _check_argument("errorbar", list(DEFAULT_LEVELS), method) + if level is not None and not isinstance(level, Number): + raise TypeError(usage) + + return method, level diff --git a/lib/python3.10/site-packages/seaborn/_testing.py b/lib/python3.10/site-packages/seaborn/_testing.py new file mode 100644 index 0000000000000000000000000000000000000000..c6f821cbe26f44a720cc8863fe6a863d61a275dd --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/_testing.py @@ -0,0 +1,90 @@ +import numpy as np +import matplotlib as mpl +from matplotlib.colors import to_rgb, to_rgba +from numpy.testing import assert_array_equal + + +USE_PROPS = [ + "alpha", + "edgecolor", + "facecolor", + "fill", + "hatch", + "height", + "linestyle", + "linewidth", + "paths", + "xy", + "xydata", + "sizes", + "zorder", +] + + +def assert_artists_equal(list1, list2): + + assert len(list1) == len(list2) + for a1, a2 in zip(list1, list2): + assert a1.__class__ == a2.__class__ + prop1 = a1.properties() + prop2 = a2.properties() + for key in USE_PROPS: + if key not in prop1: + continue + v1 = prop1[key] + v2 = prop2[key] + if key == "paths": + for p1, p2 in zip(v1, v2): + assert_array_equal(p1.vertices, p2.vertices) + assert_array_equal(p1.codes, p2.codes) + elif key == "color": + v1 = mpl.colors.to_rgba(v1) + v2 = mpl.colors.to_rgba(v2) + assert v1 == v2 + elif isinstance(v1, np.ndarray): + assert_array_equal(v1, v2) + else: + assert v1 == v2 + + +def assert_legends_equal(leg1, leg2): + + assert leg1.get_title().get_text() == leg2.get_title().get_text() + for t1, t2 in zip(leg1.get_texts(), leg2.get_texts()): + assert t1.get_text() == t2.get_text() + + assert_artists_equal( + leg1.get_patches(), leg2.get_patches(), + ) + assert_artists_equal( + leg1.get_lines(), leg2.get_lines(), + ) + + +def assert_plots_equal(ax1, ax2, labels=True): + + assert_artists_equal(ax1.patches, ax2.patches) + assert_artists_equal(ax1.lines, ax2.lines) + assert_artists_equal(ax1.collections, ax2.collections) + + if labels: + assert ax1.get_xlabel() == ax2.get_xlabel() + assert ax1.get_ylabel() == ax2.get_ylabel() + + +def assert_colors_equal(a, b, check_alpha=True): + + def handle_array(x): + + if isinstance(x, np.ndarray): + if x.ndim > 1: + x = np.unique(x, axis=0).squeeze() + if x.ndim > 1: + raise ValueError("Color arrays must be 1 dimensional") + return x + + a = handle_array(a) + b = handle_array(b) + + f = to_rgba if check_alpha else to_rgb + assert f(a) == f(b) diff --git a/lib/python3.10/site-packages/seaborn/algorithms.py b/lib/python3.10/site-packages/seaborn/algorithms.py new file mode 100644 index 0000000000000000000000000000000000000000..2867e9a06313a137e03456905049f20d36c62c51 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/algorithms.py @@ -0,0 +1,142 @@ +"""Algorithms to support fitting routines in seaborn plotting functions.""" +import numbers +import numpy as np +import warnings + + +def bootstrap(*args, **kwargs): + """Resample one or more arrays with replacement and store aggregate values. + + Positional arguments are a sequence of arrays to bootstrap along the first + axis and pass to a summary function. + + Keyword arguments: + n_boot : int, default=10000 + Number of iterations + axis : int, default=None + Will pass axis to ``func`` as a keyword argument. + units : array, default=None + Array of sampling unit IDs. When used the bootstrap resamples units + and then observations within units instead of individual + datapoints. + func : string or callable, default="mean" + Function to call on the args that are passed in. If string, uses as + name of function in the numpy namespace. If nans are present in the + data, will try to use nan-aware version of named function. + seed : Generator | SeedSequence | RandomState | int | None + Seed for the random number generator; useful if you want + reproducible resamples. + + Returns + ------- + boot_dist: array + array of bootstrapped statistic values + + """ + # Ensure list of arrays are same length + if len(np.unique(list(map(len, args)))) > 1: + raise ValueError("All input arrays must have the same length") + n = len(args[0]) + + # Default keyword arguments + n_boot = kwargs.get("n_boot", 10000) + func = kwargs.get("func", "mean") + axis = kwargs.get("axis", None) + units = kwargs.get("units", None) + random_seed = kwargs.get("random_seed", None) + if random_seed is not None: + msg = "`random_seed` has been renamed to `seed` and will be removed" + warnings.warn(msg) + seed = kwargs.get("seed", random_seed) + if axis is None: + func_kwargs = dict() + else: + func_kwargs = dict(axis=axis) + + # Initialize the resampler + rng = _handle_random_seed(seed) + + # Coerce to arrays + args = list(map(np.asarray, args)) + if units is not None: + units = np.asarray(units) + + if isinstance(func, str): + + # Allow named numpy functions + f = getattr(np, func) + + # Try to use nan-aware version of function if necessary + missing_data = np.isnan(np.sum(np.column_stack(args))) + + if missing_data and not func.startswith("nan"): + nanf = getattr(np, f"nan{func}", None) + if nanf is None: + msg = f"Data contain nans but no nan-aware version of `{func}` found" + warnings.warn(msg, UserWarning) + else: + f = nanf + + else: + f = func + + # Handle numpy changes + try: + integers = rng.integers + except AttributeError: + integers = rng.randint + + # Do the bootstrap + if units is not None: + return _structured_bootstrap(args, n_boot, units, f, + func_kwargs, integers) + + boot_dist = [] + for i in range(int(n_boot)): + resampler = integers(0, n, n, dtype=np.intp) # intp is indexing dtype + sample = [a.take(resampler, axis=0) for a in args] + boot_dist.append(f(*sample, **func_kwargs)) + return np.array(boot_dist) + + +def _structured_bootstrap(args, n_boot, units, func, func_kwargs, integers): + """Resample units instead of datapoints.""" + unique_units = np.unique(units) + n_units = len(unique_units) + + args = [[a[units == unit] for unit in unique_units] for a in args] + + boot_dist = [] + for i in range(int(n_boot)): + resampler = integers(0, n_units, n_units, dtype=np.intp) + sample = [[a[i] for i in resampler] for a in args] + lengths = map(len, sample[0]) + resampler = [integers(0, n, n, dtype=np.intp) for n in lengths] + sample = [[c.take(r, axis=0) for c, r in zip(a, resampler)] for a in sample] + sample = list(map(np.concatenate, sample)) + boot_dist.append(func(*sample, **func_kwargs)) + return np.array(boot_dist) + + +def _handle_random_seed(seed=None): + """Given a seed in one of many formats, return a random number generator. + + Generalizes across the numpy 1.17 changes, preferring newer functionality. + + """ + if isinstance(seed, np.random.RandomState): + rng = seed + else: + try: + # General interface for seeding on numpy >= 1.17 + rng = np.random.default_rng(seed) + except AttributeError: + # We are on numpy < 1.17, handle options ourselves + if isinstance(seed, (numbers.Integral, np.integer)): + rng = np.random.RandomState(seed) + elif seed is None: + rng = np.random.RandomState() + else: + err = "{} cannot be used to seed the random number generator" + raise ValueError(err.format(seed)) + return rng diff --git a/lib/python3.10/site-packages/seaborn/axisgrid.py b/lib/python3.10/site-packages/seaborn/axisgrid.py new file mode 100644 index 0000000000000000000000000000000000000000..a57836999d059b39ec447b9af7c01fbf58923b20 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/axisgrid.py @@ -0,0 +1,2400 @@ +from __future__ import annotations +from itertools import product +from inspect import signature +import warnings +from textwrap import dedent + +import numpy as np +import pandas as pd +import matplotlib as mpl +import matplotlib.pyplot as plt + +from ._oldcore import VectorPlotter, variable_type, categorical_order +from ._compat import share_axis +from . import utils +from .utils import ( + adjust_legend_subtitles, _check_argument, _draw_figure, _disable_autolayout +) +from .palettes import color_palette, blend_palette +from ._docstrings import ( + DocstringComponents, + _core_docs, +) + +__all__ = ["FacetGrid", "PairGrid", "JointGrid", "pairplot", "jointplot"] + + +_param_docs = DocstringComponents.from_nested_components( + core=_core_docs["params"], +) + + +class _BaseGrid: + """Base class for grids of subplots.""" + + def set(self, **kwargs): + """Set attributes on each subplot Axes.""" + for ax in self.axes.flat: + if ax is not None: # Handle removed axes + ax.set(**kwargs) + return self + + @property + def fig(self): + """DEPRECATED: prefer the `figure` property.""" + # Grid.figure is preferred because it matches the Axes attribute name. + # But as the maintanace burden on having this property is minimal, + # let's be slow about formally deprecating it. For now just note its deprecation + # in the docstring; add a warning in version 0.13, and eventually remove it. + return self._figure + + @property + def figure(self): + """Access the :class:`matplotlib.figure.Figure` object underlying the grid.""" + return self._figure + + def apply(self, func, *args, **kwargs): + """ + Pass the grid to a user-supplied function and return self. + + The `func` must accept an object of this type for its first + positional argument. Additional arguments are passed through. + The return value of `func` is ignored; this method returns self. + See the `pipe` method if you want the return value. + + Added in v0.12.0. + + """ + func(self, *args, **kwargs) + return self + + def pipe(self, func, *args, **kwargs): + """ + Pass the grid to a user-supplied function and return its value. + + The `func` must accept an object of this type for its first + positional argument. Additional arguments are passed through. + The return value of `func` becomes the return value of this method. + See the `apply` method if you want to return self instead. + + Added in v0.12.0. + + """ + return func(self, *args, **kwargs) + + def savefig(self, *args, **kwargs): + """ + Save an image of the plot. + + This wraps :meth:`matplotlib.figure.Figure.savefig`, using bbox_inches="tight" + by default. Parameters are passed through to the matplotlib function. + + """ + kwargs = kwargs.copy() + kwargs.setdefault("bbox_inches", "tight") + self.figure.savefig(*args, **kwargs) + + +class Grid(_BaseGrid): + """A grid that can have multiple subplots and an external legend.""" + _margin_titles = False + _legend_out = True + + def __init__(self): + + self._tight_layout_rect = [0, 0, 1, 1] + self._tight_layout_pad = None + + # This attribute is set externally and is a hack to handle newer functions that + # don't add proxy artists onto the Axes. We need an overall cleaner approach. + self._extract_legend_handles = False + + def tight_layout(self, *args, **kwargs): + """Call fig.tight_layout within rect that exclude the legend.""" + kwargs = kwargs.copy() + kwargs.setdefault("rect", self._tight_layout_rect) + if self._tight_layout_pad is not None: + kwargs.setdefault("pad", self._tight_layout_pad) + self._figure.tight_layout(*args, **kwargs) + return self + + def add_legend(self, legend_data=None, title=None, label_order=None, + adjust_subtitles=False, **kwargs): + """Draw a legend, maybe placing it outside axes and resizing the figure. + + Parameters + ---------- + legend_data : dict + Dictionary mapping label names (or two-element tuples where the + second element is a label name) to matplotlib artist handles. The + default reads from ``self._legend_data``. + title : string + Title for the legend. The default reads from ``self._hue_var``. + label_order : list of labels + The order that the legend entries should appear in. The default + reads from ``self.hue_names``. + adjust_subtitles : bool + If True, modify entries with invisible artists to left-align + the labels and set the font size to that of a title. + kwargs : key, value pairings + Other keyword arguments are passed to the underlying legend methods + on the Figure or Axes object. + + Returns + ------- + self : Grid instance + Returns self for easy chaining. + + """ + # Find the data for the legend + if legend_data is None: + legend_data = self._legend_data + if label_order is None: + if self.hue_names is None: + label_order = list(legend_data.keys()) + else: + label_order = list(map(utils.to_utf8, self.hue_names)) + + blank_handle = mpl.patches.Patch(alpha=0, linewidth=0) + handles = [legend_data.get(l, blank_handle) for l in label_order] + title = self._hue_var if title is None else title + title_size = mpl.rcParams["legend.title_fontsize"] + + # Unpack nested labels from a hierarchical legend + labels = [] + for entry in label_order: + if isinstance(entry, tuple): + _, label = entry + else: + label = entry + labels.append(label) + + # Set default legend kwargs + kwargs.setdefault("scatterpoints", 1) + + if self._legend_out: + + kwargs.setdefault("frameon", False) + kwargs.setdefault("loc", "center right") + + # Draw a full-figure legend outside the grid + figlegend = self._figure.legend(handles, labels, **kwargs) + + self._legend = figlegend + figlegend.set_title(title, prop={"size": title_size}) + + if adjust_subtitles: + adjust_legend_subtitles(figlegend) + + # Draw the plot to set the bounding boxes correctly + _draw_figure(self._figure) + + # Calculate and set the new width of the figure so the legend fits + legend_width = figlegend.get_window_extent().width / self._figure.dpi + fig_width, fig_height = self._figure.get_size_inches() + self._figure.set_size_inches(fig_width + legend_width, fig_height) + + # Draw the plot again to get the new transformations + _draw_figure(self._figure) + + # Now calculate how much space we need on the right side + legend_width = figlegend.get_window_extent().width / self._figure.dpi + space_needed = legend_width / (fig_width + legend_width) + margin = .04 if self._margin_titles else .01 + self._space_needed = margin + space_needed + right = 1 - self._space_needed + + # Place the subplot axes to give space for the legend + self._figure.subplots_adjust(right=right) + self._tight_layout_rect[2] = right + + else: + # Draw a legend in the first axis + ax = self.axes.flat[0] + kwargs.setdefault("loc", "best") + + leg = ax.legend(handles, labels, **kwargs) + leg.set_title(title, prop={"size": title_size}) + self._legend = leg + + if adjust_subtitles: + adjust_legend_subtitles(leg) + + return self + + def _update_legend_data(self, ax): + """Extract the legend data from an axes object and save it.""" + data = {} + + # Get data directly from the legend, which is necessary + # for newer functions that don't add labeled proxy artists + if ax.legend_ is not None and self._extract_legend_handles: + handles = ax.legend_.legendHandles + labels = [t.get_text() for t in ax.legend_.texts] + data.update({l: h for h, l in zip(handles, labels)}) + + handles, labels = ax.get_legend_handles_labels() + data.update({l: h for h, l in zip(handles, labels)}) + + self._legend_data.update(data) + + # Now clear the legend + ax.legend_ = None + + def _get_palette(self, data, hue, hue_order, palette): + """Get a list of colors for the hue variable.""" + if hue is None: + palette = color_palette(n_colors=1) + + else: + hue_names = categorical_order(data[hue], hue_order) + n_colors = len(hue_names) + + # By default use either the current color palette or HUSL + if palette is None: + current_palette = utils.get_color_cycle() + if n_colors > len(current_palette): + colors = color_palette("husl", n_colors) + else: + colors = color_palette(n_colors=n_colors) + + # Allow for palette to map from hue variable names + elif isinstance(palette, dict): + color_names = [palette[h] for h in hue_names] + colors = color_palette(color_names, n_colors) + + # Otherwise act as if we just got a list of colors + else: + colors = color_palette(palette, n_colors) + + palette = color_palette(colors, n_colors) + + return palette + + @property + def legend(self): + """The :class:`matplotlib.legend.Legend` object, if present.""" + try: + return self._legend + except AttributeError: + return None + + def tick_params(self, axis='both', **kwargs): + """Modify the ticks, tick labels, and gridlines. + + Parameters + ---------- + axis : {'x', 'y', 'both'} + The axis on which to apply the formatting. + kwargs : keyword arguments + Additional keyword arguments to pass to + :meth:`matplotlib.axes.Axes.tick_params`. + + Returns + ------- + self : Grid instance + Returns self for easy chaining. + + """ + for ax in self.figure.axes: + ax.tick_params(axis=axis, **kwargs) + return self + + +_facet_docs = dict( + + data=dedent("""\ + data : DataFrame + Tidy ("long-form") dataframe where each column is a variable and each + row is an observation.\ + """), + rowcol=dedent("""\ + row, col : vectors or keys in ``data`` + Variables that define subsets to plot on different facets.\ + """), + rowcol_order=dedent("""\ + {row,col}_order : vector of strings + Specify the order in which levels of the ``row`` and/or ``col`` variables + appear in the grid of subplots.\ + """), + col_wrap=dedent("""\ + col_wrap : int + "Wrap" the column variable at this width, so that the column facets + span multiple rows. Incompatible with a ``row`` facet.\ + """), + share_xy=dedent("""\ + share{x,y} : bool, 'col', or 'row' optional + If true, the facets will share y axes across columns and/or x axes + across rows.\ + """), + height=dedent("""\ + height : scalar + Height (in inches) of each facet. See also: ``aspect``.\ + """), + aspect=dedent("""\ + aspect : scalar + Aspect ratio of each facet, so that ``aspect * height`` gives the width + of each facet in inches.\ + """), + palette=dedent("""\ + palette : palette name, list, or dict + Colors to use for the different levels of the ``hue`` variable. Should + be something that can be interpreted by :func:`color_palette`, or a + dictionary mapping hue levels to matplotlib colors.\ + """), + legend_out=dedent("""\ + legend_out : bool + If ``True``, the figure size will be extended, and the legend will be + drawn outside the plot on the center right.\ + """), + margin_titles=dedent("""\ + margin_titles : bool + If ``True``, the titles for the row variable are drawn to the right of + the last column. This option is experimental and may not work in all + cases.\ + """), + facet_kws=dedent("""\ + facet_kws : dict + Additional parameters passed to :class:`FacetGrid`. + """), +) + + +class FacetGrid(Grid): + """Multi-plot grid for plotting conditional relationships.""" + + def __init__( + self, data, *, + row=None, col=None, hue=None, col_wrap=None, + sharex=True, sharey=True, height=3, aspect=1, palette=None, + row_order=None, col_order=None, hue_order=None, hue_kws=None, + dropna=False, legend_out=True, despine=True, + margin_titles=False, xlim=None, ylim=None, subplot_kws=None, + gridspec_kws=None, + ): + + super().__init__() + + # Determine the hue facet layer information + hue_var = hue + if hue is None: + hue_names = None + else: + hue_names = categorical_order(data[hue], hue_order) + + colors = self._get_palette(data, hue, hue_order, palette) + + # Set up the lists of names for the row and column facet variables + if row is None: + row_names = [] + else: + row_names = categorical_order(data[row], row_order) + + if col is None: + col_names = [] + else: + col_names = categorical_order(data[col], col_order) + + # Additional dict of kwarg -> list of values for mapping the hue var + hue_kws = hue_kws if hue_kws is not None else {} + + # Make a boolean mask that is True anywhere there is an NA + # value in one of the faceting variables, but only if dropna is True + none_na = np.zeros(len(data), bool) + if dropna: + row_na = none_na if row is None else data[row].isnull() + col_na = none_na if col is None else data[col].isnull() + hue_na = none_na if hue is None else data[hue].isnull() + not_na = ~(row_na | col_na | hue_na) + else: + not_na = ~none_na + + # Compute the grid shape + ncol = 1 if col is None else len(col_names) + nrow = 1 if row is None else len(row_names) + self._n_facets = ncol * nrow + + self._col_wrap = col_wrap + if col_wrap is not None: + if row is not None: + err = "Cannot use `row` and `col_wrap` together." + raise ValueError(err) + ncol = col_wrap + nrow = int(np.ceil(len(col_names) / col_wrap)) + self._ncol = ncol + self._nrow = nrow + + # Calculate the base figure size + # This can get stretched later by a legend + # TODO this doesn't account for axis labels + figsize = (ncol * height * aspect, nrow * height) + + # Validate some inputs + if col_wrap is not None: + margin_titles = False + + # Build the subplot keyword dictionary + subplot_kws = {} if subplot_kws is None else subplot_kws.copy() + gridspec_kws = {} if gridspec_kws is None else gridspec_kws.copy() + if xlim is not None: + subplot_kws["xlim"] = xlim + if ylim is not None: + subplot_kws["ylim"] = ylim + + # --- Initialize the subplot grid + + with _disable_autolayout(): + fig = plt.figure(figsize=figsize) + + if col_wrap is None: + + kwargs = dict(squeeze=False, + sharex=sharex, sharey=sharey, + subplot_kw=subplot_kws, + gridspec_kw=gridspec_kws) + + axes = fig.subplots(nrow, ncol, **kwargs) + + if col is None and row is None: + axes_dict = {} + elif col is None: + axes_dict = dict(zip(row_names, axes.flat)) + elif row is None: + axes_dict = dict(zip(col_names, axes.flat)) + else: + facet_product = product(row_names, col_names) + axes_dict = dict(zip(facet_product, axes.flat)) + + else: + + # If wrapping the col variable we need to make the grid ourselves + if gridspec_kws: + warnings.warn("`gridspec_kws` ignored when using `col_wrap`") + + n_axes = len(col_names) + axes = np.empty(n_axes, object) + axes[0] = fig.add_subplot(nrow, ncol, 1, **subplot_kws) + if sharex: + subplot_kws["sharex"] = axes[0] + if sharey: + subplot_kws["sharey"] = axes[0] + for i in range(1, n_axes): + axes[i] = fig.add_subplot(nrow, ncol, i + 1, **subplot_kws) + + axes_dict = dict(zip(col_names, axes)) + + # --- Set up the class attributes + + # Attributes that are part of the public API but accessed through + # a property so that Sphinx adds them to the auto class doc + self._figure = fig + self._axes = axes + self._axes_dict = axes_dict + self._legend = None + + # Public attributes that aren't explicitly documented + # (It's not obvious that having them be public was a good idea) + self.data = data + self.row_names = row_names + self.col_names = col_names + self.hue_names = hue_names + self.hue_kws = hue_kws + + # Next the private variables + self._nrow = nrow + self._row_var = row + self._ncol = ncol + self._col_var = col + + self._margin_titles = margin_titles + self._margin_titles_texts = [] + self._col_wrap = col_wrap + self._hue_var = hue_var + self._colors = colors + self._legend_out = legend_out + self._legend_data = {} + self._x_var = None + self._y_var = None + self._sharex = sharex + self._sharey = sharey + self._dropna = dropna + self._not_na = not_na + + # --- Make the axes look good + + self.set_titles() + self.tight_layout() + + if despine: + self.despine() + + if sharex in [True, 'col']: + for ax in self._not_bottom_axes: + for label in ax.get_xticklabels(): + label.set_visible(False) + ax.xaxis.offsetText.set_visible(False) + ax.xaxis.label.set_visible(False) + + if sharey in [True, 'row']: + for ax in self._not_left_axes: + for label in ax.get_yticklabels(): + label.set_visible(False) + ax.yaxis.offsetText.set_visible(False) + ax.yaxis.label.set_visible(False) + + __init__.__doc__ = dedent("""\ + Initialize the matplotlib figure and FacetGrid object. + + This class maps a dataset onto multiple axes arrayed in a grid of rows + and columns that correspond to *levels* of variables in the dataset. + The plots it produces are often called "lattice", "trellis", or + "small-multiple" graphics. + + It can also represent levels of a third variable with the ``hue`` + parameter, which plots different subsets of data in different colors. + This uses color to resolve elements on a third dimension, but only + draws subsets on top of each other and will not tailor the ``hue`` + parameter for the specific visualization the way that axes-level + functions that accept ``hue`` will. + + The basic workflow is to initialize the :class:`FacetGrid` object with + the dataset and the variables that are used to structure the grid. Then + one or more plotting functions can be applied to each subset by calling + :meth:`FacetGrid.map` or :meth:`FacetGrid.map_dataframe`. Finally, the + plot can be tweaked with other methods to do things like change the + axis labels, use different ticks, or add a legend. See the detailed + code examples below for more information. + + .. warning:: + + When using seaborn functions that infer semantic mappings from a + dataset, care must be taken to synchronize those mappings across + facets (e.g., by defining the ``hue`` mapping with a palette dict or + setting the data type of the variables to ``category``). In most cases, + it will be better to use a figure-level function (e.g. :func:`relplot` + or :func:`catplot`) than to use :class:`FacetGrid` directly. + + See the :ref:`tutorial ` for more information. + + Parameters + ---------- + {data} + row, col, hue : strings + Variables that define subsets of the data, which will be drawn on + separate facets in the grid. See the ``{{var}}_order`` parameters to + control the order of levels of this variable. + {col_wrap} + {share_xy} + {height} + {aspect} + {palette} + {{row,col,hue}}_order : lists + Order for the levels of the faceting variables. By default, this + will be the order that the levels appear in ``data`` or, if the + variables are pandas categoricals, the category order. + hue_kws : dictionary of param -> list of values mapping + Other keyword arguments to insert into the plotting call to let + other plot attributes vary across levels of the hue variable (e.g. + the markers in a scatterplot). + {legend_out} + despine : boolean + Remove the top and right spines from the plots. + {margin_titles} + {{x, y}}lim: tuples + Limits for each of the axes on each facet (only relevant when + share{{x, y}} is True). + subplot_kws : dict + Dictionary of keyword arguments passed to matplotlib subplot(s) + methods. + gridspec_kws : dict + Dictionary of keyword arguments passed to + :class:`matplotlib.gridspec.GridSpec` + (via :meth:`matplotlib.figure.Figure.subplots`). + Ignored if ``col_wrap`` is not ``None``. + + See Also + -------- + PairGrid : Subplot grid for plotting pairwise relationships + relplot : Combine a relational plot and a :class:`FacetGrid` + displot : Combine a distribution plot and a :class:`FacetGrid` + catplot : Combine a categorical plot and a :class:`FacetGrid` + lmplot : Combine a regression plot and a :class:`FacetGrid` + + Examples + -------- + + .. note:: + + These examples use seaborn functions to demonstrate some of the + advanced features of the class, but in most cases you will want + to use figue-level functions (e.g. :func:`displot`, :func:`relplot`) + to make the plots shown here. + + .. include:: ../docstrings/FacetGrid.rst + + """).format(**_facet_docs) + + def facet_data(self): + """Generator for name indices and data subsets for each facet. + + Yields + ------ + (i, j, k), data_ijk : tuple of ints, DataFrame + The ints provide an index into the {row, col, hue}_names attribute, + and the dataframe contains a subset of the full data corresponding + to each facet. The generator yields subsets that correspond with + the self.axes.flat iterator, or self.axes[i, j] when `col_wrap` + is None. + + """ + data = self.data + + # Construct masks for the row variable + if self.row_names: + row_masks = [data[self._row_var] == n for n in self.row_names] + else: + row_masks = [np.repeat(True, len(self.data))] + + # Construct masks for the column variable + if self.col_names: + col_masks = [data[self._col_var] == n for n in self.col_names] + else: + col_masks = [np.repeat(True, len(self.data))] + + # Construct masks for the hue variable + if self.hue_names: + hue_masks = [data[self._hue_var] == n for n in self.hue_names] + else: + hue_masks = [np.repeat(True, len(self.data))] + + # Here is the main generator loop + for (i, row), (j, col), (k, hue) in product(enumerate(row_masks), + enumerate(col_masks), + enumerate(hue_masks)): + data_ijk = data[row & col & hue & self._not_na] + yield (i, j, k), data_ijk + + def map(self, func, *args, **kwargs): + """Apply a plotting function to each facet's subset of the data. + + Parameters + ---------- + func : callable + A plotting function that takes data and keyword arguments. It + must plot to the currently active matplotlib Axes and take a + `color` keyword argument. If faceting on the `hue` dimension, + it must also take a `label` keyword argument. + args : strings + Column names in self.data that identify variables with data to + plot. The data for each variable is passed to `func` in the + order the variables are specified in the call. + kwargs : keyword arguments + All keyword arguments are passed to the plotting function. + + Returns + ------- + self : object + Returns self. + + """ + # If color was a keyword argument, grab it here + kw_color = kwargs.pop("color", None) + + # How we use the function depends on where it comes from + func_module = str(getattr(func, "__module__", "")) + + # Check for categorical plots without order information + if func_module == "seaborn.categorical": + if "order" not in kwargs: + warning = ("Using the {} function without specifying " + "`order` is likely to produce an incorrect " + "plot.".format(func.__name__)) + warnings.warn(warning) + if len(args) == 3 and "hue_order" not in kwargs: + warning = ("Using the {} function without specifying " + "`hue_order` is likely to produce an incorrect " + "plot.".format(func.__name__)) + warnings.warn(warning) + + # Iterate over the data subsets + for (row_i, col_j, hue_k), data_ijk in self.facet_data(): + + # If this subset is null, move on + if not data_ijk.values.size: + continue + + # Get the current axis + modify_state = not func_module.startswith("seaborn") + ax = self.facet_axis(row_i, col_j, modify_state) + + # Decide what color to plot with + kwargs["color"] = self._facet_color(hue_k, kw_color) + + # Insert the other hue aesthetics if appropriate + for kw, val_list in self.hue_kws.items(): + kwargs[kw] = val_list[hue_k] + + # Insert a label in the keyword arguments for the legend + if self._hue_var is not None: + kwargs["label"] = utils.to_utf8(self.hue_names[hue_k]) + + # Get the actual data we are going to plot with + plot_data = data_ijk[list(args)] + if self._dropna: + plot_data = plot_data.dropna() + plot_args = [v for k, v in plot_data.items()] + + # Some matplotlib functions don't handle pandas objects correctly + if func_module.startswith("matplotlib"): + plot_args = [v.values for v in plot_args] + + # Draw the plot + self._facet_plot(func, ax, plot_args, kwargs) + + # Finalize the annotations and layout + self._finalize_grid(args[:2]) + + return self + + def map_dataframe(self, func, *args, **kwargs): + """Like ``.map`` but passes args as strings and inserts data in kwargs. + + This method is suitable for plotting with functions that accept a + long-form DataFrame as a `data` keyword argument and access the + data in that DataFrame using string variable names. + + Parameters + ---------- + func : callable + A plotting function that takes data and keyword arguments. Unlike + the `map` method, a function used here must "understand" Pandas + objects. It also must plot to the currently active matplotlib Axes + and take a `color` keyword argument. If faceting on the `hue` + dimension, it must also take a `label` keyword argument. + args : strings + Column names in self.data that identify variables with data to + plot. The data for each variable is passed to `func` in the + order the variables are specified in the call. + kwargs : keyword arguments + All keyword arguments are passed to the plotting function. + + Returns + ------- + self : object + Returns self. + + """ + + # If color was a keyword argument, grab it here + kw_color = kwargs.pop("color", None) + + # Iterate over the data subsets + for (row_i, col_j, hue_k), data_ijk in self.facet_data(): + + # If this subset is null, move on + if not data_ijk.values.size: + continue + + # Get the current axis + modify_state = not str(func.__module__).startswith("seaborn") + ax = self.facet_axis(row_i, col_j, modify_state) + + # Decide what color to plot with + kwargs["color"] = self._facet_color(hue_k, kw_color) + + # Insert the other hue aesthetics if appropriate + for kw, val_list in self.hue_kws.items(): + kwargs[kw] = val_list[hue_k] + + # Insert a label in the keyword arguments for the legend + if self._hue_var is not None: + kwargs["label"] = self.hue_names[hue_k] + + # Stick the facet dataframe into the kwargs + if self._dropna: + data_ijk = data_ijk.dropna() + kwargs["data"] = data_ijk + + # Draw the plot + self._facet_plot(func, ax, args, kwargs) + + # For axis labels, prefer to use positional args for backcompat + # but also extract the x/y kwargs and use if no corresponding arg + axis_labels = [kwargs.get("x", None), kwargs.get("y", None)] + for i, val in enumerate(args[:2]): + axis_labels[i] = val + self._finalize_grid(axis_labels) + + return self + + def _facet_color(self, hue_index, kw_color): + + color = self._colors[hue_index] + if kw_color is not None: + return kw_color + elif color is not None: + return color + + def _facet_plot(self, func, ax, plot_args, plot_kwargs): + + # Draw the plot + if str(func.__module__).startswith("seaborn"): + plot_kwargs = plot_kwargs.copy() + semantics = ["x", "y", "hue", "size", "style"] + for key, val in zip(semantics, plot_args): + plot_kwargs[key] = val + plot_args = [] + plot_kwargs["ax"] = ax + func(*plot_args, **plot_kwargs) + + # Sort out the supporting information + self._update_legend_data(ax) + + def _finalize_grid(self, axlabels): + """Finalize the annotations and layout.""" + self.set_axis_labels(*axlabels) + self.tight_layout() + + def facet_axis(self, row_i, col_j, modify_state=True): + """Make the axis identified by these indices active and return it.""" + + # Calculate the actual indices of the axes to plot on + if self._col_wrap is not None: + ax = self.axes.flat[col_j] + else: + ax = self.axes[row_i, col_j] + + # Get a reference to the axes object we want, and make it active + if modify_state: + plt.sca(ax) + return ax + + def despine(self, **kwargs): + """Remove axis spines from the facets.""" + utils.despine(self._figure, **kwargs) + return self + + def set_axis_labels(self, x_var=None, y_var=None, clear_inner=True, **kwargs): + """Set axis labels on the left column and bottom row of the grid.""" + if x_var is not None: + self._x_var = x_var + self.set_xlabels(x_var, clear_inner=clear_inner, **kwargs) + if y_var is not None: + self._y_var = y_var + self.set_ylabels(y_var, clear_inner=clear_inner, **kwargs) + + return self + + def set_xlabels(self, label=None, clear_inner=True, **kwargs): + """Label the x axis on the bottom row of the grid.""" + if label is None: + label = self._x_var + for ax in self._bottom_axes: + ax.set_xlabel(label, **kwargs) + if clear_inner: + for ax in self._not_bottom_axes: + ax.set_xlabel("") + return self + + def set_ylabels(self, label=None, clear_inner=True, **kwargs): + """Label the y axis on the left column of the grid.""" + if label is None: + label = self._y_var + for ax in self._left_axes: + ax.set_ylabel(label, **kwargs) + if clear_inner: + for ax in self._not_left_axes: + ax.set_ylabel("") + return self + + def set_xticklabels(self, labels=None, step=None, **kwargs): + """Set x axis tick labels of the grid.""" + for ax in self.axes.flat: + curr_ticks = ax.get_xticks() + ax.set_xticks(curr_ticks) + if labels is None: + curr_labels = [l.get_text() for l in ax.get_xticklabels()] + if step is not None: + xticks = ax.get_xticks()[::step] + curr_labels = curr_labels[::step] + ax.set_xticks(xticks) + ax.set_xticklabels(curr_labels, **kwargs) + else: + ax.set_xticklabels(labels, **kwargs) + return self + + def set_yticklabels(self, labels=None, **kwargs): + """Set y axis tick labels on the left column of the grid.""" + for ax in self.axes.flat: + curr_ticks = ax.get_yticks() + ax.set_yticks(curr_ticks) + if labels is None: + curr_labels = [l.get_text() for l in ax.get_yticklabels()] + ax.set_yticklabels(curr_labels, **kwargs) + else: + ax.set_yticklabels(labels, **kwargs) + return self + + def set_titles(self, template=None, row_template=None, col_template=None, + **kwargs): + """Draw titles either above each facet or on the grid margins. + + Parameters + ---------- + template : string + Template for all titles with the formatting keys {col_var} and + {col_name} (if using a `col` faceting variable) and/or {row_var} + and {row_name} (if using a `row` faceting variable). + row_template: + Template for the row variable when titles are drawn on the grid + margins. Must have {row_var} and {row_name} formatting keys. + col_template: + Template for the column variable when titles are drawn on the grid + margins. Must have {col_var} and {col_name} formatting keys. + + Returns + ------- + self: object + Returns self. + + """ + args = dict(row_var=self._row_var, col_var=self._col_var) + kwargs["size"] = kwargs.pop("size", mpl.rcParams["axes.labelsize"]) + + # Establish default templates + if row_template is None: + row_template = "{row_var} = {row_name}" + if col_template is None: + col_template = "{col_var} = {col_name}" + if template is None: + if self._row_var is None: + template = col_template + elif self._col_var is None: + template = row_template + else: + template = " | ".join([row_template, col_template]) + + row_template = utils.to_utf8(row_template) + col_template = utils.to_utf8(col_template) + template = utils.to_utf8(template) + + if self._margin_titles: + + # Remove any existing title texts + for text in self._margin_titles_texts: + text.remove() + self._margin_titles_texts = [] + + if self.row_names is not None: + # Draw the row titles on the right edge of the grid + for i, row_name in enumerate(self.row_names): + ax = self.axes[i, -1] + args.update(dict(row_name=row_name)) + title = row_template.format(**args) + text = ax.annotate( + title, xy=(1.02, .5), xycoords="axes fraction", + rotation=270, ha="left", va="center", + **kwargs + ) + self._margin_titles_texts.append(text) + + if self.col_names is not None: + # Draw the column titles as normal titles + for j, col_name in enumerate(self.col_names): + args.update(dict(col_name=col_name)) + title = col_template.format(**args) + self.axes[0, j].set_title(title, **kwargs) + + return self + + # Otherwise title each facet with all the necessary information + if (self._row_var is not None) and (self._col_var is not None): + for i, row_name in enumerate(self.row_names): + for j, col_name in enumerate(self.col_names): + args.update(dict(row_name=row_name, col_name=col_name)) + title = template.format(**args) + self.axes[i, j].set_title(title, **kwargs) + elif self.row_names is not None and len(self.row_names): + for i, row_name in enumerate(self.row_names): + args.update(dict(row_name=row_name)) + title = template.format(**args) + self.axes[i, 0].set_title(title, **kwargs) + elif self.col_names is not None and len(self.col_names): + for i, col_name in enumerate(self.col_names): + args.update(dict(col_name=col_name)) + title = template.format(**args) + # Index the flat array so col_wrap works + self.axes.flat[i].set_title(title, **kwargs) + return self + + def refline(self, *, x=None, y=None, color='.5', linestyle='--', **line_kws): + """Add a reference line(s) to each facet. + + Parameters + ---------- + x, y : numeric + Value(s) to draw the line(s) at. + color : :mod:`matplotlib color ` + Specifies the color of the reference line(s). Pass ``color=None`` to + use ``hue`` mapping. + linestyle : str + Specifies the style of the reference line(s). + line_kws : key, value mappings + Other keyword arguments are passed to :meth:`matplotlib.axes.Axes.axvline` + when ``x`` is not None and :meth:`matplotlib.axes.Axes.axhline` when ``y`` + is not None. + + Returns + ------- + :class:`FacetGrid` instance + Returns ``self`` for easy method chaining. + + """ + line_kws['color'] = color + line_kws['linestyle'] = linestyle + + if x is not None: + self.map(plt.axvline, x=x, **line_kws) + + if y is not None: + self.map(plt.axhline, y=y, **line_kws) + + return self + + # ------ Properties that are part of the public API and documented by Sphinx + + @property + def axes(self): + """An array of the :class:`matplotlib.axes.Axes` objects in the grid.""" + return self._axes + + @property + def ax(self): + """The :class:`matplotlib.axes.Axes` when no faceting variables are assigned.""" + if self.axes.shape == (1, 1): + return self.axes[0, 0] + else: + err = ( + "Use the `.axes` attribute when facet variables are assigned." + ) + raise AttributeError(err) + + @property + def axes_dict(self): + """A mapping of facet names to corresponding :class:`matplotlib.axes.Axes`. + + If only one of ``row`` or ``col`` is assigned, each key is a string + representing a level of that variable. If both facet dimensions are + assigned, each key is a ``({row_level}, {col_level})`` tuple. + + """ + return self._axes_dict + + # ------ Private properties, that require some computation to get + + @property + def _inner_axes(self): + """Return a flat array of the inner axes.""" + if self._col_wrap is None: + return self.axes[:-1, 1:].flat + else: + axes = [] + n_empty = self._nrow * self._ncol - self._n_facets + for i, ax in enumerate(self.axes): + append = ( + i % self._ncol + and i < (self._ncol * (self._nrow - 1)) + and i < (self._ncol * (self._nrow - 1) - n_empty) + ) + if append: + axes.append(ax) + return np.array(axes, object).flat + + @property + def _left_axes(self): + """Return a flat array of the left column of axes.""" + if self._col_wrap is None: + return self.axes[:, 0].flat + else: + axes = [] + for i, ax in enumerate(self.axes): + if not i % self._ncol: + axes.append(ax) + return np.array(axes, object).flat + + @property + def _not_left_axes(self): + """Return a flat array of axes that aren't on the left column.""" + if self._col_wrap is None: + return self.axes[:, 1:].flat + else: + axes = [] + for i, ax in enumerate(self.axes): + if i % self._ncol: + axes.append(ax) + return np.array(axes, object).flat + + @property + def _bottom_axes(self): + """Return a flat array of the bottom row of axes.""" + if self._col_wrap is None: + return self.axes[-1, :].flat + else: + axes = [] + n_empty = self._nrow * self._ncol - self._n_facets + for i, ax in enumerate(self.axes): + append = ( + i >= (self._ncol * (self._nrow - 1)) + or i >= (self._ncol * (self._nrow - 1) - n_empty) + ) + if append: + axes.append(ax) + return np.array(axes, object).flat + + @property + def _not_bottom_axes(self): + """Return a flat array of axes that aren't on the bottom row.""" + if self._col_wrap is None: + return self.axes[:-1, :].flat + else: + axes = [] + n_empty = self._nrow * self._ncol - self._n_facets + for i, ax in enumerate(self.axes): + append = ( + i < (self._ncol * (self._nrow - 1)) + and i < (self._ncol * (self._nrow - 1) - n_empty) + ) + if append: + axes.append(ax) + return np.array(axes, object).flat + + +class PairGrid(Grid): + """Subplot grid for plotting pairwise relationships in a dataset. + + This object maps each variable in a dataset onto a column and row in a + grid of multiple axes. Different axes-level plotting functions can be + used to draw bivariate plots in the upper and lower triangles, and the + marginal distribution of each variable can be shown on the diagonal. + + Several different common plots can be generated in a single line using + :func:`pairplot`. Use :class:`PairGrid` when you need more flexibility. + + See the :ref:`tutorial ` for more information. + + """ + def __init__( + self, data, *, hue=None, vars=None, x_vars=None, y_vars=None, + hue_order=None, palette=None, hue_kws=None, corner=False, diag_sharey=True, + height=2.5, aspect=1, layout_pad=.5, despine=True, dropna=False, + ): + """Initialize the plot figure and PairGrid object. + + Parameters + ---------- + data : DataFrame + Tidy (long-form) dataframe where each column is a variable and + each row is an observation. + hue : string (variable name) + Variable in ``data`` to map plot aspects to different colors. This + variable will be excluded from the default x and y variables. + vars : list of variable names + Variables within ``data`` to use, otherwise use every column with + a numeric datatype. + {x, y}_vars : lists of variable names + Variables within ``data`` to use separately for the rows and + columns of the figure; i.e. to make a non-square plot. + hue_order : list of strings + Order for the levels of the hue variable in the palette + palette : dict or seaborn color palette + Set of colors for mapping the ``hue`` variable. If a dict, keys + should be values in the ``hue`` variable. + hue_kws : dictionary of param -> list of values mapping + Other keyword arguments to insert into the plotting call to let + other plot attributes vary across levels of the hue variable (e.g. + the markers in a scatterplot). + corner : bool + If True, don't add axes to the upper (off-diagonal) triangle of the + grid, making this a "corner" plot. + height : scalar + Height (in inches) of each facet. + aspect : scalar + Aspect * height gives the width (in inches) of each facet. + layout_pad : scalar + Padding between axes; passed to ``fig.tight_layout``. + despine : boolean + Remove the top and right spines from the plots. + dropna : boolean + Drop missing values from the data before plotting. + + See Also + -------- + pairplot : Easily drawing common uses of :class:`PairGrid`. + FacetGrid : Subplot grid for plotting conditional relationships. + + Examples + -------- + + .. include:: ../docstrings/PairGrid.rst + + """ + + super().__init__() + + # Sort out the variables that define the grid + numeric_cols = self._find_numeric_cols(data) + if hue in numeric_cols: + numeric_cols.remove(hue) + if vars is not None: + x_vars = list(vars) + y_vars = list(vars) + if x_vars is None: + x_vars = numeric_cols + if y_vars is None: + y_vars = numeric_cols + + if np.isscalar(x_vars): + x_vars = [x_vars] + if np.isscalar(y_vars): + y_vars = [y_vars] + + self.x_vars = x_vars = list(x_vars) + self.y_vars = y_vars = list(y_vars) + self.square_grid = self.x_vars == self.y_vars + + if not x_vars: + raise ValueError("No variables found for grid columns.") + if not y_vars: + raise ValueError("No variables found for grid rows.") + + # Create the figure and the array of subplots + figsize = len(x_vars) * height * aspect, len(y_vars) * height + + with _disable_autolayout(): + fig = plt.figure(figsize=figsize) + + axes = fig.subplots(len(y_vars), len(x_vars), + sharex="col", sharey="row", + squeeze=False) + + # Possibly remove upper axes to make a corner grid + # Note: setting up the axes is usually the most time-intensive part + # of using the PairGrid. We are foregoing the speed improvement that + # we would get by just not setting up the hidden axes so that we can + # avoid implementing fig.subplots ourselves. But worth thinking about. + self._corner = corner + if corner: + hide_indices = np.triu_indices_from(axes, 1) + for i, j in zip(*hide_indices): + axes[i, j].remove() + axes[i, j] = None + + self._figure = fig + self.axes = axes + self.data = data + + # Save what we are going to do with the diagonal + self.diag_sharey = diag_sharey + self.diag_vars = None + self.diag_axes = None + + self._dropna = dropna + + # Label the axes + self._add_axis_labels() + + # Sort out the hue variable + self._hue_var = hue + if hue is None: + self.hue_names = hue_order = ["_nolegend_"] + self.hue_vals = pd.Series(["_nolegend_"] * len(data), + index=data.index) + else: + # We need hue_order and hue_names because the former is used to control + # the order of drawing and the latter is used to control the order of + # the legend. hue_names can become string-typed while hue_order must + # retain the type of the input data. This is messy but results from + # the fact that PairGrid can implement the hue-mapping logic itself + # (and was originally written exclusively that way) but now can delegate + # to the axes-level functions, while always handling legend creation. + # See GH2307 + hue_names = hue_order = categorical_order(data[hue], hue_order) + if dropna: + # Filter NA from the list of unique hue names + hue_names = list(filter(pd.notnull, hue_names)) + self.hue_names = hue_names + self.hue_vals = data[hue] + + # Additional dict of kwarg -> list of values for mapping the hue var + self.hue_kws = hue_kws if hue_kws is not None else {} + + self._orig_palette = palette + self._hue_order = hue_order + self.palette = self._get_palette(data, hue, hue_order, palette) + self._legend_data = {} + + # Make the plot look nice + for ax in axes[:-1, :].flat: + if ax is None: + continue + for label in ax.get_xticklabels(): + label.set_visible(False) + ax.xaxis.offsetText.set_visible(False) + ax.xaxis.label.set_visible(False) + + for ax in axes[:, 1:].flat: + if ax is None: + continue + for label in ax.get_yticklabels(): + label.set_visible(False) + ax.yaxis.offsetText.set_visible(False) + ax.yaxis.label.set_visible(False) + + self._tight_layout_rect = [.01, .01, .99, .99] + self._tight_layout_pad = layout_pad + self._despine = despine + if despine: + utils.despine(fig=fig) + self.tight_layout(pad=layout_pad) + + def map(self, func, **kwargs): + """Plot with the same function in every subplot. + + Parameters + ---------- + func : callable plotting function + Must take x, y arrays as positional arguments and draw onto the + "currently active" matplotlib Axes. Also needs to accept kwargs + called ``color`` and ``label``. + + """ + row_indices, col_indices = np.indices(self.axes.shape) + indices = zip(row_indices.flat, col_indices.flat) + self._map_bivariate(func, indices, **kwargs) + + return self + + def map_lower(self, func, **kwargs): + """Plot with a bivariate function on the lower diagonal subplots. + + Parameters + ---------- + func : callable plotting function + Must take x, y arrays as positional arguments and draw onto the + "currently active" matplotlib Axes. Also needs to accept kwargs + called ``color`` and ``label``. + + """ + indices = zip(*np.tril_indices_from(self.axes, -1)) + self._map_bivariate(func, indices, **kwargs) + return self + + def map_upper(self, func, **kwargs): + """Plot with a bivariate function on the upper diagonal subplots. + + Parameters + ---------- + func : callable plotting function + Must take x, y arrays as positional arguments and draw onto the + "currently active" matplotlib Axes. Also needs to accept kwargs + called ``color`` and ``label``. + + """ + indices = zip(*np.triu_indices_from(self.axes, 1)) + self._map_bivariate(func, indices, **kwargs) + return self + + def map_offdiag(self, func, **kwargs): + """Plot with a bivariate function on the off-diagonal subplots. + + Parameters + ---------- + func : callable plotting function + Must take x, y arrays as positional arguments and draw onto the + "currently active" matplotlib Axes. Also needs to accept kwargs + called ``color`` and ``label``. + + """ + if self.square_grid: + self.map_lower(func, **kwargs) + if not self._corner: + self.map_upper(func, **kwargs) + else: + indices = [] + for i, (y_var) in enumerate(self.y_vars): + for j, (x_var) in enumerate(self.x_vars): + if x_var != y_var: + indices.append((i, j)) + self._map_bivariate(func, indices, **kwargs) + return self + + def map_diag(self, func, **kwargs): + """Plot with a univariate function on each diagonal subplot. + + Parameters + ---------- + func : callable plotting function + Must take an x array as a positional argument and draw onto the + "currently active" matplotlib Axes. Also needs to accept kwargs + called ``color`` and ``label``. + + """ + # Add special diagonal axes for the univariate plot + if self.diag_axes is None: + diag_vars = [] + diag_axes = [] + for i, y_var in enumerate(self.y_vars): + for j, x_var in enumerate(self.x_vars): + if x_var == y_var: + + # Make the density axes + diag_vars.append(x_var) + ax = self.axes[i, j] + diag_ax = ax.twinx() + diag_ax.set_axis_off() + diag_axes.append(diag_ax) + + # Work around matplotlib bug + # https://github.com/matplotlib/matplotlib/issues/15188 + if not plt.rcParams.get("ytick.left", True): + for tick in ax.yaxis.majorTicks: + tick.tick1line.set_visible(False) + + # Remove main y axis from density axes in a corner plot + if self._corner: + ax.yaxis.set_visible(False) + if self._despine: + utils.despine(ax=ax, left=True) + # TODO add optional density ticks (on the right) + # when drawing a corner plot? + + if self.diag_sharey and diag_axes: + for ax in diag_axes[1:]: + share_axis(diag_axes[0], ax, "y") + + self.diag_vars = np.array(diag_vars, np.object_) + self.diag_axes = np.array(diag_axes, np.object_) + + if "hue" not in signature(func).parameters: + return self._map_diag_iter_hue(func, **kwargs) + + # Loop over diagonal variables and axes, making one plot in each + for var, ax in zip(self.diag_vars, self.diag_axes): + + plot_kwargs = kwargs.copy() + if str(func.__module__).startswith("seaborn"): + plot_kwargs["ax"] = ax + else: + plt.sca(ax) + + vector = self.data[var] + if self._hue_var is not None: + hue = self.data[self._hue_var] + else: + hue = None + + if self._dropna: + not_na = vector.notna() + if hue is not None: + not_na &= hue.notna() + vector = vector[not_na] + if hue is not None: + hue = hue[not_na] + + plot_kwargs.setdefault("hue", hue) + plot_kwargs.setdefault("hue_order", self._hue_order) + plot_kwargs.setdefault("palette", self._orig_palette) + func(x=vector, **plot_kwargs) + ax.legend_ = None + + self._add_axis_labels() + return self + + def _map_diag_iter_hue(self, func, **kwargs): + """Put marginal plot on each diagonal axes, iterating over hue.""" + # Plot on each of the diagonal axes + fixed_color = kwargs.pop("color", None) + + for var, ax in zip(self.diag_vars, self.diag_axes): + hue_grouped = self.data[var].groupby(self.hue_vals) + + plot_kwargs = kwargs.copy() + if str(func.__module__).startswith("seaborn"): + plot_kwargs["ax"] = ax + else: + plt.sca(ax) + + for k, label_k in enumerate(self._hue_order): + + # Attempt to get data for this level, allowing for empty + try: + data_k = hue_grouped.get_group(label_k) + except KeyError: + data_k = pd.Series([], dtype=float) + + if fixed_color is None: + color = self.palette[k] + else: + color = fixed_color + + if self._dropna: + data_k = utils.remove_na(data_k) + + if str(func.__module__).startswith("seaborn"): + func(x=data_k, label=label_k, color=color, **plot_kwargs) + else: + func(data_k, label=label_k, color=color, **plot_kwargs) + + self._add_axis_labels() + + return self + + def _map_bivariate(self, func, indices, **kwargs): + """Draw a bivariate plot on the indicated axes.""" + # This is a hack to handle the fact that new distribution plots don't add + # their artists onto the axes. This is probably superior in general, but + # we'll need a better way to handle it in the axisgrid functions. + from .distributions import histplot, kdeplot + if func is histplot or func is kdeplot: + self._extract_legend_handles = True + + kws = kwargs.copy() # Use copy as we insert other kwargs + for i, j in indices: + x_var = self.x_vars[j] + y_var = self.y_vars[i] + ax = self.axes[i, j] + if ax is None: # i.e. we are in corner mode + continue + self._plot_bivariate(x_var, y_var, ax, func, **kws) + self._add_axis_labels() + + if "hue" in signature(func).parameters: + self.hue_names = list(self._legend_data) + + def _plot_bivariate(self, x_var, y_var, ax, func, **kwargs): + """Draw a bivariate plot on the specified axes.""" + if "hue" not in signature(func).parameters: + self._plot_bivariate_iter_hue(x_var, y_var, ax, func, **kwargs) + return + + kwargs = kwargs.copy() + if str(func.__module__).startswith("seaborn"): + kwargs["ax"] = ax + else: + plt.sca(ax) + + if x_var == y_var: + axes_vars = [x_var] + else: + axes_vars = [x_var, y_var] + + if self._hue_var is not None and self._hue_var not in axes_vars: + axes_vars.append(self._hue_var) + + data = self.data[axes_vars] + if self._dropna: + data = data.dropna() + + x = data[x_var] + y = data[y_var] + if self._hue_var is None: + hue = None + else: + hue = data.get(self._hue_var) + + if "hue" not in kwargs: + kwargs.update({ + "hue": hue, "hue_order": self._hue_order, "palette": self._orig_palette, + }) + func(x=x, y=y, **kwargs) + + self._update_legend_data(ax) + + def _plot_bivariate_iter_hue(self, x_var, y_var, ax, func, **kwargs): + """Draw a bivariate plot while iterating over hue subsets.""" + kwargs = kwargs.copy() + if str(func.__module__).startswith("seaborn"): + kwargs["ax"] = ax + else: + plt.sca(ax) + + if x_var == y_var: + axes_vars = [x_var] + else: + axes_vars = [x_var, y_var] + + hue_grouped = self.data.groupby(self.hue_vals) + for k, label_k in enumerate(self._hue_order): + + kws = kwargs.copy() + + # Attempt to get data for this level, allowing for empty + try: + data_k = hue_grouped.get_group(label_k) + except KeyError: + data_k = pd.DataFrame(columns=axes_vars, + dtype=float) + + if self._dropna: + data_k = data_k[axes_vars].dropna() + + x = data_k[x_var] + y = data_k[y_var] + + for kw, val_list in self.hue_kws.items(): + kws[kw] = val_list[k] + kws.setdefault("color", self.palette[k]) + if self._hue_var is not None: + kws["label"] = label_k + + if str(func.__module__).startswith("seaborn"): + func(x=x, y=y, **kws) + else: + func(x, y, **kws) + + self._update_legend_data(ax) + + def _add_axis_labels(self): + """Add labels to the left and bottom Axes.""" + for ax, label in zip(self.axes[-1, :], self.x_vars): + ax.set_xlabel(label) + for ax, label in zip(self.axes[:, 0], self.y_vars): + ax.set_ylabel(label) + + def _find_numeric_cols(self, data): + """Find which variables in a DataFrame are numeric.""" + numeric_cols = [] + for col in data: + if variable_type(data[col]) == "numeric": + numeric_cols.append(col) + return numeric_cols + + +class JointGrid(_BaseGrid): + """Grid for drawing a bivariate plot with marginal univariate plots. + + Many plots can be drawn by using the figure-level interface :func:`jointplot`. + Use this class directly when you need more flexibility. + + """ + + def __init__( + self, data=None, *, + x=None, y=None, hue=None, + height=6, ratio=5, space=.2, + palette=None, hue_order=None, hue_norm=None, + dropna=False, xlim=None, ylim=None, marginal_ticks=False, + ): + + # Set up the subplot grid + f = plt.figure(figsize=(height, height)) + gs = plt.GridSpec(ratio + 1, ratio + 1) + + ax_joint = f.add_subplot(gs[1:, :-1]) + ax_marg_x = f.add_subplot(gs[0, :-1], sharex=ax_joint) + ax_marg_y = f.add_subplot(gs[1:, -1], sharey=ax_joint) + + self._figure = f + self.ax_joint = ax_joint + self.ax_marg_x = ax_marg_x + self.ax_marg_y = ax_marg_y + + # Turn off tick visibility for the measure axis on the marginal plots + plt.setp(ax_marg_x.get_xticklabels(), visible=False) + plt.setp(ax_marg_y.get_yticklabels(), visible=False) + plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False) + plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False) + + # Turn off the ticks on the density axis for the marginal plots + if not marginal_ticks: + plt.setp(ax_marg_x.yaxis.get_majorticklines(), visible=False) + plt.setp(ax_marg_x.yaxis.get_minorticklines(), visible=False) + plt.setp(ax_marg_y.xaxis.get_majorticklines(), visible=False) + plt.setp(ax_marg_y.xaxis.get_minorticklines(), visible=False) + plt.setp(ax_marg_x.get_yticklabels(), visible=False) + plt.setp(ax_marg_y.get_xticklabels(), visible=False) + plt.setp(ax_marg_x.get_yticklabels(minor=True), visible=False) + plt.setp(ax_marg_y.get_xticklabels(minor=True), visible=False) + ax_marg_x.yaxis.grid(False) + ax_marg_y.xaxis.grid(False) + + # Process the input variables + p = VectorPlotter(data=data, variables=dict(x=x, y=y, hue=hue)) + plot_data = p.plot_data.loc[:, p.plot_data.notna().any()] + + # Possibly drop NA + if dropna: + plot_data = plot_data.dropna() + + def get_var(var): + vector = plot_data.get(var, None) + if vector is not None: + vector = vector.rename(p.variables.get(var, None)) + return vector + + self.x = get_var("x") + self.y = get_var("y") + self.hue = get_var("hue") + + for axis in "xy": + name = p.variables.get(axis, None) + if name is not None: + getattr(ax_joint, f"set_{axis}label")(name) + + if xlim is not None: + ax_joint.set_xlim(xlim) + if ylim is not None: + ax_joint.set_ylim(ylim) + + # Store the semantic mapping parameters for axes-level functions + self._hue_params = dict(palette=palette, hue_order=hue_order, hue_norm=hue_norm) + + # Make the grid look nice + utils.despine(f) + if not marginal_ticks: + utils.despine(ax=ax_marg_x, left=True) + utils.despine(ax=ax_marg_y, bottom=True) + for axes in [ax_marg_x, ax_marg_y]: + for axis in [axes.xaxis, axes.yaxis]: + axis.label.set_visible(False) + f.tight_layout() + f.subplots_adjust(hspace=space, wspace=space) + + def _inject_kwargs(self, func, kws, params): + """Add params to kws if they are accepted by func.""" + func_params = signature(func).parameters + for key, val in params.items(): + if key in func_params: + kws.setdefault(key, val) + + def plot(self, joint_func, marginal_func, **kwargs): + """Draw the plot by passing functions for joint and marginal axes. + + This method passes the ``kwargs`` dictionary to both functions. If you + need more control, call :meth:`JointGrid.plot_joint` and + :meth:`JointGrid.plot_marginals` directly with specific parameters. + + Parameters + ---------- + joint_func, marginal_func : callables + Functions to draw the bivariate and univariate plots. See methods + referenced above for information about the required characteristics + of these functions. + kwargs + Additional keyword arguments are passed to both functions. + + Returns + ------- + :class:`JointGrid` instance + Returns ``self`` for easy method chaining. + + """ + self.plot_marginals(marginal_func, **kwargs) + self.plot_joint(joint_func, **kwargs) + return self + + def plot_joint(self, func, **kwargs): + """Draw a bivariate plot on the joint axes of the grid. + + Parameters + ---------- + func : plotting callable + If a seaborn function, it should accept ``x`` and ``y``. Otherwise, + it must accept ``x`` and ``y`` vectors of data as the first two + positional arguments, and it must plot on the "current" axes. + If ``hue`` was defined in the class constructor, the function must + accept ``hue`` as a parameter. + kwargs + Keyword argument are passed to the plotting function. + + Returns + ------- + :class:`JointGrid` instance + Returns ``self`` for easy method chaining. + + """ + kwargs = kwargs.copy() + if str(func.__module__).startswith("seaborn"): + kwargs["ax"] = self.ax_joint + else: + plt.sca(self.ax_joint) + if self.hue is not None: + kwargs["hue"] = self.hue + self._inject_kwargs(func, kwargs, self._hue_params) + + if str(func.__module__).startswith("seaborn"): + func(x=self.x, y=self.y, **kwargs) + else: + func(self.x, self.y, **kwargs) + + return self + + def plot_marginals(self, func, **kwargs): + """Draw univariate plots on each marginal axes. + + Parameters + ---------- + func : plotting callable + If a seaborn function, it should accept ``x`` and ``y`` and plot + when only one of them is defined. Otherwise, it must accept a vector + of data as the first positional argument and determine its orientation + using the ``vertical`` parameter, and it must plot on the "current" axes. + If ``hue`` was defined in the class constructor, it must accept ``hue`` + as a parameter. + kwargs + Keyword argument are passed to the plotting function. + + Returns + ------- + :class:`JointGrid` instance + Returns ``self`` for easy method chaining. + + """ + seaborn_func = ( + str(func.__module__).startswith("seaborn") + # deprecated distplot has a legacy API, special case it + and not func.__name__ == "distplot" + ) + func_params = signature(func).parameters + kwargs = kwargs.copy() + if self.hue is not None: + kwargs["hue"] = self.hue + self._inject_kwargs(func, kwargs, self._hue_params) + + if "legend" in func_params: + kwargs.setdefault("legend", False) + + if "orientation" in func_params: + # e.g. plt.hist + orient_kw_x = {"orientation": "vertical"} + orient_kw_y = {"orientation": "horizontal"} + elif "vertical" in func_params: + # e.g. sns.distplot (also how did this get backwards?) + orient_kw_x = {"vertical": False} + orient_kw_y = {"vertical": True} + + if seaborn_func: + func(x=self.x, ax=self.ax_marg_x, **kwargs) + else: + plt.sca(self.ax_marg_x) + func(self.x, **orient_kw_x, **kwargs) + + if seaborn_func: + func(y=self.y, ax=self.ax_marg_y, **kwargs) + else: + plt.sca(self.ax_marg_y) + func(self.y, **orient_kw_y, **kwargs) + + self.ax_marg_x.yaxis.get_label().set_visible(False) + self.ax_marg_y.xaxis.get_label().set_visible(False) + + return self + + def refline( + self, *, x=None, y=None, joint=True, marginal=True, + color='.5', linestyle='--', **line_kws + ): + """Add a reference line(s) to joint and/or marginal axes. + + Parameters + ---------- + x, y : numeric + Value(s) to draw the line(s) at. + joint, marginal : bools + Whether to add the reference line(s) to the joint/marginal axes. + color : :mod:`matplotlib color ` + Specifies the color of the reference line(s). + linestyle : str + Specifies the style of the reference line(s). + line_kws : key, value mappings + Other keyword arguments are passed to :meth:`matplotlib.axes.Axes.axvline` + when ``x`` is not None and :meth:`matplotlib.axes.Axes.axhline` when ``y`` + is not None. + + Returns + ------- + :class:`JointGrid` instance + Returns ``self`` for easy method chaining. + + """ + line_kws['color'] = color + line_kws['linestyle'] = linestyle + + if x is not None: + if joint: + self.ax_joint.axvline(x, **line_kws) + if marginal: + self.ax_marg_x.axvline(x, **line_kws) + + if y is not None: + if joint: + self.ax_joint.axhline(y, **line_kws) + if marginal: + self.ax_marg_y.axhline(y, **line_kws) + + return self + + def set_axis_labels(self, xlabel="", ylabel="", **kwargs): + """Set axis labels on the bivariate axes. + + Parameters + ---------- + xlabel, ylabel : strings + Label names for the x and y variables. + kwargs : key, value mappings + Other keyword arguments are passed to the following functions: + + - :meth:`matplotlib.axes.Axes.set_xlabel` + - :meth:`matplotlib.axes.Axes.set_ylabel` + + Returns + ------- + :class:`JointGrid` instance + Returns ``self`` for easy method chaining. + + """ + self.ax_joint.set_xlabel(xlabel, **kwargs) + self.ax_joint.set_ylabel(ylabel, **kwargs) + return self + + +JointGrid.__init__.__doc__ = """\ +Set up the grid of subplots and store data internally for easy plotting. + +Parameters +---------- +{params.core.data} +{params.core.xy} +height : number + Size of each side of the figure in inches (it will be square). +ratio : number + Ratio of joint axes height to marginal axes height. +space : number + Space between the joint and marginal axes +dropna : bool + If True, remove missing observations before plotting. +{{x, y}}lim : pairs of numbers + Set axis limits to these values before plotting. +marginal_ticks : bool + If False, suppress ticks on the count/density axis of the marginal plots. +{params.core.hue} + Note: unlike in :class:`FacetGrid` or :class:`PairGrid`, the axes-level + functions must support ``hue`` to use it in :class:`JointGrid`. +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} + +See Also +-------- +{seealso.jointplot} +{seealso.pairgrid} +{seealso.pairplot} + +Examples +-------- + +.. include:: ../docstrings/JointGrid.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def pairplot( + data, *, + hue=None, hue_order=None, palette=None, + vars=None, x_vars=None, y_vars=None, + kind="scatter", diag_kind="auto", markers=None, + height=2.5, aspect=1, corner=False, dropna=False, + plot_kws=None, diag_kws=None, grid_kws=None, size=None, +): + """Plot pairwise relationships in a dataset. + + By default, this function will create a grid of Axes such that each numeric + variable in ``data`` will by shared across the y-axes across a single row and + the x-axes across a single column. The diagonal plots are treated + differently: a univariate distribution plot is drawn to show the marginal + distribution of the data in each column. + + It is also possible to show a subset of variables or plot different + variables on the rows and columns. + + This is a high-level interface for :class:`PairGrid` that is intended to + make it easy to draw a few common styles. You should use :class:`PairGrid` + directly if you need more flexibility. + + Parameters + ---------- + data : `pandas.DataFrame` + Tidy (long-form) dataframe where each column is a variable and + each row is an observation. + hue : name of variable in ``data`` + Variable in ``data`` to map plot aspects to different colors. + hue_order : list of strings + Order for the levels of the hue variable in the palette + palette : dict or seaborn color palette + Set of colors for mapping the ``hue`` variable. If a dict, keys + should be values in the ``hue`` variable. + vars : list of variable names + Variables within ``data`` to use, otherwise use every column with + a numeric datatype. + {x, y}_vars : lists of variable names + Variables within ``data`` to use separately for the rows and + columns of the figure; i.e. to make a non-square plot. + kind : {'scatter', 'kde', 'hist', 'reg'} + Kind of plot to make. + diag_kind : {'auto', 'hist', 'kde', None} + Kind of plot for the diagonal subplots. If 'auto', choose based on + whether or not ``hue`` is used. + markers : single matplotlib marker code or list + Either the marker to use for all scatterplot points or a list of markers + with a length the same as the number of levels in the hue variable so that + differently colored points will also have different scatterplot + markers. + height : scalar + Height (in inches) of each facet. + aspect : scalar + Aspect * height gives the width (in inches) of each facet. + corner : bool + If True, don't add axes to the upper (off-diagonal) triangle of the + grid, making this a "corner" plot. + dropna : boolean + Drop missing values from the data before plotting. + {plot, diag, grid}_kws : dicts + Dictionaries of keyword arguments. ``plot_kws`` are passed to the + bivariate plotting function, ``diag_kws`` are passed to the univariate + plotting function, and ``grid_kws`` are passed to the :class:`PairGrid` + constructor. + + Returns + ------- + grid : :class:`PairGrid` + Returns the underlying :class:`PairGrid` instance for further tweaking. + + See Also + -------- + PairGrid : Subplot grid for more flexible plotting of pairwise relationships. + JointGrid : Grid for plotting joint and marginal distributions of two variables. + + Examples + -------- + + .. include:: ../docstrings/pairplot.rst + + """ + # Avoid circular import + from .distributions import histplot, kdeplot + + # Handle deprecations + if size is not None: + height = size + msg = ("The `size` parameter has been renamed to `height`; " + "please update your code.") + warnings.warn(msg, UserWarning) + + if not isinstance(data, pd.DataFrame): + raise TypeError( + f"'data' must be pandas DataFrame object, not: {type(data)}") + + plot_kws = {} if plot_kws is None else plot_kws.copy() + diag_kws = {} if diag_kws is None else diag_kws.copy() + grid_kws = {} if grid_kws is None else grid_kws.copy() + + # Resolve "auto" diag kind + if diag_kind == "auto": + if hue is None: + diag_kind = "kde" if kind == "kde" else "hist" + else: + diag_kind = "hist" if kind == "hist" else "kde" + + # Set up the PairGrid + grid_kws.setdefault("diag_sharey", diag_kind == "hist") + grid = PairGrid(data, vars=vars, x_vars=x_vars, y_vars=y_vars, hue=hue, + hue_order=hue_order, palette=palette, corner=corner, + height=height, aspect=aspect, dropna=dropna, **grid_kws) + + # Add the markers here as PairGrid has figured out how many levels of the + # hue variable are needed and we don't want to duplicate that process + if markers is not None: + if kind == "reg": + # Needed until regplot supports style + if grid.hue_names is None: + n_markers = 1 + else: + n_markers = len(grid.hue_names) + if not isinstance(markers, list): + markers = [markers] * n_markers + if len(markers) != n_markers: + raise ValueError("markers must be a singleton or a list of " + "markers for each level of the hue variable") + grid.hue_kws = {"marker": markers} + elif kind == "scatter": + if isinstance(markers, str): + plot_kws["marker"] = markers + elif hue is not None: + plot_kws["style"] = data[hue] + plot_kws["markers"] = markers + + # Draw the marginal plots on the diagonal + diag_kws = diag_kws.copy() + diag_kws.setdefault("legend", False) + if diag_kind == "hist": + grid.map_diag(histplot, **diag_kws) + elif diag_kind == "kde": + diag_kws.setdefault("fill", True) + diag_kws.setdefault("warn_singular", False) + grid.map_diag(kdeplot, **diag_kws) + + # Maybe plot on the off-diagonals + if diag_kind is not None: + plotter = grid.map_offdiag + else: + plotter = grid.map + + if kind == "scatter": + from .relational import scatterplot # Avoid circular import + plotter(scatterplot, **plot_kws) + elif kind == "reg": + from .regression import regplot # Avoid circular import + plotter(regplot, **plot_kws) + elif kind == "kde": + from .distributions import kdeplot # Avoid circular import + plot_kws.setdefault("warn_singular", False) + plotter(kdeplot, **plot_kws) + elif kind == "hist": + from .distributions import histplot # Avoid circular import + plotter(histplot, **plot_kws) + + # Add a legend + if hue is not None: + grid.add_legend() + + grid.tight_layout() + + return grid + + +def jointplot( + data=None, *, x=None, y=None, hue=None, kind="scatter", + height=6, ratio=5, space=.2, dropna=False, xlim=None, ylim=None, + color=None, palette=None, hue_order=None, hue_norm=None, marginal_ticks=False, + joint_kws=None, marginal_kws=None, + **kwargs +): + # Avoid circular imports + from .relational import scatterplot + from .regression import regplot, residplot + from .distributions import histplot, kdeplot, _freedman_diaconis_bins + + if kwargs.pop("ax", None) is not None: + msg = "Ignoring `ax`; jointplot is a figure-level function." + warnings.warn(msg, UserWarning, stacklevel=2) + + # Set up empty default kwarg dicts + joint_kws = {} if joint_kws is None else joint_kws.copy() + joint_kws.update(kwargs) + marginal_kws = {} if marginal_kws is None else marginal_kws.copy() + + # Handle deprecations of distplot-specific kwargs + distplot_keys = [ + "rug", "fit", "hist_kws", "norm_hist" "hist_kws", "rug_kws", + ] + unused_keys = [] + for key in distplot_keys: + if key in marginal_kws: + unused_keys.append(key) + marginal_kws.pop(key) + if unused_keys and kind != "kde": + msg = ( + "The marginal plotting function has changed to `histplot`," + " which does not accept the following argument(s): {}." + ).format(", ".join(unused_keys)) + warnings.warn(msg, UserWarning) + + # Validate the plot kind + plot_kinds = ["scatter", "hist", "hex", "kde", "reg", "resid"] + _check_argument("kind", plot_kinds, kind) + + # Raise early if using `hue` with a kind that does not support it + if hue is not None and kind in ["hex", "reg", "resid"]: + msg = ( + f"Use of `hue` with `kind='{kind}'` is not currently supported." + ) + raise ValueError(msg) + + # Make a colormap based off the plot color + # (Currently used only for kind="hex") + if color is None: + color = "C0" + color_rgb = mpl.colors.colorConverter.to_rgb(color) + colors = [utils.set_hls_values(color_rgb, l=l) # noqa + for l in np.linspace(1, 0, 12)] + cmap = blend_palette(colors, as_cmap=True) + + # Matplotlib's hexbin plot is not na-robust + if kind == "hex": + dropna = True + + # Initialize the JointGrid object + grid = JointGrid( + data=data, x=x, y=y, hue=hue, + palette=palette, hue_order=hue_order, hue_norm=hue_norm, + dropna=dropna, height=height, ratio=ratio, space=space, + xlim=xlim, ylim=ylim, marginal_ticks=marginal_ticks, + ) + + if grid.hue is not None: + marginal_kws.setdefault("legend", False) + + # Plot the data using the grid + if kind.startswith("scatter"): + + joint_kws.setdefault("color", color) + grid.plot_joint(scatterplot, **joint_kws) + + if grid.hue is None: + marg_func = histplot + else: + marg_func = kdeplot + marginal_kws.setdefault("warn_singular", False) + marginal_kws.setdefault("fill", True) + + marginal_kws.setdefault("color", color) + grid.plot_marginals(marg_func, **marginal_kws) + + elif kind.startswith("hist"): + + # TODO process pair parameters for bins, etc. and pass + # to both joint and marginal plots + + joint_kws.setdefault("color", color) + grid.plot_joint(histplot, **joint_kws) + + marginal_kws.setdefault("kde", False) + marginal_kws.setdefault("color", color) + + marg_x_kws = marginal_kws.copy() + marg_y_kws = marginal_kws.copy() + + pair_keys = "bins", "binwidth", "binrange" + for key in pair_keys: + if isinstance(joint_kws.get(key), tuple): + x_val, y_val = joint_kws[key] + marg_x_kws.setdefault(key, x_val) + marg_y_kws.setdefault(key, y_val) + + histplot(data=data, x=x, hue=hue, **marg_x_kws, ax=grid.ax_marg_x) + histplot(data=data, y=y, hue=hue, **marg_y_kws, ax=grid.ax_marg_y) + + elif kind.startswith("kde"): + + joint_kws.setdefault("color", color) + joint_kws.setdefault("warn_singular", False) + grid.plot_joint(kdeplot, **joint_kws) + + marginal_kws.setdefault("color", color) + if "fill" in joint_kws: + marginal_kws.setdefault("fill", joint_kws["fill"]) + + grid.plot_marginals(kdeplot, **marginal_kws) + + elif kind.startswith("hex"): + + x_bins = min(_freedman_diaconis_bins(grid.x), 50) + y_bins = min(_freedman_diaconis_bins(grid.y), 50) + gridsize = int(np.mean([x_bins, y_bins])) + + joint_kws.setdefault("gridsize", gridsize) + joint_kws.setdefault("cmap", cmap) + grid.plot_joint(plt.hexbin, **joint_kws) + + marginal_kws.setdefault("kde", False) + marginal_kws.setdefault("color", color) + grid.plot_marginals(histplot, **marginal_kws) + + elif kind.startswith("reg"): + + marginal_kws.setdefault("color", color) + marginal_kws.setdefault("kde", True) + grid.plot_marginals(histplot, **marginal_kws) + + joint_kws.setdefault("color", color) + grid.plot_joint(regplot, **joint_kws) + + elif kind.startswith("resid"): + + joint_kws.setdefault("color", color) + grid.plot_joint(residplot, **joint_kws) + + x, y = grid.ax_joint.collections[0].get_offsets().T + marginal_kws.setdefault("color", color) + histplot(x=x, hue=hue, ax=grid.ax_marg_x, **marginal_kws) + histplot(y=y, hue=hue, ax=grid.ax_marg_y, **marginal_kws) + + # Make the main axes active in the matplotlib state machine + plt.sca(grid.ax_joint) + + return grid + + +jointplot.__doc__ = """\ +Draw a plot of two variables with bivariate and univariate graphs. + +This function provides a convenient interface to the :class:`JointGrid` +class, with several canned plot kinds. This is intended to be a fairly +lightweight wrapper; if you need more flexibility, you should use +:class:`JointGrid` directly. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} + Semantic variable that is mapped to determine the color of plot elements. +kind : {{ "scatter" | "kde" | "hist" | "hex" | "reg" | "resid" }} + Kind of plot to draw. See the examples for references to the underlying functions. +height : numeric + Size of the figure (it will be square). +ratio : numeric + Ratio of joint axes height to marginal axes height. +space : numeric + Space between the joint and marginal axes +dropna : bool + If True, remove observations that are missing from ``x`` and ``y``. +{{x, y}}lim : pairs of numbers + Axis limits to set before plotting. +{params.core.color} +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +marginal_ticks : bool + If False, suppress ticks on the count/density axis of the marginal plots. +{{joint, marginal}}_kws : dicts + Additional keyword arguments for the plot components. +kwargs + Additional keyword arguments are passed to the function used to + draw the plot on the joint Axes, superseding items in the + ``joint_kws`` dictionary. + +Returns +------- +{returns.jointgrid} + +See Also +-------- +{seealso.jointgrid} +{seealso.pairgrid} +{seealso.pairplot} + +Examples +-------- + +.. include:: ../docstrings/jointplot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) diff --git a/lib/python3.10/site-packages/seaborn/categorical.py b/lib/python3.10/site-packages/seaborn/categorical.py new file mode 100644 index 0000000000000000000000000000000000000000..e22d301b75e47cc41400ffd545e3f91bd5ad52d1 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/categorical.py @@ -0,0 +1,3546 @@ +from textwrap import dedent +from numbers import Number +import warnings +from colorsys import rgb_to_hls +from functools import partial + +import numpy as np +import pandas as pd +try: + from scipy.stats import gaussian_kde + _no_scipy = False +except ImportError: + from .external.kde import gaussian_kde + _no_scipy = True + +import matplotlib as mpl +from matplotlib.collections import PatchCollection +import matplotlib.patches as Patches +import matplotlib.pyplot as plt + +from seaborn._oldcore import ( + variable_type, + infer_orient, + categorical_order, +) +from seaborn.relational import _RelationalPlotter +from seaborn import utils +from seaborn.utils import remove_na, _normal_quantile_func, _draw_figure, _default_color +from seaborn._statistics import EstimateAggregator +from seaborn.palettes import color_palette, husl_palette, light_palette, dark_palette +from seaborn.axisgrid import FacetGrid, _facet_docs + + +__all__ = [ + "catplot", + "stripplot", "swarmplot", + "boxplot", "violinplot", "boxenplot", + "pointplot", "barplot", "countplot", +] + + +# Subclassing _RelationalPlotter for the legend machinery, +# but probably should move that more centrally +class _CategoricalPlotterNew(_RelationalPlotter): + + semantics = "x", "y", "hue", "units" + + wide_structure = {"x": "@columns", "y": "@values", "hue": "@columns"} + + # flat_structure = {"x": "@values", "y": "@values"} + flat_structure = {"y": "@values"} + + _legend_func = "scatter" + _legend_attributes = ["color"] + + def __init__( + self, + data=None, + variables={}, + order=None, + orient=None, + require_numeric=False, + legend="auto", + ): + + super().__init__(data=data, variables=variables) + + # This method takes care of some bookkeeping that is necessary because the + # original categorical plots (prior to the 2021 refactor) had some rules that + # don't fit exactly into the logic of _core. It may be wise to have a second + # round of refactoring that moves the logic deeper, but this will keep things + # relatively sensible for now. + + # For wide data, orient determines assignment to x/y differently from the + # wide_structure rules in _core. If we do decide to make orient part of the + # _core variable assignment, we'll want to figure out how to express that. + if self.input_format == "wide" and orient == "h": + self.plot_data = self.plot_data.rename(columns={"x": "y", "y": "x"}) + orig_variables = set(self.variables) + orig_x = self.variables.pop("x", None) + orig_y = self.variables.pop("y", None) + orig_x_type = self.var_types.pop("x", None) + orig_y_type = self.var_types.pop("y", None) + if "x" in orig_variables: + self.variables["y"] = orig_x + self.var_types["y"] = orig_x_type + if "y" in orig_variables: + self.variables["x"] = orig_y + self.var_types["x"] = orig_y_type + + # The concept of an "orientation" is important to the original categorical + # plots, but there's no provision for it in _core, so we need to do it here. + # Note that it could be useful for the other functions in at least two ways + # (orienting a univariate distribution plot from long-form data and selecting + # the aggregation axis in lineplot), so we may want to eventually refactor it. + self.orient = infer_orient( + x=self.plot_data.get("x", None), + y=self.plot_data.get("y", None), + orient=orient, + require_numeric=require_numeric, + ) + + self.legend = legend + + # Short-circuit in the case of an empty plot + if not self.has_xy_data: + return + + # Categorical plots can be "univariate" in which case they get an anonymous + # category label on the opposite axis. Note: this duplicates code in the core + # scale_categorical function. We need to do it here because of the next line. + if self.cat_axis not in self.variables: + self.variables[self.cat_axis] = None + self.var_types[self.cat_axis] = "categorical" + self.plot_data[self.cat_axis] = "" + + # Categorical variables have discrete levels that we need to track + cat_levels = categorical_order(self.plot_data[self.cat_axis], order) + self.var_levels[self.cat_axis] = cat_levels + + def _hue_backcompat(self, color, palette, hue_order, force_hue=False): + """Implement backwards compatibility for hue parametrization. + + Note: the force_hue parameter is used so that functions can be shown to + pass existing tests during refactoring and then tested for new behavior. + It can be removed after completion of the work. + + """ + # The original categorical functions applied a palette to the categorical axis + # by default. We want to require an explicit hue mapping, to be more consistent + # with how things work elsewhere now. I don't think there's any good way to + # do this gently -- because it's triggered by the default value of hue=None, + # users would always get a warning, unless we introduce some sentinel "default" + # argument for this change. That's possible, but asking users to set `hue=None` + # on every call is annoying. + # We are keeping the logic for implementing the old behavior in with the current + # system so that (a) we can punt on that decision and (b) we can ensure that + # refactored code passes old tests. + default_behavior = color is None or palette is not None + if force_hue and "hue" not in self.variables and default_behavior: + self._redundant_hue = True + self.plot_data["hue"] = self.plot_data[self.cat_axis] + self.variables["hue"] = self.variables[self.cat_axis] + self.var_types["hue"] = "categorical" + hue_order = self.var_levels[self.cat_axis] + + # Because we convert the categorical axis variable to string, + # we need to update a dictionary palette too + if isinstance(palette, dict): + palette = {str(k): v for k, v in palette.items()} + + else: + self._redundant_hue = False + + # Previously, categorical plots had a trick where color= could seed the palette. + # Because that's an explicit parameterization, we are going to give it one + # release cycle with a warning before removing. + if "hue" in self.variables and palette is None and color is not None: + if not isinstance(color, str): + color = mpl.colors.to_hex(color) + palette = f"dark:{color}" + msg = ( + "Setting a gradient palette using color= is deprecated and will be " + f"removed in version 0.13. Set `palette='{palette}'` for same effect." + ) + warnings.warn(msg, FutureWarning) + + return palette, hue_order + + def _palette_without_hue_backcompat(self, palette, hue_order): + """Provide one cycle where palette= implies hue= when not provided""" + if "hue" not in self.variables and palette is not None: + msg = "Passing `palette` without assigning `hue` is deprecated." + warnings.warn(msg, FutureWarning, stacklevel=3) + self.legend = False + self.plot_data["hue"] = self.plot_data[self.cat_axis] + self.variables["hue"] = self.variables.get(self.cat_axis) + self.var_types["hue"] = self.var_types.get(self.cat_axis) + hue_order = self.var_levels.get(self.cat_axis) + return hue_order + + @property + def cat_axis(self): + return {"v": "x", "h": "y"}[self.orient] + + def _get_gray(self, colors): + """Get a grayscale value that looks good with color.""" + if not len(colors): + return None + unique_colors = np.unique(colors, axis=0) + light_vals = [rgb_to_hls(*rgb[:3])[1] for rgb in unique_colors] + lum = min(light_vals) * .6 + return (lum, lum, lum) + + def _adjust_cat_axis(self, ax, axis): + """Set ticks and limits for a categorical variable.""" + # Note: in theory, this could happen in _attach for all categorical axes + # But two reasons not to do that: + # - If it happens before plotting, autoscaling messes up the plot limits + # - It would change existing plots from other seaborn functions + if self.var_types[axis] != "categorical": + return + + # If both x/y data are empty, the correct way to set up the plot is + # somewhat undefined; because we don't add null category data to the plot in + # this case we don't *have* a categorical axis (yet), so best to just bail. + if self.plot_data[axis].empty: + return + + # We can infer the total number of categories (including those from previous + # plots that are not part of the plot we are currently making) from the number + # of ticks, which matplotlib sets up while doing unit conversion. This feels + # slightly risky, as if we are relying on something that may be a matplotlib + # implementation detail. But I cannot think of a better way to keep track of + # the state from previous categorical calls (see GH2516 for context) + n = len(getattr(ax, f"get_{axis}ticks")()) + + if axis == "x": + ax.xaxis.grid(False) + ax.set_xlim(-.5, n - .5, auto=None) + else: + ax.yaxis.grid(False) + # Note limits that correspond to previously-inverted y axis + ax.set_ylim(n - .5, -.5, auto=None) + + @property + def _native_width(self): + """Return unit of width separating categories on native numeric scale.""" + unique_values = np.unique(self.comp_data[self.cat_axis]) + if len(unique_values) > 1: + native_width = np.nanmin(np.diff(unique_values)) + else: + native_width = 1 + return native_width + + def _nested_offsets(self, width, dodge): + """Return offsets for each hue level for dodged plots.""" + offsets = None + if "hue" in self.variables and self._hue_map.levels is not None: + n_levels = len(self._hue_map.levels) + if dodge: + each_width = width / n_levels + offsets = np.linspace(0, width - each_width, n_levels) + offsets -= offsets.mean() + else: + offsets = np.zeros(n_levels) + return offsets + + # Note that the plotting methods here aim (in most cases) to produce the + # exact same artists as the original (pre 0.12) version of the code, so + # there is some weirdness that might not otherwise be clean or make sense in + # this context, such as adding empty artists for combinations of variables + # with no observations + + def plot_strips( + self, + jitter, + dodge, + color, + edgecolor, + plot_kws, + ): + + width = .8 * self._native_width + offsets = self._nested_offsets(width, dodge) + + if jitter is True: + jlim = 0.1 + else: + jlim = float(jitter) + if "hue" in self.variables and dodge and self._hue_map.levels is not None: + jlim /= len(self._hue_map.levels) + jlim *= self._native_width + jitterer = partial(np.random.uniform, low=-jlim, high=+jlim) + + iter_vars = [self.cat_axis] + if dodge: + iter_vars.append("hue") + + ax = self.ax + dodge_move = jitter_move = 0 + + for sub_vars, sub_data in self.iter_data(iter_vars, + from_comp_data=True, + allow_empty=True): + if offsets is not None and (offsets != 0).any(): + dodge_move = offsets[sub_data["hue"].map(self._hue_map.levels.index)] + + jitter_move = jitterer(size=len(sub_data)) if len(sub_data) > 1 else 0 + + adjusted_data = sub_data[self.cat_axis] + dodge_move + jitter_move + sub_data[self.cat_axis] = adjusted_data + + for var in "xy": + if self._log_scaled(var): + sub_data[var] = np.power(10, sub_data[var]) + + ax = self._get_axes(sub_vars) + points = ax.scatter(sub_data["x"], sub_data["y"], color=color, **plot_kws) + + if "hue" in self.variables: + points.set_facecolors(self._hue_map(sub_data["hue"])) + + if edgecolor == "gray": # XXX TODO change to "auto" + points.set_edgecolors(self._get_gray(points.get_facecolors())) + else: + points.set_edgecolors(edgecolor) + + # Finalize the axes details + if self.legend == "auto": + show_legend = not self._redundant_hue and self.input_format != "wide" + else: + show_legend = bool(self.legend) + + if show_legend: + self.add_legend_data(ax) + handles, _ = ax.get_legend_handles_labels() + if handles: + ax.legend(title=self.legend_title) + + def plot_swarms( + self, + dodge, + color, + edgecolor, + warn_thresh, + plot_kws, + ): + + width = .8 * self._native_width + offsets = self._nested_offsets(width, dodge) + + iter_vars = [self.cat_axis] + if dodge: + iter_vars.append("hue") + + ax = self.ax + point_collections = {} + dodge_move = 0 + + for sub_vars, sub_data in self.iter_data(iter_vars, + from_comp_data=True, + allow_empty=True): + + if offsets is not None: + dodge_move = offsets[sub_data["hue"].map(self._hue_map.levels.index)] + + if not sub_data.empty: + sub_data[self.cat_axis] = sub_data[self.cat_axis] + dodge_move + + for var in "xy": + if self._log_scaled(var): + sub_data[var] = np.power(10, sub_data[var]) + + ax = self._get_axes(sub_vars) + points = ax.scatter(sub_data["x"], sub_data["y"], color=color, **plot_kws) + + if "hue" in self.variables: + points.set_facecolors(self._hue_map(sub_data["hue"])) + + if edgecolor == "gray": # XXX TODO change to "auto" + points.set_edgecolors(self._get_gray(points.get_facecolors())) + else: + points.set_edgecolors(edgecolor) + + if not sub_data.empty: + point_collections[(ax, sub_data[self.cat_axis].iloc[0])] = points + + beeswarm = Beeswarm( + width=width, orient=self.orient, warn_thresh=warn_thresh, + ) + for (ax, center), points in point_collections.items(): + if points.get_offsets().shape[0] > 1: + + def draw(points, renderer, *, center=center): + + beeswarm(points, center) + + if self.orient == "h": + scalex = False + scaley = ax.get_autoscaley_on() + else: + scalex = ax.get_autoscalex_on() + scaley = False + + # This prevents us from undoing the nice categorical axis limits + # set in _adjust_cat_axis, because that method currently leave + # the autoscale flag in its original setting. It may be better + # to disable autoscaling there to avoid needing to do this. + fixed_scale = self.var_types[self.cat_axis] == "categorical" + ax.update_datalim(points.get_datalim(ax.transData)) + if not fixed_scale and (scalex or scaley): + ax.autoscale_view(scalex=scalex, scaley=scaley) + + super(points.__class__, points).draw(renderer) + + points.draw = draw.__get__(points) + + _draw_figure(ax.figure) + + # Finalize the axes details + if self.legend == "auto": + show_legend = not self._redundant_hue and self.input_format != "wide" + else: + show_legend = bool(self.legend) + + if show_legend: + self.add_legend_data(ax) + handles, _ = ax.get_legend_handles_labels() + if handles: + ax.legend(title=self.legend_title) + + +class _CategoricalFacetPlotter(_CategoricalPlotterNew): + + semantics = _CategoricalPlotterNew.semantics + ("col", "row") + + +class _CategoricalPlotter: + + width = .8 + default_palette = "light" + require_numeric = True + + def establish_variables(self, x=None, y=None, hue=None, data=None, + orient=None, order=None, hue_order=None, + units=None): + """Convert input specification into a common representation.""" + # Option 1: + # We are plotting a wide-form dataset + # ----------------------------------- + if x is None and y is None: + + # Do a sanity check on the inputs + if hue is not None: + error = "Cannot use `hue` without `x` and `y`" + raise ValueError(error) + + # No hue grouping with wide inputs + plot_hues = None + hue_title = None + hue_names = None + + # No statistical units with wide inputs + plot_units = None + + # We also won't get a axes labels here + value_label = None + group_label = None + + # Option 1a: + # The input data is a Pandas DataFrame + # ------------------------------------ + + if isinstance(data, pd.DataFrame): + + # Order the data correctly + if order is None: + order = [] + # Reduce to just numeric columns + for col in data: + if variable_type(data[col]) == "numeric": + order.append(col) + plot_data = data[order] + group_names = order + group_label = data.columns.name + + # Convert to a list of arrays, the common representation + iter_data = plot_data.items() + plot_data = [np.asarray(s, float) for k, s in iter_data] + + # Option 1b: + # The input data is an array or list + # ---------------------------------- + + else: + + # We can't reorder the data + if order is not None: + error = "Input data must be a pandas object to reorder" + raise ValueError(error) + + # The input data is an array + if hasattr(data, "shape"): + if len(data.shape) == 1: + if np.isscalar(data[0]): + plot_data = [data] + else: + plot_data = list(data) + elif len(data.shape) == 2: + nr, nc = data.shape + if nr == 1 or nc == 1: + plot_data = [data.ravel()] + else: + plot_data = [data[:, i] for i in range(nc)] + else: + error = ("Input `data` can have no " + "more than 2 dimensions") + raise ValueError(error) + + # Check if `data` is None to let us bail out here (for testing) + elif data is None: + plot_data = [[]] + + # The input data is a flat list + elif np.isscalar(data[0]): + plot_data = [data] + + # The input data is a nested list + # This will catch some things that might fail later + # but exhaustive checks are hard + else: + plot_data = data + + # Convert to a list of arrays, the common representation + plot_data = [np.asarray(d, float) for d in plot_data] + + # The group names will just be numeric indices + group_names = list(range(len(plot_data))) + + # Figure out the plotting orientation + orient = "h" if str(orient).startswith("h") else "v" + + # Option 2: + # We are plotting a long-form dataset + # ----------------------------------- + + else: + + # See if we need to get variables from `data` + if data is not None: + x = data.get(x, x) + y = data.get(y, y) + hue = data.get(hue, hue) + units = data.get(units, units) + + # Validate the inputs + for var in [x, y, hue, units]: + if isinstance(var, str): + err = f"Could not interpret input '{var}'" + raise ValueError(err) + + # Figure out the plotting orientation + orient = infer_orient( + x, y, orient, require_numeric=self.require_numeric + ) + + # Option 2a: + # We are plotting a single set of data + # ------------------------------------ + if x is None or y is None: + + # Determine where the data are + vals = y if x is None else x + + # Put them into the common representation + plot_data = [np.asarray(vals)] + + # Get a label for the value axis + if hasattr(vals, "name"): + value_label = vals.name + else: + value_label = None + + # This plot will not have group labels or hue nesting + groups = None + group_label = None + group_names = [] + plot_hues = None + hue_names = None + hue_title = None + plot_units = None + + # Option 2b: + # We are grouping the data values by another variable + # --------------------------------------------------- + else: + + # Determine which role each variable will play + if orient == "v": + vals, groups = y, x + else: + vals, groups = x, y + + # Get the categorical axis label + group_label = None + if hasattr(groups, "name"): + group_label = groups.name + + # Get the order on the categorical axis + group_names = categorical_order(groups, order) + + # Group the numeric data + plot_data, value_label = self._group_longform(vals, groups, + group_names) + + # Now handle the hue levels for nested ordering + if hue is None: + plot_hues = None + hue_title = None + hue_names = None + else: + + # Get the order of the hue levels + hue_names = categorical_order(hue, hue_order) + + # Group the hue data + plot_hues, hue_title = self._group_longform(hue, groups, + group_names) + + # Now handle the units for nested observations + if units is None: + plot_units = None + else: + plot_units, _ = self._group_longform(units, groups, + group_names) + + # Assign object attributes + # ------------------------ + self.orient = orient + self.plot_data = plot_data + self.group_label = group_label + self.value_label = value_label + self.group_names = group_names + self.plot_hues = plot_hues + self.hue_title = hue_title + self.hue_names = hue_names + self.plot_units = plot_units + + def _group_longform(self, vals, grouper, order): + """Group a long-form variable by another with correct order.""" + # Ensure that the groupby will work + if not isinstance(vals, pd.Series): + if isinstance(grouper, pd.Series): + index = grouper.index + else: + index = None + vals = pd.Series(vals, index=index) + + # Group the val data + grouped_vals = vals.groupby(grouper) + out_data = [] + for g in order: + try: + g_vals = grouped_vals.get_group(g) + except KeyError: + g_vals = np.array([]) + out_data.append(g_vals) + + # Get the vals axis label + label = vals.name + + return out_data, label + + def establish_colors(self, color, palette, saturation): + """Get a list of colors for the main component of the plots.""" + if self.hue_names is None: + n_colors = len(self.plot_data) + else: + n_colors = len(self.hue_names) + + # Determine the main colors + if color is None and palette is None: + # Determine whether the current palette will have enough values + # If not, we'll default to the husl palette so each is distinct + current_palette = utils.get_color_cycle() + if n_colors <= len(current_palette): + colors = color_palette(n_colors=n_colors) + else: + colors = husl_palette(n_colors, l=.7) # noqa + + elif palette is None: + # When passing a specific color, the interpretation depends + # on whether there is a hue variable or not. + # If so, we will make a blend palette so that the different + # levels have some amount of variation. + if self.hue_names is None: + colors = [color] * n_colors + else: + if self.default_palette == "light": + colors = light_palette(color, n_colors) + elif self.default_palette == "dark": + colors = dark_palette(color, n_colors) + else: + raise RuntimeError("No default palette specified") + else: + + # Let `palette` be a dict mapping level to color + if isinstance(palette, dict): + if self.hue_names is None: + levels = self.group_names + else: + levels = self.hue_names + palette = [palette[l] for l in levels] + + colors = color_palette(palette, n_colors) + + # Desaturate a bit because these are patches + if saturation < 1: + colors = color_palette(colors, desat=saturation) + + # Convert the colors to a common representations + rgb_colors = color_palette(colors) + + # Determine the gray color to use for the lines framing the plot + light_vals = [rgb_to_hls(*c)[1] for c in rgb_colors] + lum = min(light_vals) * .6 + gray = mpl.colors.rgb2hex((lum, lum, lum)) + + # Assign object attributes + self.colors = rgb_colors + self.gray = gray + + @property + def hue_offsets(self): + """A list of center positions for plots when hue nesting is used.""" + n_levels = len(self.hue_names) + if self.dodge: + each_width = self.width / n_levels + offsets = np.linspace(0, self.width - each_width, n_levels) + offsets -= offsets.mean() + else: + offsets = np.zeros(n_levels) + + return offsets + + @property + def nested_width(self): + """A float with the width of plot elements when hue nesting is used.""" + if self.dodge: + width = self.width / len(self.hue_names) * .98 + else: + width = self.width + return width + + def annotate_axes(self, ax): + """Add descriptive labels to an Axes object.""" + if self.orient == "v": + xlabel, ylabel = self.group_label, self.value_label + else: + xlabel, ylabel = self.value_label, self.group_label + + if xlabel is not None: + ax.set_xlabel(xlabel) + if ylabel is not None: + ax.set_ylabel(ylabel) + + group_names = self.group_names + if not group_names: + group_names = ["" for _ in range(len(self.plot_data))] + + if self.orient == "v": + ax.set_xticks(np.arange(len(self.plot_data))) + ax.set_xticklabels(group_names) + else: + ax.set_yticks(np.arange(len(self.plot_data))) + ax.set_yticklabels(group_names) + + if self.orient == "v": + ax.xaxis.grid(False) + ax.set_xlim(-.5, len(self.plot_data) - .5, auto=None) + else: + ax.yaxis.grid(False) + ax.set_ylim(-.5, len(self.plot_data) - .5, auto=None) + + if self.hue_names is not None: + ax.legend(loc="best", title=self.hue_title) + + def add_legend_data(self, ax, color, label): + """Add a dummy patch object so we can get legend data.""" + rect = plt.Rectangle([0, 0], 0, 0, + linewidth=self.linewidth / 2, + edgecolor=self.gray, + facecolor=color, + label=label) + ax.add_patch(rect) + + +class _BoxPlotter(_CategoricalPlotter): + + def __init__(self, x, y, hue, data, order, hue_order, + orient, color, palette, saturation, + width, dodge, fliersize, linewidth): + + self.establish_variables(x, y, hue, data, orient, order, hue_order) + self.establish_colors(color, palette, saturation) + + self.dodge = dodge + self.width = width + self.fliersize = fliersize + + if linewidth is None: + linewidth = mpl.rcParams["lines.linewidth"] + self.linewidth = linewidth + + def draw_boxplot(self, ax, kws): + """Use matplotlib to draw a boxplot on an Axes.""" + vert = self.orient == "v" + + props = {} + for obj in ["box", "whisker", "cap", "median", "flier"]: + props[obj] = kws.pop(obj + "props", {}) + + for i, group_data in enumerate(self.plot_data): + + if self.plot_hues is None: + + # Handle case where there is data at this level + if group_data.size == 0: + continue + + # Draw a single box or a set of boxes + # with a single level of grouping + box_data = np.asarray(remove_na(group_data)) + + # Handle case where there is no non-null data + if box_data.size == 0: + continue + + artist_dict = ax.boxplot(box_data, + vert=vert, + patch_artist=True, + positions=[i], + widths=self.width, + **kws) + color = self.colors[i] + self.restyle_boxplot(artist_dict, color, props) + else: + # Draw nested groups of boxes + offsets = self.hue_offsets + for j, hue_level in enumerate(self.hue_names): + + # Add a legend for this hue level + if not i: + self.add_legend_data(ax, self.colors[j], hue_level) + + # Handle case where there is data at this level + if group_data.size == 0: + continue + + hue_mask = self.plot_hues[i] == hue_level + box_data = np.asarray(remove_na(group_data[hue_mask])) + + # Handle case where there is no non-null data + if box_data.size == 0: + continue + + center = i + offsets[j] + artist_dict = ax.boxplot(box_data, + vert=vert, + patch_artist=True, + positions=[center], + widths=self.nested_width, + **kws) + self.restyle_boxplot(artist_dict, self.colors[j], props) + # Add legend data, but just for one set of boxes + + def restyle_boxplot(self, artist_dict, color, props): + """Take a drawn matplotlib boxplot and make it look nice.""" + for box in artist_dict["boxes"]: + box.update(dict(facecolor=color, + zorder=.9, + edgecolor=self.gray, + linewidth=self.linewidth)) + box.update(props["box"]) + for whisk in artist_dict["whiskers"]: + whisk.update(dict(color=self.gray, + linewidth=self.linewidth, + linestyle="-")) + whisk.update(props["whisker"]) + for cap in artist_dict["caps"]: + cap.update(dict(color=self.gray, + linewidth=self.linewidth)) + cap.update(props["cap"]) + for med in artist_dict["medians"]: + med.update(dict(color=self.gray, + linewidth=self.linewidth)) + med.update(props["median"]) + for fly in artist_dict["fliers"]: + fly.update(dict(markerfacecolor=self.gray, + marker="d", + markeredgecolor=self.gray, + markersize=self.fliersize)) + fly.update(props["flier"]) + + def plot(self, ax, boxplot_kws): + """Make the plot.""" + self.draw_boxplot(ax, boxplot_kws) + self.annotate_axes(ax) + if self.orient == "h": + ax.invert_yaxis() + + +class _ViolinPlotter(_CategoricalPlotter): + + def __init__(self, x, y, hue, data, order, hue_order, + bw, cut, scale, scale_hue, gridsize, + width, inner, split, dodge, orient, linewidth, + color, palette, saturation): + + self.establish_variables(x, y, hue, data, orient, order, hue_order) + self.establish_colors(color, palette, saturation) + self.estimate_densities(bw, cut, scale, scale_hue, gridsize) + + self.gridsize = gridsize + self.width = width + self.dodge = dodge + + if inner is not None: + if not any([inner.startswith("quart"), + inner.startswith("box"), + inner.startswith("stick"), + inner.startswith("point")]): + err = f"Inner style '{inner}' not recognized" + raise ValueError(err) + self.inner = inner + + if split and self.hue_names is not None and len(self.hue_names) != 2: + msg = "There must be exactly two hue levels to use `split`.'" + raise ValueError(msg) + self.split = split + + if linewidth is None: + linewidth = mpl.rcParams["lines.linewidth"] + self.linewidth = linewidth + + def estimate_densities(self, bw, cut, scale, scale_hue, gridsize): + """Find the support and density for all of the data.""" + # Initialize data structures to keep track of plotting data + if self.hue_names is None: + support = [] + density = [] + counts = np.zeros(len(self.plot_data)) + max_density = np.zeros(len(self.plot_data)) + else: + support = [[] for _ in self.plot_data] + density = [[] for _ in self.plot_data] + size = len(self.group_names), len(self.hue_names) + counts = np.zeros(size) + max_density = np.zeros(size) + + for i, group_data in enumerate(self.plot_data): + + # Option 1: we have a single level of grouping + # -------------------------------------------- + + if self.plot_hues is None: + + # Strip missing datapoints + kde_data = remove_na(group_data) + + # Handle special case of no data at this level + if kde_data.size == 0: + support.append(np.array([])) + density.append(np.array([1.])) + counts[i] = 0 + max_density[i] = 0 + continue + + # Handle special case of a single unique datapoint + elif np.unique(kde_data).size == 1: + support.append(np.unique(kde_data)) + density.append(np.array([1.])) + counts[i] = 1 + max_density[i] = 0 + continue + + # Fit the KDE and get the used bandwidth size + kde, bw_used = self.fit_kde(kde_data, bw) + + # Determine the support grid and get the density over it + support_i = self.kde_support(kde_data, bw_used, cut, gridsize) + density_i = kde.evaluate(support_i) + + # Update the data structures with these results + support.append(support_i) + density.append(density_i) + counts[i] = kde_data.size + max_density[i] = density_i.max() + + # Option 2: we have nested grouping by a hue variable + # --------------------------------------------------- + + else: + for j, hue_level in enumerate(self.hue_names): + + # Handle special case of no data at this category level + if not group_data.size: + support[i].append(np.array([])) + density[i].append(np.array([1.])) + counts[i, j] = 0 + max_density[i, j] = 0 + continue + + # Select out the observations for this hue level + hue_mask = self.plot_hues[i] == hue_level + + # Strip missing datapoints + kde_data = remove_na(group_data[hue_mask]) + + # Handle special case of no data at this level + if kde_data.size == 0: + support[i].append(np.array([])) + density[i].append(np.array([1.])) + counts[i, j] = 0 + max_density[i, j] = 0 + continue + + # Handle special case of a single unique datapoint + elif np.unique(kde_data).size == 1: + support[i].append(np.unique(kde_data)) + density[i].append(np.array([1.])) + counts[i, j] = 1 + max_density[i, j] = 0 + continue + + # Fit the KDE and get the used bandwidth size + kde, bw_used = self.fit_kde(kde_data, bw) + + # Determine the support grid and get the density over it + support_ij = self.kde_support(kde_data, bw_used, + cut, gridsize) + density_ij = kde.evaluate(support_ij) + + # Update the data structures with these results + support[i].append(support_ij) + density[i].append(density_ij) + counts[i, j] = kde_data.size + max_density[i, j] = density_ij.max() + + # Scale the height of the density curve. + # For a violinplot the density is non-quantitative. + # The objective here is to scale the curves relative to 1 so that + # they can be multiplied by the width parameter during plotting. + + if scale == "area": + self.scale_area(density, max_density, scale_hue) + + elif scale == "width": + self.scale_width(density) + + elif scale == "count": + self.scale_count(density, counts, scale_hue) + + else: + raise ValueError(f"scale method '{scale}' not recognized") + + # Set object attributes that will be used while plotting + self.support = support + self.density = density + + def fit_kde(self, x, bw): + """Estimate a KDE for a vector of data with flexible bandwidth.""" + kde = gaussian_kde(x, bw) + + # Extract the numeric bandwidth from the KDE object + bw_used = kde.factor + + # At this point, bw will be a numeric scale factor. + # To get the actual bandwidth of the kernel, we multiple by the + # unbiased standard deviation of the data, which we will use + # elsewhere to compute the range of the support. + bw_used = bw_used * x.std(ddof=1) + + return kde, bw_used + + def kde_support(self, x, bw, cut, gridsize): + """Define a grid of support for the violin.""" + support_min = x.min() - bw * cut + support_max = x.max() + bw * cut + return np.linspace(support_min, support_max, gridsize) + + def scale_area(self, density, max_density, scale_hue): + """Scale the relative area under the KDE curve. + + This essentially preserves the "standard" KDE scaling, but the + resulting maximum density will be 1 so that the curve can be + properly multiplied by the violin width. + + """ + if self.hue_names is None: + for d in density: + if d.size > 1: + d /= max_density.max() + else: + for i, group in enumerate(density): + for d in group: + if scale_hue: + max = max_density[i].max() + else: + max = max_density.max() + if d.size > 1: + d /= max + + def scale_width(self, density): + """Scale each density curve to the same height.""" + if self.hue_names is None: + for d in density: + d /= d.max() + else: + for group in density: + for d in group: + d /= d.max() + + def scale_count(self, density, counts, scale_hue): + """Scale each density curve by the number of observations.""" + if self.hue_names is None: + if counts.max() == 0: + d = 0 + else: + for count, d in zip(counts, density): + d /= d.max() + d *= count / counts.max() + else: + for i, group in enumerate(density): + for j, d in enumerate(group): + if counts[i].max() == 0: + d = 0 + else: + count = counts[i, j] + if scale_hue: + scaler = count / counts[i].max() + else: + scaler = count / counts.max() + d /= d.max() + d *= scaler + + @property + def dwidth(self): + + if self.hue_names is None or not self.dodge: + return self.width / 2 + elif self.split: + return self.width / 2 + else: + return self.width / (2 * len(self.hue_names)) + + def draw_violins(self, ax): + """Draw the violins onto `ax`.""" + fill_func = ax.fill_betweenx if self.orient == "v" else ax.fill_between + for i, group_data in enumerate(self.plot_data): + + kws = dict(edgecolor=self.gray, linewidth=self.linewidth) + + # Option 1: we have a single level of grouping + # -------------------------------------------- + + if self.plot_hues is None: + + support, density = self.support[i], self.density[i] + + # Handle special case of no observations in this bin + if support.size == 0: + continue + + # Handle special case of a single observation + elif support.size == 1: + val = support.item() + d = density.item() + self.draw_single_observation(ax, i, val, d) + continue + + # Draw the violin for this group + grid = np.ones(self.gridsize) * i + fill_func(support, + grid - density * self.dwidth, + grid + density * self.dwidth, + facecolor=self.colors[i], + **kws) + + # Draw the interior representation of the data + if self.inner is None: + continue + + # Get a nan-free vector of datapoints + violin_data = remove_na(group_data) + + # Draw box and whisker information + if self.inner.startswith("box"): + self.draw_box_lines(ax, violin_data, i) + + # Draw quartile lines + elif self.inner.startswith("quart"): + self.draw_quartiles(ax, violin_data, support, density, i) + + # Draw stick observations + elif self.inner.startswith("stick"): + self.draw_stick_lines(ax, violin_data, support, density, i) + + # Draw point observations + elif self.inner.startswith("point"): + self.draw_points(ax, violin_data, i) + + # Option 2: we have nested grouping by a hue variable + # --------------------------------------------------- + + else: + offsets = self.hue_offsets + for j, hue_level in enumerate(self.hue_names): + + support, density = self.support[i][j], self.density[i][j] + kws["facecolor"] = self.colors[j] + + # Add legend data, but just for one set of violins + if not i: + self.add_legend_data(ax, self.colors[j], hue_level) + + # Handle the special case where we have no observations + if support.size == 0: + continue + + # Handle the special case where we have one observation + elif support.size == 1: + val = support.item() + d = density.item() + if self.split: + d = d / 2 + at_group = i + offsets[j] + self.draw_single_observation(ax, at_group, val, d) + continue + + # Option 2a: we are drawing a single split violin + # ----------------------------------------------- + + if self.split: + + grid = np.ones(self.gridsize) * i + if j: + fill_func(support, + grid, + grid + density * self.dwidth, + **kws) + else: + fill_func(support, + grid - density * self.dwidth, + grid, + **kws) + + # Draw the interior representation of the data + if self.inner is None: + continue + + # Get a nan-free vector of datapoints + hue_mask = self.plot_hues[i] == hue_level + violin_data = remove_na(group_data[hue_mask]) + + # Draw quartile lines + if self.inner.startswith("quart"): + self.draw_quartiles(ax, violin_data, + support, density, i, + ["left", "right"][j]) + + # Draw stick observations + elif self.inner.startswith("stick"): + self.draw_stick_lines(ax, violin_data, + support, density, i, + ["left", "right"][j]) + + # The box and point interior plots are drawn for + # all data at the group level, so we just do that once + if j and any(self.plot_hues[0] == hue_level): + continue + + # Get the whole vector for this group level + violin_data = remove_na(group_data) + + # Draw box and whisker information + if self.inner.startswith("box"): + self.draw_box_lines(ax, violin_data, i) + + # Draw point observations + elif self.inner.startswith("point"): + self.draw_points(ax, violin_data, i) + + # Option 2b: we are drawing full nested violins + # ----------------------------------------------- + + else: + grid = np.ones(self.gridsize) * (i + offsets[j]) + fill_func(support, + grid - density * self.dwidth, + grid + density * self.dwidth, + **kws) + + # Draw the interior representation + if self.inner is None: + continue + + # Get a nan-free vector of datapoints + hue_mask = self.plot_hues[i] == hue_level + violin_data = remove_na(group_data[hue_mask]) + + # Draw box and whisker information + if self.inner.startswith("box"): + self.draw_box_lines(ax, violin_data, i + offsets[j]) + + # Draw quartile lines + elif self.inner.startswith("quart"): + self.draw_quartiles(ax, violin_data, + support, density, + i + offsets[j]) + + # Draw stick observations + elif self.inner.startswith("stick"): + self.draw_stick_lines(ax, violin_data, + support, density, + i + offsets[j]) + + # Draw point observations + elif self.inner.startswith("point"): + self.draw_points(ax, violin_data, i + offsets[j]) + + def draw_single_observation(self, ax, at_group, at_quant, density): + """Draw a line to mark a single observation.""" + d_width = density * self.dwidth + if self.orient == "v": + ax.plot([at_group - d_width, at_group + d_width], + [at_quant, at_quant], + color=self.gray, + linewidth=self.linewidth) + else: + ax.plot([at_quant, at_quant], + [at_group - d_width, at_group + d_width], + color=self.gray, + linewidth=self.linewidth) + + def draw_box_lines(self, ax, data, center): + """Draw boxplot information at center of the density.""" + # Compute the boxplot statistics + q25, q50, q75 = np.percentile(data, [25, 50, 75]) + whisker_lim = 1.5 * (q75 - q25) + h1 = np.min(data[data >= (q25 - whisker_lim)]) + h2 = np.max(data[data <= (q75 + whisker_lim)]) + + # Draw a boxplot using lines and a point + if self.orient == "v": + ax.plot([center, center], [h1, h2], + linewidth=self.linewidth, + color=self.gray) + ax.plot([center, center], [q25, q75], + linewidth=self.linewidth * 3, + color=self.gray) + ax.scatter(center, q50, + zorder=3, + color="white", + edgecolor=self.gray, + s=np.square(self.linewidth * 2)) + else: + ax.plot([h1, h2], [center, center], + linewidth=self.linewidth, + color=self.gray) + ax.plot([q25, q75], [center, center], + linewidth=self.linewidth * 3, + color=self.gray) + ax.scatter(q50, center, + zorder=3, + color="white", + edgecolor=self.gray, + s=np.square(self.linewidth * 2)) + + def draw_quartiles(self, ax, data, support, density, center, split=False): + """Draw the quartiles as lines at width of density.""" + q25, q50, q75 = np.percentile(data, [25, 50, 75]) + + self.draw_to_density(ax, center, q25, support, density, split, + linewidth=self.linewidth, + dashes=[self.linewidth * 1.5] * 2) + self.draw_to_density(ax, center, q50, support, density, split, + linewidth=self.linewidth, + dashes=[self.linewidth * 3] * 2) + self.draw_to_density(ax, center, q75, support, density, split, + linewidth=self.linewidth, + dashes=[self.linewidth * 1.5] * 2) + + def draw_points(self, ax, data, center): + """Draw individual observations as points at middle of the violin.""" + kws = dict(s=np.square(self.linewidth * 2), + color=self.gray, + edgecolor=self.gray) + + grid = np.ones(len(data)) * center + + if self.orient == "v": + ax.scatter(grid, data, **kws) + else: + ax.scatter(data, grid, **kws) + + def draw_stick_lines(self, ax, data, support, density, + center, split=False): + """Draw individual observations as sticks at width of density.""" + for val in data: + self.draw_to_density(ax, center, val, support, density, split, + linewidth=self.linewidth * .5) + + def draw_to_density(self, ax, center, val, support, density, split, **kws): + """Draw a line orthogonal to the value axis at width of density.""" + idx = np.argmin(np.abs(support - val)) + width = self.dwidth * density[idx] * .99 + + kws["color"] = self.gray + + if self.orient == "v": + if split == "left": + ax.plot([center - width, center], [val, val], **kws) + elif split == "right": + ax.plot([center, center + width], [val, val], **kws) + else: + ax.plot([center - width, center + width], [val, val], **kws) + else: + if split == "left": + ax.plot([val, val], [center - width, center], **kws) + elif split == "right": + ax.plot([val, val], [center, center + width], **kws) + else: + ax.plot([val, val], [center - width, center + width], **kws) + + def plot(self, ax): + """Make the violin plot.""" + self.draw_violins(ax) + self.annotate_axes(ax) + if self.orient == "h": + ax.invert_yaxis() + + +class _CategoricalStatPlotter(_CategoricalPlotter): + + require_numeric = True + + @property + def nested_width(self): + """A float with the width of plot elements when hue nesting is used.""" + if self.dodge: + width = self.width / len(self.hue_names) + else: + width = self.width + return width + + def estimate_statistic(self, estimator, errorbar, n_boot, seed): + + if self.hue_names is None: + statistic = [] + confint = [] + else: + statistic = [[] for _ in self.plot_data] + confint = [[] for _ in self.plot_data] + + var = {"v": "y", "h": "x"}[self.orient] + + agg = EstimateAggregator(estimator, errorbar, n_boot=n_boot, seed=seed) + + for i, group_data in enumerate(self.plot_data): + + # Option 1: we have a single layer of grouping + # -------------------------------------------- + if self.plot_hues is None: + + df = pd.DataFrame({var: group_data}) + if self.plot_units is not None: + df["units"] = self.plot_units[i] + + res = agg(df, var) + + statistic.append(res[var]) + if errorbar is not None: + confint.append((res[f"{var}min"], res[f"{var}max"])) + + # Option 2: we are grouping by a hue layer + # ---------------------------------------- + + else: + for hue_level in self.hue_names: + + if not self.plot_hues[i].size: + statistic[i].append(np.nan) + if errorbar is not None: + confint[i].append((np.nan, np.nan)) + continue + + hue_mask = self.plot_hues[i] == hue_level + df = pd.DataFrame({var: group_data[hue_mask]}) + if self.plot_units is not None: + df["units"] = self.plot_units[i][hue_mask] + + res = agg(df, var) + + statistic[i].append(res[var]) + if errorbar is not None: + confint[i].append((res[f"{var}min"], res[f"{var}max"])) + + # Save the resulting values for plotting + self.statistic = np.array(statistic) + self.confint = np.array(confint) + + def draw_confints(self, ax, at_group, confint, colors, + errwidth=None, capsize=None, **kws): + + if errwidth is not None: + kws.setdefault("lw", errwidth) + else: + kws.setdefault("lw", mpl.rcParams["lines.linewidth"] * 1.8) + + for at, (ci_low, ci_high), color in zip(at_group, + confint, + colors): + if self.orient == "v": + ax.plot([at, at], [ci_low, ci_high], color=color, **kws) + if capsize is not None: + ax.plot([at - capsize / 2, at + capsize / 2], + [ci_low, ci_low], color=color, **kws) + ax.plot([at - capsize / 2, at + capsize / 2], + [ci_high, ci_high], color=color, **kws) + else: + ax.plot([ci_low, ci_high], [at, at], color=color, **kws) + if capsize is not None: + ax.plot([ci_low, ci_low], + [at - capsize / 2, at + capsize / 2], + color=color, **kws) + ax.plot([ci_high, ci_high], + [at - capsize / 2, at + capsize / 2], + color=color, **kws) + + +class _BarPlotter(_CategoricalStatPlotter): + + def __init__(self, x, y, hue, data, order, hue_order, + estimator, errorbar, n_boot, units, seed, + orient, color, palette, saturation, width, + errcolor, errwidth, capsize, dodge): + """Initialize the plotter.""" + self.establish_variables(x, y, hue, data, orient, + order, hue_order, units) + self.establish_colors(color, palette, saturation) + self.estimate_statistic(estimator, errorbar, n_boot, seed) + + self.dodge = dodge + self.width = width + + self.errcolor = errcolor + self.errwidth = errwidth + self.capsize = capsize + + def draw_bars(self, ax, kws): + """Draw the bars onto `ax`.""" + # Get the right matplotlib function depending on the orientation + barfunc = ax.bar if self.orient == "v" else ax.barh + barpos = np.arange(len(self.statistic)) + + if self.plot_hues is None: + + # Draw the bars + barfunc(barpos, self.statistic, self.width, + color=self.colors, align="center", **kws) + + # Draw the confidence intervals + errcolors = [self.errcolor] * len(barpos) + self.draw_confints(ax, + barpos, + self.confint, + errcolors, + self.errwidth, + self.capsize) + + else: + + for j, hue_level in enumerate(self.hue_names): + + # Draw the bars + offpos = barpos + self.hue_offsets[j] + barfunc(offpos, self.statistic[:, j], self.nested_width, + color=self.colors[j], align="center", + label=hue_level, **kws) + + # Draw the confidence intervals + if self.confint.size: + confint = self.confint[:, j] + errcolors = [self.errcolor] * len(offpos) + self.draw_confints(ax, + offpos, + confint, + errcolors, + self.errwidth, + self.capsize) + + def plot(self, ax, bar_kws): + """Make the plot.""" + self.draw_bars(ax, bar_kws) + self.annotate_axes(ax) + if self.orient == "h": + ax.invert_yaxis() + + +class _PointPlotter(_CategoricalStatPlotter): + + default_palette = "dark" + + def __init__(self, x, y, hue, data, order, hue_order, + estimator, errorbar, n_boot, units, seed, + markers, linestyles, dodge, join, scale, + orient, color, palette, errwidth, capsize, label): + """Initialize the plotter.""" + self.establish_variables(x, y, hue, data, orient, + order, hue_order, units) + self.establish_colors(color, palette, 1) + self.estimate_statistic(estimator, errorbar, n_boot, seed) + + # Override the default palette for single-color plots + if hue is None and color is None and palette is None: + self.colors = [color_palette()[0]] * len(self.colors) + + # Don't join single-layer plots with different colors + if hue is None and palette is not None: + join = False + + # Use a good default for `dodge=True` + if dodge is True and self.hue_names is not None: + dodge = .025 * len(self.hue_names) + + # Make sure we have a marker for each hue level + if isinstance(markers, str): + markers = [markers] * len(self.colors) + self.markers = markers + + # Make sure we have a line style for each hue level + if isinstance(linestyles, str): + linestyles = [linestyles] * len(self.colors) + self.linestyles = linestyles + + # Set the other plot components + self.dodge = dodge + self.join = join + self.scale = scale + self.errwidth = errwidth + self.capsize = capsize + self.label = label + + @property + def hue_offsets(self): + """Offsets relative to the center position for each hue level.""" + if self.dodge: + offset = np.linspace(0, self.dodge, len(self.hue_names)) + offset -= offset.mean() + else: + offset = np.zeros(len(self.hue_names)) + return offset + + def draw_points(self, ax): + """Draw the main data components of the plot.""" + # Get the center positions on the categorical axis + pointpos = np.arange(len(self.statistic)) + + # Get the size of the plot elements + lw = mpl.rcParams["lines.linewidth"] * 1.8 * self.scale + mew = lw * .75 + markersize = np.pi * np.square(lw) * 2 + + if self.plot_hues is None: + + # Draw lines joining each estimate point + if self.join: + color = self.colors[0] + ls = self.linestyles[0] + if self.orient == "h": + ax.plot(self.statistic, pointpos, + color=color, ls=ls, lw=lw) + else: + ax.plot(pointpos, self.statistic, + color=color, ls=ls, lw=lw) + + # Draw the confidence intervals + self.draw_confints(ax, pointpos, self.confint, self.colors, + self.errwidth, self.capsize) + + # Draw the estimate points + marker = self.markers[0] + colors = [mpl.colors.colorConverter.to_rgb(c) for c in self.colors] + if self.orient == "h": + x, y = self.statistic, pointpos + else: + x, y = pointpos, self.statistic + ax.scatter(x, y, + linewidth=mew, marker=marker, s=markersize, + facecolor=colors, edgecolor=colors, label=self.label) + + else: + + offsets = self.hue_offsets + for j, hue_level in enumerate(self.hue_names): + + # Determine the values to plot for this level + statistic = self.statistic[:, j] + + # Determine the position on the categorical and z axes + offpos = pointpos + offsets[j] + z = j + 1 + + # Draw lines joining each estimate point + if self.join: + color = self.colors[j] + ls = self.linestyles[j] + if self.orient == "h": + ax.plot(statistic, offpos, color=color, + zorder=z, ls=ls, lw=lw) + else: + ax.plot(offpos, statistic, color=color, + zorder=z, ls=ls, lw=lw) + + # Draw the confidence intervals + if self.confint.size: + confint = self.confint[:, j] + errcolors = [self.colors[j]] * len(offpos) + self.draw_confints(ax, offpos, confint, errcolors, + self.errwidth, self.capsize, + zorder=z) + + # Draw the estimate points + n_points = len(remove_na(offpos)) + marker = self.markers[j] + color = mpl.colors.colorConverter.to_rgb(self.colors[j]) + + if self.orient == "h": + x, y = statistic, offpos + else: + x, y = offpos, statistic + + if not len(remove_na(statistic)): + x = y = [np.nan] * n_points + + ax.scatter(x, y, label=hue_level, + facecolor=color, edgecolor=color, + linewidth=mew, marker=marker, s=markersize, + zorder=z) + + def plot(self, ax): + """Make the plot.""" + self.draw_points(ax) + self.annotate_axes(ax) + if self.orient == "h": + ax.invert_yaxis() + + +class _CountPlotter(_BarPlotter): + require_numeric = False + + +class _LVPlotter(_CategoricalPlotter): + + def __init__(self, x, y, hue, data, order, hue_order, + orient, color, palette, saturation, + width, dodge, k_depth, linewidth, scale, outlier_prop, + trust_alpha, showfliers=True): + + self.width = width + self.dodge = dodge + self.saturation = saturation + + k_depth_methods = ['proportion', 'tukey', 'trustworthy', 'full'] + if not (k_depth in k_depth_methods or isinstance(k_depth, Number)): + msg = (f'k_depth must be one of {k_depth_methods} or a number, ' + f'but {k_depth} was passed.') + raise ValueError(msg) + self.k_depth = k_depth + + if linewidth is None: + linewidth = mpl.rcParams["lines.linewidth"] + self.linewidth = linewidth + + scales = ['linear', 'exponential', 'area'] + if scale not in scales: + msg = f'scale must be one of {scales}, but {scale} was passed.' + raise ValueError(msg) + self.scale = scale + + if ((outlier_prop > 1) or (outlier_prop <= 0)): + msg = f'outlier_prop {outlier_prop} not in range (0, 1]' + raise ValueError(msg) + self.outlier_prop = outlier_prop + + if not 0 < trust_alpha < 1: + msg = f'trust_alpha {trust_alpha} not in range (0, 1)' + raise ValueError(msg) + self.trust_alpha = trust_alpha + + self.showfliers = showfliers + + self.establish_variables(x, y, hue, data, orient, order, hue_order) + self.establish_colors(color, palette, saturation) + + def _lv_box_ends(self, vals): + """Get the number of data points and calculate `depth` of + letter-value plot.""" + vals = np.asarray(vals) + # Remove infinite values while handling a 'object' dtype + # that can come from pd.Float64Dtype() input + with pd.option_context('mode.use_inf_as_na', True): + vals = vals[~pd.isnull(vals)] + n = len(vals) + p = self.outlier_prop + + # Select the depth, i.e. number of boxes to draw, based on the method + if self.k_depth == 'full': + # extend boxes to 100% of the data + k = int(np.log2(n)) + 1 + elif self.k_depth == 'tukey': + # This results with 5-8 points in each tail + k = int(np.log2(n)) - 3 + elif self.k_depth == 'proportion': + k = int(np.log2(n)) - int(np.log2(n * p)) + 1 + elif self.k_depth == 'trustworthy': + point_conf = 2 * _normal_quantile_func(1 - self.trust_alpha / 2) ** 2 + k = int(np.log2(n / point_conf)) + 1 + else: + k = int(self.k_depth) # allow having k as input + # If the number happens to be less than 1, set k to 1 + if k < 1: + k = 1 + + # Calculate the upper end for each of the k boxes + upper = [100 * (1 - 0.5 ** (i + 1)) for i in range(k, 0, -1)] + # Calculate the lower end for each of the k boxes + lower = [100 * (0.5 ** (i + 1)) for i in range(k, 0, -1)] + # Stitch the box ends together + percentile_ends = [(i, j) for i, j in zip(lower, upper)] + box_ends = [np.percentile(vals, q) for q in percentile_ends] + return box_ends, k + + def _lv_outliers(self, vals, k): + """Find the outliers based on the letter value depth.""" + box_edge = 0.5 ** (k + 1) + perc_ends = (100 * box_edge, 100 * (1 - box_edge)) + edges = np.percentile(vals, perc_ends) + lower_out = vals[np.where(vals < edges[0])[0]] + upper_out = vals[np.where(vals > edges[1])[0]] + return np.concatenate((lower_out, upper_out)) + + def _width_functions(self, width_func): + # Dictionary of functions for computing the width of the boxes + width_functions = {'linear': lambda h, i, k: (i + 1.) / k, + 'exponential': lambda h, i, k: 2**(-k + i - 1), + 'area': lambda h, i, k: (1 - 2**(-k + i - 2)) / h} + return width_functions[width_func] + + def _lvplot(self, box_data, positions, + color=[255. / 256., 185. / 256., 0.], + widths=1, ax=None, box_kws=None, + flier_kws=None, + line_kws=None): + + # -- Default keyword dicts - based on + # distributions.plot_univariate_histogram + box_kws = {} if box_kws is None else box_kws.copy() + flier_kws = {} if flier_kws is None else flier_kws.copy() + line_kws = {} if line_kws is None else line_kws.copy() + + # Set the default kwargs for the boxes + box_default_kws = dict(edgecolor=self.gray, + linewidth=self.linewidth) + for k, v in box_default_kws.items(): + box_kws.setdefault(k, v) + + # Set the default kwargs for the lines denoting medians + line_default_kws = dict( + color=".15", alpha=0.45, solid_capstyle="butt", linewidth=self.linewidth + ) + for k, v in line_default_kws.items(): + line_kws.setdefault(k, v) + + # Set the default kwargs for the outliers scatterplot + flier_default_kws = dict(marker='d', color=self.gray) + for k, v in flier_default_kws.items(): + flier_kws.setdefault(k, v) + + vert = self.orient == "v" + x = positions[0] + box_data = np.asarray(box_data) + + # If we only have one data point, plot a line + if len(box_data) == 1: + line_kws.update({ + 'color': box_kws['edgecolor'], + 'linestyle': box_kws.get('linestyle', '-'), + 'linewidth': max(box_kws["linewidth"], line_kws["linewidth"]) + }) + ys = [box_data[0], box_data[0]] + xs = [x - widths / 2, x + widths / 2] + if vert: + xx, yy = xs, ys + else: + xx, yy = ys, xs + ax.plot(xx, yy, **line_kws) + else: + # Get the number of data points and calculate "depth" of + # letter-value plot + box_ends, k = self._lv_box_ends(box_data) + + # Anonymous functions for calculating the width and height + # of the letter value boxes + width = self._width_functions(self.scale) + + # Function to find height of boxes + def height(b): + return b[1] - b[0] + + # Functions to construct the letter value boxes + def vert_perc_box(x, b, i, k, w): + rect = Patches.Rectangle((x - widths * w / 2, b[0]), + widths * w, + height(b), fill=True) + return rect + + def horz_perc_box(x, b, i, k, w): + rect = Patches.Rectangle((b[0], x - widths * w / 2), + height(b), widths * w, + fill=True) + return rect + + # Scale the width of the boxes so the biggest starts at 1 + w_area = np.array([width(height(b), i, k) + for i, b in enumerate(box_ends)]) + w_area = w_area / np.max(w_area) + + # Calculate the medians + y = np.median(box_data) + + # Calculate the outliers and plot (only if showfliers == True) + outliers = [] + if self.showfliers: + outliers = self._lv_outliers(box_data, k) + hex_color = mpl.colors.rgb2hex(color) + + if vert: + box_func = vert_perc_box + xs_median = [x - widths / 2, x + widths / 2] + ys_median = [y, y] + xs_outliers = np.full(len(outliers), x) + ys_outliers = outliers + + else: + box_func = horz_perc_box + xs_median = [y, y] + ys_median = [x - widths / 2, x + widths / 2] + xs_outliers = outliers + ys_outliers = np.full(len(outliers), x) + + # Plot the medians + ax.plot( + xs_median, + ys_median, + **line_kws + ) + + # Plot outliers (if any) + if len(outliers) > 0: + ax.scatter(xs_outliers, ys_outliers, + **flier_kws + ) + + # Construct a color map from the input color + rgb = [hex_color, (1, 1, 1)] + cmap = mpl.colors.LinearSegmentedColormap.from_list('new_map', rgb) + # Make sure that the last boxes contain hue and are not pure white + rgb = [hex_color, cmap(.85)] + cmap = mpl.colors.LinearSegmentedColormap.from_list('new_map', rgb) + + # Update box_kws with `cmap` if not defined in dict until now + box_kws.setdefault('cmap', cmap) + + boxes = [box_func(x, b[0], i, k, b[1]) + for i, b in enumerate(zip(box_ends, w_area))] + + collection = PatchCollection(boxes, **box_kws) + + # Set the color gradation, first box will have color=hex_color + collection.set_array(np.array(np.linspace(1, 0, len(boxes)))) + + # Plot the boxes + ax.add_collection(collection) + + def draw_letter_value_plot(self, ax, box_kws=None, flier_kws=None, + line_kws=None): + """Use matplotlib to draw a letter value plot on an Axes.""" + + for i, group_data in enumerate(self.plot_data): + + if self.plot_hues is None: + + # Handle case where there is data at this level + if group_data.size == 0: + continue + + # Draw a single box or a set of boxes + # with a single level of grouping + box_data = remove_na(group_data) + + # Handle case where there is no non-null data + if box_data.size == 0: + continue + + color = self.colors[i] + + self._lvplot(box_data, + positions=[i], + color=color, + widths=self.width, + ax=ax, + box_kws=box_kws, + flier_kws=flier_kws, + line_kws=line_kws) + + else: + # Draw nested groups of boxes + offsets = self.hue_offsets + for j, hue_level in enumerate(self.hue_names): + + # Add a legend for this hue level + if not i: + self.add_legend_data(ax, self.colors[j], hue_level) + + # Handle case where there is data at this level + if group_data.size == 0: + continue + + hue_mask = self.plot_hues[i] == hue_level + box_data = remove_na(group_data[hue_mask]) + + # Handle case where there is no non-null data + if box_data.size == 0: + continue + + color = self.colors[j] + center = i + offsets[j] + self._lvplot(box_data, + positions=[center], + color=color, + widths=self.nested_width, + ax=ax, + box_kws=box_kws, + flier_kws=flier_kws, + line_kws=line_kws) + + # Autoscale the values axis to make sure all patches are visible + ax.autoscale_view(scalex=self.orient == "h", scaley=self.orient == "v") + + def plot(self, ax, box_kws, flier_kws, line_kws): + """Make the plot.""" + self.draw_letter_value_plot(ax, box_kws, flier_kws, line_kws) + self.annotate_axes(ax) + if self.orient == "h": + ax.invert_yaxis() + + +_categorical_docs = dict( + + # Shared narrative docs + categorical_narrative=dedent("""\ + .. note:: + This function always treats one of the variables as categorical and + draws data at ordinal positions (0, 1, ... n) on the relevant axis, + even when the data has a numeric or date type. + + See the :ref:`tutorial ` for more information.\ + """), + + new_categorical_narrative=dedent("""\ + .. note:: + By default, this function treats one of the variables as categorical + and draws data at ordinal positions (0, 1, ... n) on the relevant axis. + This can be disabled with the `native_scale` parameter. + + See the :ref:`tutorial ` for more information.\ + """), + + # Shared function parameters + input_params=dedent("""\ + x, y, hue : names of variables in ``data`` or vector data, optional + Inputs for plotting long-form data. See examples for interpretation.\ + """), + string_input_params=dedent("""\ + x, y, hue : names of variables in ``data`` + Inputs for plotting long-form data. See examples for interpretation.\ + """), + categorical_data=dedent("""\ + data : DataFrame, array, or list of arrays, optional + Dataset for plotting. If ``x`` and ``y`` are absent, this is + interpreted as wide-form. Otherwise it is expected to be long-form.\ + """), + long_form_data=dedent("""\ + data : DataFrame + Long-form (tidy) dataset for plotting. Each column should correspond + to a variable, and each row should correspond to an observation.\ + """), + order_vars=dedent("""\ + order, hue_order : lists of strings, optional + Order to plot the categorical levels in; otherwise the levels are + inferred from the data objects.\ + """), + stat_api_params=dedent("""\ + estimator : string or callable that maps vector -> scalar, optional + Statistical function to estimate within each categorical bin. + errorbar : string, (string, number) tuple, callable or None + Name of errorbar method (either "ci", "pi", "se", or "sd"), or a tuple + with a method name and a level parameter, or a function that maps from a + vector to a (min, max) interval, or None to hide errorbar. + n_boot : int, optional + Number of bootstrap samples used to compute confidence intervals. + units : name of variable in ``data`` or vector data, optional + Identifier of sampling units, which will be used to perform a + multilevel bootstrap and account for repeated measures design. + seed : int, numpy.random.Generator, or numpy.random.RandomState, optional + Seed or random number generator for reproducible bootstrapping.\ + """), + orient=dedent("""\ + orient : "v" | "h", optional + Orientation of the plot (vertical or horizontal). This is usually + inferred based on the type of the input variables, but it can be used + to resolve ambiguity when both `x` and `y` are numeric or when + plotting wide-form data.\ + """), + color=dedent("""\ + color : matplotlib color, optional + Single color for the elements in the plot.\ + """), + palette=dedent("""\ + palette : palette name, list, or dict, optional + Color palette that maps the hue variable. If the palette is a dictionary, + keys should be names of levels and values should be matplotlib colors.\ + """), + hue_norm=dedent("""\ + hue_norm : tuple or :class:`matplotlib.colors.Normalize` object + Normalization in data units for colormap applied to the `hue` + variable when it is numeric. Not relevant if `hue` is categorical.\ + """), + saturation=dedent("""\ + saturation : float, optional + Proportion of the original saturation to draw colors at. Large patches + often look better with slightly desaturated colors, but set this to + `1` if you want the plot colors to perfectly match the input color.\ + """), + capsize=dedent("""\ + capsize : float, optional + Width of the "caps" on error bars.\ + """), + errwidth=dedent("""\ + errwidth : float, optional + Thickness of error bar lines (and caps).\ + """), + width=dedent("""\ + width : float, optional + Width of a full element when not using hue nesting, or width of all the + elements for one level of the major grouping variable.\ + """), + dodge=dedent("""\ + dodge : bool, optional + When hue nesting is used, whether elements should be shifted along the + categorical axis.\ + """), + linewidth=dedent("""\ + linewidth : float, optional + Width of the gray lines that frame the plot elements.\ + """), + native_scale=dedent("""\ + native_scale : bool, optional + When True, numeric or datetime values on the categorical axis will maintain + their original scaling rather than being converted to fixed indices.\ + """), + formatter=dedent("""\ + formatter : callable, optional + Function for converting categorical data into strings. Affects both grouping + and tick labels.\ + """), + legend=dedent("""\ +legend : "auto", "brief", "full", or False + How to draw the legend. If "brief", numeric `hue` and `size` + variables will be represented with a sample of evenly spaced values. + If "full", every group will get an entry in the legend. If "auto", + choose between brief or full representation based on number of levels. + If `False`, no legend data is added and no legend is drawn. + """), + ax_in=dedent("""\ + ax : matplotlib Axes, optional + Axes object to draw the plot onto, otherwise uses the current Axes.\ + """), + ax_out=dedent("""\ + ax : matplotlib Axes + Returns the Axes object with the plot drawn onto it.\ + """), + + # Shared see also + boxplot=dedent("""\ + boxplot : A traditional box-and-whisker plot with a similar API.\ + """), + violinplot=dedent("""\ + violinplot : A combination of boxplot and kernel density estimation.\ + """), + stripplot=dedent("""\ + stripplot : A scatterplot where one variable is categorical. Can be used + in conjunction with other plots to show each observation.\ + """), + swarmplot=dedent("""\ + swarmplot : A categorical scatterplot where the points do not overlap. Can + be used with other plots to show each observation.\ + """), + barplot=dedent("""\ + barplot : Show point estimates and confidence intervals using bars.\ + """), + countplot=dedent("""\ + countplot : Show the counts of observations in each categorical bin.\ + """), + pointplot=dedent("""\ + pointplot : Show point estimates and confidence intervals using scatterplot + glyphs.\ + """), + catplot=dedent("""\ + catplot : Combine a categorical plot with a :class:`FacetGrid`.\ + """), + boxenplot=dedent("""\ + boxenplot : An enhanced boxplot for larger datasets.\ + """), + +) + +_categorical_docs.update(_facet_docs) + + +def boxplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + orient=None, color=None, palette=None, saturation=.75, width=.8, + dodge=True, fliersize=5, linewidth=None, whis=1.5, ax=None, + **kwargs +): + + plotter = _BoxPlotter(x, y, hue, data, order, hue_order, + orient, color, palette, saturation, + width, dodge, fliersize, linewidth) + + if ax is None: + ax = plt.gca() + kwargs.update(dict(whis=whis)) + + plotter.plot(ax, kwargs) + return ax + + +boxplot.__doc__ = dedent("""\ + Draw a box plot to show distributions with respect to categories. + + A box plot (or box-and-whisker plot) shows the distribution of quantitative + data in a way that facilitates comparisons between variables or across + levels of a categorical variable. The box shows the quartiles of the + dataset while the whiskers extend to show the rest of the distribution, + except for points that are determined to be "outliers" using a method + that is a function of the inter-quartile range. + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + {orient} + {color} + {palette} + {saturation} + {width} + {dodge} + fliersize : float, optional + Size of the markers used to indicate outlier observations. + {linewidth} + whis : float, optional + Maximum length of the plot whiskers as proportion of the + interquartile range. Whiskers extend to the furthest datapoint + within that range. More extreme points are marked as outliers. + {ax_in} + kwargs : key, value mappings + Other keyword arguments are passed through to + :meth:`matplotlib.axes.Axes.boxplot`. + + Returns + ------- + {ax_out} + + See Also + -------- + {violinplot} + {stripplot} + {swarmplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/boxplot.rst + + """).format(**_categorical_docs) + + +def violinplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + bw="scott", cut=2, scale="area", scale_hue=True, gridsize=100, + width=.8, inner="box", split=False, dodge=True, orient=None, + linewidth=None, color=None, palette=None, saturation=.75, + ax=None, **kwargs, +): + + plotter = _ViolinPlotter(x, y, hue, data, order, hue_order, + bw, cut, scale, scale_hue, gridsize, + width, inner, split, dodge, orient, linewidth, + color, palette, saturation) + + if ax is None: + ax = plt.gca() + + plotter.plot(ax) + return ax + + +violinplot.__doc__ = dedent("""\ + Draw a combination of boxplot and kernel density estimate. + + A violin plot plays a similar role as a box and whisker plot. It shows the + distribution of quantitative data across several levels of one (or more) + categorical variables such that those distributions can be compared. Unlike + a box plot, in which all of the plot components correspond to actual + datapoints, the violin plot features a kernel density estimation of the + underlying distribution. + + This can be an effective and attractive way to show multiple distributions + of data at once, but keep in mind that the estimation procedure is + influenced by the sample size, and violins for relatively small samples + might look misleadingly smooth. + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + bw : {{'scott', 'silverman', float}}, optional + Either the name of a reference rule or the scale factor to use when + computing the kernel bandwidth. The actual kernel size will be + determined by multiplying the scale factor by the standard deviation of + the data within each bin. + cut : float, optional + Distance, in units of bandwidth size, to extend the density past the + extreme datapoints. Set to 0 to limit the violin range within the range + of the observed data (i.e., to have the same effect as ``trim=True`` in + ``ggplot``. + scale : {{"area", "count", "width"}}, optional + The method used to scale the width of each violin. If ``area``, each + violin will have the same area. If ``count``, the width of the violins + will be scaled by the number of observations in that bin. If ``width``, + each violin will have the same width. + scale_hue : bool, optional + When nesting violins using a ``hue`` variable, this parameter + determines whether the scaling is computed within each level of the + major grouping variable (``scale_hue=True``) or across all the violins + on the plot (``scale_hue=False``). + gridsize : int, optional + Number of points in the discrete grid used to compute the kernel + density estimate. + {width} + inner : {{"box", "quartile", "point", "stick", None}}, optional + Representation of the datapoints in the violin interior. If ``box``, + draw a miniature boxplot. If ``quartiles``, draw the quartiles of the + distribution. If ``point`` or ``stick``, show each underlying + datapoint. Using ``None`` will draw unadorned violins. + split : bool, optional + When using hue nesting with a variable that takes two levels, setting + ``split`` to True will draw half of a violin for each level. This can + make it easier to directly compare the distributions. + {dodge} + {orient} + {linewidth} + {color} + {palette} + {saturation} + {ax_in} + + Returns + ------- + {ax_out} + + See Also + -------- + {boxplot} + {stripplot} + {swarmplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/violinplot.rst + + """).format(**_categorical_docs) + + +def boxenplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + orient=None, color=None, palette=None, saturation=.75, + width=.8, dodge=True, k_depth='tukey', linewidth=None, + scale='exponential', outlier_prop=0.007, trust_alpha=0.05, + showfliers=True, + ax=None, box_kws=None, flier_kws=None, line_kws=None, +): + plotter = _LVPlotter(x, y, hue, data, order, hue_order, + orient, color, palette, saturation, + width, dodge, k_depth, linewidth, scale, + outlier_prop, trust_alpha, showfliers) + + if ax is None: + ax = plt.gca() + + plotter.plot(ax, box_kws, flier_kws, line_kws) + return ax + + +boxenplot.__doc__ = dedent("""\ + Draw an enhanced box plot for larger datasets. + + This style of plot was originally named a "letter value" plot because it + shows a large number of quantiles that are defined as "letter values". It + is similar to a box plot in plotting a nonparametric representation of a + distribution in which all features correspond to actual observations. By + plotting more quantiles, it provides more information about the shape of + the distribution, particularly in the tails. For a more extensive + explanation, you can read the paper that introduced the plot: + https://vita.had.co.nz/papers/letter-value-plot.html + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + {orient} + {color} + {palette} + {saturation} + {width} + {dodge} + k_depth : {{"tukey", "proportion", "trustworthy", "full"}} or scalar + The number of boxes, and by extension number of percentiles, to draw. + All methods are detailed in Wickham's paper. Each makes different + assumptions about the number of outliers and leverages different + statistical properties. If "proportion", draw no more than + `outlier_prop` extreme observations. If "full", draw `log(n)+1` boxes. + {linewidth} + scale : {{"exponential", "linear", "area"}}, optional + Method to use for the width of the letter value boxes. All give similar + results visually. "linear" reduces the width by a constant linear + factor, "exponential" uses the proportion of data not covered, "area" + is proportional to the percentage of data covered. + outlier_prop : float, optional + Proportion of data believed to be outliers. Must be in the range + (0, 1]. Used to determine the number of boxes to plot when + `k_depth="proportion"`. + trust_alpha : float, optional + Confidence level for a box to be plotted. Used to determine the + number of boxes to plot when `k_depth="trustworthy"`. Must be in the + range (0, 1). + showfliers : bool, optional + If False, suppress the plotting of outliers. + {ax_in} + box_kws: dict, optional + Keyword arguments for the box artists; passed to + :class:`matplotlib.patches.Rectangle`. + line_kws: dict, optional + Keyword arguments for the line denoting the median; passed to + :meth:`matplotlib.axes.Axes.plot`. + flier_kws: dict, optional + Keyword arguments for the scatter denoting the outlier observations; + passed to :meth:`matplotlib.axes.Axes.scatter`. + + Returns + ------- + {ax_out} + + See Also + -------- + {violinplot} + {boxplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/boxenplot.rst + + """).format(**_categorical_docs) + + +def stripplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + jitter=True, dodge=False, orient=None, color=None, palette=None, + size=5, edgecolor="gray", linewidth=0, + hue_norm=None, native_scale=False, formatter=None, legend="auto", + ax=None, **kwargs +): + + p = _CategoricalPlotterNew( + data=data, + variables=_CategoricalPlotterNew.get_semantics(locals()), + order=order, + orient=orient, + require_numeric=False, + legend=legend, + ) + + if ax is None: + ax = plt.gca() + + if p.var_types.get(p.cat_axis) == "categorical" or not native_scale: + p.scale_categorical(p.cat_axis, order=order, formatter=formatter) + + p._attach(ax) + + hue_order = p._palette_without_hue_backcompat(palette, hue_order) + palette, hue_order = p._hue_backcompat(color, palette, hue_order) + + color = _default_color(ax.scatter, hue, color, kwargs) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + # XXX Copying possibly bad default decisions from original code for now + kwargs.setdefault("zorder", 3) + size = kwargs.get("s", size) + + kwargs.update(dict( + s=size ** 2, + edgecolor=edgecolor, + linewidth=linewidth) + ) + + p.plot_strips( + jitter=jitter, + dodge=dodge, + color=color, + edgecolor=edgecolor, + plot_kws=kwargs, + ) + + # XXX this happens inside a plotting method in the distribution plots + # but maybe it's better out here? Alternatively, we have an open issue + # suggesting that _attach could add default axes labels, which seems smart. + p._add_axis_labels(ax) + p._adjust_cat_axis(ax, axis=p.cat_axis) + + return ax + + +stripplot.__doc__ = dedent("""\ + Draw a categorical scatterplot using jitter to reduce overplotting. + + A strip plot can be drawn on its own, but it is also a good complement + to a box or violin plot in cases where you want to show all observations + along with some representation of the underlying distribution. + + {new_categorical_narrative} + + Parameters + ---------- + {input_params} + {categorical_data} + {order_vars} + jitter : float, ``True``/``1`` is special-cased, optional + Amount of jitter (only along the categorical axis) to apply. This + can be useful when you have many points and they overlap, so that + it is easier to see the distribution. You can specify the amount + of jitter (half the width of the uniform random variable support), + or just use ``True`` for a good default. + dodge : bool, optional + When using ``hue`` nesting, setting this to ``True`` will separate + the strips for different hue levels along the categorical axis. + Otherwise, the points for each level will be plotted on top of + each other. + {orient} + {color} + {palette} + size : float, optional + Radius of the markers, in points. + edgecolor : matplotlib color, "gray" is special-cased, optional + Color of the lines around each point. If you pass ``"gray"``, the + brightness is determined by the color palette used for the body + of the points. Note that `stripplot` has `linewidth=0` by default, + so edge colors are only visible with nonzero line width. + {linewidth} + {native_scale} + {formatter} + {legend} + {ax_in} + kwargs : key, value mappings + Other keyword arguments are passed through to + :meth:`matplotlib.axes.Axes.scatter`. + + Returns + ------- + {ax_out} + + See Also + -------- + {swarmplot} + {boxplot} + {violinplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/stripplot.rst + + """).format(**_categorical_docs) + + +def swarmplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + dodge=False, orient=None, color=None, palette=None, + size=5, edgecolor="gray", linewidth=0, hue_norm=None, + native_scale=False, formatter=None, legend="auto", warn_thresh=.05, + ax=None, **kwargs +): + + p = _CategoricalPlotterNew( + data=data, + variables=_CategoricalPlotterNew.get_semantics(locals()), + order=order, + orient=orient, + require_numeric=False, + legend=legend, + ) + + if ax is None: + ax = plt.gca() + + if p.var_types.get(p.cat_axis) == "categorical" or not native_scale: + p.scale_categorical(p.cat_axis, order=order, formatter=formatter) + + p._attach(ax) + + if not p.has_xy_data: + return ax + + hue_order = p._palette_without_hue_backcompat(palette, hue_order) + palette, hue_order = p._hue_backcompat(color, palette, hue_order) + + color = _default_color(ax.scatter, hue, color, kwargs) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + # XXX Copying possibly bad default decisions from original code for now + kwargs.setdefault("zorder", 3) + size = kwargs.get("s", size) + + if linewidth is None: + linewidth = size / 10 + + kwargs.update(dict( + s=size ** 2, + linewidth=linewidth, + )) + + p.plot_swarms( + dodge=dodge, + color=color, + edgecolor=edgecolor, + warn_thresh=warn_thresh, + plot_kws=kwargs, + ) + + p._add_axis_labels(ax) + p._adjust_cat_axis(ax, axis=p.cat_axis) + + return ax + + +swarmplot.__doc__ = dedent("""\ + Draw a categorical scatterplot with points adjusted to be non-overlapping. + + This function is similar to :func:`stripplot`, but the points are adjusted + (only along the categorical axis) so that they don't overlap. This gives a + better representation of the distribution of values, but it does not scale + well to large numbers of observations. This style of plot is sometimes + called a "beeswarm". + + A swarm plot can be drawn on its own, but it is also a good complement + to a box or violin plot in cases where you want to show all observations + along with some representation of the underlying distribution. + + {new_categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + dodge : bool, optional + When using ``hue`` nesting, setting this to ``True`` will separate + the strips for different hue levels along the categorical axis. + Otherwise, the points for each level will be plotted in one swarm. + {orient} + {color} + {palette} + size : float, optional + Radius of the markers, in points. + edgecolor : matplotlib color, "gray" is special-cased, optional + Color of the lines around each point. If you pass ``"gray"``, the + brightness is determined by the color palette used for the body + of the points. + {linewidth} + {native_scale} + {formatter} + {legend} + {ax_in} + kwargs : key, value mappings + Other keyword arguments are passed through to + :meth:`matplotlib.axes.Axes.scatter`. + + Returns + ------- + {ax_out} + + See Also + -------- + {boxplot} + {violinplot} + {stripplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/swarmplot.rst + + """).format(**_categorical_docs) + + +def barplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + estimator="mean", errorbar=("ci", 95), n_boot=1000, units=None, seed=None, + orient=None, color=None, palette=None, saturation=.75, width=.8, + errcolor=".26", errwidth=None, capsize=None, dodge=True, ci="deprecated", + ax=None, + **kwargs, +): + + errorbar = utils._deprecate_ci(errorbar, ci) + + # Be backwards compatible with len passed directly, which + # does not work in Series.agg (maybe a pandas bug?) + if estimator is len: + estimator = "size" + + plotter = _BarPlotter(x, y, hue, data, order, hue_order, + estimator, errorbar, n_boot, units, seed, + orient, color, palette, saturation, + width, errcolor, errwidth, capsize, dodge) + + if ax is None: + ax = plt.gca() + + plotter.plot(ax, kwargs) + return ax + + +barplot.__doc__ = dedent("""\ + Show point estimates and errors as rectangular bars. + + A bar plot represents an estimate of central tendency for a numeric + variable with the height of each rectangle and provides some indication of + the uncertainty around that estimate using error bars. Bar plots include 0 + in the quantitative axis range, and they are a good choice when 0 is a + meaningful value for the quantitative variable, and you want to make + comparisons against it. + + For datasets where 0 is not a meaningful value, a point plot will allow you + to focus on differences between levels of one or more categorical + variables. + + It is also important to keep in mind that a bar plot shows only the mean + (or other estimator) value, but in many cases it may be more informative to + show the distribution of values at each level of the categorical variables. + In that case, other approaches such as a box or violin plot may be more + appropriate. + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + {stat_api_params} + {orient} + {color} + {palette} + {saturation} + {width} + errcolor : matplotlib color + Color used for the error bar lines. + {errwidth} + {capsize} + {dodge} + {ax_in} + kwargs : key, value mappings + Other keyword arguments are passed through to + :meth:`matplotlib.axes.Axes.bar`. + + Returns + ------- + {ax_out} + + See Also + -------- + {countplot} + {pointplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/barplot.rst + + + """).format(**_categorical_docs) + + +def pointplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + estimator="mean", errorbar=("ci", 95), n_boot=1000, units=None, seed=None, + markers="o", linestyles="-", dodge=False, join=True, scale=1, + orient=None, color=None, palette=None, errwidth=None, ci="deprecated", + capsize=None, label=None, ax=None, +): + + errorbar = utils._deprecate_ci(errorbar, ci) + + plotter = _PointPlotter(x, y, hue, data, order, hue_order, + estimator, errorbar, n_boot, units, seed, + markers, linestyles, dodge, join, scale, + orient, color, palette, errwidth, capsize, label) + + if ax is None: + ax = plt.gca() + + plotter.plot(ax) + return ax + + +pointplot.__doc__ = dedent("""\ + Show point estimates and errors using dot marks. + + A point plot represents an estimate of central tendency for a numeric + variable by the position of the dot and provides some indication of the + uncertainty around that estimate using error bars. + + Point plots can be more useful than bar plots for focusing comparisons + between different levels of one or more categorical variables. They are + particularly adept at showing interactions: how the relationship between + levels of one categorical variable changes across levels of a second + categorical variable. The lines that join each point from the same `hue` + level allow interactions to be judged by differences in slope, which is + easier for the eyes than comparing the heights of several groups of points + or bars. + + It is important to keep in mind that a point plot shows only the mean (or + other estimator) value, but in many cases it may be more informative to + show the distribution of values at each level of the categorical variables. + In that case, other approaches such as a box or violin plot may be more + appropriate. + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + {stat_api_params} + markers : string or list of strings, optional + Markers to use for each of the ``hue`` levels. + linestyles : string or list of strings, optional + Line styles to use for each of the ``hue`` levels. + dodge : bool or float, optional + Amount to separate the points for each level of the ``hue`` variable + along the categorical axis. + join : bool, optional + If ``True``, lines will be drawn between point estimates at the same + ``hue`` level. + scale : float, optional + Scale factor for the plot elements. + {orient} + {color} + {palette} + {errwidth} + {capsize} + label : string, optional + Label to represent the plot in a legend, only relevant when not using `hue`. + {ax_in} + + Returns + ------- + {ax_out} + + See Also + -------- + {barplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/pointplot.rst + + """).format(**_categorical_docs) + + +def countplot( + data=None, *, x=None, y=None, hue=None, order=None, hue_order=None, + orient=None, color=None, palette=None, saturation=.75, width=.8, + dodge=True, ax=None, **kwargs +): + + estimator = "size" + errorbar = None + n_boot = 0 + units = None + seed = None + errcolor = None + errwidth = None + capsize = None + + if x is None and y is not None: + orient = "h" + x = y + elif y is None and x is not None: + orient = "v" + y = x + elif x is not None and y is not None: + raise ValueError("Cannot pass values for both `x` and `y`") + + plotter = _CountPlotter( + x, y, hue, data, order, hue_order, + estimator, errorbar, n_boot, units, seed, + orient, color, palette, saturation, + width, errcolor, errwidth, capsize, dodge + ) + + plotter.value_label = "count" + + if ax is None: + ax = plt.gca() + + plotter.plot(ax, kwargs) + return ax + + +countplot.__doc__ = dedent("""\ + Show the counts of observations in each categorical bin using bars. + + A count plot can be thought of as a histogram across a categorical, instead + of quantitative, variable. The basic API and options are identical to those + for :func:`barplot`, so you can compare counts across nested variables. + + Note that the newer :func:`histplot` function offers more functionality, although + its default behavior is somewhat different. + + {categorical_narrative} + + Parameters + ---------- + {categorical_data} + {input_params} + {order_vars} + {orient} + {color} + {palette} + {saturation} + {dodge} + {ax_in} + kwargs : key, value mappings + Other keyword arguments are passed through to + :meth:`matplotlib.axes.Axes.bar`. + + Returns + ------- + {ax_out} + + See Also + -------- + {barplot} + {catplot} + + Examples + -------- + + .. include:: ../docstrings/countplot.rst + + """).format(**_categorical_docs) + + +def catplot( + data=None, *, x=None, y=None, hue=None, row=None, col=None, + col_wrap=None, estimator="mean", errorbar=("ci", 95), n_boot=1000, + units=None, seed=None, order=None, hue_order=None, row_order=None, + col_order=None, height=5, aspect=1, kind="strip", native_scale=False, + formatter=None, orient=None, color=None, palette=None, hue_norm=None, + legend="auto", legend_out=True, sharex=True, sharey=True, + margin_titles=False, facet_kws=None, ci="deprecated", + **kwargs +): + + # Determine the plotting function + try: + plot_func = globals()[kind + "plot"] + except KeyError: + err = f"Plot kind '{kind}' is not recognized" + raise ValueError(err) + + # Check for attempt to plot onto specific axes and warn + if "ax" in kwargs: + msg = ("catplot is a figure-level function and does not accept " + f"target axes. You may wish to try {kind}plot") + warnings.warn(msg, UserWarning) + kwargs.pop("ax") + + refactored_kinds = ["strip", "swarm"] + if kind in refactored_kinds: + + p = _CategoricalFacetPlotter( + data=data, + variables=_CategoricalFacetPlotter.get_semantics(locals()), + order=order, + orient=orient, + require_numeric=False, + legend=legend, + ) + + # XXX Copying a fair amount from displot, which is not ideal + + for var in ["row", "col"]: + # Handle faceting variables that lack name information + if var in p.variables and p.variables[var] is None: + p.variables[var] = f"_{var}_" + + # Adapt the plot_data dataframe for use with FacetGrid + data = p.plot_data.rename(columns=p.variables) + data = data.loc[:, ~data.columns.duplicated()] + + col_name = p.variables.get("col", None) + row_name = p.variables.get("row", None) + + if facet_kws is None: + facet_kws = {} + + g = FacetGrid( + data=data, row=row_name, col=col_name, + col_wrap=col_wrap, row_order=row_order, + col_order=col_order, height=height, + sharex=sharex, sharey=sharey, + aspect=aspect, + **facet_kws, + ) + + # Capture this here because scale_categorical is going to insert a (null) + # x variable even if it is empty. It's not clear whether that needs to + # happen or if disabling that is the cleaner solution. + has_xy_data = p.has_xy_data + + if not native_scale or p.var_types[p.cat_axis] == "categorical": + p.scale_categorical(p.cat_axis, order=order, formatter=formatter) + + p._attach(g) + + if not has_xy_data: + return g + + hue_order = p._palette_without_hue_backcompat(palette, hue_order) + palette, hue_order = p._hue_backcompat(color, palette, hue_order) + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + # Set a default color + # Otherwise each artist will be plotted separately and trip the color cycle + if hue is None and color is None: + color = "C0" + + if kind == "strip": + + # TODO get these defaults programmatically? + jitter = kwargs.pop("jitter", True) + dodge = kwargs.pop("dodge", False) + edgecolor = kwargs.pop("edgecolor", "gray") # XXX TODO default + + plot_kws = kwargs.copy() + + # XXX Copying possibly bad default decisions from original code for now + plot_kws.setdefault("zorder", 3) + plot_kws.setdefault("s", plot_kws.pop("size", 5) ** 2) + plot_kws.setdefault("linewidth", 0) + + p.plot_strips( + jitter=jitter, + dodge=dodge, + color=color, + edgecolor=edgecolor, + plot_kws=plot_kws, + ) + + elif kind == "swarm": + + # TODO get these defaults programmatically? + dodge = kwargs.pop("dodge", False) + edgecolor = kwargs.pop("edgecolor", "gray") # XXX TODO default + warn_thresh = kwargs.pop("warn_thresh", .05) + + plot_kws = kwargs.copy() + + # XXX Copying possibly bad default decisions from original code for now + plot_kws.setdefault("zorder", 3) + plot_kws.setdefault("s", plot_kws.pop("size", 5) ** 2) + + if plot_kws.setdefault("linewidth", 0) is None: + plot_kws["linewidth"] = np.sqrt(plot_kws["s"]) / 10 + + p.plot_swarms( + dodge=dodge, + color=color, + edgecolor=edgecolor, + warn_thresh=warn_thresh, + plot_kws=plot_kws, + ) + + # XXX best way to do this housekeeping? + for ax in g.axes.flat: + p._adjust_cat_axis(ax, axis=p.cat_axis) + + g.set_axis_labels( + p.variables.get("x", None), + p.variables.get("y", None), + ) + g.set_titles() + g.tight_layout() + + # XXX Hack to get the legend data in the right place + for ax in g.axes.flat: + g._update_legend_data(ax) + ax.legend_ = None + + if legend and (hue is not None) and (hue not in [x, row, col]): + g.add_legend(title=hue, label_order=hue_order) + + return g + + # Don't allow usage of forthcoming functionality + if native_scale is True: + err = f"native_scale not yet implemented for `kind={kind}`" + raise ValueError(err) + if formatter is not None: + err = f"formatter not yet implemented for `kind={kind}`" + raise ValueError(err) + + # Alias the input variables to determine categorical order and palette + # correctly in the case of a count plot + if kind == "count": + if x is None and y is not None: + x_, y_, orient = y, y, "h" + elif y is None and x is not None: + x_, y_, orient = x, x, "v" + else: + raise ValueError("Either `x` or `y` must be None for kind='count'") + else: + x_, y_ = x, y + + # Determine the order for the whole dataset, which will be used in all + # facets to ensure representation of all data in the final plot + plotter_class = { + "box": _BoxPlotter, + "violin": _ViolinPlotter, + "boxen": _LVPlotter, + "bar": _BarPlotter, + "point": _PointPlotter, + "count": _CountPlotter, + }[kind] + p = _CategoricalPlotter() + p.require_numeric = plotter_class.require_numeric + p.establish_variables(x_, y_, hue, data, orient, order, hue_order) + if ( + order is not None + or (sharex and p.orient == "v") + or (sharey and p.orient == "h") + ): + # Sync categorical axis between facets to have the same categories + order = p.group_names + elif color is None and hue is None: + msg = ( + "Setting `{}=False` with `color=None` may cause different levels of the " + "`{}` variable to share colors. This will change in a future version." + ) + if not sharex and p.orient == "v": + warnings.warn(msg.format("sharex", "x"), UserWarning) + if not sharey and p.orient == "h": + warnings.warn(msg.format("sharey", "y"), UserWarning) + + hue_order = p.hue_names + + # Determine the palette to use + # (FacetGrid will pass a value for ``color`` to the plotting function + # so we need to define ``palette`` to get default behavior for the + # categorical functions + p.establish_colors(color, palette, 1) + if kind != "point" or hue is not None: + palette = p.colors + + # Determine keyword arguments for the facets + facet_kws = {} if facet_kws is None else facet_kws + facet_kws.update( + data=data, row=row, col=col, + row_order=row_order, col_order=col_order, + col_wrap=col_wrap, height=height, aspect=aspect, + sharex=sharex, sharey=sharey, + legend_out=legend_out, margin_titles=margin_titles, + dropna=False, + ) + + # Determine keyword arguments for the plotting function + plot_kws = dict( + order=order, hue_order=hue_order, + orient=orient, color=color, palette=palette, + ) + plot_kws.update(kwargs) + + if kind in ["bar", "point"]: + errorbar = utils._deprecate_ci(errorbar, ci) + plot_kws.update( + estimator=estimator, errorbar=errorbar, + n_boot=n_boot, units=units, seed=seed, + ) + + # Initialize the facets + g = FacetGrid(**facet_kws) + + # Draw the plot onto the facets + g.map_dataframe(plot_func, x=x, y=y, hue=hue, **plot_kws) + + if p.orient == "h": + g.set_axis_labels(p.value_label, p.group_label) + else: + g.set_axis_labels(p.group_label, p.value_label) + + # Special case axis labels for a count type plot + if kind == "count": + if x is None: + g.set_axis_labels(x_var="count") + if y is None: + g.set_axis_labels(y_var="count") + + if legend and (hue is not None) and (hue not in [x, row, col]): + hue_order = list(map(utils.to_utf8, hue_order)) + g.add_legend(title=hue, label_order=hue_order) + + return g + + +catplot.__doc__ = dedent("""\ + Figure-level interface for drawing categorical plots onto a FacetGrid. + + This function provides access to several axes-level functions that + show the relationship between a numerical and one or more categorical + variables using one of several visual representations. The `kind` + parameter selects the underlying axes-level function to use: + + Categorical scatterplots: + + - :func:`stripplot` (with `kind="strip"`; the default) + - :func:`swarmplot` (with `kind="swarm"`) + + Categorical distribution plots: + + - :func:`boxplot` (with `kind="box"`) + - :func:`violinplot` (with `kind="violin"`) + - :func:`boxenplot` (with `kind="boxen"`) + + Categorical estimate plots: + + - :func:`pointplot` (with `kind="point"`) + - :func:`barplot` (with `kind="bar"`) + - :func:`countplot` (with `kind="count"`) + + Extra keyword arguments are passed to the underlying function, so you + should refer to the documentation for each to see kind-specific options. + + Note that unlike when using the axes-level functions directly, data must be + passed in a long-form DataFrame with variables specified by passing strings + to `x`, `y`, `hue`, etc. + + {categorical_narrative} + + After plotting, the :class:`FacetGrid` with the plot is returned and can + be used directly to tweak supporting plot details or add other layers. + + Parameters + ---------- + {long_form_data} + {string_input_params} + row, col : names of variables in `data`, optional + Categorical variables that will determine the faceting of the grid. + {col_wrap} + {stat_api_params} + {order_vars} + row_order, col_order : lists of strings, optional + Order to organize the rows and/or columns of the grid in, otherwise the + orders are inferred from the data objects. + {height} + {aspect} + kind : str, optional + The kind of plot to draw, corresponds to the name of a categorical + axes-level plotting function. Options are: "strip", "swarm", "box", "violin", + "boxen", "point", "bar", or "count". + {native_scale} + {formatter} + {orient} + {color} + {palette} + {hue_norm} + legend : str or bool, optional + Set to `False` to disable the legend. With `strip` or `swarm` plots, + this also accepts a string, as described in the axes-level docstrings. + {legend_out} + {share_xy} + {margin_titles} + facet_kws : dict, optional + Dictionary of other keyword arguments to pass to :class:`FacetGrid`. + kwargs : key, value pairings + Other keyword arguments are passed through to the underlying plotting + function. + + Returns + ------- + g : :class:`FacetGrid` + Returns the :class:`FacetGrid` object with the plot on it for further + tweaking. + + Examples + -------- + + .. include:: ../docstrings/catplot.rst + + """).format(**_categorical_docs) + + +class Beeswarm: + """Modifies a scatterplot artist to show a beeswarm plot.""" + def __init__(self, orient="v", width=0.8, warn_thresh=.05): + + # XXX should we keep the orient parameterization or specify the swarm axis? + + self.orient = orient + self.width = width + self.warn_thresh = warn_thresh + + def __call__(self, points, center): + """Swarm `points`, a PathCollection, around the `center` position.""" + # Convert from point size (area) to diameter + + ax = points.axes + dpi = ax.figure.dpi + + # Get the original positions of the points + orig_xy_data = points.get_offsets() + + # Reset the categorical positions to the center line + cat_idx = 1 if self.orient == "h" else 0 + orig_xy_data[:, cat_idx] = center + + # Transform the data coordinates to point coordinates. + # We'll figure out the swarm positions in the latter + # and then convert back to data coordinates and replot + orig_x_data, orig_y_data = orig_xy_data.T + orig_xy = ax.transData.transform(orig_xy_data) + + # Order the variables so that x is the categorical axis + if self.orient == "h": + orig_xy = orig_xy[:, [1, 0]] + + # Add a column with each point's radius + sizes = points.get_sizes() + if sizes.size == 1: + sizes = np.repeat(sizes, orig_xy.shape[0]) + edge = points.get_linewidth().item() + radii = (np.sqrt(sizes) + edge) / 2 * (dpi / 72) + orig_xy = np.c_[orig_xy, radii] + + # Sort along the value axis to facilitate the beeswarm + sorter = np.argsort(orig_xy[:, 1]) + orig_xyr = orig_xy[sorter] + + # Adjust points along the categorical axis to prevent overlaps + new_xyr = np.empty_like(orig_xyr) + new_xyr[sorter] = self.beeswarm(orig_xyr) + + # Transform the point coordinates back to data coordinates + if self.orient == "h": + new_xy = new_xyr[:, [1, 0]] + else: + new_xy = new_xyr[:, :2] + new_x_data, new_y_data = ax.transData.inverted().transform(new_xy).T + + swarm_axis = {"h": "y", "v": "x"}[self.orient] + log_scale = getattr(ax, f"get_{swarm_axis}scale")() == "log" + + # Add gutters + if self.orient == "h": + self.add_gutters(new_y_data, center, log_scale=log_scale) + else: + self.add_gutters(new_x_data, center, log_scale=log_scale) + + # Reposition the points so they do not overlap + if self.orient == "h": + points.set_offsets(np.c_[orig_x_data, new_y_data]) + else: + points.set_offsets(np.c_[new_x_data, orig_y_data]) + + def beeswarm(self, orig_xyr): + """Adjust x position of points to avoid overlaps.""" + # In this method, `x` is always the categorical axis + # Center of the swarm, in point coordinates + midline = orig_xyr[0, 0] + + # Start the swarm with the first point + swarm = np.atleast_2d(orig_xyr[0]) + + # Loop over the remaining points + for xyr_i in orig_xyr[1:]: + + # Find the points in the swarm that could possibly + # overlap with the point we are currently placing + neighbors = self.could_overlap(xyr_i, swarm) + + # Find positions that would be valid individually + # with respect to each of the swarm neighbors + candidates = self.position_candidates(xyr_i, neighbors) + + # Sort candidates by their centrality + offsets = np.abs(candidates[:, 0] - midline) + candidates = candidates[np.argsort(offsets)] + + # Find the first candidate that does not overlap any neighbors + new_xyr_i = self.first_non_overlapping_candidate(candidates, neighbors) + + # Place it into the swarm + swarm = np.vstack([swarm, new_xyr_i]) + + return swarm + + def could_overlap(self, xyr_i, swarm): + """Return a list of all swarm points that could overlap with target.""" + # Because we work backwards through the swarm and can short-circuit, + # the for-loop is faster than vectorization + _, y_i, r_i = xyr_i + neighbors = [] + for xyr_j in reversed(swarm): + _, y_j, r_j = xyr_j + if (y_i - y_j) < (r_i + r_j): + neighbors.append(xyr_j) + else: + break + return np.array(neighbors)[::-1] + + def position_candidates(self, xyr_i, neighbors): + """Return a list of coordinates that might be valid by adjusting x.""" + candidates = [xyr_i] + x_i, y_i, r_i = xyr_i + left_first = True + for x_j, y_j, r_j in neighbors: + dy = y_i - y_j + dx = np.sqrt(max((r_i + r_j) ** 2 - dy ** 2, 0)) * 1.05 + cl, cr = (x_j - dx, y_i, r_i), (x_j + dx, y_i, r_i) + if left_first: + new_candidates = [cl, cr] + else: + new_candidates = [cr, cl] + candidates.extend(new_candidates) + left_first = not left_first + return np.array(candidates) + + def first_non_overlapping_candidate(self, candidates, neighbors): + """Find the first candidate that does not overlap with the swarm.""" + + # If we have no neighbors, all candidates are good. + if len(neighbors) == 0: + return candidates[0] + + neighbors_x = neighbors[:, 0] + neighbors_y = neighbors[:, 1] + neighbors_r = neighbors[:, 2] + + for xyr_i in candidates: + + x_i, y_i, r_i = xyr_i + + dx = neighbors_x - x_i + dy = neighbors_y - y_i + sq_distances = np.square(dx) + np.square(dy) + + sep_needed = np.square(neighbors_r + r_i) + + # Good candidate does not overlap any of neighbors which means that + # squared distance between candidate and any of the neighbors has + # to be at least square of the summed radii + good_candidate = np.all(sq_distances >= sep_needed) + + if good_candidate: + return xyr_i + + raise RuntimeError( + "No non-overlapping candidates found. This should not happen." + ) + + def add_gutters(self, points, center, log_scale=False): + """Stop points from extending beyond their territory.""" + half_width = self.width / 2 + if log_scale: + low_gutter = 10 ** (np.log10(center) - half_width) + else: + low_gutter = center - half_width + off_low = points < low_gutter + if off_low.any(): + points[off_low] = low_gutter + if log_scale: + high_gutter = 10 ** (np.log10(center) + half_width) + else: + high_gutter = center + half_width + off_high = points > high_gutter + if off_high.any(): + points[off_high] = high_gutter + + gutter_prop = (off_high + off_low).sum() / len(points) + if gutter_prop > self.warn_thresh: + msg = ( + "{:.1%} of the points cannot be placed; you may want " + "to decrease the size of the markers or use stripplot." + ).format(gutter_prop) + warnings.warn(msg, UserWarning) + + return points diff --git a/lib/python3.10/site-packages/seaborn/cm.py b/lib/python3.10/site-packages/seaborn/cm.py new file mode 100644 index 0000000000000000000000000000000000000000..df7ce61997882d7d7f734052292438e4234a5cc7 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/cm.py @@ -0,0 +1,1586 @@ +from matplotlib import colors +from seaborn._compat import register_colormap + + +_rocket_lut = [ + [ 0.01060815, 0.01808215, 0.10018654], + [ 0.01428972, 0.02048237, 0.10374486], + [ 0.01831941, 0.0229766 , 0.10738511], + [ 0.02275049, 0.02554464, 0.11108639], + [ 0.02759119, 0.02818316, 0.11483751], + [ 0.03285175, 0.03088792, 0.11863035], + [ 0.03853466, 0.03365771, 0.12245873], + [ 0.04447016, 0.03648425, 0.12631831], + [ 0.05032105, 0.03936808, 0.13020508], + [ 0.05611171, 0.04224835, 0.13411624], + [ 0.0618531 , 0.04504866, 0.13804929], + [ 0.06755457, 0.04778179, 0.14200206], + [ 0.0732236 , 0.05045047, 0.14597263], + [ 0.0788708 , 0.05305461, 0.14995981], + [ 0.08450105, 0.05559631, 0.15396203], + [ 0.09011319, 0.05808059, 0.15797687], + [ 0.09572396, 0.06050127, 0.16200507], + [ 0.10132312, 0.06286782, 0.16604287], + [ 0.10692823, 0.06517224, 0.17009175], + [ 0.1125315 , 0.06742194, 0.17414848], + [ 0.11813947, 0.06961499, 0.17821272], + [ 0.12375803, 0.07174938, 0.18228425], + [ 0.12938228, 0.07383015, 0.18636053], + [ 0.13501631, 0.07585609, 0.19044109], + [ 0.14066867, 0.0778224 , 0.19452676], + [ 0.14633406, 0.07973393, 0.1986151 ], + [ 0.15201338, 0.08159108, 0.20270523], + [ 0.15770877, 0.08339312, 0.20679668], + [ 0.16342174, 0.0851396 , 0.21088893], + [ 0.16915387, 0.08682996, 0.21498104], + [ 0.17489524, 0.08848235, 0.2190294 ], + [ 0.18065495, 0.09009031, 0.22303512], + [ 0.18643324, 0.09165431, 0.22699705], + [ 0.19223028, 0.09317479, 0.23091409], + [ 0.19804623, 0.09465217, 0.23478512], + [ 0.20388117, 0.09608689, 0.23860907], + [ 0.20973515, 0.09747934, 0.24238489], + [ 0.21560818, 0.09882993, 0.24611154], + [ 0.22150014, 0.10013944, 0.2497868 ], + [ 0.22741085, 0.10140876, 0.25340813], + [ 0.23334047, 0.10263737, 0.25697736], + [ 0.23928891, 0.10382562, 0.2604936 ], + [ 0.24525608, 0.10497384, 0.26395596], + [ 0.25124182, 0.10608236, 0.26736359], + [ 0.25724602, 0.10715148, 0.27071569], + [ 0.26326851, 0.1081815 , 0.27401148], + [ 0.26930915, 0.1091727 , 0.2772502 ], + [ 0.27536766, 0.11012568, 0.28043021], + [ 0.28144375, 0.11104133, 0.2835489 ], + [ 0.2875374 , 0.11191896, 0.28660853], + [ 0.29364846, 0.11275876, 0.2896085 ], + [ 0.29977678, 0.11356089, 0.29254823], + [ 0.30592213, 0.11432553, 0.29542718], + [ 0.31208435, 0.11505284, 0.29824485], + [ 0.31826327, 0.1157429 , 0.30100076], + [ 0.32445869, 0.11639585, 0.30369448], + [ 0.33067031, 0.11701189, 0.30632563], + [ 0.33689808, 0.11759095, 0.3088938 ], + [ 0.34314168, 0.11813362, 0.31139721], + [ 0.34940101, 0.11863987, 0.3138355 ], + [ 0.355676 , 0.11910909, 0.31620996], + [ 0.36196644, 0.1195413 , 0.31852037], + [ 0.36827206, 0.11993653, 0.32076656], + [ 0.37459292, 0.12029443, 0.32294825], + [ 0.38092887, 0.12061482, 0.32506528], + [ 0.38727975, 0.12089756, 0.3271175 ], + [ 0.39364518, 0.12114272, 0.32910494], + [ 0.40002537, 0.12134964, 0.33102734], + [ 0.40642019, 0.12151801, 0.33288464], + [ 0.41282936, 0.12164769, 0.33467689], + [ 0.41925278, 0.12173833, 0.33640407], + [ 0.42569057, 0.12178916, 0.33806605], + [ 0.43214263, 0.12179973, 0.33966284], + [ 0.43860848, 0.12177004, 0.34119475], + [ 0.44508855, 0.12169883, 0.34266151], + [ 0.45158266, 0.12158557, 0.34406324], + [ 0.45809049, 0.12142996, 0.34540024], + [ 0.46461238, 0.12123063, 0.34667231], + [ 0.47114798, 0.12098721, 0.34787978], + [ 0.47769736, 0.12069864, 0.34902273], + [ 0.48426077, 0.12036349, 0.35010104], + [ 0.49083761, 0.11998161, 0.35111537], + [ 0.49742847, 0.11955087, 0.35206533], + [ 0.50403286, 0.11907081, 0.35295152], + [ 0.51065109, 0.11853959, 0.35377385], + [ 0.51728314, 0.1179558 , 0.35453252], + [ 0.52392883, 0.11731817, 0.35522789], + [ 0.53058853, 0.11662445, 0.35585982], + [ 0.53726173, 0.11587369, 0.35642903], + [ 0.54394898, 0.11506307, 0.35693521], + [ 0.5506426 , 0.11420757, 0.35737863], + [ 0.55734473, 0.11330456, 0.35775059], + [ 0.56405586, 0.11235265, 0.35804813], + [ 0.57077365, 0.11135597, 0.35827146], + [ 0.5774991 , 0.11031233, 0.35841679], + [ 0.58422945, 0.10922707, 0.35848469], + [ 0.59096382, 0.10810205, 0.35847347], + [ 0.59770215, 0.10693774, 0.35838029], + [ 0.60444226, 0.10573912, 0.35820487], + [ 0.61118304, 0.10450943, 0.35794557], + [ 0.61792306, 0.10325288, 0.35760108], + [ 0.62466162, 0.10197244, 0.35716891], + [ 0.63139686, 0.10067417, 0.35664819], + [ 0.63812122, 0.09938212, 0.35603757], + [ 0.64483795, 0.0980891 , 0.35533555], + [ 0.65154562, 0.09680192, 0.35454107], + [ 0.65824241, 0.09552918, 0.3536529 ], + [ 0.66492652, 0.09428017, 0.3526697 ], + [ 0.67159578, 0.09306598, 0.35159077], + [ 0.67824099, 0.09192342, 0.3504148 ], + [ 0.684863 , 0.09085633, 0.34914061], + [ 0.69146268, 0.0898675 , 0.34776864], + [ 0.69803757, 0.08897226, 0.3462986 ], + [ 0.70457834, 0.0882129 , 0.34473046], + [ 0.71108138, 0.08761223, 0.3430635 ], + [ 0.7175507 , 0.08716212, 0.34129974], + [ 0.72398193, 0.08688725, 0.33943958], + [ 0.73035829, 0.0868623 , 0.33748452], + [ 0.73669146, 0.08704683, 0.33543669], + [ 0.74297501, 0.08747196, 0.33329799], + [ 0.74919318, 0.08820542, 0.33107204], + [ 0.75535825, 0.08919792, 0.32876184], + [ 0.76145589, 0.09050716, 0.32637117], + [ 0.76748424, 0.09213602, 0.32390525], + [ 0.77344838, 0.09405684, 0.32136808], + [ 0.77932641, 0.09634794, 0.31876642], + [ 0.78513609, 0.09892473, 0.31610488], + [ 0.79085854, 0.10184672, 0.313391 ], + [ 0.7965014 , 0.10506637, 0.31063031], + [ 0.80205987, 0.10858333, 0.30783 ], + [ 0.80752799, 0.11239964, 0.30499738], + [ 0.81291606, 0.11645784, 0.30213802], + [ 0.81820481, 0.12080606, 0.29926105], + [ 0.82341472, 0.12535343, 0.2963705 ], + [ 0.82852822, 0.13014118, 0.29347474], + [ 0.83355779, 0.13511035, 0.29057852], + [ 0.83850183, 0.14025098, 0.2876878 ], + [ 0.84335441, 0.14556683, 0.28480819], + [ 0.84813096, 0.15099892, 0.281943 ], + [ 0.85281737, 0.15657772, 0.27909826], + [ 0.85742602, 0.1622583 , 0.27627462], + [ 0.86196552, 0.16801239, 0.27346473], + [ 0.86641628, 0.17387796, 0.27070818], + [ 0.87079129, 0.17982114, 0.26797378], + [ 0.87507281, 0.18587368, 0.26529697], + [ 0.87925878, 0.19203259, 0.26268136], + [ 0.8833417 , 0.19830556, 0.26014181], + [ 0.88731387, 0.20469941, 0.25769539], + [ 0.89116859, 0.21121788, 0.2553592 ], + [ 0.89490337, 0.21785614, 0.25314362], + [ 0.8985026 , 0.22463251, 0.25108745], + [ 0.90197527, 0.23152063, 0.24918223], + [ 0.90530097, 0.23854541, 0.24748098], + [ 0.90848638, 0.24568473, 0.24598324], + [ 0.911533 , 0.25292623, 0.24470258], + [ 0.9144225 , 0.26028902, 0.24369359], + [ 0.91717106, 0.26773821, 0.24294137], + [ 0.91978131, 0.27526191, 0.24245973], + [ 0.92223947, 0.28287251, 0.24229568], + [ 0.92456587, 0.29053388, 0.24242622], + [ 0.92676657, 0.29823282, 0.24285536], + [ 0.92882964, 0.30598085, 0.24362274], + [ 0.93078135, 0.31373977, 0.24468803], + [ 0.93262051, 0.3215093 , 0.24606461], + [ 0.93435067, 0.32928362, 0.24775328], + [ 0.93599076, 0.33703942, 0.24972157], + [ 0.93752831, 0.34479177, 0.25199928], + [ 0.93899289, 0.35250734, 0.25452808], + [ 0.94036561, 0.36020899, 0.25734661], + [ 0.94167588, 0.36786594, 0.2603949 ], + [ 0.94291042, 0.37549479, 0.26369821], + [ 0.94408513, 0.3830811 , 0.26722004], + [ 0.94520419, 0.39062329, 0.27094924], + [ 0.94625977, 0.39813168, 0.27489742], + [ 0.94727016, 0.4055909 , 0.27902322], + [ 0.94823505, 0.41300424, 0.28332283], + [ 0.94914549, 0.42038251, 0.28780969], + [ 0.95001704, 0.42771398, 0.29244728], + [ 0.95085121, 0.43500005, 0.29722817], + [ 0.95165009, 0.44224144, 0.30214494], + [ 0.9524044 , 0.44944853, 0.3072105 ], + [ 0.95312556, 0.45661389, 0.31239776], + [ 0.95381595, 0.46373781, 0.31769923], + [ 0.95447591, 0.47082238, 0.32310953], + [ 0.95510255, 0.47787236, 0.32862553], + [ 0.95569679, 0.48489115, 0.33421404], + [ 0.95626788, 0.49187351, 0.33985601], + [ 0.95681685, 0.49882008, 0.34555431], + [ 0.9573439 , 0.50573243, 0.35130912], + [ 0.95784842, 0.51261283, 0.35711942], + [ 0.95833051, 0.51946267, 0.36298589], + [ 0.95879054, 0.52628305, 0.36890904], + [ 0.95922872, 0.53307513, 0.3748895 ], + [ 0.95964538, 0.53983991, 0.38092784], + [ 0.96004345, 0.54657593, 0.3870292 ], + [ 0.96042097, 0.55328624, 0.39319057], + [ 0.96077819, 0.55997184, 0.39941173], + [ 0.9611152 , 0.5666337 , 0.40569343], + [ 0.96143273, 0.57327231, 0.41203603], + [ 0.96173392, 0.57988594, 0.41844491], + [ 0.96201757, 0.58647675, 0.42491751], + [ 0.96228344, 0.59304598, 0.43145271], + [ 0.96253168, 0.5995944 , 0.43805131], + [ 0.96276513, 0.60612062, 0.44471698], + [ 0.96298491, 0.6126247 , 0.45145074], + [ 0.96318967, 0.61910879, 0.45824902], + [ 0.96337949, 0.6255736 , 0.46511271], + [ 0.96355923, 0.63201624, 0.47204746], + [ 0.96372785, 0.63843852, 0.47905028], + [ 0.96388426, 0.64484214, 0.4861196 ], + [ 0.96403203, 0.65122535, 0.4932578 ], + [ 0.96417332, 0.65758729, 0.50046894], + [ 0.9643063 , 0.66393045, 0.5077467 ], + [ 0.96443322, 0.67025402, 0.51509334], + [ 0.96455845, 0.67655564, 0.52251447], + [ 0.96467922, 0.68283846, 0.53000231], + [ 0.96479861, 0.68910113, 0.53756026], + [ 0.96492035, 0.69534192, 0.5451917 ], + [ 0.96504223, 0.7015636 , 0.5528892 ], + [ 0.96516917, 0.70776351, 0.5606593 ], + [ 0.96530224, 0.71394212, 0.56849894], + [ 0.96544032, 0.72010124, 0.57640375], + [ 0.96559206, 0.72623592, 0.58438387], + [ 0.96575293, 0.73235058, 0.59242739], + [ 0.96592829, 0.73844258, 0.60053991], + [ 0.96612013, 0.74451182, 0.60871954], + [ 0.96632832, 0.75055966, 0.61696136], + [ 0.96656022, 0.75658231, 0.62527295], + [ 0.96681185, 0.76258381, 0.63364277], + [ 0.96709183, 0.76855969, 0.64207921], + [ 0.96739773, 0.77451297, 0.65057302], + [ 0.96773482, 0.78044149, 0.65912731], + [ 0.96810471, 0.78634563, 0.66773889], + [ 0.96850919, 0.79222565, 0.6764046 ], + [ 0.96893132, 0.79809112, 0.68512266], + [ 0.96935926, 0.80395415, 0.69383201], + [ 0.9698028 , 0.80981139, 0.70252255], + [ 0.97025511, 0.81566605, 0.71120296], + [ 0.97071849, 0.82151775, 0.71987163], + [ 0.97120159, 0.82736371, 0.72851999], + [ 0.97169389, 0.83320847, 0.73716071], + [ 0.97220061, 0.83905052, 0.74578903], + [ 0.97272597, 0.84488881, 0.75440141], + [ 0.97327085, 0.85072354, 0.76299805], + [ 0.97383206, 0.85655639, 0.77158353], + [ 0.97441222, 0.86238689, 0.78015619], + [ 0.97501782, 0.86821321, 0.78871034], + [ 0.97564391, 0.87403763, 0.79725261], + [ 0.97628674, 0.87986189, 0.8057883 ], + [ 0.97696114, 0.88568129, 0.81430324], + [ 0.97765722, 0.89149971, 0.82280948], + [ 0.97837585, 0.89731727, 0.83130786], + [ 0.97912374, 0.90313207, 0.83979337], + [ 0.979891 , 0.90894778, 0.84827858], + [ 0.98067764, 0.91476465, 0.85676611], + [ 0.98137749, 0.92061729, 0.86536915] +] + + +_mako_lut = [ + [ 0.04503935, 0.01482344, 0.02092227], + [ 0.04933018, 0.01709292, 0.02535719], + [ 0.05356262, 0.01950702, 0.03018802], + [ 0.05774337, 0.02205989, 0.03545515], + [ 0.06188095, 0.02474764, 0.04115287], + [ 0.06598247, 0.0275665 , 0.04691409], + [ 0.07005374, 0.03051278, 0.05264306], + [ 0.07409947, 0.03358324, 0.05834631], + [ 0.07812339, 0.03677446, 0.06403249], + [ 0.08212852, 0.0400833 , 0.06970862], + [ 0.08611731, 0.04339148, 0.07538208], + [ 0.09009161, 0.04664706, 0.08105568], + [ 0.09405308, 0.04985685, 0.08673591], + [ 0.09800301, 0.05302279, 0.09242646], + [ 0.10194255, 0.05614641, 0.09813162], + [ 0.10587261, 0.05922941, 0.103854 ], + [ 0.1097942 , 0.06227277, 0.10959847], + [ 0.11370826, 0.06527747, 0.11536893], + [ 0.11761516, 0.06824548, 0.12116393], + [ 0.12151575, 0.07117741, 0.12698763], + [ 0.12541095, 0.07407363, 0.1328442 ], + [ 0.12930083, 0.07693611, 0.13873064], + [ 0.13317849, 0.07976988, 0.14465095], + [ 0.13701138, 0.08259683, 0.15060265], + [ 0.14079223, 0.08542126, 0.15659379], + [ 0.14452486, 0.08824175, 0.16262484], + [ 0.14820351, 0.09106304, 0.16869476], + [ 0.15183185, 0.09388372, 0.17480366], + [ 0.15540398, 0.09670855, 0.18094993], + [ 0.15892417, 0.09953561, 0.18713384], + [ 0.16238588, 0.10236998, 0.19335329], + [ 0.16579435, 0.10520905, 0.19960847], + [ 0.16914226, 0.10805832, 0.20589698], + [ 0.17243586, 0.11091443, 0.21221911], + [ 0.17566717, 0.11378321, 0.21857219], + [ 0.17884322, 0.11666074, 0.2249565 ], + [ 0.18195582, 0.11955283, 0.23136943], + [ 0.18501213, 0.12245547, 0.23781116], + [ 0.18800459, 0.12537395, 0.24427914], + [ 0.19093944, 0.1283047 , 0.25077369], + [ 0.19381092, 0.13125179, 0.25729255], + [ 0.19662307, 0.13421303, 0.26383543], + [ 0.19937337, 0.13719028, 0.27040111], + [ 0.20206187, 0.14018372, 0.27698891], + [ 0.20469116, 0.14319196, 0.28359861], + [ 0.20725547, 0.14621882, 0.29022775], + [ 0.20976258, 0.14925954, 0.29687795], + [ 0.21220409, 0.15231929, 0.30354703], + [ 0.21458611, 0.15539445, 0.31023563], + [ 0.21690827, 0.15848519, 0.31694355], + [ 0.21916481, 0.16159489, 0.32366939], + [ 0.2213631 , 0.16471913, 0.33041431], + [ 0.22349947, 0.1678599 , 0.33717781], + [ 0.2255714 , 0.1710185 , 0.34395925], + [ 0.22758415, 0.17419169, 0.35075983], + [ 0.22953569, 0.17738041, 0.35757941], + [ 0.23142077, 0.18058733, 0.3644173 ], + [ 0.2332454 , 0.18380872, 0.37127514], + [ 0.2350092 , 0.18704459, 0.3781528 ], + [ 0.23670785, 0.190297 , 0.38504973], + [ 0.23834119, 0.19356547, 0.39196711], + [ 0.23991189, 0.19684817, 0.39890581], + [ 0.24141903, 0.20014508, 0.4058667 ], + [ 0.24286214, 0.20345642, 0.4128484 ], + [ 0.24423453, 0.20678459, 0.41985299], + [ 0.24554109, 0.21012669, 0.42688124], + [ 0.2467815 , 0.21348266, 0.43393244], + [ 0.24795393, 0.21685249, 0.4410088 ], + [ 0.24905614, 0.22023618, 0.448113 ], + [ 0.25007383, 0.22365053, 0.45519562], + [ 0.25098926, 0.22710664, 0.46223892], + [ 0.25179696, 0.23060342, 0.46925447], + [ 0.25249346, 0.23414353, 0.47623196], + [ 0.25307401, 0.23772973, 0.48316271], + [ 0.25353152, 0.24136961, 0.49001976], + [ 0.25386167, 0.24506548, 0.49679407], + [ 0.25406082, 0.2488164 , 0.50348932], + [ 0.25412435, 0.25262843, 0.51007843], + [ 0.25404842, 0.25650743, 0.51653282], + [ 0.25383134, 0.26044852, 0.52286845], + [ 0.2534705 , 0.26446165, 0.52903422], + [ 0.25296722, 0.2685428 , 0.53503572], + [ 0.2523226 , 0.27269346, 0.54085315], + [ 0.25153974, 0.27691629, 0.54645752], + [ 0.25062402, 0.28120467, 0.55185939], + [ 0.24958205, 0.28556371, 0.55701246], + [ 0.24842386, 0.28998148, 0.56194601], + [ 0.24715928, 0.29446327, 0.56660884], + [ 0.24580099, 0.29899398, 0.57104399], + [ 0.24436202, 0.30357852, 0.57519929], + [ 0.24285591, 0.30819938, 0.57913247], + [ 0.24129828, 0.31286235, 0.58278615], + [ 0.23970131, 0.3175495 , 0.5862272 ], + [ 0.23807973, 0.32226344, 0.58941872], + [ 0.23644557, 0.32699241, 0.59240198], + [ 0.2348113 , 0.33173196, 0.59518282], + [ 0.23318874, 0.33648036, 0.59775543], + [ 0.2315855 , 0.34122763, 0.60016456], + [ 0.23001121, 0.34597357, 0.60240251], + [ 0.2284748 , 0.35071512, 0.6044784 ], + [ 0.22698081, 0.35544612, 0.60642528], + [ 0.22553305, 0.36016515, 0.60825252], + [ 0.22413977, 0.36487341, 0.60994938], + [ 0.22280246, 0.36956728, 0.61154118], + [ 0.22152555, 0.37424409, 0.61304472], + [ 0.22030752, 0.37890437, 0.61446646], + [ 0.2191538 , 0.38354668, 0.61581561], + [ 0.21806257, 0.38817169, 0.61709794], + [ 0.21703799, 0.39277882, 0.61831922], + [ 0.21607792, 0.39736958, 0.61948028], + [ 0.21518463, 0.40194196, 0.62059763], + [ 0.21435467, 0.40649717, 0.62167507], + [ 0.21358663, 0.41103579, 0.62271724], + [ 0.21288172, 0.41555771, 0.62373011], + [ 0.21223835, 0.42006355, 0.62471794], + [ 0.21165312, 0.42455441, 0.62568371], + [ 0.21112526, 0.42903064, 0.6266318 ], + [ 0.21065161, 0.43349321, 0.62756504], + [ 0.21023306, 0.43794288, 0.62848279], + [ 0.20985996, 0.44238227, 0.62938329], + [ 0.20951045, 0.44680966, 0.63030696], + [ 0.20916709, 0.45122981, 0.63124483], + [ 0.20882976, 0.45564335, 0.63219599], + [ 0.20849798, 0.46005094, 0.63315928], + [ 0.20817199, 0.46445309, 0.63413391], + [ 0.20785149, 0.46885041, 0.63511876], + [ 0.20753716, 0.47324327, 0.63611321], + [ 0.20722876, 0.47763224, 0.63711608], + [ 0.20692679, 0.48201774, 0.63812656], + [ 0.20663156, 0.48640018, 0.63914367], + [ 0.20634336, 0.49078002, 0.64016638], + [ 0.20606303, 0.49515755, 0.6411939 ], + [ 0.20578999, 0.49953341, 0.64222457], + [ 0.20552612, 0.50390766, 0.64325811], + [ 0.20527189, 0.50828072, 0.64429331], + [ 0.20502868, 0.51265277, 0.64532947], + [ 0.20479718, 0.51702417, 0.64636539], + [ 0.20457804, 0.52139527, 0.64739979], + [ 0.20437304, 0.52576622, 0.64843198], + [ 0.20418396, 0.53013715, 0.64946117], + [ 0.20401238, 0.53450825, 0.65048638], + [ 0.20385896, 0.53887991, 0.65150606], + [ 0.20372653, 0.54325208, 0.65251978], + [ 0.20361709, 0.5476249 , 0.6535266 ], + [ 0.20353258, 0.55199854, 0.65452542], + [ 0.20347472, 0.55637318, 0.655515 ], + [ 0.20344718, 0.56074869, 0.65649508], + [ 0.20345161, 0.56512531, 0.65746419], + [ 0.20349089, 0.56950304, 0.65842151], + [ 0.20356842, 0.57388184, 0.65936642], + [ 0.20368663, 0.57826181, 0.66029768], + [ 0.20384884, 0.58264293, 0.6612145 ], + [ 0.20405904, 0.58702506, 0.66211645], + [ 0.20431921, 0.59140842, 0.66300179], + [ 0.20463464, 0.59579264, 0.66387079], + [ 0.20500731, 0.60017798, 0.66472159], + [ 0.20544449, 0.60456387, 0.66555409], + [ 0.20596097, 0.60894927, 0.66636568], + [ 0.20654832, 0.61333521, 0.66715744], + [ 0.20721003, 0.61772167, 0.66792838], + [ 0.20795035, 0.62210845, 0.66867802], + [ 0.20877302, 0.62649546, 0.66940555], + [ 0.20968223, 0.63088252, 0.6701105 ], + [ 0.21068163, 0.63526951, 0.67079211], + [ 0.21177544, 0.63965621, 0.67145005], + [ 0.21298582, 0.64404072, 0.67208182], + [ 0.21430361, 0.64842404, 0.67268861], + [ 0.21572716, 0.65280655, 0.67326978], + [ 0.21726052, 0.65718791, 0.6738255 ], + [ 0.21890636, 0.66156803, 0.67435491], + [ 0.220668 , 0.66594665, 0.67485792], + [ 0.22255447, 0.67032297, 0.67533374], + [ 0.22458372, 0.67469531, 0.67578061], + [ 0.22673713, 0.67906542, 0.67620044], + [ 0.22901625, 0.6834332 , 0.67659251], + [ 0.23142316, 0.68779836, 0.67695703], + [ 0.23395924, 0.69216072, 0.67729378], + [ 0.23663857, 0.69651881, 0.67760151], + [ 0.23946645, 0.70087194, 0.67788018], + [ 0.24242624, 0.70522162, 0.67813088], + [ 0.24549008, 0.70957083, 0.67835215], + [ 0.24863372, 0.71392166, 0.67854868], + [ 0.25187832, 0.71827158, 0.67872193], + [ 0.25524083, 0.72261873, 0.67887024], + [ 0.25870947, 0.72696469, 0.67898912], + [ 0.26229238, 0.73130855, 0.67907645], + [ 0.26604085, 0.73564353, 0.67914062], + [ 0.26993099, 0.73997282, 0.67917264], + [ 0.27397488, 0.74429484, 0.67917096], + [ 0.27822463, 0.74860229, 0.67914468], + [ 0.28264201, 0.75290034, 0.67907959], + [ 0.2873016 , 0.75717817, 0.67899164], + [ 0.29215894, 0.76144162, 0.67886578], + [ 0.29729823, 0.76567816, 0.67871894], + [ 0.30268199, 0.76989232, 0.67853896], + [ 0.30835665, 0.77407636, 0.67833512], + [ 0.31435139, 0.77822478, 0.67811118], + [ 0.3206671 , 0.78233575, 0.67786729], + [ 0.32733158, 0.78640315, 0.67761027], + [ 0.33437168, 0.79042043, 0.67734882], + [ 0.34182112, 0.79437948, 0.67709394], + [ 0.34968889, 0.79827511, 0.67685638], + [ 0.35799244, 0.80210037, 0.67664969], + [ 0.36675371, 0.80584651, 0.67649539], + [ 0.3759816 , 0.80950627, 0.67641393], + [ 0.38566792, 0.81307432, 0.67642947], + [ 0.39579804, 0.81654592, 0.67656899], + [ 0.40634556, 0.81991799, 0.67686215], + [ 0.41730243, 0.82318339, 0.67735255], + [ 0.4285828 , 0.82635051, 0.6780564 ], + [ 0.44012728, 0.82942353, 0.67900049], + [ 0.45189421, 0.83240398, 0.68021733], + [ 0.46378379, 0.83530763, 0.6817062 ], + [ 0.47573199, 0.83814472, 0.68347352], + [ 0.48769865, 0.84092197, 0.68552698], + [ 0.49962354, 0.84365379, 0.68783929], + [ 0.5114027 , 0.8463718 , 0.69029789], + [ 0.52301693, 0.84908401, 0.69288545], + [ 0.53447549, 0.85179048, 0.69561066], + [ 0.54578602, 0.8544913 , 0.69848331], + [ 0.55695565, 0.85718723, 0.70150427], + [ 0.56798832, 0.85987893, 0.70468261], + [ 0.57888639, 0.86256715, 0.70802931], + [ 0.5896541 , 0.8652532 , 0.71154204], + [ 0.60028928, 0.86793835, 0.71523675], + [ 0.61079441, 0.87062438, 0.71910895], + [ 0.62116633, 0.87331311, 0.72317003], + [ 0.63140509, 0.87600675, 0.72741689], + [ 0.64150735, 0.87870746, 0.73185717], + [ 0.65147219, 0.8814179 , 0.73648495], + [ 0.66129632, 0.8841403 , 0.74130658], + [ 0.67097934, 0.88687758, 0.74631123], + [ 0.68051833, 0.88963189, 0.75150483], + [ 0.68991419, 0.89240612, 0.75687187], + [ 0.69916533, 0.89520211, 0.76241714], + [ 0.70827373, 0.89802257, 0.76812286], + [ 0.71723995, 0.90086891, 0.77399039], + [ 0.72606665, 0.90374337, 0.7800041 ], + [ 0.73475675, 0.90664718, 0.78615802], + [ 0.74331358, 0.90958151, 0.79244474], + [ 0.75174143, 0.91254787, 0.79884925], + [ 0.76004473, 0.91554656, 0.80536823], + [ 0.76827704, 0.91856549, 0.81196513], + [ 0.77647029, 0.921603 , 0.81855729], + [ 0.78462009, 0.92466151, 0.82514119], + [ 0.79273542, 0.92773848, 0.83172131], + [ 0.8008109 , 0.93083672, 0.83829355], + [ 0.80885107, 0.93395528, 0.84485982], + [ 0.81685878, 0.9370938 , 0.85142101], + [ 0.82483206, 0.94025378, 0.8579751 ], + [ 0.83277661, 0.94343371, 0.86452477], + [ 0.84069127, 0.94663473, 0.87106853], + [ 0.84857662, 0.9498573 , 0.8776059 ], + [ 0.8564431 , 0.95309792, 0.88414253], + [ 0.86429066, 0.95635719, 0.89067759], + [ 0.87218969, 0.95960708, 0.89725384] +] + + +_vlag_lut = [ + [ 0.13850039, 0.41331206, 0.74052025], + [ 0.15077609, 0.41762684, 0.73970427], + [ 0.16235219, 0.4219191 , 0.7389667 ], + [ 0.1733322 , 0.42619024, 0.73832537], + [ 0.18382538, 0.43044226, 0.73776764], + [ 0.19394034, 0.4346772 , 0.73725867], + [ 0.20367115, 0.43889576, 0.73685314], + [ 0.21313625, 0.44310003, 0.73648045], + [ 0.22231173, 0.44729079, 0.73619681], + [ 0.23125148, 0.45146945, 0.73597803], + [ 0.23998101, 0.45563715, 0.7358223 ], + [ 0.24853358, 0.45979489, 0.73571524], + [ 0.25691416, 0.4639437 , 0.73566943], + [ 0.26513894, 0.46808455, 0.73568319], + [ 0.27322194, 0.47221835, 0.73575497], + [ 0.28117543, 0.47634598, 0.73588332], + [ 0.28901021, 0.48046826, 0.73606686], + [ 0.2967358 , 0.48458597, 0.73630433], + [ 0.30436071, 0.48869986, 0.73659451], + [ 0.3118955 , 0.49281055, 0.73693255], + [ 0.31935389, 0.49691847, 0.73730851], + [ 0.32672701, 0.5010247 , 0.73774013], + [ 0.33402607, 0.50512971, 0.73821941], + [ 0.34125337, 0.50923419, 0.73874905], + [ 0.34840921, 0.51333892, 0.73933402], + [ 0.35551826, 0.51744353, 0.73994642], + [ 0.3625676 , 0.52154929, 0.74060763], + [ 0.36956356, 0.52565656, 0.74131327], + [ 0.37649902, 0.52976642, 0.74207698], + [ 0.38340273, 0.53387791, 0.74286286], + [ 0.39025859, 0.53799253, 0.7436962 ], + [ 0.39706821, 0.54211081, 0.744578 ], + [ 0.40384046, 0.54623277, 0.74549872], + [ 0.41058241, 0.55035849, 0.74645094], + [ 0.41728385, 0.55448919, 0.74745174], + [ 0.42395178, 0.55862494, 0.74849357], + [ 0.4305964 , 0.56276546, 0.74956387], + [ 0.4372044 , 0.56691228, 0.75068412], + [ 0.4437909 , 0.57106468, 0.75183427], + [ 0.45035117, 0.5752235 , 0.75302312], + [ 0.45687824, 0.57938983, 0.75426297], + [ 0.46339713, 0.58356191, 0.75551816], + [ 0.46988778, 0.58774195, 0.75682037], + [ 0.47635605, 0.59192986, 0.75816245], + [ 0.48281101, 0.5961252 , 0.75953212], + [ 0.4892374 , 0.60032986, 0.76095418], + [ 0.49566225, 0.60454154, 0.76238852], + [ 0.50206137, 0.60876307, 0.76387371], + [ 0.50845128, 0.61299312, 0.76538551], + [ 0.5148258 , 0.61723272, 0.76693475], + [ 0.52118385, 0.62148236, 0.76852436], + [ 0.52753571, 0.62574126, 0.77013939], + [ 0.53386831, 0.63001125, 0.77180152], + [ 0.54020159, 0.63429038, 0.7734803 ], + [ 0.54651272, 0.63858165, 0.77521306], + [ 0.55282975, 0.64288207, 0.77695608], + [ 0.55912585, 0.64719519, 0.77875327], + [ 0.56542599, 0.65151828, 0.78056551], + [ 0.57170924, 0.65585426, 0.78242747], + [ 0.57799572, 0.6602009 , 0.78430751], + [ 0.58426817, 0.66456073, 0.78623458], + [ 0.590544 , 0.66893178, 0.78818117], + [ 0.59680758, 0.67331643, 0.79017369], + [ 0.60307553, 0.67771273, 0.79218572], + [ 0.60934065, 0.68212194, 0.79422987], + [ 0.61559495, 0.68654548, 0.7963202 ], + [ 0.62185554, 0.69098125, 0.79842918], + [ 0.62810662, 0.69543176, 0.80058381], + [ 0.63436425, 0.69989499, 0.80275812], + [ 0.64061445, 0.70437326, 0.80497621], + [ 0.6468706 , 0.70886488, 0.80721641], + [ 0.65312213, 0.7133717 , 0.80949719], + [ 0.65937818, 0.71789261, 0.81180392], + [ 0.66563334, 0.72242871, 0.81414642], + [ 0.67189155, 0.72697967, 0.81651872], + [ 0.67815314, 0.73154569, 0.81892097], + [ 0.68441395, 0.73612771, 0.82136094], + [ 0.69068321, 0.74072452, 0.82382353], + [ 0.69694776, 0.7453385 , 0.82633199], + [ 0.70322431, 0.74996721, 0.8288583 ], + [ 0.70949595, 0.75461368, 0.83143221], + [ 0.7157774 , 0.75927574, 0.83402904], + [ 0.72206299, 0.76395461, 0.83665922], + [ 0.72835227, 0.76865061, 0.8393242 ], + [ 0.73465238, 0.7733628 , 0.84201224], + [ 0.74094862, 0.77809393, 0.84474951], + [ 0.74725683, 0.78284158, 0.84750915], + [ 0.75357103, 0.78760701, 0.85030217], + [ 0.75988961, 0.79239077, 0.85313207], + [ 0.76621987, 0.79719185, 0.85598668], + [ 0.77255045, 0.8020125 , 0.85888658], + [ 0.77889241, 0.80685102, 0.86181298], + [ 0.78524572, 0.81170768, 0.86476656], + [ 0.79159841, 0.81658489, 0.86776906], + [ 0.79796459, 0.82148036, 0.8707962 ], + [ 0.80434168, 0.82639479, 0.87385315], + [ 0.8107221 , 0.83132983, 0.87695392], + [ 0.81711301, 0.8362844 , 0.88008641], + [ 0.82351479, 0.84125863, 0.88325045], + [ 0.82992772, 0.84625263, 0.88644594], + [ 0.83634359, 0.85126806, 0.8896878 ], + [ 0.84277295, 0.85630293, 0.89295721], + [ 0.84921192, 0.86135782, 0.89626076], + [ 0.85566206, 0.866432 , 0.89959467], + [ 0.86211514, 0.87152627, 0.90297183], + [ 0.86857483, 0.87663856, 0.90638248], + [ 0.87504231, 0.88176648, 0.90981938], + [ 0.88151194, 0.88690782, 0.91328493], + [ 0.88797938, 0.89205857, 0.91677544], + [ 0.89443865, 0.89721298, 0.9202854 ], + [ 0.90088204, 0.90236294, 0.92380601], + [ 0.90729768, 0.90749778, 0.92732797], + [ 0.91367037, 0.91260329, 0.93083814], + [ 0.91998105, 0.91766106, 0.93431861], + [ 0.92620596, 0.92264789, 0.93774647], + [ 0.93231683, 0.9275351 , 0.94109192], + [ 0.93827772, 0.9322888 , 0.94432312], + [ 0.94404755, 0.93686925, 0.94740137], + [ 0.94958284, 0.94123072, 0.95027696], + [ 0.95482682, 0.9453245 , 0.95291103], + [ 0.9597248 , 0.94909728, 0.95525103], + [ 0.96422552, 0.95249273, 0.95723271], + [ 0.96826161, 0.95545812, 0.95882188], + [ 0.97178458, 0.95793984, 0.95995705], + [ 0.97474105, 0.95989142, 0.96059997], + [ 0.97708604, 0.96127366, 0.96071853], + [ 0.97877855, 0.96205832, 0.96030095], + [ 0.97978484, 0.96222949, 0.95935496], + [ 0.9805997 , 0.96155216, 0.95813083], + [ 0.98152619, 0.95993719, 0.95639322], + [ 0.9819726 , 0.95766608, 0.95399269], + [ 0.98191855, 0.9547873 , 0.95098107], + [ 0.98138514, 0.95134771, 0.94740644], + [ 0.98040845, 0.94739906, 0.94332125], + [ 0.97902107, 0.94300131, 0.93878672], + [ 0.97729348, 0.93820409, 0.93385135], + [ 0.9752533 , 0.933073 , 0.92858252], + [ 0.97297834, 0.92765261, 0.92302309], + [ 0.97049104, 0.92200317, 0.91723505], + [ 0.96784372, 0.91616744, 0.91126063], + [ 0.96507281, 0.91018664, 0.90514124], + [ 0.96222034, 0.90409203, 0.89890756], + [ 0.9593079 , 0.89791478, 0.89259122], + [ 0.95635626, 0.89167908, 0.88621654], + [ 0.95338303, 0.88540373, 0.87980238], + [ 0.95040174, 0.87910333, 0.87336339], + [ 0.94742246, 0.87278899, 0.86691076], + [ 0.94445249, 0.86646893, 0.86045277], + [ 0.94150476, 0.86014606, 0.85399191], + [ 0.93857394, 0.85382798, 0.84753642], + [ 0.93566206, 0.84751766, 0.84108935], + [ 0.93277194, 0.8412164 , 0.83465197], + [ 0.92990106, 0.83492672, 0.82822708], + [ 0.92704736, 0.82865028, 0.82181656], + [ 0.92422703, 0.82238092, 0.81541333], + [ 0.92142581, 0.81612448, 0.80902415], + [ 0.91864501, 0.80988032, 0.80264838], + [ 0.91587578, 0.80365187, 0.79629001], + [ 0.9131367 , 0.79743115, 0.78994 ], + [ 0.91041602, 0.79122265, 0.78360361], + [ 0.90771071, 0.78502727, 0.77728196], + [ 0.90501581, 0.77884674, 0.7709771 ], + [ 0.90235365, 0.77267117, 0.76467793], + [ 0.8997019 , 0.76650962, 0.75839484], + [ 0.89705346, 0.76036481, 0.752131 ], + [ 0.89444021, 0.75422253, 0.74587047], + [ 0.89183355, 0.74809474, 0.73962689], + [ 0.88923216, 0.74198168, 0.73340061], + [ 0.88665892, 0.73587283, 0.72717995], + [ 0.88408839, 0.72977904, 0.72097718], + [ 0.88153537, 0.72369332, 0.71478461], + [ 0.87899389, 0.7176179 , 0.70860487], + [ 0.87645157, 0.71155805, 0.7024439 ], + [ 0.8739399 , 0.70549893, 0.6962854 ], + [ 0.87142626, 0.6994551 , 0.69014561], + [ 0.8689268 , 0.69341868, 0.68401597], + [ 0.86643562, 0.687392 , 0.67789917], + [ 0.86394434, 0.68137863, 0.67179927], + [ 0.86147586, 0.67536728, 0.665704 ], + [ 0.85899928, 0.66937226, 0.6596292 ], + [ 0.85654668, 0.66337773, 0.6535577 ], + [ 0.85408818, 0.65739772, 0.64750494], + [ 0.85164413, 0.65142189, 0.64145983], + [ 0.84920091, 0.6454565 , 0.63542932], + [ 0.84676427, 0.63949827, 0.62941 ], + [ 0.84433231, 0.63354773, 0.62340261], + [ 0.84190106, 0.62760645, 0.61740899], + [ 0.83947935, 0.62166951, 0.61142404], + [ 0.8370538 , 0.61574332, 0.60545478], + [ 0.83463975, 0.60981951, 0.59949247], + [ 0.83221877, 0.60390724, 0.593547 ], + [ 0.82980985, 0.59799607, 0.58760751], + [ 0.82740268, 0.59209095, 0.58167944], + [ 0.82498638, 0.5861973 , 0.57576866], + [ 0.82258181, 0.5803034 , 0.56986307], + [ 0.82016611, 0.57442123, 0.56397539], + [ 0.81776305, 0.56853725, 0.55809173], + [ 0.81534551, 0.56266602, 0.55222741], + [ 0.81294293, 0.55679056, 0.5463651 ], + [ 0.81052113, 0.55092973, 0.54052443], + [ 0.80811509, 0.54506305, 0.53468464], + [ 0.80568952, 0.53921036, 0.52886622], + [ 0.80327506, 0.53335335, 0.52305077], + [ 0.80084727, 0.52750583, 0.51725256], + [ 0.79842217, 0.5216578 , 0.51146173], + [ 0.79599382, 0.51581223, 0.50568155], + [ 0.79355781, 0.50997127, 0.49991444], + [ 0.79112596, 0.50412707, 0.49415289], + [ 0.78867442, 0.49829386, 0.48841129], + [ 0.7862306 , 0.49245398, 0.48267247], + [ 0.7837687 , 0.48662309, 0.47695216], + [ 0.78130809, 0.4807883 , 0.47123805], + [ 0.77884467, 0.47495151, 0.46553236], + [ 0.77636283, 0.46912235, 0.45984473], + [ 0.77388383, 0.46328617, 0.45416141], + [ 0.77138912, 0.45745466, 0.44849398], + [ 0.76888874, 0.45162042, 0.44283573], + [ 0.76638802, 0.44577901, 0.43718292], + [ 0.76386116, 0.43994762, 0.43155211], + [ 0.76133542, 0.43410655, 0.42592523], + [ 0.75880631, 0.42825801, 0.42030488], + [ 0.75624913, 0.42241905, 0.41470727], + [ 0.7536919 , 0.41656866, 0.40911347], + [ 0.75112748, 0.41071104, 0.40352792], + [ 0.74854331, 0.40485474, 0.3979589 ], + [ 0.74594723, 0.39899309, 0.39240088], + [ 0.74334332, 0.39312199, 0.38685075], + [ 0.74073277, 0.38723941, 0.3813074 ], + [ 0.73809409, 0.38136133, 0.37578553], + [ 0.73544692, 0.37547129, 0.37027123], + [ 0.73278943, 0.36956954, 0.36476549], + [ 0.73011829, 0.36365761, 0.35927038], + [ 0.72743485, 0.35773314, 0.35378465], + [ 0.72472722, 0.35180504, 0.34831662], + [ 0.72200473, 0.34586421, 0.34285937], + [ 0.71927052, 0.33990649, 0.33741033], + [ 0.71652049, 0.33393396, 0.33197219], + [ 0.71375362, 0.32794602, 0.32654545], + [ 0.71096951, 0.32194148, 0.32113016], + [ 0.70816772, 0.31591904, 0.31572637], + [ 0.70534784, 0.30987734, 0.31033414], + [ 0.70250944, 0.30381489, 0.30495353], + [ 0.69965211, 0.2977301 , 0.2995846 ], + [ 0.6967754 , 0.29162126, 0.29422741], + [ 0.69388446, 0.28548074, 0.28887769], + [ 0.69097561, 0.2793096 , 0.28353795], + [ 0.68803513, 0.27311993, 0.27821876], + [ 0.6850794 , 0.26689144, 0.27290694], + [ 0.682108 , 0.26062114, 0.26760246], + [ 0.67911013, 0.2543177 , 0.26231367], + [ 0.67609393, 0.24796818, 0.25703372], + [ 0.67305921, 0.24156846, 0.25176238], + [ 0.67000176, 0.23511902, 0.24650278], + [ 0.66693423, 0.22859879, 0.24124404], + [ 0.6638441 , 0.22201742, 0.2359961 ], + [ 0.66080672, 0.21526712, 0.23069468] +] + + +_icefire_lut = [ + [ 0.73936227, 0.90443867, 0.85757238], + [ 0.72888063, 0.89639109, 0.85488394], + [ 0.71834255, 0.88842162, 0.8521605 ], + [ 0.70773866, 0.88052939, 0.849422 ], + [ 0.69706215, 0.87271313, 0.84668315], + [ 0.68629021, 0.86497329, 0.84398721], + [ 0.67543654, 0.85730617, 0.84130969], + [ 0.66448539, 0.84971123, 0.83868005], + [ 0.65342679, 0.84218728, 0.83611512], + [ 0.64231804, 0.83471867, 0.83358584], + [ 0.63117745, 0.827294 , 0.83113431], + [ 0.62000484, 0.81991069, 0.82876741], + [ 0.60879435, 0.81256797, 0.82648905], + [ 0.59754118, 0.80526458, 0.82430414], + [ 0.58624247, 0.79799884, 0.82221573], + [ 0.57489525, 0.7907688 , 0.82022901], + [ 0.56349779, 0.78357215, 0.81834861], + [ 0.55204294, 0.77640827, 0.81657563], + [ 0.54052516, 0.76927562, 0.81491462], + [ 0.52894085, 0.76217215, 0.81336913], + [ 0.51728854, 0.75509528, 0.81194156], + [ 0.50555676, 0.74804469, 0.81063503], + [ 0.49373871, 0.7410187 , 0.80945242], + [ 0.48183174, 0.73401449, 0.80839675], + [ 0.46982587, 0.72703075, 0.80747097], + [ 0.45770893, 0.72006648, 0.80667756], + [ 0.44547249, 0.71311941, 0.80601991], + [ 0.43318643, 0.70617126, 0.80549278], + [ 0.42110294, 0.69916972, 0.80506683], + [ 0.40925101, 0.69211059, 0.80473246], + [ 0.3976693 , 0.68498786, 0.80448272], + [ 0.38632002, 0.67781125, 0.80431024], + [ 0.37523981, 0.67057537, 0.80420832], + [ 0.36442578, 0.66328229, 0.80417474], + [ 0.35385939, 0.65593699, 0.80420591], + [ 0.34358916, 0.64853177, 0.8043 ], + [ 0.33355526, 0.64107876, 0.80445484], + [ 0.32383062, 0.63356578, 0.80467091], + [ 0.31434372, 0.62600624, 0.8049475 ], + [ 0.30516161, 0.618389 , 0.80528692], + [ 0.29623491, 0.61072284, 0.80569021], + [ 0.28759072, 0.60300319, 0.80616055], + [ 0.27923924, 0.59522877, 0.80669803], + [ 0.27114651, 0.5874047 , 0.80730545], + [ 0.26337153, 0.57952055, 0.80799113], + [ 0.25588696, 0.57157984, 0.80875922], + [ 0.248686 , 0.56358255, 0.80961366], + [ 0.24180668, 0.55552289, 0.81055123], + [ 0.23526251, 0.54739477, 0.8115939 ], + [ 0.22921445, 0.53918506, 0.81267292], + [ 0.22397687, 0.53086094, 0.8137141 ], + [ 0.21977058, 0.52241482, 0.81457651], + [ 0.21658989, 0.51384321, 0.81528511], + [ 0.21452772, 0.50514155, 0.81577278], + [ 0.21372783, 0.49630865, 0.81589566], + [ 0.21409503, 0.48734861, 0.81566163], + [ 0.2157176 , 0.47827123, 0.81487615], + [ 0.21842857, 0.46909168, 0.81351614], + [ 0.22211705, 0.45983212, 0.81146983], + [ 0.22665681, 0.45052233, 0.80860217], + [ 0.23176013, 0.44119137, 0.80494325], + [ 0.23727775, 0.43187704, 0.80038017], + [ 0.24298285, 0.42261123, 0.79493267], + [ 0.24865068, 0.41341842, 0.78869164], + [ 0.25423116, 0.40433127, 0.78155831], + [ 0.25950239, 0.39535521, 0.77376848], + [ 0.2644736 , 0.38651212, 0.76524809], + [ 0.26901584, 0.37779582, 0.75621942], + [ 0.27318141, 0.36922056, 0.746605 ], + [ 0.27690355, 0.3607736 , 0.73659374], + [ 0.28023585, 0.35244234, 0.72622103], + [ 0.28306009, 0.34438449, 0.71500731], + [ 0.28535896, 0.33660243, 0.70303975], + [ 0.28708711, 0.32912157, 0.69034504], + [ 0.28816354, 0.32200604, 0.67684067], + [ 0.28862749, 0.31519824, 0.66278813], + [ 0.28847904, 0.30869064, 0.6482815 ], + [ 0.28770912, 0.30250126, 0.63331265], + [ 0.28640325, 0.29655509, 0.61811374], + [ 0.28458943, 0.29082155, 0.60280913], + [ 0.28233561, 0.28527482, 0.58742866], + [ 0.27967038, 0.2798938 , 0.57204225], + [ 0.27665361, 0.27465357, 0.55667809], + [ 0.27332564, 0.2695165 , 0.54145387], + [ 0.26973851, 0.26447054, 0.52634916], + [ 0.2659204 , 0.25949691, 0.511417 ], + [ 0.26190145, 0.25458123, 0.49668768], + [ 0.2577151 , 0.24971691, 0.48214874], + [ 0.25337618, 0.24490494, 0.46778758], + [ 0.24890842, 0.24013332, 0.45363816], + [ 0.24433654, 0.23539226, 0.4397245 ], + [ 0.23967922, 0.23067729, 0.4260591 ], + [ 0.23495608, 0.22598894, 0.41262952], + [ 0.23018113, 0.22132414, 0.39945577], + [ 0.22534609, 0.21670847, 0.38645794], + [ 0.22048761, 0.21211723, 0.37372555], + [ 0.2156198 , 0.20755389, 0.36125301], + [ 0.21074637, 0.20302717, 0.34903192], + [ 0.20586893, 0.19855368, 0.33701661], + [ 0.20101757, 0.19411573, 0.32529173], + [ 0.19619947, 0.18972425, 0.31383846], + [ 0.19140726, 0.18540157, 0.30260777], + [ 0.1866769 , 0.1811332 , 0.29166583], + [ 0.18201285, 0.17694992, 0.28088776], + [ 0.17745228, 0.17282141, 0.27044211], + [ 0.17300684, 0.16876921, 0.26024893], + [ 0.16868273, 0.16479861, 0.25034479], + [ 0.16448691, 0.16091728, 0.24075373], + [ 0.16043195, 0.15714351, 0.23141745], + [ 0.15652427, 0.15348248, 0.22238175], + [ 0.15277065, 0.14994111, 0.21368395], + [ 0.14918274, 0.14653431, 0.20529486], + [ 0.14577095, 0.14327403, 0.19720829], + [ 0.14254381, 0.14016944, 0.18944326], + [ 0.13951035, 0.13723063, 0.18201072], + [ 0.13667798, 0.13446606, 0.17493774], + [ 0.13405762, 0.13188822, 0.16820842], + [ 0.13165767, 0.12950667, 0.16183275], + [ 0.12948748, 0.12733187, 0.15580631], + [ 0.12755435, 0.1253723 , 0.15014098], + [ 0.12586516, 0.12363617, 0.1448459 ], + [ 0.12442647, 0.12213143, 0.13992571], + [ 0.12324241, 0.12086419, 0.13539995], + [ 0.12232067, 0.11984278, 0.13124644], + [ 0.12166209, 0.11907077, 0.12749671], + [ 0.12126982, 0.11855309, 0.12415079], + [ 0.12114244, 0.11829179, 0.1212385 ], + [ 0.12127766, 0.11828837, 0.11878534], + [ 0.12284806, 0.1179729 , 0.11772022], + [ 0.12619498, 0.11721796, 0.11770203], + [ 0.129968 , 0.11663788, 0.11792377], + [ 0.13410011, 0.11625146, 0.11839138], + [ 0.13855459, 0.11606618, 0.11910584], + [ 0.14333775, 0.11607038, 0.1200606 ], + [ 0.148417 , 0.11626929, 0.12125453], + [ 0.15377389, 0.11666192, 0.12268364], + [ 0.15941427, 0.11723486, 0.12433911], + [ 0.16533376, 0.11797856, 0.12621303], + [ 0.17152547, 0.11888403, 0.12829735], + [ 0.17797765, 0.11994436, 0.13058435], + [ 0.18468769, 0.12114722, 0.13306426], + [ 0.19165663, 0.12247737, 0.13572616], + [ 0.19884415, 0.12394381, 0.1385669 ], + [ 0.20627181, 0.12551883, 0.14157124], + [ 0.21394877, 0.12718055, 0.14472604], + [ 0.22184572, 0.12893119, 0.14802579], + [ 0.22994394, 0.13076731, 0.15146314], + [ 0.23823937, 0.13267611, 0.15502793], + [ 0.24676041, 0.13462172, 0.15870321], + [ 0.25546457, 0.13661751, 0.16248722], + [ 0.26433628, 0.13865956, 0.16637301], + [ 0.27341345, 0.14070412, 0.17034221], + [ 0.28264773, 0.14277192, 0.1743957 ], + [ 0.29202272, 0.14486161, 0.17852793], + [ 0.30159648, 0.14691224, 0.1827169 ], + [ 0.31129002, 0.14897583, 0.18695213], + [ 0.32111555, 0.15103351, 0.19119629], + [ 0.33107961, 0.1530674 , 0.19543758], + [ 0.34119892, 0.15504762, 0.1996803 ], + [ 0.35142388, 0.15701131, 0.20389086], + [ 0.36178937, 0.1589124 , 0.20807639], + [ 0.37229381, 0.16073993, 0.21223189], + [ 0.38288348, 0.16254006, 0.2163249 ], + [ 0.39359592, 0.16426336, 0.22036577], + [ 0.40444332, 0.16588767, 0.22434027], + [ 0.41537995, 0.16745325, 0.2282297 ], + [ 0.42640867, 0.16894939, 0.23202755], + [ 0.43754706, 0.17034847, 0.23572899], + [ 0.44878564, 0.1716535 , 0.23932344], + [ 0.4601126 , 0.17287365, 0.24278607], + [ 0.47151732, 0.17401641, 0.24610337], + [ 0.48300689, 0.17506676, 0.2492737 ], + [ 0.49458302, 0.17601892, 0.25227688], + [ 0.50623876, 0.17687777, 0.255096 ], + [ 0.5179623 , 0.17765528, 0.2577162 ], + [ 0.52975234, 0.17835232, 0.2601134 ], + [ 0.54159776, 0.17898292, 0.26226847], + [ 0.55348804, 0.17956232, 0.26416003], + [ 0.56541729, 0.18010175, 0.26575971], + [ 0.57736669, 0.180631 , 0.26704888], + [ 0.58932081, 0.18117827, 0.26800409], + [ 0.60127582, 0.18175888, 0.26858488], + [ 0.61319563, 0.1824336 , 0.2687872 ], + [ 0.62506376, 0.18324015, 0.26858301], + [ 0.63681202, 0.18430173, 0.26795276], + [ 0.64842603, 0.18565472, 0.26689463], + [ 0.65988195, 0.18734638, 0.26543435], + [ 0.67111966, 0.18948885, 0.26357955], + [ 0.68209194, 0.19216636, 0.26137175], + [ 0.69281185, 0.19535326, 0.25887063], + [ 0.70335022, 0.19891271, 0.25617971], + [ 0.71375229, 0.20276438, 0.25331365], + [ 0.72401436, 0.20691287, 0.25027366], + [ 0.73407638, 0.21145051, 0.24710661], + [ 0.74396983, 0.21631913, 0.24380715], + [ 0.75361506, 0.22163653, 0.24043996], + [ 0.7630579 , 0.22731637, 0.23700095], + [ 0.77222228, 0.23346231, 0.23356628], + [ 0.78115441, 0.23998404, 0.23013825], + [ 0.78979746, 0.24694858, 0.22678822], + [ 0.79819286, 0.25427223, 0.22352658], + [ 0.80630444, 0.26198807, 0.22040877], + [ 0.81417437, 0.27001406, 0.21744645], + [ 0.82177364, 0.27837336, 0.21468316], + [ 0.82915955, 0.28696963, 0.21210766], + [ 0.83628628, 0.2958499 , 0.20977813], + [ 0.84322168, 0.30491136, 0.20766435], + [ 0.84995458, 0.31415945, 0.2057863 ], + [ 0.85648867, 0.32358058, 0.20415327], + [ 0.86286243, 0.33312058, 0.20274969], + [ 0.86908321, 0.34276705, 0.20157271], + [ 0.87512876, 0.3525416 , 0.20064949], + [ 0.88100349, 0.36243385, 0.19999078], + [ 0.8866469 , 0.37249496, 0.1997976 ], + [ 0.89203964, 0.38273475, 0.20013431], + [ 0.89713496, 0.39318156, 0.20121514], + [ 0.90195099, 0.40380687, 0.20301555], + [ 0.90648379, 0.41460191, 0.20558847], + [ 0.9106967 , 0.42557857, 0.20918529], + [ 0.91463791, 0.43668557, 0.21367954], + [ 0.91830723, 0.44790913, 0.21916352], + [ 0.92171507, 0.45922856, 0.22568002], + [ 0.92491786, 0.4705936 , 0.23308207], + [ 0.92790792, 0.48200153, 0.24145932], + [ 0.93073701, 0.49341219, 0.25065486], + [ 0.93343918, 0.5048017 , 0.26056148], + [ 0.93602064, 0.51616486, 0.27118485], + [ 0.93850535, 0.52748892, 0.28242464], + [ 0.94092933, 0.53875462, 0.29416042], + [ 0.94330011, 0.5499628 , 0.30634189], + [ 0.94563159, 0.56110987, 0.31891624], + [ 0.94792955, 0.57219822, 0.33184256], + [ 0.95020929, 0.5832232 , 0.34508419], + [ 0.95247324, 0.59419035, 0.35859866], + [ 0.95471709, 0.60510869, 0.37236035], + [ 0.95698411, 0.61595766, 0.38629631], + [ 0.95923863, 0.62676473, 0.40043317], + [ 0.9615041 , 0.6375203 , 0.41474106], + [ 0.96371553, 0.64826619, 0.42928335], + [ 0.96591497, 0.65899621, 0.44380444], + [ 0.96809871, 0.66971662, 0.45830232], + [ 0.9702495 , 0.6804394 , 0.47280492], + [ 0.9723881 , 0.69115622, 0.48729272], + [ 0.97450723, 0.70187358, 0.50178034], + [ 0.9766108 , 0.712592 , 0.51626837], + [ 0.97871716, 0.72330511, 0.53074053], + [ 0.98082222, 0.73401769, 0.54520694], + [ 0.9829001 , 0.74474445, 0.5597019 ], + [ 0.98497466, 0.75547635, 0.57420239], + [ 0.98705581, 0.76621129, 0.58870185], + [ 0.98913325, 0.77695637, 0.60321626], + [ 0.99119918, 0.78771716, 0.61775821], + [ 0.9932672 , 0.79848979, 0.63231691], + [ 0.99535958, 0.80926704, 0.64687278], + [ 0.99740544, 0.82008078, 0.66150571], + [ 0.9992197 , 0.83100723, 0.6764127 ] +] + + +_flare_lut = [ + [0.92907237, 0.68878959, 0.50411509], + [0.92891402, 0.68494686, 0.50173994], + [0.92864754, 0.68116207, 0.4993754], + [0.92836112, 0.67738527, 0.49701572], + [0.9280599, 0.67361354, 0.49466044], + [0.92775569, 0.66983999, 0.49230866], + [0.9274375, 0.66607098, 0.48996097], + [0.927111, 0.66230315, 0.48761688], + [0.92677996, 0.6585342, 0.485276], + [0.92644317, 0.65476476, 0.48293832], + [0.92609759, 0.65099658, 0.48060392], + [0.925747, 0.64722729, 0.47827244], + [0.92539502, 0.64345456, 0.47594352], + [0.92503106, 0.6396848, 0.47361782], + [0.92466877, 0.6359095, 0.47129427], + [0.92429828, 0.63213463, 0.46897349], + [0.92392172, 0.62835879, 0.46665526], + [0.92354597, 0.62457749, 0.46433898], + [0.9231622, 0.6207962, 0.46202524], + [0.92277222, 0.61701365, 0.45971384], + [0.92237978, 0.61322733, 0.45740444], + [0.92198615, 0.60943622, 0.45509686], + [0.92158735, 0.60564276, 0.45279137], + [0.92118373, 0.60184659, 0.45048789], + [0.92077582, 0.59804722, 0.44818634], + [0.92036413, 0.59424414, 0.44588663], + [0.91994924, 0.5904368, 0.44358868], + [0.91952943, 0.58662619, 0.4412926], + [0.91910675, 0.58281075, 0.43899817], + [0.91868096, 0.57899046, 0.4367054], + [0.91825103, 0.57516584, 0.43441436], + [0.91781857, 0.57133556, 0.43212486], + [0.9173814, 0.56750099, 0.4298371], + [0.91694139, 0.56366058, 0.42755089], + [0.91649756, 0.55981483, 0.42526631], + [0.91604942, 0.55596387, 0.42298339], + [0.9155979, 0.55210684, 0.42070204], + [0.9151409, 0.54824485, 0.4184247], + [0.91466138, 0.54438817, 0.41617858], + [0.91416896, 0.54052962, 0.41396347], + [0.91366559, 0.53666778, 0.41177769], + [0.91315173, 0.53280208, 0.40962196], + [0.91262605, 0.52893336, 0.40749715], + [0.91208866, 0.52506133, 0.40540404], + [0.91153952, 0.52118582, 0.40334346], + [0.91097732, 0.51730767, 0.4013163], + [0.910403, 0.51342591, 0.39932342], + [0.90981494, 0.50954168, 0.39736571], + [0.90921368, 0.5056543, 0.39544411], + [0.90859797, 0.50176463, 0.39355952], + [0.90796841, 0.49787195, 0.39171297], + [0.90732341, 0.4939774, 0.38990532], + [0.90666382, 0.49008006, 0.38813773], + [0.90598815, 0.486181, 0.38641107], + [0.90529624, 0.48228017, 0.38472641], + [0.90458808, 0.47837738, 0.38308489], + [0.90386248, 0.47447348, 0.38148746], + [0.90311921, 0.4705685, 0.37993524], + [0.90235809, 0.46666239, 0.37842943], + [0.90157824, 0.46275577, 0.37697105], + [0.90077904, 0.45884905, 0.37556121], + [0.89995995, 0.45494253, 0.37420106], + [0.89912041, 0.4510366, 0.37289175], + [0.8982602, 0.44713126, 0.37163458], + [0.89737819, 0.44322747, 0.37043052], + [0.89647387, 0.43932557, 0.36928078], + [0.89554477, 0.43542759, 0.36818855], + [0.89458871, 0.4315354, 0.36715654], + [0.89360794, 0.42764714, 0.36618273], + [0.89260152, 0.42376366, 0.36526813], + [0.8915687, 0.41988565, 0.36441384], + [0.89050882, 0.41601371, 0.36362102], + [0.8894159, 0.41215334, 0.36289639], + [0.888292, 0.40830288, 0.36223756], + [0.88713784, 0.40446193, 0.36164328], + [0.88595253, 0.40063149, 0.36111438], + [0.88473115, 0.39681635, 0.3606566], + [0.88347246, 0.39301805, 0.36027074], + [0.88217931, 0.38923439, 0.35995244], + [0.880851, 0.38546632, 0.35970244], + [0.87947728, 0.38172422, 0.35953127], + [0.87806542, 0.37800172, 0.35942941], + [0.87661509, 0.37429964, 0.35939659], + [0.87511668, 0.37062819, 0.35944178], + [0.87357554, 0.36698279, 0.35955811], + [0.87199254, 0.3633634, 0.35974223], + [0.87035691, 0.35978174, 0.36000516], + [0.86867647, 0.35623087, 0.36033559], + [0.86694949, 0.35271349, 0.36073358], + [0.86516775, 0.34923921, 0.36120624], + [0.86333996, 0.34580008, 0.36174113], + [0.86145909, 0.3424046, 0.36234402], + [0.85952586, 0.33905327, 0.36301129], + [0.85754536, 0.33574168, 0.36373567], + [0.855514, 0.33247568, 0.36451271], + [0.85344392, 0.32924217, 0.36533344], + [0.8513284, 0.32604977, 0.36620106], + [0.84916723, 0.32289973, 0.36711424], + [0.84696243, 0.31979068, 0.36806976], + [0.84470627, 0.31673295, 0.36907066], + [0.84240761, 0.31371695, 0.37010969], + [0.84005337, 0.31075974, 0.37119284], + [0.83765537, 0.30784814, 0.3723105], + [0.83520234, 0.30499724, 0.37346726], + [0.83270291, 0.30219766, 0.37465552], + [0.83014895, 0.29946081, 0.37587769], + [0.82754694, 0.29677989, 0.37712733], + [0.82489111, 0.29416352, 0.37840532], + [0.82218644, 0.29160665, 0.37970606], + [0.81942908, 0.28911553, 0.38102921], + [0.81662276, 0.28668665, 0.38236999], + [0.81376555, 0.28432371, 0.383727], + [0.81085964, 0.28202508, 0.38509649], + [0.8079055, 0.27979128, 0.38647583], + [0.80490309, 0.27762348, 0.3878626], + [0.80185613, 0.2755178, 0.38925253], + [0.79876118, 0.27347974, 0.39064559], + [0.79562644, 0.27149928, 0.39203532], + [0.79244362, 0.2695883, 0.39342447], + [0.78922456, 0.26773176, 0.3948046], + [0.78596161, 0.26594053, 0.39617873], + [0.7826624, 0.26420493, 0.39754146], + [0.77932717, 0.26252522, 0.39889102], + [0.77595363, 0.2609049, 0.4002279], + [0.77254999, 0.25933319, 0.40154704], + [0.76911107, 0.25781758, 0.40284959], + [0.76564158, 0.25635173, 0.40413341], + [0.76214598, 0.25492998, 0.40539471], + [0.75861834, 0.25356035, 0.40663694], + [0.75506533, 0.25223402, 0.40785559], + [0.75148963, 0.2509473, 0.40904966], + [0.74788835, 0.24970413, 0.41022028], + [0.74426345, 0.24850191, 0.41136599], + [0.74061927, 0.24733457, 0.41248516], + [0.73695678, 0.24620072, 0.41357737], + [0.73327278, 0.24510469, 0.41464364], + [0.72957096, 0.24404127, 0.4156828], + [0.72585394, 0.24300672, 0.41669383], + [0.7221226, 0.24199971, 0.41767651], + [0.71837612, 0.24102046, 0.41863486], + [0.71463236, 0.24004289, 0.41956983], + [0.7108932, 0.23906316, 0.42048681], + [0.70715842, 0.23808142, 0.42138647], + [0.70342811, 0.2370976, 0.42226844], + [0.69970218, 0.23611179, 0.42313282], + [0.69598055, 0.2351247, 0.42397678], + [0.69226314, 0.23413578, 0.42480327], + [0.68854988, 0.23314511, 0.42561234], + [0.68484064, 0.23215279, 0.42640419], + [0.68113541, 0.23115942, 0.42717615], + [0.67743412, 0.23016472, 0.42792989], + [0.67373662, 0.22916861, 0.42866642], + [0.67004287, 0.22817117, 0.42938576], + [0.66635279, 0.22717328, 0.43008427], + [0.66266621, 0.22617435, 0.43076552], + [0.65898313, 0.22517434, 0.43142956], + [0.65530349, 0.22417381, 0.43207427], + [0.65162696, 0.22317307, 0.4327001], + [0.64795375, 0.22217149, 0.43330852], + [0.64428351, 0.22116972, 0.43389854], + [0.64061624, 0.22016818, 0.43446845], + [0.63695183, 0.21916625, 0.43502123], + [0.63329016, 0.21816454, 0.43555493], + [0.62963102, 0.2171635, 0.43606881], + [0.62597451, 0.21616235, 0.43656529], + [0.62232019, 0.21516239, 0.43704153], + [0.61866821, 0.21416307, 0.43749868], + [0.61501835, 0.21316435, 0.43793808], + [0.61137029, 0.21216761, 0.4383556], + [0.60772426, 0.2111715, 0.43875552], + [0.60407977, 0.21017746, 0.43913439], + [0.60043678, 0.20918503, 0.43949412], + [0.59679524, 0.20819447, 0.43983393], + [0.59315487, 0.20720639, 0.44015254], + [0.58951566, 0.20622027, 0.44045213], + [0.58587715, 0.20523751, 0.44072926], + [0.5822395, 0.20425693, 0.44098758], + [0.57860222, 0.20328034, 0.44122241], + [0.57496549, 0.20230637, 0.44143805], + [0.57132875, 0.20133689, 0.4416298], + [0.56769215, 0.20037071, 0.44180142], + [0.5640552, 0.19940936, 0.44194923], + [0.56041794, 0.19845221, 0.44207535], + [0.55678004, 0.1975, 0.44217824], + [0.55314129, 0.19655316, 0.44225723], + [0.54950166, 0.19561118, 0.44231412], + [0.54585987, 0.19467771, 0.44234111], + [0.54221157, 0.19375869, 0.44233698], + [0.5385549, 0.19285696, 0.44229959], + [0.5348913, 0.19197036, 0.44222958], + [0.53122177, 0.1910974, 0.44212735], + [0.52754464, 0.19024042, 0.44199159], + [0.52386353, 0.18939409, 0.44182449], + [0.52017476, 0.18856368, 0.44162345], + [0.51648277, 0.18774266, 0.44139128], + [0.51278481, 0.18693492, 0.44112605], + [0.50908361, 0.18613639, 0.4408295], + [0.50537784, 0.18534893, 0.44050064], + [0.50166912, 0.18457008, 0.44014054], + [0.49795686, 0.18380056, 0.43974881], + [0.49424218, 0.18303865, 0.43932623], + [0.49052472, 0.18228477, 0.43887255], + [0.48680565, 0.1815371, 0.43838867], + [0.48308419, 0.18079663, 0.43787408], + [0.47936222, 0.18006056, 0.43733022], + [0.47563799, 0.17933127, 0.43675585], + [0.47191466, 0.17860416, 0.43615337], + [0.46818879, 0.17788392, 0.43552047], + [0.46446454, 0.17716458, 0.43486036], + [0.46073893, 0.17645017, 0.43417097], + [0.45701462, 0.17573691, 0.43345429], + [0.45329097, 0.17502549, 0.43271025], + [0.44956744, 0.17431649, 0.4319386], + [0.44584668, 0.17360625, 0.43114133], + [0.44212538, 0.17289906, 0.43031642], + [0.43840678, 0.17219041, 0.42946642], + [0.43469046, 0.17148074, 0.42859124], + [0.4309749, 0.17077192, 0.42769008], + [0.42726297, 0.17006003, 0.42676519], + [0.42355299, 0.16934709, 0.42581586], + [0.41984535, 0.16863258, 0.42484219], + [0.41614149, 0.16791429, 0.42384614], + [0.41244029, 0.16719372, 0.42282661], + [0.40874177, 0.16647061, 0.42178429], + [0.40504765, 0.16574261, 0.42072062], + [0.401357, 0.16501079, 0.41963528], + [0.397669, 0.16427607, 0.418528], + [0.39398585, 0.16353554, 0.41740053], + [0.39030735, 0.16278924, 0.41625344], + [0.3866314, 0.16203977, 0.41508517], + [0.38295904, 0.16128519, 0.41389849], + [0.37928736, 0.16052483, 0.41270599], + [0.37562649, 0.15974704, 0.41151182], + [0.37197803, 0.15895049, 0.41031532], + [0.36833779, 0.15813871, 0.40911916], + [0.36470944, 0.15730861, 0.40792149], + [0.36109117, 0.15646169, 0.40672362], + [0.35748213, 0.15559861, 0.40552633], + [0.353885, 0.15471714, 0.40432831], + [0.35029682, 0.15381967, 0.4031316], + [0.34671861, 0.1529053, 0.40193587], + [0.34315191, 0.15197275, 0.40074049], + [0.33959331, 0.15102466, 0.3995478], + [0.33604378, 0.15006017, 0.39835754], + [0.33250529, 0.14907766, 0.39716879], + [0.32897621, 0.14807831, 0.39598285], + [0.3254559, 0.14706248, 0.39480044], + [0.32194567, 0.14602909, 0.39362106], + [0.31844477, 0.14497857, 0.39244549], + [0.31494974, 0.14391333, 0.39127626], + [0.31146605, 0.14282918, 0.39011024], + [0.30798857, 0.1417297, 0.38895105], + [0.30451661, 0.14061515, 0.38779953], + [0.30105136, 0.13948445, 0.38665531], + [0.2975886, 0.1383403, 0.38552159], + [0.29408557, 0.13721193, 0.38442775] +] + + +_crest_lut = [ + [0.6468274, 0.80289262, 0.56592265], + [0.64233318, 0.80081141, 0.56639461], + [0.63791969, 0.7987162, 0.56674976], + [0.6335316, 0.79661833, 0.56706128], + [0.62915226, 0.7945212, 0.56735066], + [0.62477862, 0.79242543, 0.56762143], + [0.62042003, 0.79032918, 0.56786129], + [0.61606327, 0.78823508, 0.56808666], + [0.61171322, 0.78614216, 0.56829092], + [0.60736933, 0.78405055, 0.56847436], + [0.60302658, 0.78196121, 0.56864272], + [0.59868708, 0.77987374, 0.56879289], + [0.59435366, 0.77778758, 0.56892099], + [0.59001953, 0.77570403, 0.56903477], + [0.58568753, 0.77362254, 0.56913028], + [0.58135593, 0.77154342, 0.56920908], + [0.57702623, 0.76946638, 0.56926895], + [0.57269165, 0.76739266, 0.5693172], + [0.56835934, 0.76532092, 0.56934507], + [0.56402533, 0.76325185, 0.56935664], + [0.55968429, 0.76118643, 0.56935732], + [0.55534159, 0.75912361, 0.56934052], + [0.55099572, 0.75706366, 0.56930743], + [0.54664626, 0.75500662, 0.56925799], + [0.54228969, 0.75295306, 0.56919546], + [0.53792417, 0.75090328, 0.56912118], + [0.53355172, 0.74885687, 0.5690324], + [0.52917169, 0.74681387, 0.56892926], + [0.52478243, 0.74477453, 0.56881287], + [0.52038338, 0.74273888, 0.56868323], + [0.5159739, 0.74070697, 0.56854039], + [0.51155269, 0.73867895, 0.56838507], + [0.50711872, 0.73665492, 0.56821764], + [0.50267118, 0.73463494, 0.56803826], + [0.49822926, 0.73261388, 0.56785146], + [0.49381422, 0.73058524, 0.56767484], + [0.48942421, 0.72854938, 0.56751036], + [0.48505993, 0.72650623, 0.56735752], + [0.48072207, 0.72445575, 0.56721583], + [0.4764113, 0.72239788, 0.56708475], + [0.47212827, 0.72033258, 0.56696376], + [0.46787361, 0.71825983, 0.56685231], + [0.46364792, 0.71617961, 0.56674986], + [0.45945271, 0.71409167, 0.56665625], + [0.45528878, 0.71199595, 0.56657103], + [0.45115557, 0.70989276, 0.5664931], + [0.44705356, 0.70778212, 0.56642189], + [0.44298321, 0.70566406, 0.56635683], + [0.43894492, 0.70353863, 0.56629734], + [0.43493911, 0.70140588, 0.56624286], + [0.43096612, 0.69926587, 0.5661928], + [0.42702625, 0.69711868, 0.56614659], + [0.42311977, 0.69496438, 0.56610368], + [0.41924689, 0.69280308, 0.56606355], + [0.41540778, 0.69063486, 0.56602564], + [0.41160259, 0.68845984, 0.56598944], + [0.40783143, 0.68627814, 0.56595436], + [0.40409434, 0.68408988, 0.56591994], + [0.40039134, 0.68189518, 0.56588564], + [0.39672238, 0.6796942, 0.56585103], + [0.39308781, 0.67748696, 0.56581581], + [0.38949137, 0.67527276, 0.56578084], + [0.38592889, 0.67305266, 0.56574422], + [0.38240013, 0.67082685, 0.56570561], + [0.37890483, 0.66859548, 0.56566462], + [0.37544276, 0.66635871, 0.56562081], + [0.37201365, 0.66411673, 0.56557372], + [0.36861709, 0.6618697, 0.5655231], + [0.36525264, 0.65961782, 0.56546873], + [0.36191986, 0.65736125, 0.56541032], + [0.35861935, 0.65509998, 0.56534768], + [0.35535621, 0.65283302, 0.56528211], + [0.35212361, 0.65056188, 0.56521171], + [0.34892097, 0.64828676, 0.56513633], + [0.34574785, 0.64600783, 0.56505539], + [0.34260357, 0.64372528, 0.5649689], + [0.33948744, 0.64143931, 0.56487679], + [0.33639887, 0.6391501, 0.56477869], + [0.33334501, 0.63685626, 0.56467661], + [0.33031952, 0.63455911, 0.564569], + [0.3273199, 0.63225924, 0.56445488], + [0.32434526, 0.62995682, 0.56433457], + [0.32139487, 0.62765201, 0.56420795], + [0.31846807, 0.62534504, 0.56407446], + [0.3155731, 0.62303426, 0.56393695], + [0.31270304, 0.62072111, 0.56379321], + [0.30985436, 0.61840624, 0.56364307], + [0.30702635, 0.61608984, 0.56348606], + [0.30421803, 0.61377205, 0.56332267], + [0.30143611, 0.61145167, 0.56315419], + [0.29867863, 0.60912907, 0.56298054], + [0.29593872, 0.60680554, 0.56280022], + [0.29321538, 0.60448121, 0.56261376], + [0.2905079, 0.60215628, 0.56242036], + [0.28782827, 0.5998285, 0.56222366], + [0.28516521, 0.59749996, 0.56202093], + [0.28251558, 0.59517119, 0.56181204], + [0.27987847, 0.59284232, 0.56159709], + [0.27726216, 0.59051189, 0.56137785], + [0.27466434, 0.58818027, 0.56115433], + [0.2720767, 0.58584893, 0.56092486], + [0.26949829, 0.58351797, 0.56068983], + [0.26693801, 0.58118582, 0.56045121], + [0.26439366, 0.57885288, 0.56020858], + [0.26185616, 0.57652063, 0.55996077], + [0.25932459, 0.57418919, 0.55970795], + [0.25681303, 0.57185614, 0.55945297], + [0.25431024, 0.56952337, 0.55919385], + [0.25180492, 0.56719255, 0.5589305], + [0.24929311, 0.56486397, 0.5586654], + [0.24678356, 0.56253666, 0.55839491], + [0.24426587, 0.56021153, 0.55812473], + [0.24174022, 0.55788852, 0.55785448], + [0.23921167, 0.55556705, 0.55758211], + [0.23668315, 0.55324675, 0.55730676], + [0.23414742, 0.55092825, 0.55703167], + [0.23160473, 0.54861143, 0.5567573], + [0.22905996, 0.54629572, 0.55648168], + [0.22651648, 0.54398082, 0.5562029], + [0.22396709, 0.54166721, 0.55592542], + [0.22141221, 0.53935481, 0.55564885], + [0.21885269, 0.53704347, 0.55537294], + [0.21629986, 0.53473208, 0.55509319], + [0.21374297, 0.53242154, 0.5548144], + [0.21118255, 0.53011166, 0.55453708], + [0.2086192, 0.52780237, 0.55426067], + [0.20605624, 0.52549322, 0.55398479], + [0.20350004, 0.5231837, 0.55370601], + [0.20094292, 0.52087429, 0.55342884], + [0.19838567, 0.51856489, 0.55315283], + [0.19582911, 0.51625531, 0.55287818], + [0.19327413, 0.51394542, 0.55260469], + [0.19072933, 0.51163448, 0.5523289], + [0.18819045, 0.50932268, 0.55205372], + [0.18565609, 0.50701014, 0.55177937], + [0.18312739, 0.50469666, 0.55150597], + [0.18060561, 0.50238204, 0.55123374], + [0.178092, 0.50006616, 0.55096224], + [0.17558808, 0.49774882, 0.55069118], + [0.17310341, 0.49542924, 0.5504176], + [0.17063111, 0.49310789, 0.55014445], + [0.1681728, 0.49078458, 0.54987159], + [0.1657302, 0.48845913, 0.54959882], + [0.16330517, 0.48613135, 0.54932605], + [0.16089963, 0.48380104, 0.54905306], + [0.15851561, 0.48146803, 0.54877953], + [0.15615526, 0.47913212, 0.54850526], + [0.15382083, 0.47679313, 0.54822991], + [0.15151471, 0.47445087, 0.54795318], + [0.14924112, 0.47210502, 0.54767411], + [0.1470032, 0.46975537, 0.54739226], + [0.14480101, 0.46740187, 0.54710832], + [0.14263736, 0.46504434, 0.54682188], + [0.14051521, 0.46268258, 0.54653253], + [0.13843761, 0.46031639, 0.54623985], + [0.13640774, 0.45794558, 0.5459434], + [0.13442887, 0.45556994, 0.54564272], + [0.1325044, 0.45318928, 0.54533736], + [0.13063777, 0.4508034, 0.54502674], + [0.12883252, 0.44841211, 0.5447104], + [0.12709242, 0.44601517, 0.54438795], + [0.1254209, 0.44361244, 0.54405855], + [0.12382162, 0.44120373, 0.54372156], + [0.12229818, 0.43878887, 0.54337634], + [0.12085453, 0.4363676, 0.54302253], + [0.11949938, 0.43393955, 0.54265715], + [0.11823166, 0.43150478, 0.54228104], + [0.11705496, 0.42906306, 0.54189388], + [0.115972, 0.42661431, 0.54149449], + [0.11498598, 0.42415835, 0.54108222], + [0.11409965, 0.42169502, 0.54065622], + [0.11331533, 0.41922424, 0.5402155], + [0.11263542, 0.41674582, 0.53975931], + [0.1120615, 0.4142597, 0.53928656], + [0.11159738, 0.41176567, 0.53879549], + [0.11125248, 0.40926325, 0.53828203], + [0.11101698, 0.40675289, 0.53774864], + [0.11089152, 0.40423445, 0.53719455], + [0.11085121, 0.4017095, 0.53662425], + [0.11087217, 0.39917938, 0.53604354], + [0.11095515, 0.39664394, 0.53545166], + [0.11110676, 0.39410282, 0.53484509], + [0.11131735, 0.39155635, 0.53422678], + [0.11158595, 0.38900446, 0.53359634], + [0.11191139, 0.38644711, 0.5329534], + [0.11229224, 0.38388426, 0.53229748], + [0.11273683, 0.38131546, 0.53162393], + [0.11323438, 0.37874109, 0.53093619], + [0.11378271, 0.37616112, 0.53023413], + [0.11437992, 0.37357557, 0.52951727], + [0.11502681, 0.37098429, 0.52878396], + [0.11572661, 0.36838709, 0.52803124], + [0.11646936, 0.36578429, 0.52726234], + [0.11725299, 0.3631759, 0.52647685], + [0.1180755, 0.36056193, 0.52567436], + [0.1189438, 0.35794203, 0.5248497], + [0.11984752, 0.35531657, 0.52400649], + [0.1207833, 0.35268564, 0.52314492], + [0.12174895, 0.35004927, 0.52226461], + [0.12274959, 0.34740723, 0.52136104], + [0.12377809, 0.34475975, 0.52043639], + [0.12482961, 0.34210702, 0.51949179], + [0.125902, 0.33944908, 0.51852688], + [0.12699998, 0.33678574, 0.51753708], + [0.12811691, 0.33411727, 0.51652464], + [0.12924811, 0.33144384, 0.51549084], + [0.13039157, 0.32876552, 0.51443538], + [0.13155228, 0.32608217, 0.51335321], + [0.13272282, 0.32339407, 0.51224759], + [0.13389954, 0.32070138, 0.51111946], + [0.13508064, 0.31800419, 0.50996862], + [0.13627149, 0.31530238, 0.50878942], + [0.13746376, 0.31259627, 0.50758645], + [0.13865499, 0.30988598, 0.50636017], + [0.13984364, 0.30717161, 0.50511042], + [0.14103515, 0.30445309, 0.50383119], + [0.14222093, 0.30173071, 0.50252813], + [0.14339946, 0.2990046, 0.50120127], + [0.14456941, 0.29627483, 0.49985054], + [0.14573579, 0.29354139, 0.49847009], + [0.14689091, 0.29080452, 0.49706566], + [0.1480336, 0.28806432, 0.49563732], + [0.1491628, 0.28532086, 0.49418508], + [0.15028228, 0.28257418, 0.49270402], + [0.15138673, 0.27982444, 0.49119848], + [0.15247457, 0.27707172, 0.48966925], + [0.15354487, 0.2743161, 0.48811641], + [0.15459955, 0.27155765, 0.4865371], + [0.15563716, 0.26879642, 0.4849321], + [0.1566572, 0.26603191, 0.48330429], + [0.15765823, 0.26326032, 0.48167456], + [0.15862147, 0.26048295, 0.48005785], + [0.15954301, 0.25770084, 0.47845341], + [0.16043267, 0.25491144, 0.4768626], + [0.16129262, 0.25211406, 0.4752857], + [0.1621119, 0.24931169, 0.47372076], + [0.16290577, 0.24649998, 0.47217025], + [0.16366819, 0.24368054, 0.47063302], + [0.1644021, 0.24085237, 0.46910949], + [0.16510882, 0.2380149, 0.46759982], + [0.16579015, 0.23516739, 0.46610429], + [0.1664433, 0.2323105, 0.46462219], + [0.16707586, 0.22944155, 0.46315508], + [0.16768475, 0.22656122, 0.46170223], + [0.16826815, 0.22366984, 0.46026308], + [0.16883174, 0.22076514, 0.45883891], + [0.16937589, 0.21784655, 0.45742976], + [0.16990129, 0.21491339, 0.45603578], + [0.1704074, 0.21196535, 0.45465677], + [0.17089473, 0.20900176, 0.4532928], + [0.17136819, 0.20602012, 0.45194524], + [0.17182683, 0.20302012, 0.45061386], + [0.17227059, 0.20000106, 0.44929865], + [0.17270583, 0.19695949, 0.44800165], + [0.17313804, 0.19389201, 0.44672488], + [0.17363177, 0.19076859, 0.44549087] +] + + +_lut_dict = dict( + rocket=_rocket_lut, + mako=_mako_lut, + icefire=_icefire_lut, + vlag=_vlag_lut, + flare=_flare_lut, + crest=_crest_lut, + +) + +for _name, _lut in _lut_dict.items(): + + _cmap = colors.ListedColormap(_lut, _name) + locals()[_name] = _cmap + + _cmap_r = colors.ListedColormap(_lut[::-1], _name + "_r") + locals()[_name + "_r"] = _cmap_r + + register_colormap(_name, _cmap) + register_colormap(_name + "_r", _cmap_r) + +del colors, register_colormap diff --git a/lib/python3.10/site-packages/seaborn/distributions.py b/lib/python3.10/site-packages/seaborn/distributions.py new file mode 100644 index 0000000000000000000000000000000000000000..9f0cfacbdf8c5c117307bc906aa90fad390816ca --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/distributions.py @@ -0,0 +1,2546 @@ +"""Plotting functions for visualizing distributions.""" +from numbers import Number +from functools import partial +import math +import textwrap +import warnings + +import numpy as np +import pandas as pd +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.transforms as tx +from matplotlib.colors import to_rgba +from matplotlib.collections import LineCollection + +from ._oldcore import ( + VectorPlotter, +) + +# We have moved univariate histogram computation over to the new Hist class, +# but still use the older Histogram for bivariate computation. +from ._statistics import ECDF, Histogram, KDE +from ._stats.counting import Hist + +from .axisgrid import ( + FacetGrid, + _facet_docs, +) +from .utils import ( + remove_na, + _kde_support, + _normalize_kwargs, + _check_argument, + _assign_default_kwargs, + _default_color, +) +from .palettes import color_palette +from .external import husl +from .external.kde import gaussian_kde +from ._docstrings import ( + DocstringComponents, + _core_docs, +) + + +__all__ = ["displot", "histplot", "kdeplot", "ecdfplot", "rugplot", "distplot"] + +# ==================================================================================== # +# Module documentation +# ==================================================================================== # + +_dist_params = dict( + + multiple=""" +multiple : {{"layer", "stack", "fill"}} + Method for drawing multiple elements when semantic mapping creates subsets. + Only relevant with univariate data. + """, + log_scale=""" +log_scale : bool or number, or pair of bools or numbers + Set axis scale(s) to log. A single value sets the data axis for univariate + distributions and both axes for bivariate distributions. A pair of values + sets each axis independently. Numeric values are interpreted as the desired + base (default 10). If `False`, defer to the existing Axes scale. + """, + legend=""" +legend : bool + If False, suppress the legend for semantic variables. + """, + cbar=""" +cbar : bool + If True, add a colorbar to annotate the color mapping in a bivariate plot. + Note: Does not currently support plots with a ``hue`` variable well. + """, + cbar_ax=""" +cbar_ax : :class:`matplotlib.axes.Axes` + Pre-existing axes for the colorbar. + """, + cbar_kws=""" +cbar_kws : dict + Additional parameters passed to :meth:`matplotlib.figure.Figure.colorbar`. + """, +) + +_param_docs = DocstringComponents.from_nested_components( + core=_core_docs["params"], + facets=DocstringComponents(_facet_docs), + dist=DocstringComponents(_dist_params), + kde=DocstringComponents.from_function_params(KDE.__init__), + hist=DocstringComponents.from_function_params(Histogram.__init__), + ecdf=DocstringComponents.from_function_params(ECDF.__init__), +) + + +# ==================================================================================== # +# Internal API +# ==================================================================================== # + + +class _DistributionPlotter(VectorPlotter): + + semantics = "x", "y", "hue", "weights" + + wide_structure = {"x": "@values", "hue": "@columns"} + flat_structure = {"x": "@values"} + + def __init__( + self, + data=None, + variables={}, + ): + + super().__init__(data=data, variables=variables) + + @property + def univariate(self): + """Return True if only x or y are used.""" + # TODO this could go down to core, but putting it here now. + # We'd want to be conceptually clear that univariate only applies + # to x/y and not to other semantics, which can exist. + # We haven't settled on a good conceptual name for x/y. + return bool({"x", "y"} - set(self.variables)) + + @property + def data_variable(self): + """Return the variable with data for univariate plots.""" + # TODO This could also be in core, but it should have a better name. + if not self.univariate: + raise AttributeError("This is not a univariate plot") + return {"x", "y"}.intersection(self.variables).pop() + + @property + def has_xy_data(self): + """Return True at least one of x or y is defined.""" + # TODO see above points about where this should go + return bool({"x", "y"} & set(self.variables)) + + def _add_legend( + self, + ax_obj, artist, fill, element, multiple, alpha, artist_kws, legend_kws, + ): + """Add artists that reflect semantic mappings and put then in a legend.""" + # TODO note that this doesn't handle numeric mappings like the relational plots + handles = [] + labels = [] + for level in self._hue_map.levels: + color = self._hue_map(level) + + kws = self._artist_kws( + artist_kws, fill, element, multiple, color, alpha + ) + + # color gets added to the kws to workaround an issue with barplot's color + # cycle integration but it causes problems in this context where we are + # setting artist properties directly, so pop it off here + if "facecolor" in kws: + kws.pop("color", None) + + handles.append(artist(**kws)) + labels.append(level) + + if isinstance(ax_obj, mpl.axes.Axes): + ax_obj.legend(handles, labels, title=self.variables["hue"], **legend_kws) + else: # i.e. a FacetGrid. TODO make this better + legend_data = dict(zip(labels, handles)) + ax_obj.add_legend( + legend_data, + title=self.variables["hue"], + label_order=self.var_levels["hue"], + **legend_kws + ) + + def _artist_kws(self, kws, fill, element, multiple, color, alpha): + """Handle differences between artists in filled/unfilled plots.""" + kws = kws.copy() + if fill: + kws = _normalize_kwargs(kws, mpl.collections.PolyCollection) + kws.setdefault("facecolor", to_rgba(color, alpha)) + + if element == "bars": + # Make bar() interface with property cycle correctly + # https://github.com/matplotlib/matplotlib/issues/19385 + kws["color"] = "none" + + if multiple in ["stack", "fill"] or element == "bars": + kws.setdefault("edgecolor", mpl.rcParams["patch.edgecolor"]) + else: + kws.setdefault("edgecolor", to_rgba(color, 1)) + elif element == "bars": + kws["facecolor"] = "none" + kws["edgecolor"] = to_rgba(color, alpha) + else: + kws["color"] = to_rgba(color, alpha) + return kws + + def _quantile_to_level(self, data, quantile): + """Return data levels corresponding to quantile cuts of mass.""" + isoprop = np.asarray(quantile) + values = np.ravel(data) + sorted_values = np.sort(values)[::-1] + normalized_values = np.cumsum(sorted_values) / values.sum() + idx = np.searchsorted(normalized_values, 1 - isoprop) + levels = np.take(sorted_values, idx, mode="clip") + return levels + + def _cmap_from_color(self, color): + """Return a sequential colormap given a color seed.""" + # Like so much else here, this is broadly useful, but keeping it + # in this class to signify that I haven't thought overly hard about it... + r, g, b, _ = to_rgba(color) + h, s, _ = husl.rgb_to_husl(r, g, b) + xx = np.linspace(-1, 1, int(1.15 * 256))[:256] + ramp = np.zeros((256, 3)) + ramp[:, 0] = h + ramp[:, 1] = s * np.cos(xx) + ramp[:, 2] = np.linspace(35, 80, 256) + colors = np.clip([husl.husl_to_rgb(*hsl) for hsl in ramp], 0, 1) + return mpl.colors.ListedColormap(colors[::-1]) + + def _default_discrete(self): + """Find default values for discrete hist estimation based on variable type.""" + if self.univariate: + discrete = self.var_types[self.data_variable] == "categorical" + else: + discrete_x = self.var_types["x"] == "categorical" + discrete_y = self.var_types["y"] == "categorical" + discrete = discrete_x, discrete_y + return discrete + + def _resolve_multiple(self, curves, multiple): + """Modify the density data structure to handle multiple densities.""" + + # Default baselines have all densities starting at 0 + baselines = {k: np.zeros_like(v) for k, v in curves.items()} + + # TODO we should have some central clearinghouse for checking if any + # "grouping" (terminnology?) semantics have been assigned + if "hue" not in self.variables: + return curves, baselines + + if multiple in ("stack", "fill"): + + # Setting stack or fill means that the curves share a + # support grid / set of bin edges, so we can make a dataframe + # Reverse the column order to plot from top to bottom + curves = pd.DataFrame(curves).iloc[:, ::-1] + + # Find column groups that are nested within col/row variables + column_groups = {} + for i, keyd in enumerate(map(dict, curves.columns)): + facet_key = keyd.get("col", None), keyd.get("row", None) + column_groups.setdefault(facet_key, []) + column_groups[facet_key].append(i) + + baselines = curves.copy() + for col_idxs in column_groups.values(): + cols = curves.columns[col_idxs] + + norm_constant = curves[cols].sum(axis="columns") + + # Take the cumulative sum to stack + curves[cols] = curves[cols].cumsum(axis="columns") + + # Normalize by row sum to fill + if multiple == "fill": + curves[cols] = curves[cols].div(norm_constant, axis="index") + + # Define where each segment starts + baselines[cols] = curves[cols].shift(1, axis=1).fillna(0) + + if multiple == "dodge": + + # Account for the unique semantic (non-faceting) levels + # This will require rethiniking if we add other semantics! + hue_levels = self.var_levels["hue"] + n = len(hue_levels) + for key in curves: + level = dict(key)["hue"] + hist = curves[key].reset_index(name="heights") + level_idx = hue_levels.index(level) + if self._log_scaled(self.data_variable): + log_min = np.log10(hist["edges"]) + log_max = np.log10(hist["edges"] + hist["widths"]) + log_width = (log_max - log_min) / n + new_min = np.power(10, log_min + level_idx * log_width) + new_max = np.power(10, log_min + (level_idx + 1) * log_width) + hist["widths"] = new_max - new_min + hist["edges"] = new_min + else: + hist["widths"] /= n + hist["edges"] += level_idx * hist["widths"] + + curves[key] = hist.set_index(["edges", "widths"])["heights"] + + return curves, baselines + + # -------------------------------------------------------------------------------- # + # Computation + # -------------------------------------------------------------------------------- # + + def _compute_univariate_density( + self, + data_variable, + common_norm, + common_grid, + estimate_kws, + log_scale, + warn_singular=True, + ): + + # Initialize the estimator object + estimator = KDE(**estimate_kws) + + if set(self.variables) - {"x", "y"}: + if common_grid: + all_observations = self.comp_data.dropna() + estimator.define_support(all_observations[data_variable]) + else: + common_norm = False + + all_data = self.plot_data.dropna() + if common_norm and "weights" in all_data: + whole_weight = all_data["weights"].sum() + else: + whole_weight = len(all_data) + + densities = {} + + for sub_vars, sub_data in self.iter_data("hue", from_comp_data=True): + + # Extract the data points from this sub set and remove nulls + observations = sub_data[data_variable] + + # Extract the weights for this subset of observations + if "weights" in self.variables: + weights = sub_data["weights"] + part_weight = weights.sum() + else: + weights = None + part_weight = len(sub_data) + + # Estimate the density of observations at this level + variance = np.nan_to_num(observations.var()) + singular = len(observations) < 2 or math.isclose(variance, 0) + try: + if not singular: + # Convoluted approach needed because numerical failures + # can manifest in a few different ways. + density, support = estimator(observations, weights=weights) + except np.linalg.LinAlgError: + singular = True + + if singular: + msg = ( + "Dataset has 0 variance; skipping density estimate. " + "Pass `warn_singular=False` to disable this warning." + ) + if warn_singular: + warnings.warn(msg, UserWarning, stacklevel=4) + continue + + if log_scale: + support = np.power(10, support) + + # Apply a scaling factor so that the integral over all subsets is 1 + if common_norm: + density *= part_weight / whole_weight + + # Store the density for this level + key = tuple(sub_vars.items()) + densities[key] = pd.Series(density, index=support) + + return densities + + # -------------------------------------------------------------------------------- # + # Plotting + # -------------------------------------------------------------------------------- # + + def plot_univariate_histogram( + self, + multiple, + element, + fill, + common_norm, + common_bins, + shrink, + kde, + kde_kws, + color, + legend, + line_kws, + estimate_kws, + **plot_kws, + ): + + # -- Default keyword dicts + kde_kws = {} if kde_kws is None else kde_kws.copy() + line_kws = {} if line_kws is None else line_kws.copy() + estimate_kws = {} if estimate_kws is None else estimate_kws.copy() + + # -- Input checking + _check_argument("multiple", ["layer", "stack", "fill", "dodge"], multiple) + _check_argument("element", ["bars", "step", "poly"], element) + + auto_bins_with_weights = ( + "weights" in self.variables + and estimate_kws["bins"] == "auto" + and estimate_kws["binwidth"] is None + and not estimate_kws["discrete"] + ) + if auto_bins_with_weights: + msg = ( + "`bins` cannot be 'auto' when using weights. " + "Setting `bins=10`, but you will likely want to adjust." + ) + warnings.warn(msg, UserWarning) + estimate_kws["bins"] = 10 + + # Simplify downstream code if we are not normalizing + if estimate_kws["stat"] == "count": + common_norm = False + + orient = self.data_variable + + # Now initialize the Histogram estimator + estimator = Hist(**estimate_kws) + histograms = {} + + # Do pre-compute housekeeping related to multiple groups + all_data = self.comp_data.dropna() + all_weights = all_data.get("weights", None) + + multiple_histograms = set(self.variables) - {"x", "y"} + if multiple_histograms: + if common_bins: + bin_kws = estimator._define_bin_params(all_data, orient, None) + else: + common_norm = False + + if common_norm and all_weights is not None: + whole_weight = all_weights.sum() + else: + whole_weight = len(all_data) + + # Estimate the smoothed kernel densities, for use later + if kde: + # TODO alternatively, clip at min/max bins? + kde_kws.setdefault("cut", 0) + kde_kws["cumulative"] = estimate_kws["cumulative"] + log_scale = self._log_scaled(self.data_variable) + densities = self._compute_univariate_density( + self.data_variable, + common_norm, + common_bins, + kde_kws, + log_scale, + warn_singular=False, + ) + + # First pass through the data to compute the histograms + for sub_vars, sub_data in self.iter_data("hue", from_comp_data=True): + + # Prepare the relevant data + key = tuple(sub_vars.items()) + orient = self.data_variable + + if "weights" in self.variables: + sub_data["weight"] = sub_data.pop("weights") + part_weight = sub_data["weight"].sum() + else: + part_weight = len(sub_data) + + # Do the histogram computation + if not (multiple_histograms and common_bins): + bin_kws = estimator._define_bin_params(sub_data, orient, None) + res = estimator._normalize(estimator._eval(sub_data, orient, bin_kws)) + heights = res[estimator.stat].to_numpy() + widths = res["space"].to_numpy() + edges = res[orient].to_numpy() - widths / 2 + + # Rescale the smoothed curve to match the histogram + if kde and key in densities: + density = densities[key] + if estimator.cumulative: + hist_norm = heights.max() + else: + hist_norm = (heights * widths).sum() + densities[key] *= hist_norm + + # Convert edges back to original units for plotting + if self._log_scaled(self.data_variable): + widths = np.power(10, edges + widths) - np.power(10, edges) + edges = np.power(10, edges) + + # Pack the histogram data and metadata together + edges = edges + (1 - shrink) / 2 * widths + widths *= shrink + index = pd.MultiIndex.from_arrays([ + pd.Index(edges, name="edges"), + pd.Index(widths, name="widths"), + ]) + hist = pd.Series(heights, index=index, name="heights") + + # Apply scaling to normalize across groups + if common_norm: + hist *= part_weight / whole_weight + + # Store the finalized histogram data for future plotting + histograms[key] = hist + + # Modify the histogram and density data to resolve multiple groups + histograms, baselines = self._resolve_multiple(histograms, multiple) + if kde: + densities, _ = self._resolve_multiple( + densities, None if multiple == "dodge" else multiple + ) + + # Set autoscaling-related meta + sticky_stat = (0, 1) if multiple == "fill" else (0, np.inf) + if multiple == "fill": + # Filled plots should not have any margins + bin_vals = histograms.index.to_frame() + edges = bin_vals["edges"] + widths = bin_vals["widths"] + sticky_data = ( + edges.min(), + edges.max() + widths.loc[edges.idxmax()] + ) + else: + sticky_data = [] + + # --- Handle default visual attributes + + # Note: default linewidth is determined after plotting + + # Default alpha should depend on other parameters + if fill: + # Note: will need to account for other grouping semantics if added + if "hue" in self.variables and multiple == "layer": + default_alpha = .5 if element == "bars" else .25 + elif kde: + default_alpha = .5 + else: + default_alpha = .75 + else: + default_alpha = 1 + alpha = plot_kws.pop("alpha", default_alpha) # TODO make parameter? + + hist_artists = [] + + # Go back through the dataset and draw the plots + for sub_vars, _ in self.iter_data("hue", reverse=True): + + key = tuple(sub_vars.items()) + hist = histograms[key].rename("heights").reset_index() + bottom = np.asarray(baselines[key]) + + ax = self._get_axes(sub_vars) + + # Define the matplotlib attributes that depend on semantic mapping + if "hue" in self.variables: + sub_color = self._hue_map(sub_vars["hue"]) + else: + sub_color = color + + artist_kws = self._artist_kws( + plot_kws, fill, element, multiple, sub_color, alpha + ) + + if element == "bars": + + # Use matplotlib bar plotting + + plot_func = ax.bar if self.data_variable == "x" else ax.barh + artists = plot_func( + hist["edges"], + hist["heights"] - bottom, + hist["widths"], + bottom, + align="edge", + **artist_kws, + ) + + for bar in artists: + if self.data_variable == "x": + bar.sticky_edges.x[:] = sticky_data + bar.sticky_edges.y[:] = sticky_stat + else: + bar.sticky_edges.x[:] = sticky_stat + bar.sticky_edges.y[:] = sticky_data + + hist_artists.extend(artists) + + else: + + # Use either fill_between or plot to draw hull of histogram + if element == "step": + + final = hist.iloc[-1] + x = np.append(hist["edges"], final["edges"] + final["widths"]) + y = np.append(hist["heights"], final["heights"]) + b = np.append(bottom, bottom[-1]) + + if self.data_variable == "x": + step = "post" + drawstyle = "steps-post" + else: + step = "post" # fillbetweenx handles mapping internally + drawstyle = "steps-pre" + + elif element == "poly": + + x = hist["edges"] + hist["widths"] / 2 + y = hist["heights"] + b = bottom + + step = None + drawstyle = None + + if self.data_variable == "x": + if fill: + artist = ax.fill_between(x, b, y, step=step, **artist_kws) + else: + artist, = ax.plot(x, y, drawstyle=drawstyle, **artist_kws) + artist.sticky_edges.x[:] = sticky_data + artist.sticky_edges.y[:] = sticky_stat + else: + if fill: + artist = ax.fill_betweenx(x, b, y, step=step, **artist_kws) + else: + artist, = ax.plot(y, x, drawstyle=drawstyle, **artist_kws) + artist.sticky_edges.x[:] = sticky_stat + artist.sticky_edges.y[:] = sticky_data + + hist_artists.append(artist) + + if kde: + + # Add in the density curves + + try: + density = densities[key] + except KeyError: + continue + support = density.index + + if "x" in self.variables: + line_args = support, density + sticky_x, sticky_y = None, (0, np.inf) + else: + line_args = density, support + sticky_x, sticky_y = (0, np.inf), None + + line_kws["color"] = to_rgba(sub_color, 1) + line, = ax.plot( + *line_args, **line_kws, + ) + + if sticky_x is not None: + line.sticky_edges.x[:] = sticky_x + if sticky_y is not None: + line.sticky_edges.y[:] = sticky_y + + if element == "bars" and "linewidth" not in plot_kws: + + # Now we handle linewidth, which depends on the scaling of the plot + + # We will base everything on the minimum bin width + hist_metadata = pd.concat([ + # Use .items for generality over dict or df + h.index.to_frame() for _, h in histograms.items() + ]).reset_index(drop=True) + thin_bar_idx = hist_metadata["widths"].idxmin() + binwidth = hist_metadata.loc[thin_bar_idx, "widths"] + left_edge = hist_metadata.loc[thin_bar_idx, "edges"] + + # Set initial value + default_linewidth = math.inf + + # Loop through subsets based only on facet variables + for sub_vars, _ in self.iter_data(): + + ax = self._get_axes(sub_vars) + + # Needed in some cases to get valid transforms. + # Innocuous in other cases? + ax.autoscale_view() + + # Convert binwidth from data coordinates to pixels + pts_x, pts_y = 72 / ax.figure.dpi * abs( + ax.transData.transform([left_edge + binwidth] * 2) + - ax.transData.transform([left_edge] * 2) + ) + if self.data_variable == "x": + binwidth_points = pts_x + else: + binwidth_points = pts_y + + # The relative size of the lines depends on the appearance + # This is a provisional value and may need more tweaking + default_linewidth = min(.1 * binwidth_points, default_linewidth) + + # Set the attributes + for bar in hist_artists: + + # Don't let the lines get too thick + max_linewidth = bar.get_linewidth() + if not fill: + max_linewidth *= 1.5 + + linewidth = min(default_linewidth, max_linewidth) + + # If not filling, don't let lines disappear + if not fill: + min_linewidth = .5 + linewidth = max(linewidth, min_linewidth) + + bar.set_linewidth(linewidth) + + # --- Finalize the plot ---- + + # Axis labels + ax = self.ax if self.ax is not None else self.facets.axes.flat[0] + default_x = default_y = "" + if self.data_variable == "x": + default_y = estimator.stat.capitalize() + if self.data_variable == "y": + default_x = estimator.stat.capitalize() + self._add_axis_labels(ax, default_x, default_y) + + # Legend for semantic variables + if "hue" in self.variables and legend: + + if fill or element == "bars": + artist = partial(mpl.patches.Patch) + else: + artist = partial(mpl.lines.Line2D, [], []) + + ax_obj = self.ax if self.ax is not None else self.facets + self._add_legend( + ax_obj, artist, fill, element, multiple, alpha, plot_kws, {}, + ) + + def plot_bivariate_histogram( + self, + common_bins, common_norm, + thresh, pthresh, pmax, + color, legend, + cbar, cbar_ax, cbar_kws, + estimate_kws, + **plot_kws, + ): + + # Default keyword dicts + cbar_kws = {} if cbar_kws is None else cbar_kws.copy() + + # Now initialize the Histogram estimator + estimator = Histogram(**estimate_kws) + + # Do pre-compute housekeeping related to multiple groups + if set(self.variables) - {"x", "y"}: + all_data = self.comp_data.dropna() + if common_bins: + estimator.define_bin_params( + all_data["x"], + all_data["y"], + all_data.get("weights", None), + ) + else: + common_norm = False + + # -- Determine colormap threshold and norm based on the full data + + full_heights = [] + for _, sub_data in self.iter_data(from_comp_data=True): + sub_heights, _ = estimator( + sub_data["x"], sub_data["y"], sub_data.get("weights", None) + ) + full_heights.append(sub_heights) + + common_color_norm = not set(self.variables) - {"x", "y"} or common_norm + + if pthresh is not None and common_color_norm: + thresh = self._quantile_to_level(full_heights, pthresh) + + plot_kws.setdefault("vmin", 0) + if common_color_norm: + if pmax is not None: + vmax = self._quantile_to_level(full_heights, pmax) + else: + vmax = plot_kws.pop("vmax", max(map(np.max, full_heights))) + else: + vmax = None + + # Get a default color + # (We won't follow the color cycle here, as multiple plots are unlikely) + if color is None: + color = "C0" + + # --- Loop over data (subsets) and draw the histograms + for sub_vars, sub_data in self.iter_data("hue", from_comp_data=True): + + if sub_data.empty: + continue + + # Do the histogram computation + heights, (x_edges, y_edges) = estimator( + sub_data["x"], + sub_data["y"], + weights=sub_data.get("weights", None), + ) + + # Check for log scaling on the data axis + if self._log_scaled("x"): + x_edges = np.power(10, x_edges) + if self._log_scaled("y"): + y_edges = np.power(10, y_edges) + + # Apply scaling to normalize across groups + if estimator.stat != "count" and common_norm: + heights *= len(sub_data) / len(all_data) + + # Define the specific kwargs for this artist + artist_kws = plot_kws.copy() + if "hue" in self.variables: + color = self._hue_map(sub_vars["hue"]) + cmap = self._cmap_from_color(color) + artist_kws["cmap"] = cmap + else: + cmap = artist_kws.pop("cmap", None) + if isinstance(cmap, str): + cmap = color_palette(cmap, as_cmap=True) + elif cmap is None: + cmap = self._cmap_from_color(color) + artist_kws["cmap"] = cmap + + # Set the upper norm on the colormap + if not common_color_norm and pmax is not None: + vmax = self._quantile_to_level(heights, pmax) + if vmax is not None: + artist_kws["vmax"] = vmax + + # Make cells at or below the threshold transparent + if not common_color_norm and pthresh: + thresh = self._quantile_to_level(heights, pthresh) + if thresh is not None: + heights = np.ma.masked_less_equal(heights, thresh) + + # Get the axes for this plot + ax = self._get_axes(sub_vars) + + # pcolormesh is going to turn the grid off, but we want to keep it + # I'm not sure if there's a better way to get the grid state + x_grid = any([l.get_visible() for l in ax.xaxis.get_gridlines()]) + y_grid = any([l.get_visible() for l in ax.yaxis.get_gridlines()]) + + mesh = ax.pcolormesh( + x_edges, + y_edges, + heights.T, + **artist_kws, + ) + + # pcolormesh sets sticky edges, but we only want them if not thresholding + if thresh is not None: + mesh.sticky_edges.x[:] = [] + mesh.sticky_edges.y[:] = [] + + # Add an optional colorbar + # Note, we want to improve this. When hue is used, it will stack + # multiple colorbars with redundant ticks in an ugly way. + # But it's going to take some work to have multiple colorbars that + # share ticks nicely. + if cbar: + ax.figure.colorbar(mesh, cbar_ax, ax, **cbar_kws) + + # Reset the grid state + if x_grid: + ax.grid(True, axis="x") + if y_grid: + ax.grid(True, axis="y") + + # --- Finalize the plot + + ax = self.ax if self.ax is not None else self.facets.axes.flat[0] + self._add_axis_labels(ax) + + if "hue" in self.variables and legend: + + # TODO if possible, I would like to move the contour + # intensity information into the legend too and label the + # iso proportions rather than the raw density values + + artist_kws = {} + artist = partial(mpl.patches.Patch) + ax_obj = self.ax if self.ax is not None else self.facets + self._add_legend( + ax_obj, artist, True, False, "layer", 1, artist_kws, {}, + ) + + def plot_univariate_density( + self, + multiple, + common_norm, + common_grid, + warn_singular, + fill, + color, + legend, + estimate_kws, + **plot_kws, + ): + + # Handle conditional defaults + if fill is None: + fill = multiple in ("stack", "fill") + + # Preprocess the matplotlib keyword dictionaries + if fill: + artist = mpl.collections.PolyCollection + else: + artist = mpl.lines.Line2D + plot_kws = _normalize_kwargs(plot_kws, artist) + + # Input checking + _check_argument("multiple", ["layer", "stack", "fill"], multiple) + + # Always share the evaluation grid when stacking + subsets = bool(set(self.variables) - {"x", "y"}) + if subsets and multiple in ("stack", "fill"): + common_grid = True + + # Check if the data axis is log scaled + log_scale = self._log_scaled(self.data_variable) + + # Do the computation + densities = self._compute_univariate_density( + self.data_variable, + common_norm, + common_grid, + estimate_kws, + log_scale, + warn_singular, + ) + + # Adjust densities based on the `multiple` rule + densities, baselines = self._resolve_multiple(densities, multiple) + + # Control the interaction with autoscaling by defining sticky_edges + # i.e. we don't want autoscale margins below the density curve + sticky_density = (0, 1) if multiple == "fill" else (0, np.inf) + + if multiple == "fill": + # Filled plots should not have any margins + sticky_support = densities.index.min(), densities.index.max() + else: + sticky_support = [] + + if fill: + if multiple == "layer": + default_alpha = .25 + else: + default_alpha = .75 + else: + default_alpha = 1 + alpha = plot_kws.pop("alpha", default_alpha) # TODO make parameter? + + # Now iterate through the subsets and draw the densities + # We go backwards so stacked densities read from top-to-bottom + for sub_vars, _ in self.iter_data("hue", reverse=True): + + # Extract the support grid and density curve for this level + key = tuple(sub_vars.items()) + try: + density = densities[key] + except KeyError: + continue + support = density.index + fill_from = baselines[key] + + ax = self._get_axes(sub_vars) + + if "hue" in self.variables: + sub_color = self._hue_map(sub_vars["hue"]) + else: + sub_color = color + + artist_kws = self._artist_kws( + plot_kws, fill, False, multiple, sub_color, alpha + ) + + # Either plot a curve with observation values on the x axis + if "x" in self.variables: + + if fill: + artist = ax.fill_between(support, fill_from, density, **artist_kws) + + else: + artist, = ax.plot(support, density, **artist_kws) + + artist.sticky_edges.x[:] = sticky_support + artist.sticky_edges.y[:] = sticky_density + + # Or plot a curve with observation values on the y axis + else: + if fill: + artist = ax.fill_betweenx(support, fill_from, density, **artist_kws) + else: + artist, = ax.plot(density, support, **artist_kws) + + artist.sticky_edges.x[:] = sticky_density + artist.sticky_edges.y[:] = sticky_support + + # --- Finalize the plot ---- + + ax = self.ax if self.ax is not None else self.facets.axes.flat[0] + default_x = default_y = "" + if self.data_variable == "x": + default_y = "Density" + if self.data_variable == "y": + default_x = "Density" + self._add_axis_labels(ax, default_x, default_y) + + if "hue" in self.variables and legend: + + if fill: + artist = partial(mpl.patches.Patch) + else: + artist = partial(mpl.lines.Line2D, [], []) + + ax_obj = self.ax if self.ax is not None else self.facets + self._add_legend( + ax_obj, artist, fill, False, multiple, alpha, plot_kws, {}, + ) + + def plot_bivariate_density( + self, + common_norm, + fill, + levels, + thresh, + color, + legend, + cbar, + warn_singular, + cbar_ax, + cbar_kws, + estimate_kws, + **contour_kws, + ): + + contour_kws = contour_kws.copy() + + estimator = KDE(**estimate_kws) + + if not set(self.variables) - {"x", "y"}: + common_norm = False + + all_data = self.plot_data.dropna() + + # Loop through the subsets and estimate the KDEs + densities, supports = {}, {} + + for sub_vars, sub_data in self.iter_data("hue", from_comp_data=True): + + # Extract the data points from this sub set + observations = sub_data[["x", "y"]] + min_variance = observations.var().fillna(0).min() + observations = observations["x"], observations["y"] + + # Extract the weights for this subset of observations + if "weights" in self.variables: + weights = sub_data["weights"] + else: + weights = None + + # Estimate the density of observations at this level + singular = math.isclose(min_variance, 0) + try: + if not singular: + density, support = estimator(*observations, weights=weights) + except np.linalg.LinAlgError: + # Testing for 0 variance doesn't catch all cases where scipy raises, + # but we can also get a ValueError, so we need this convoluted approach + singular = True + + if singular: + msg = ( + "KDE cannot be estimated (0 variance or perfect covariance). " + "Pass `warn_singular=False` to disable this warning." + ) + if warn_singular: + warnings.warn(msg, UserWarning, stacklevel=3) + continue + + # Transform the support grid back to the original scale + xx, yy = support + if self._log_scaled("x"): + xx = np.power(10, xx) + if self._log_scaled("y"): + yy = np.power(10, yy) + support = xx, yy + + # Apply a scaling factor so that the integral over all subsets is 1 + if common_norm: + density *= len(sub_data) / len(all_data) + + key = tuple(sub_vars.items()) + densities[key] = density + supports[key] = support + + # Define a grid of iso-proportion levels + if thresh is None: + thresh = 0 + if isinstance(levels, Number): + levels = np.linspace(thresh, 1, levels) + else: + if min(levels) < 0 or max(levels) > 1: + raise ValueError("levels must be in [0, 1]") + + # Transform from iso-proportions to iso-densities + if common_norm: + common_levels = self._quantile_to_level( + list(densities.values()), levels, + ) + draw_levels = {k: common_levels for k in densities} + else: + draw_levels = { + k: self._quantile_to_level(d, levels) + for k, d in densities.items() + } + + # Define the coloring of the contours + if "hue" in self.variables: + for param in ["cmap", "colors"]: + if param in contour_kws: + msg = f"{param} parameter ignored when using hue mapping." + warnings.warn(msg, UserWarning) + contour_kws.pop(param) + else: + + # Work out a default coloring of the contours + coloring_given = set(contour_kws) & {"cmap", "colors"} + if fill and not coloring_given: + cmap = self._cmap_from_color(color) + contour_kws["cmap"] = cmap + if not fill and not coloring_given: + contour_kws["colors"] = [color] + + # Use our internal colormap lookup + cmap = contour_kws.pop("cmap", None) + if isinstance(cmap, str): + cmap = color_palette(cmap, as_cmap=True) + if cmap is not None: + contour_kws["cmap"] = cmap + + # Loop through the subsets again and plot the data + for sub_vars, _ in self.iter_data("hue"): + + if "hue" in sub_vars: + color = self._hue_map(sub_vars["hue"]) + if fill: + contour_kws["cmap"] = self._cmap_from_color(color) + else: + contour_kws["colors"] = [color] + + ax = self._get_axes(sub_vars) + + # Choose the function to plot with + # TODO could add a pcolormesh based option as well + # Which would look something like element="raster" + if fill: + contour_func = ax.contourf + else: + contour_func = ax.contour + + key = tuple(sub_vars.items()) + if key not in densities: + continue + density = densities[key] + xx, yy = supports[key] + + label = contour_kws.pop("label", None) + + cset = contour_func( + xx, yy, density, + levels=draw_levels[key], + **contour_kws, + ) + + if "hue" not in self.variables: + cset.collections[0].set_label(label) + + # Add a color bar representing the contour heights + # Note: this shows iso densities, not iso proportions + # See more notes in histplot about how this could be improved + if cbar: + cbar_kws = {} if cbar_kws is None else cbar_kws + ax.figure.colorbar(cset, cbar_ax, ax, **cbar_kws) + + # --- Finalize the plot + ax = self.ax if self.ax is not None else self.facets.axes.flat[0] + self._add_axis_labels(ax) + + if "hue" in self.variables and legend: + + # TODO if possible, I would like to move the contour + # intensity information into the legend too and label the + # iso proportions rather than the raw density values + + artist_kws = {} + if fill: + artist = partial(mpl.patches.Patch) + else: + artist = partial(mpl.lines.Line2D, [], []) + + ax_obj = self.ax if self.ax is not None else self.facets + self._add_legend( + ax_obj, artist, fill, False, "layer", 1, artist_kws, {}, + ) + + def plot_univariate_ecdf(self, estimate_kws, legend, **plot_kws): + + estimator = ECDF(**estimate_kws) + + # Set the draw style to step the right way for the data variable + drawstyles = dict(x="steps-post", y="steps-pre") + plot_kws["drawstyle"] = drawstyles[self.data_variable] + + # Loop through the subsets, transform and plot the data + for sub_vars, sub_data in self.iter_data( + "hue", reverse=True, from_comp_data=True, + ): + + # Compute the ECDF + if sub_data.empty: + continue + + observations = sub_data[self.data_variable] + weights = sub_data.get("weights", None) + stat, vals = estimator(observations, weights=weights) + + # Assign attributes based on semantic mapping + artist_kws = plot_kws.copy() + if "hue" in self.variables: + artist_kws["color"] = self._hue_map(sub_vars["hue"]) + + # Return the data variable to the linear domain + # This needs an automatic solution; see GH2409 + if self._log_scaled(self.data_variable): + vals = np.power(10, vals) + vals[0] = -np.inf + + # Work out the orientation of the plot + if self.data_variable == "x": + plot_args = vals, stat + stat_variable = "y" + else: + plot_args = stat, vals + stat_variable = "x" + + if estimator.stat == "count": + top_edge = len(observations) + else: + top_edge = 1 + + # Draw the line for this subset + ax = self._get_axes(sub_vars) + artist, = ax.plot(*plot_args, **artist_kws) + sticky_edges = getattr(artist.sticky_edges, stat_variable) + sticky_edges[:] = 0, top_edge + + # --- Finalize the plot ---- + ax = self.ax if self.ax is not None else self.facets.axes.flat[0] + stat = estimator.stat.capitalize() + default_x = default_y = "" + if self.data_variable == "x": + default_y = stat + if self.data_variable == "y": + default_x = stat + self._add_axis_labels(ax, default_x, default_y) + + if "hue" in self.variables and legend: + artist = partial(mpl.lines.Line2D, [], []) + alpha = plot_kws.get("alpha", 1) + ax_obj = self.ax if self.ax is not None else self.facets + self._add_legend( + ax_obj, artist, False, False, None, alpha, plot_kws, {}, + ) + + def plot_rug(self, height, expand_margins, legend, **kws): + + for sub_vars, sub_data, in self.iter_data(from_comp_data=True): + + ax = self._get_axes(sub_vars) + + kws.setdefault("linewidth", 1) + + if expand_margins: + xmarg, ymarg = ax.margins() + if "x" in self.variables: + ymarg += height * 2 + if "y" in self.variables: + xmarg += height * 2 + ax.margins(x=xmarg, y=ymarg) + + if "hue" in self.variables: + kws.pop("c", None) + kws.pop("color", None) + + if "x" in self.variables: + self._plot_single_rug(sub_data, "x", height, ax, kws) + if "y" in self.variables: + self._plot_single_rug(sub_data, "y", height, ax, kws) + + # --- Finalize the plot + self._add_axis_labels(ax) + if "hue" in self.variables and legend: + # TODO ideally i'd like the legend artist to look like a rug + legend_artist = partial(mpl.lines.Line2D, [], []) + self._add_legend( + ax, legend_artist, False, False, None, 1, {}, {}, + ) + + def _plot_single_rug(self, sub_data, var, height, ax, kws): + """Draw a rugplot along one axis of the plot.""" + vector = sub_data[var] + n = len(vector) + + # Return data to linear domain + # This needs an automatic solution; see GH2409 + if self._log_scaled(var): + vector = np.power(10, vector) + + # We'll always add a single collection with varying colors + if "hue" in self.variables: + colors = self._hue_map(sub_data["hue"]) + else: + colors = None + + # Build the array of values for the LineCollection + if var == "x": + + trans = tx.blended_transform_factory(ax.transData, ax.transAxes) + xy_pairs = np.column_stack([ + np.repeat(vector, 2), np.tile([0, height], n) + ]) + + if var == "y": + + trans = tx.blended_transform_factory(ax.transAxes, ax.transData) + xy_pairs = np.column_stack([ + np.tile([0, height], n), np.repeat(vector, 2) + ]) + + # Draw the lines on the plot + line_segs = xy_pairs.reshape([n, 2, 2]) + ax.add_collection(LineCollection( + line_segs, transform=trans, colors=colors, **kws + )) + + ax.autoscale_view(scalex=var == "x", scaley=var == "y") + + +class _DistributionFacetPlotter(_DistributionPlotter): + + semantics = _DistributionPlotter.semantics + ("col", "row") + + +# ==================================================================================== # +# External API +# ==================================================================================== # + +def histplot( + data=None, *, + # Vector variables + x=None, y=None, hue=None, weights=None, + # Histogram computation parameters + stat="count", bins="auto", binwidth=None, binrange=None, + discrete=None, cumulative=False, common_bins=True, common_norm=True, + # Histogram appearance parameters + multiple="layer", element="bars", fill=True, shrink=1, + # Histogram smoothing with a kernel density estimate + kde=False, kde_kws=None, line_kws=None, + # Bivariate histogram parameters + thresh=0, pthresh=None, pmax=None, cbar=False, cbar_ax=None, cbar_kws=None, + # Hue mapping parameters + palette=None, hue_order=None, hue_norm=None, color=None, + # Axes information + log_scale=None, legend=True, ax=None, + # Other appearance keywords + **kwargs, +): + + p = _DistributionPlotter( + data=data, + variables=_DistributionPlotter.get_semantics(locals()) + ) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + if ax is None: + ax = plt.gca() + + p._attach(ax, log_scale=log_scale) + + if p.univariate: # Note, bivariate plots won't cycle + if fill: + method = ax.bar if element == "bars" else ax.fill_between + else: + method = ax.plot + color = _default_color(method, hue, color, kwargs) + + if not p.has_xy_data: + return ax + + # Default to discrete bins for categorical variables + if discrete is None: + discrete = p._default_discrete() + + estimate_kws = dict( + stat=stat, + bins=bins, + binwidth=binwidth, + binrange=binrange, + discrete=discrete, + cumulative=cumulative, + ) + + if p.univariate: + + p.plot_univariate_histogram( + multiple=multiple, + element=element, + fill=fill, + shrink=shrink, + common_norm=common_norm, + common_bins=common_bins, + kde=kde, + kde_kws=kde_kws, + color=color, + legend=legend, + estimate_kws=estimate_kws, + line_kws=line_kws, + **kwargs, + ) + + else: + + p.plot_bivariate_histogram( + common_bins=common_bins, + common_norm=common_norm, + thresh=thresh, + pthresh=pthresh, + pmax=pmax, + color=color, + legend=legend, + cbar=cbar, + cbar_ax=cbar_ax, + cbar_kws=cbar_kws, + estimate_kws=estimate_kws, + **kwargs, + ) + + return ax + + +histplot.__doc__ = """\ +Plot univariate or bivariate histograms to show distributions of datasets. + +A histogram is a classic visualization tool that represents the distribution +of one or more variables by counting the number of observations that fall within +discrete bins. + +This function can normalize the statistic computed within each bin to estimate +frequency, density or probability mass, and it can add a smooth curve obtained +using a kernel density estimate, similar to :func:`kdeplot`. + +More information is provided in the :ref:`user guide `. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} +weights : vector or key in ``data`` + If provided, weight the contribution of the corresponding data points + towards the count in each bin by these factors. +{params.hist.stat} +{params.hist.bins} +{params.hist.binwidth} +{params.hist.binrange} +discrete : bool + If True, default to ``binwidth=1`` and draw the bars so that they are + centered on their corresponding data points. This avoids "gaps" that may + otherwise appear when using discrete (integer) data. +cumulative : bool + If True, plot the cumulative counts as bins increase. +common_bins : bool + If True, use the same bins when semantic variables produce multiple + plots. If using a reference rule to determine the bins, it will be computed + with the full dataset. +common_norm : bool + If True and using a normalized statistic, the normalization will apply over + the full dataset. Otherwise, normalize each histogram independently. +multiple : {{"layer", "dodge", "stack", "fill"}} + Approach to resolving multiple elements when semantic mapping creates subsets. + Only relevant with univariate data. +element : {{"bars", "step", "poly"}} + Visual representation of the histogram statistic. + Only relevant with univariate data. +fill : bool + If True, fill in the space under the histogram. + Only relevant with univariate data. +shrink : number + Scale the width of each bar relative to the binwidth by this factor. + Only relevant with univariate data. +kde : bool + If True, compute a kernel density estimate to smooth the distribution + and show on the plot as (one or more) line(s). + Only relevant with univariate data. +kde_kws : dict + Parameters that control the KDE computation, as in :func:`kdeplot`. +line_kws : dict + Parameters that control the KDE visualization, passed to + :meth:`matplotlib.axes.Axes.plot`. +thresh : number or None + Cells with a statistic less than or equal to this value will be transparent. + Only relevant with bivariate data. +pthresh : number or None + Like ``thresh``, but a value in [0, 1] such that cells with aggregate counts + (or other statistics, when used) up to this proportion of the total will be + transparent. +pmax : number or None + A value in [0, 1] that sets that saturation point for the colormap at a value + such that cells below constitute this proportion of the total count (or + other statistic, when used). +{params.dist.cbar} +{params.dist.cbar_ax} +{params.dist.cbar_kws} +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.core.color} +{params.dist.log_scale} +{params.dist.legend} +{params.core.ax} +kwargs + Other keyword arguments are passed to one of the following matplotlib + functions: + + - :meth:`matplotlib.axes.Axes.bar` (univariate, element="bars") + - :meth:`matplotlib.axes.Axes.fill_between` (univariate, other element, fill=True) + - :meth:`matplotlib.axes.Axes.plot` (univariate, other element, fill=False) + - :meth:`matplotlib.axes.Axes.pcolormesh` (bivariate) + +Returns +------- +{returns.ax} + +See Also +-------- +{seealso.displot} +{seealso.kdeplot} +{seealso.rugplot} +{seealso.ecdfplot} +{seealso.jointplot} + +Notes +----- + +The choice of bins for computing and plotting a histogram can exert +substantial influence on the insights that one is able to draw from the +visualization. If the bins are too large, they may erase important features. +On the other hand, bins that are too small may be dominated by random +variability, obscuring the shape of the true underlying distribution. The +default bin size is determined using a reference rule that depends on the +sample size and variance. This works well in many cases, (i.e., with +"well-behaved" data) but it fails in others. It is always a good to try +different bin sizes to be sure that you are not missing something important. +This function allows you to specify bins in several different ways, such as +by setting the total number of bins to use, the width of each bin, or the +specific locations where the bins should break. + +Examples +-------- + +.. include:: ../docstrings/histplot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def kdeplot( + data=None, *, x=None, y=None, hue=None, weights=None, + palette=None, hue_order=None, hue_norm=None, color=None, fill=None, + multiple="layer", common_norm=True, common_grid=False, cumulative=False, + bw_method="scott", bw_adjust=1, warn_singular=True, log_scale=None, + levels=10, thresh=.05, gridsize=200, cut=3, clip=None, + legend=True, cbar=False, cbar_ax=None, cbar_kws=None, ax=None, + **kwargs, +): + + # --- Start with backwards compatability for versions < 0.11.0 ---------------- + + # Handle (past) deprecation of `data2` + if "data2" in kwargs: + msg = "`data2` has been removed (replaced by `y`); please update your code." + TypeError(msg) + + # Handle deprecation of `vertical` + vertical = kwargs.pop("vertical", None) + if vertical is not None: + if vertical: + action_taken = "assigning data to `y`." + if x is None: + data, y = y, data + else: + x, y = y, x + else: + action_taken = "assigning data to `x`." + msg = textwrap.dedent(f"""\n + The `vertical` parameter is deprecated; {action_taken} + This will become an error in seaborn v0.13.0; please update your code. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + # Handle deprecation of `bw` + bw = kwargs.pop("bw", None) + if bw is not None: + msg = textwrap.dedent(f"""\n + The `bw` parameter is deprecated in favor of `bw_method` and `bw_adjust`. + Setting `bw_method={bw}`, but please see the docs for the new parameters + and update your code. This will become an error in seaborn v0.13.0. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + bw_method = bw + + # Handle deprecation of `kernel` + if kwargs.pop("kernel", None) is not None: + msg = textwrap.dedent("""\n + Support for alternate kernels has been removed; using Gaussian kernel. + This will become an error in seaborn v0.13.0; please update your code. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + # Handle deprecation of shade_lowest + shade_lowest = kwargs.pop("shade_lowest", None) + if shade_lowest is not None: + if shade_lowest: + thresh = 0 + msg = textwrap.dedent(f"""\n + `shade_lowest` has been replaced by `thresh`; setting `thresh={thresh}. + This will become an error in seaborn v0.13.0; please update your code. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + # Handle "soft" deprecation of shade `shade` is not really the right + # terminology here, but unlike some of the other deprecated parameters it + # is probably very commonly used and much hard to remove. This is therefore + # going to be a longer process where, first, `fill` will be introduced and + # be used throughout the documentation. In 0.12, when kwarg-only + # enforcement hits, we can remove the shade/shade_lowest out of the + # function signature all together and pull them out of the kwargs. Then we + # can actually fire a FutureWarning, and eventually remove. + shade = kwargs.pop("shade", None) + if shade is not None: + fill = shade + msg = textwrap.dedent(f"""\n + `shade` is now deprecated in favor of `fill`; setting `fill={shade}`. + This will become an error in seaborn v0.14.0; please update your code. + """) + warnings.warn(msg, FutureWarning, stacklevel=2) + + # Handle `n_levels` + # This was never in the formal API but it was processed, and appeared in an + # example. We can treat as an alias for `levels` now and deprecate later. + levels = kwargs.pop("n_levels", levels) + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # + + p = _DistributionPlotter( + data=data, + variables=_DistributionPlotter.get_semantics(locals()), + ) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + if ax is None: + ax = plt.gca() + + p._attach(ax, allowed_types=["numeric", "datetime"], log_scale=log_scale) + + method = ax.fill_between if fill else ax.plot + color = _default_color(method, hue, color, kwargs) + + if not p.has_xy_data: + return ax + + # Pack the kwargs for statistics.KDE + estimate_kws = dict( + bw_method=bw_method, + bw_adjust=bw_adjust, + gridsize=gridsize, + cut=cut, + clip=clip, + cumulative=cumulative, + ) + + if p.univariate: + + plot_kws = kwargs.copy() + + p.plot_univariate_density( + multiple=multiple, + common_norm=common_norm, + common_grid=common_grid, + fill=fill, + color=color, + legend=legend, + warn_singular=warn_singular, + estimate_kws=estimate_kws, + **plot_kws, + ) + + else: + + p.plot_bivariate_density( + common_norm=common_norm, + fill=fill, + levels=levels, + thresh=thresh, + legend=legend, + color=color, + warn_singular=warn_singular, + cbar=cbar, + cbar_ax=cbar_ax, + cbar_kws=cbar_kws, + estimate_kws=estimate_kws, + **kwargs, + ) + + return ax + + +kdeplot.__doc__ = """\ +Plot univariate or bivariate distributions using kernel density estimation. + +A kernel density estimate (KDE) plot is a method for visualizing the +distribution of observations in a dataset, analogous to a histogram. KDE +represents the data using a continuous probability density curve in one or +more dimensions. + +The approach is explained further in the :ref:`user guide `. + +Relative to a histogram, KDE can produce a plot that is less cluttered and +more interpretable, especially when drawing multiple distributions. But it +has the potential to introduce distortions if the underlying distribution is +bounded or not smooth. Like a histogram, the quality of the representation +also depends on the selection of good smoothing parameters. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} +weights : vector or key in ``data`` + If provided, weight the kernel density estimation using these values. +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.core.color} +fill : bool or None + If True, fill in the area under univariate density curves or between + bivariate contours. If None, the default depends on ``multiple``. +{params.dist.multiple} +common_norm : bool + If True, scale each conditional density by the number of observations + such that the total area under all densities sums to 1. Otherwise, + normalize each density independently. +common_grid : bool + If True, use the same evaluation grid for each kernel density estimate. + Only relevant with univariate data. +{params.kde.cumulative} +{params.kde.bw_method} +{params.kde.bw_adjust} +warn_singular : bool + If True, issue a warning when trying to estimate the density of data + with zero variance. +{params.dist.log_scale} +levels : int or vector + Number of contour levels or values to draw contours at. A vector argument + must have increasing values in [0, 1]. Levels correspond to iso-proportions + of the density: e.g., 20% of the probability mass will lie below the + contour drawn for 0.2. Only relevant with bivariate data. +thresh : number in [0, 1] + Lowest iso-proportion level at which to draw a contour line. Ignored when + ``levels`` is a vector. Only relevant with bivariate data. +gridsize : int + Number of points on each dimension of the evaluation grid. +{params.kde.cut} +{params.kde.clip} +{params.dist.legend} +{params.dist.cbar} +{params.dist.cbar_ax} +{params.dist.cbar_kws} +{params.core.ax} +kwargs + Other keyword arguments are passed to one of the following matplotlib + functions: + + - :meth:`matplotlib.axes.Axes.plot` (univariate, ``fill=False``), + - :meth:`matplotlib.axes.Axes.fill_between` (univariate, ``fill=True``), + - :meth:`matplotlib.axes.Axes.contour` (bivariate, ``fill=False``), + - :meth:`matplotlib.axes.contourf` (bivariate, ``fill=True``). + +Returns +------- +{returns.ax} + +See Also +-------- +{seealso.displot} +{seealso.histplot} +{seealso.ecdfplot} +{seealso.jointplot} +{seealso.violinplot} + +Notes +----- + +The *bandwidth*, or standard deviation of the smoothing kernel, is an +important parameter. Misspecification of the bandwidth can produce a +distorted representation of the data. Much like the choice of bin width in a +histogram, an over-smoothed curve can erase true features of a +distribution, while an under-smoothed curve can create false features out of +random variability. The rule-of-thumb that sets the default bandwidth works +best when the true distribution is smooth, unimodal, and roughly bell-shaped. +It is always a good idea to check the default behavior by using ``bw_adjust`` +to increase or decrease the amount of smoothing. + +Because the smoothing algorithm uses a Gaussian kernel, the estimated density +curve can extend to values that do not make sense for a particular dataset. +For example, the curve may be drawn over negative values when smoothing data +that are naturally positive. The ``cut`` and ``clip`` parameters can be used +to control the extent of the curve, but datasets that have many observations +close to a natural boundary may be better served by a different visualization +method. + +Similar considerations apply when a dataset is naturally discrete or "spiky" +(containing many repeated observations of the same value). Kernel density +estimation will always produce a smooth curve, which would be misleading +in these situations. + +The units on the density axis are a common source of confusion. While kernel +density estimation produces a probability distribution, the height of the curve +at each point gives a density, not a probability. A probability can be obtained +only by integrating the density across a range. The curve is normalized so +that the integral over all possible values is 1, meaning that the scale of +the density axis depends on the data values. + +Examples +-------- + +.. include:: ../docstrings/kdeplot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def ecdfplot( + data=None, *, + # Vector variables + x=None, y=None, hue=None, weights=None, + # Computation parameters + stat="proportion", complementary=False, + # Hue mapping parameters + palette=None, hue_order=None, hue_norm=None, + # Axes information + log_scale=None, legend=True, ax=None, + # Other appearance keywords + **kwargs, +): + + p = _DistributionPlotter( + data=data, + variables=_DistributionPlotter.get_semantics(locals()) + ) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + # We could support other semantics (size, style) here fairly easily + # But it would make distplot a bit more complicated. + # It's always possible to add features like that later, so I am going to defer. + # It will be even easier to wait until after there is a more general/abstract + # way to go from semantic specs to artist attributes. + + if ax is None: + ax = plt.gca() + + p._attach(ax, log_scale=log_scale) + + color = kwargs.pop("color", kwargs.pop("c", None)) + kwargs["color"] = _default_color(ax.plot, hue, color, kwargs) + + if not p.has_xy_data: + return ax + + # We could add this one day, but it's of dubious value + if not p.univariate: + raise NotImplementedError("Bivariate ECDF plots are not implemented") + + estimate_kws = dict( + stat=stat, + complementary=complementary, + ) + + p.plot_univariate_ecdf( + estimate_kws=estimate_kws, + legend=legend, + **kwargs, + ) + + return ax + + +ecdfplot.__doc__ = """\ +Plot empirical cumulative distribution functions. + +An ECDF represents the proportion or count of observations falling below each +unique value in a dataset. Compared to a histogram or density plot, it has the +advantage that each observation is visualized directly, meaning that there are +no binning or smoothing parameters that need to be adjusted. It also aids direct +comparisons between multiple distributions. A downside is that the relationship +between the appearance of the plot and the basic properties of the distribution +(such as its central tendency, variance, and the presence of any bimodality) +may not be as intuitive. + +More information is provided in the :ref:`user guide `. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} +weights : vector or key in ``data`` + If provided, weight the contribution of the corresponding data points + towards the cumulative distribution using these values. +{params.ecdf.stat} +{params.ecdf.complementary} +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.dist.log_scale} +{params.dist.legend} +{params.core.ax} +kwargs + Other keyword arguments are passed to :meth:`matplotlib.axes.Axes.plot`. + +Returns +------- +{returns.ax} + +See Also +-------- +{seealso.displot} +{seealso.histplot} +{seealso.kdeplot} +{seealso.rugplot} + +Examples +-------- + +.. include:: ../docstrings/ecdfplot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def rugplot( + data=None, *, x=None, y=None, hue=None, height=.025, expand_margins=True, + palette=None, hue_order=None, hue_norm=None, legend=True, ax=None, **kwargs +): + + # A note: I think it would make sense to add multiple= to rugplot and allow + # rugs for different hue variables to be shifted orthogonal to the data axis + # But is this stacking, or dodging? + + # A note: if we want to add a style semantic to rugplot, + # we could make an option that draws the rug using scatterplot + + # A note, it would also be nice to offer some kind of histogram/density + # rugplot, since alpha blending doesn't work great in the large n regime + + # --- Start with backwards compatability for versions < 0.11.0 ---------------- + + a = kwargs.pop("a", None) + axis = kwargs.pop("axis", None) + + if a is not None: + data = a + msg = textwrap.dedent("""\n + The `a` parameter has been replaced; use `x`, `y`, and/or `data` instead. + Please update your code; This will become an error in seaborn v0.13.0. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + if axis is not None: + if axis == "x": + x = data + elif axis == "y": + y = data + msg = textwrap.dedent(f"""\n + The `axis` parameter has been deprecated; use the `{axis}` parameter instead. + Please update your code; this will become an error in seaborn v0.13.0. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + vertical = kwargs.pop("vertical", None) + if vertical is not None: + if vertical: + action_taken = "assigning data to `y`." + if x is None: + data, y = y, data + else: + x, y = y, x + else: + action_taken = "assigning data to `x`." + msg = textwrap.dedent(f"""\n + The `vertical` parameter is deprecated; {action_taken} + This will become an error in seaborn v0.13.0; please update your code. + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # + + weights = None + p = _DistributionPlotter( + data=data, + variables=_DistributionPlotter.get_semantics(locals()), + ) + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + if ax is None: + ax = plt.gca() + + p._attach(ax) + + color = kwargs.pop("color", kwargs.pop("c", None)) + kwargs["color"] = _default_color(ax.plot, hue, color, kwargs) + + if not p.has_xy_data: + return ax + + p.plot_rug(height, expand_margins, legend, **kwargs) + + return ax + + +rugplot.__doc__ = """\ +Plot marginal distributions by drawing ticks along the x and y axes. + +This function is intended to complement other plots by showing the location +of individual observations in an unobtrusive way. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} +height : float + Proportion of axes extent covered by each rug element. Can be negative. +expand_margins : bool + If True, increase the axes margins by the height of the rug to avoid + overlap with other elements. +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +legend : bool + If False, do not add a legend for semantic variables. +{params.core.ax} +kwargs + Other keyword arguments are passed to + :meth:`matplotlib.collections.LineCollection` + +Returns +------- +{returns.ax} + +Examples +-------- + +.. include:: ../docstrings/rugplot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def displot( + data=None, *, + # Vector variables + x=None, y=None, hue=None, row=None, col=None, weights=None, + # Other plot parameters + kind="hist", rug=False, rug_kws=None, log_scale=None, legend=True, + # Hue-mapping parameters + palette=None, hue_order=None, hue_norm=None, color=None, + # Faceting parameters + col_wrap=None, row_order=None, col_order=None, + height=5, aspect=1, facet_kws=None, + **kwargs, +): + + p = _DistributionFacetPlotter( + data=data, + variables=_DistributionFacetPlotter.get_semantics(locals()) + ) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + + _check_argument("kind", ["hist", "kde", "ecdf"], kind) + + # --- Initialize the FacetGrid object + + # Check for attempt to plot onto specific axes and warn + if "ax" in kwargs: + msg = ( + "`displot` is a figure-level function and does not accept " + "the ax= parameter. You may wish to try {}plot.".format(kind) + ) + warnings.warn(msg, UserWarning) + kwargs.pop("ax") + + for var in ["row", "col"]: + # Handle faceting variables that lack name information + if var in p.variables and p.variables[var] is None: + p.variables[var] = f"_{var}_" + + # Adapt the plot_data dataframe for use with FacetGrid + grid_data = p.plot_data.rename(columns=p.variables) + grid_data = grid_data.loc[:, ~grid_data.columns.duplicated()] + + col_name = p.variables.get("col") + row_name = p.variables.get("row") + + if facet_kws is None: + facet_kws = {} + + g = FacetGrid( + data=grid_data, row=row_name, col=col_name, + col_wrap=col_wrap, row_order=row_order, + col_order=col_order, height=height, + aspect=aspect, + **facet_kws, + ) + + # Now attach the axes object to the plotter object + if kind == "kde": + allowed_types = ["numeric", "datetime"] + else: + allowed_types = None + p._attach(g, allowed_types=allowed_types, log_scale=log_scale) + + # Check for a specification that lacks x/y data and return early + if not p.has_xy_data: + return g + + if color is None and hue is None: + color = "C0" + # XXX else warn if hue is not None? + + kwargs["legend"] = legend + + # --- Draw the plots + + if kind == "hist": + + hist_kws = kwargs.copy() + + # Extract the parameters that will go directly to Histogram + estimate_defaults = {} + _assign_default_kwargs(estimate_defaults, Histogram.__init__, histplot) + + estimate_kws = {} + for key, default_val in estimate_defaults.items(): + estimate_kws[key] = hist_kws.pop(key, default_val) + + # Handle derivative defaults + if estimate_kws["discrete"] is None: + estimate_kws["discrete"] = p._default_discrete() + + hist_kws["estimate_kws"] = estimate_kws + + hist_kws.setdefault("color", color) + + if p.univariate: + + _assign_default_kwargs(hist_kws, p.plot_univariate_histogram, histplot) + p.plot_univariate_histogram(**hist_kws) + + else: + + _assign_default_kwargs(hist_kws, p.plot_bivariate_histogram, histplot) + p.plot_bivariate_histogram(**hist_kws) + + elif kind == "kde": + + kde_kws = kwargs.copy() + + # Extract the parameters that will go directly to KDE + estimate_defaults = {} + _assign_default_kwargs(estimate_defaults, KDE.__init__, kdeplot) + + estimate_kws = {} + for key, default_val in estimate_defaults.items(): + estimate_kws[key] = kde_kws.pop(key, default_val) + + kde_kws["estimate_kws"] = estimate_kws + kde_kws["color"] = color + + if p.univariate: + + _assign_default_kwargs(kde_kws, p.plot_univariate_density, kdeplot) + p.plot_univariate_density(**kde_kws) + + else: + + _assign_default_kwargs(kde_kws, p.plot_bivariate_density, kdeplot) + p.plot_bivariate_density(**kde_kws) + + elif kind == "ecdf": + + ecdf_kws = kwargs.copy() + + # Extract the parameters that will go directly to the estimator + estimate_kws = {} + estimate_defaults = {} + _assign_default_kwargs(estimate_defaults, ECDF.__init__, ecdfplot) + for key, default_val in estimate_defaults.items(): + estimate_kws[key] = ecdf_kws.pop(key, default_val) + + ecdf_kws["estimate_kws"] = estimate_kws + ecdf_kws["color"] = color + + if p.univariate: + + _assign_default_kwargs(ecdf_kws, p.plot_univariate_ecdf, ecdfplot) + p.plot_univariate_ecdf(**ecdf_kws) + + else: + + raise NotImplementedError("Bivariate ECDF plots are not implemented") + + # All plot kinds can include a rug + if rug: + # TODO with expand_margins=True, each facet expands margins... annoying! + if rug_kws is None: + rug_kws = {} + _assign_default_kwargs(rug_kws, p.plot_rug, rugplot) + rug_kws["legend"] = False + if color is not None: + rug_kws["color"] = color + p.plot_rug(**rug_kws) + + # Call FacetGrid annotation methods + # Note that the legend is currently set inside the plotting method + g.set_axis_labels( + x_var=p.variables.get("x", g.axes.flat[0].get_xlabel()), + y_var=p.variables.get("y", g.axes.flat[0].get_ylabel()), + ) + g.set_titles() + g.tight_layout() + + if data is not None and (x is not None or y is not None): + if not isinstance(data, pd.DataFrame): + data = pd.DataFrame(data) + g.data = pd.merge( + data, + g.data[g.data.columns.difference(data.columns)], + left_index=True, + right_index=True, + ) + else: + wide_cols = { + k: f"_{k}_" if v is None else v for k, v in p.variables.items() + } + g.data = p.plot_data.rename(columns=wide_cols) + + return g + + +displot.__doc__ = """\ +Figure-level interface for drawing distribution plots onto a FacetGrid. + +This function provides access to several approaches for visualizing the +univariate or bivariate distribution of data, including subsets of data +defined by semantic mapping and faceting across multiple subplots. The +``kind`` parameter selects the approach to use: + +- :func:`histplot` (with ``kind="hist"``; the default) +- :func:`kdeplot` (with ``kind="kde"``) +- :func:`ecdfplot` (with ``kind="ecdf"``; univariate-only) + +Additionally, a :func:`rugplot` can be added to any kind of plot to show +individual observations. + +Extra keyword arguments are passed to the underlying function, so you should +refer to the documentation for each to understand the complete set of options +for making plots with this interface. + +See the :doc:`distribution plots tutorial <../tutorial/distributions>` for a more +in-depth discussion of the relative strengths and weaknesses of each approach. +The distinction between figure-level and axes-level functions is explained +further in the :doc:`user guide <../tutorial/function_overview>`. + +Parameters +---------- +{params.core.data} +{params.core.xy} +{params.core.hue} +{params.facets.rowcol} +kind : {{"hist", "kde", "ecdf"}} + Approach for visualizing the data. Selects the underlying plotting function + and determines the additional set of valid parameters. +rug : bool + If True, show each observation with marginal ticks (as in :func:`rugplot`). +rug_kws : dict + Parameters to control the appearance of the rug plot. +{params.dist.log_scale} +{params.dist.legend} +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.core.color} +{params.facets.col_wrap} +{params.facets.rowcol_order} +{params.facets.height} +{params.facets.aspect} +{params.facets.facet_kws} +kwargs + Other keyword arguments are documented with the relevant axes-level function: + + - :func:`histplot` (with ``kind="hist"``) + - :func:`kdeplot` (with ``kind="kde"``) + - :func:`ecdfplot` (with ``kind="ecdf"``) + +Returns +------- +{returns.facetgrid} + +See Also +-------- +{seealso.histplot} +{seealso.kdeplot} +{seealso.rugplot} +{seealso.ecdfplot} +{seealso.jointplot} + +Examples +-------- + +See the API documentation for the axes-level functions for more details +about the breadth of options available for each plot kind. + +.. include:: ../docstrings/displot.rst + +""".format( + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +# =========================================================================== # +# DEPRECATED FUNCTIONS LIVE BELOW HERE +# =========================================================================== # + + +def _freedman_diaconis_bins(a): + """Calculate number of hist bins using Freedman-Diaconis rule.""" + # From https://stats.stackexchange.com/questions/798/ + a = np.asarray(a) + if len(a) < 2: + return 1 + iqr = np.subtract.reduce(np.nanpercentile(a, [75, 25])) + h = 2 * iqr / (len(a) ** (1 / 3)) + # fall back to sqrt(a) bins if iqr is 0 + if h == 0: + return int(np.sqrt(a.size)) + else: + return int(np.ceil((a.max() - a.min()) / h)) + + +def distplot(a=None, bins=None, hist=True, kde=True, rug=False, fit=None, + hist_kws=None, kde_kws=None, rug_kws=None, fit_kws=None, + color=None, vertical=False, norm_hist=False, axlabel=None, + label=None, ax=None, x=None): + """ + DEPRECATED + + This function has been deprecated and will be removed in seaborn v0.14.0. + It has been replaced by :func:`histplot` and :func:`displot`, two functions + with a modern API and many more capabilities. + + For a guide to updating, please see this notebook: + + https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751 + + """ + + if kde and not hist: + axes_level_suggestion = ( + "`kdeplot` (an axes-level function for kernel density plots)" + ) + else: + axes_level_suggestion = ( + "`histplot` (an axes-level function for histograms)" + ) + + msg = textwrap.dedent(f""" + + `distplot` is a deprecated function and will be removed in seaborn v0.14.0. + + Please adapt your code to use either `displot` (a figure-level function with + similar flexibility) or {axes_level_suggestion}. + + For a guide to updating your code to use the new functions, please see + https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751 + """) + warnings.warn(msg, UserWarning, stacklevel=2) + + if ax is None: + ax = plt.gca() + + # Intelligently label the support axis + label_ax = bool(axlabel) + if axlabel is None and hasattr(a, "name"): + axlabel = a.name + if axlabel is not None: + label_ax = True + + # Support new-style API + if x is not None: + a = x + + # Make a a 1-d float array + a = np.asarray(a, float) + if a.ndim > 1: + a = a.squeeze() + + # Drop null values from array + a = remove_na(a) + + # Decide if the hist is normed + norm_hist = norm_hist or kde or (fit is not None) + + # Handle dictionary defaults + hist_kws = {} if hist_kws is None else hist_kws.copy() + kde_kws = {} if kde_kws is None else kde_kws.copy() + rug_kws = {} if rug_kws is None else rug_kws.copy() + fit_kws = {} if fit_kws is None else fit_kws.copy() + + # Get the color from the current color cycle + if color is None: + if vertical: + line, = ax.plot(0, a.mean()) + else: + line, = ax.plot(a.mean(), 0) + color = line.get_color() + line.remove() + + # Plug the label into the right kwarg dictionary + if label is not None: + if hist: + hist_kws["label"] = label + elif kde: + kde_kws["label"] = label + elif rug: + rug_kws["label"] = label + elif fit: + fit_kws["label"] = label + + if hist: + if bins is None: + bins = min(_freedman_diaconis_bins(a), 50) + hist_kws.setdefault("alpha", 0.4) + hist_kws.setdefault("density", norm_hist) + + orientation = "horizontal" if vertical else "vertical" + hist_color = hist_kws.pop("color", color) + ax.hist(a, bins, orientation=orientation, + color=hist_color, **hist_kws) + if hist_color != color: + hist_kws["color"] = hist_color + + axis = "y" if vertical else "x" + + if kde: + kde_color = kde_kws.pop("color", color) + kdeplot(**{axis: a}, ax=ax, color=kde_color, **kde_kws) + if kde_color != color: + kde_kws["color"] = kde_color + + if rug: + rug_color = rug_kws.pop("color", color) + rugplot(**{axis: a}, ax=ax, color=rug_color, **rug_kws) + if rug_color != color: + rug_kws["color"] = rug_color + + if fit is not None: + + def pdf(x): + return fit.pdf(x, *params) + + fit_color = fit_kws.pop("color", "#282828") + gridsize = fit_kws.pop("gridsize", 200) + cut = fit_kws.pop("cut", 3) + clip = fit_kws.pop("clip", (-np.inf, np.inf)) + bw = gaussian_kde(a).scotts_factor() * a.std(ddof=1) + x = _kde_support(a, bw, gridsize, cut, clip) + params = fit.fit(a) + y = pdf(x) + if vertical: + x, y = y, x + ax.plot(x, y, color=fit_color, **fit_kws) + if fit_color != "#282828": + fit_kws["color"] = fit_color + + if label_ax: + if vertical: + ax.set_ylabel(axlabel) + else: + ax.set_xlabel(axlabel) + + return ax diff --git a/lib/python3.10/site-packages/seaborn/matrix.py b/lib/python3.10/site-packages/seaborn/matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..76f22b89afc38a52782eca9560394c60335de508 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/matrix.py @@ -0,0 +1,1262 @@ +"""Functions to visualize matrices of data.""" +import warnings + +import matplotlib as mpl +from matplotlib.collections import LineCollection +import matplotlib.pyplot as plt +from matplotlib import gridspec +import numpy as np +import pandas as pd +try: + from scipy.cluster import hierarchy + _no_scipy = False +except ImportError: + _no_scipy = True + +from . import cm +from .axisgrid import Grid +from ._compat import get_colormap +from .utils import ( + despine, + axis_ticklabels_overlap, + relative_luminance, + to_utf8, + _draw_figure, +) + + +__all__ = ["heatmap", "clustermap"] + + +def _index_to_label(index): + """Convert a pandas index or multiindex to an axis label.""" + if isinstance(index, pd.MultiIndex): + return "-".join(map(to_utf8, index.names)) + else: + return index.name + + +def _index_to_ticklabels(index): + """Convert a pandas index or multiindex into ticklabels.""" + if isinstance(index, pd.MultiIndex): + return ["-".join(map(to_utf8, i)) for i in index.values] + else: + return index.values + + +def _convert_colors(colors): + """Convert either a list of colors or nested lists of colors to RGB.""" + to_rgb = mpl.colors.to_rgb + + try: + to_rgb(colors[0]) + # If this works, there is only one level of colors + return list(map(to_rgb, colors)) + except ValueError: + # If we get here, we have nested lists + return [list(map(to_rgb, l)) for l in colors] + + +def _matrix_mask(data, mask): + """Ensure that data and mask are compatible and add missing values. + + Values will be plotted for cells where ``mask`` is ``False``. + + ``data`` is expected to be a DataFrame; ``mask`` can be an array or + a DataFrame. + + """ + if mask is None: + mask = np.zeros(data.shape, bool) + + if isinstance(mask, np.ndarray): + # For array masks, ensure that shape matches data then convert + if mask.shape != data.shape: + raise ValueError("Mask must have the same shape as data.") + + mask = pd.DataFrame(mask, + index=data.index, + columns=data.columns, + dtype=bool) + + elif isinstance(mask, pd.DataFrame): + # For DataFrame masks, ensure that semantic labels match data + if not mask.index.equals(data.index) \ + and mask.columns.equals(data.columns): + err = "Mask must have the same index and columns as data." + raise ValueError(err) + + # Add any cells with missing data to the mask + # This works around an issue where `plt.pcolormesh` doesn't represent + # missing data properly + mask = mask | pd.isnull(data) + + return mask + + +class _HeatMapper: + """Draw a heatmap plot of a matrix with nice labels and colormaps.""" + + def __init__(self, data, vmin, vmax, cmap, center, robust, annot, fmt, + annot_kws, cbar, cbar_kws, + xticklabels=True, yticklabels=True, mask=None): + """Initialize the plotting object.""" + # We always want to have a DataFrame with semantic information + # and an ndarray to pass to matplotlib + if isinstance(data, pd.DataFrame): + plot_data = data.values + else: + plot_data = np.asarray(data) + data = pd.DataFrame(plot_data) + + # Validate the mask and convert to DataFrame + mask = _matrix_mask(data, mask) + + plot_data = np.ma.masked_where(np.asarray(mask), plot_data) + + # Get good names for the rows and columns + xtickevery = 1 + if isinstance(xticklabels, int): + xtickevery = xticklabels + xticklabels = _index_to_ticklabels(data.columns) + elif xticklabels is True: + xticklabels = _index_to_ticklabels(data.columns) + elif xticklabels is False: + xticklabels = [] + + ytickevery = 1 + if isinstance(yticklabels, int): + ytickevery = yticklabels + yticklabels = _index_to_ticklabels(data.index) + elif yticklabels is True: + yticklabels = _index_to_ticklabels(data.index) + elif yticklabels is False: + yticklabels = [] + + if not len(xticklabels): + self.xticks = [] + self.xticklabels = [] + elif isinstance(xticklabels, str) and xticklabels == "auto": + self.xticks = "auto" + self.xticklabels = _index_to_ticklabels(data.columns) + else: + self.xticks, self.xticklabels = self._skip_ticks(xticklabels, + xtickevery) + + if not len(yticklabels): + self.yticks = [] + self.yticklabels = [] + elif isinstance(yticklabels, str) and yticklabels == "auto": + self.yticks = "auto" + self.yticklabels = _index_to_ticklabels(data.index) + else: + self.yticks, self.yticklabels = self._skip_ticks(yticklabels, + ytickevery) + + # Get good names for the axis labels + xlabel = _index_to_label(data.columns) + ylabel = _index_to_label(data.index) + self.xlabel = xlabel if xlabel is not None else "" + self.ylabel = ylabel if ylabel is not None else "" + + # Determine good default values for the colormapping + self._determine_cmap_params(plot_data, vmin, vmax, + cmap, center, robust) + + # Sort out the annotations + if annot is None or annot is False: + annot = False + annot_data = None + else: + if isinstance(annot, bool): + annot_data = plot_data + else: + annot_data = np.asarray(annot) + if annot_data.shape != plot_data.shape: + err = "`data` and `annot` must have same shape." + raise ValueError(err) + annot = True + + # Save other attributes to the object + self.data = data + self.plot_data = plot_data + + self.annot = annot + self.annot_data = annot_data + + self.fmt = fmt + self.annot_kws = {} if annot_kws is None else annot_kws.copy() + self.cbar = cbar + self.cbar_kws = {} if cbar_kws is None else cbar_kws.copy() + + def _determine_cmap_params(self, plot_data, vmin, vmax, + cmap, center, robust): + """Use some heuristics to set good defaults for colorbar and range.""" + + # plot_data is a np.ma.array instance + calc_data = plot_data.astype(float).filled(np.nan) + if vmin is None: + if robust: + vmin = np.nanpercentile(calc_data, 2) + else: + vmin = np.nanmin(calc_data) + if vmax is None: + if robust: + vmax = np.nanpercentile(calc_data, 98) + else: + vmax = np.nanmax(calc_data) + self.vmin, self.vmax = vmin, vmax + + # Choose default colormaps if not provided + if cmap is None: + if center is None: + self.cmap = cm.rocket + else: + self.cmap = cm.icefire + elif isinstance(cmap, str): + self.cmap = get_colormap(cmap) + elif isinstance(cmap, list): + self.cmap = mpl.colors.ListedColormap(cmap) + else: + self.cmap = cmap + + # Recenter a divergent colormap + if center is not None: + + # Copy bad values + # in mpl<3.2 only masked values are honored with "bad" color spec + # (see https://github.com/matplotlib/matplotlib/pull/14257) + bad = self.cmap(np.ma.masked_invalid([np.nan]))[0] + + # under/over values are set for sure when cmap extremes + # do not map to the same color as +-inf + under = self.cmap(-np.inf) + over = self.cmap(np.inf) + under_set = under != self.cmap(0) + over_set = over != self.cmap(self.cmap.N - 1) + + vrange = max(vmax - center, center - vmin) + normlize = mpl.colors.Normalize(center - vrange, center + vrange) + cmin, cmax = normlize([vmin, vmax]) + cc = np.linspace(cmin, cmax, 256) + self.cmap = mpl.colors.ListedColormap(self.cmap(cc)) + self.cmap.set_bad(bad) + if under_set: + self.cmap.set_under(under) + if over_set: + self.cmap.set_over(over) + + def _annotate_heatmap(self, ax, mesh): + """Add textual labels with the value in each cell.""" + mesh.update_scalarmappable() + height, width = self.annot_data.shape + xpos, ypos = np.meshgrid(np.arange(width) + .5, np.arange(height) + .5) + for x, y, m, color, val in zip(xpos.flat, ypos.flat, + mesh.get_array(), mesh.get_facecolors(), + self.annot_data.flat): + if m is not np.ma.masked: + lum = relative_luminance(color) + text_color = ".15" if lum > .408 else "w" + annotation = ("{:" + self.fmt + "}").format(val) + text_kwargs = dict(color=text_color, ha="center", va="center") + text_kwargs.update(self.annot_kws) + ax.text(x, y, annotation, **text_kwargs) + + def _skip_ticks(self, labels, tickevery): + """Return ticks and labels at evenly spaced intervals.""" + n = len(labels) + if tickevery == 0: + ticks, labels = [], [] + elif tickevery == 1: + ticks, labels = np.arange(n) + .5, labels + else: + start, end, step = 0, n, tickevery + ticks = np.arange(start, end, step) + .5 + labels = labels[start:end:step] + return ticks, labels + + def _auto_ticks(self, ax, labels, axis): + """Determine ticks and ticklabels that minimize overlap.""" + transform = ax.figure.dpi_scale_trans.inverted() + bbox = ax.get_window_extent().transformed(transform) + size = [bbox.width, bbox.height][axis] + axis = [ax.xaxis, ax.yaxis][axis] + tick, = axis.set_ticks([0]) + fontsize = tick.label1.get_size() + max_ticks = int(size // (fontsize / 72)) + if max_ticks < 1: + return [], [] + tick_every = len(labels) // max_ticks + 1 + tick_every = 1 if tick_every == 0 else tick_every + ticks, labels = self._skip_ticks(labels, tick_every) + return ticks, labels + + def plot(self, ax, cax, kws): + """Draw the heatmap on the provided Axes.""" + # Remove all the Axes spines + despine(ax=ax, left=True, bottom=True) + + # setting vmin/vmax in addition to norm is deprecated + # so avoid setting if norm is set + if "norm" not in kws: + kws.setdefault("vmin", self.vmin) + kws.setdefault("vmax", self.vmax) + + # Draw the heatmap + mesh = ax.pcolormesh(self.plot_data, cmap=self.cmap, **kws) + + # Set the axis limits + ax.set(xlim=(0, self.data.shape[1]), ylim=(0, self.data.shape[0])) + + # Invert the y axis to show the plot in matrix form + ax.invert_yaxis() + + # Possibly add a colorbar + if self.cbar: + cb = ax.figure.colorbar(mesh, cax, ax, **self.cbar_kws) + cb.outline.set_linewidth(0) + # If rasterized is passed to pcolormesh, also rasterize the + # colorbar to avoid white lines on the PDF rendering + if kws.get('rasterized', False): + cb.solids.set_rasterized(True) + + # Add row and column labels + if isinstance(self.xticks, str) and self.xticks == "auto": + xticks, xticklabels = self._auto_ticks(ax, self.xticklabels, 0) + else: + xticks, xticklabels = self.xticks, self.xticklabels + + if isinstance(self.yticks, str) and self.yticks == "auto": + yticks, yticklabels = self._auto_ticks(ax, self.yticklabels, 1) + else: + yticks, yticklabels = self.yticks, self.yticklabels + + ax.set(xticks=xticks, yticks=yticks) + xtl = ax.set_xticklabels(xticklabels) + ytl = ax.set_yticklabels(yticklabels, rotation="vertical") + plt.setp(ytl, va="center") # GH2484 + + # Possibly rotate them if they overlap + _draw_figure(ax.figure) + + if axis_ticklabels_overlap(xtl): + plt.setp(xtl, rotation="vertical") + if axis_ticklabels_overlap(ytl): + plt.setp(ytl, rotation="horizontal") + + # Add the axis labels + ax.set(xlabel=self.xlabel, ylabel=self.ylabel) + + # Annotate the cells with the formatted values + if self.annot: + self._annotate_heatmap(ax, mesh) + + +def heatmap( + data, *, + vmin=None, vmax=None, cmap=None, center=None, robust=False, + annot=None, fmt=".2g", annot_kws=None, + linewidths=0, linecolor="white", + cbar=True, cbar_kws=None, cbar_ax=None, + square=False, xticklabels="auto", yticklabels="auto", + mask=None, ax=None, + **kwargs +): + """Plot rectangular data as a color-encoded matrix. + + This is an Axes-level function and will draw the heatmap into the + currently-active Axes if none is provided to the ``ax`` argument. Part of + this Axes space will be taken and used to plot a colormap, unless ``cbar`` + is False or a separate Axes is provided to ``cbar_ax``. + + Parameters + ---------- + data : rectangular dataset + 2D dataset that can be coerced into an ndarray. If a Pandas DataFrame + is provided, the index/column information will be used to label the + columns and rows. + vmin, vmax : floats, optional + Values to anchor the colormap, otherwise they are inferred from the + data and other keyword arguments. + cmap : matplotlib colormap name or object, or list of colors, optional + The mapping from data values to color space. If not provided, the + default will depend on whether ``center`` is set. + center : float, optional + The value at which to center the colormap when plotting divergent data. + Using this parameter will change the default ``cmap`` if none is + specified. + robust : bool, optional + If True and ``vmin`` or ``vmax`` are absent, the colormap range is + computed with robust quantiles instead of the extreme values. + annot : bool or rectangular dataset, optional + If True, write the data value in each cell. If an array-like with the + same shape as ``data``, then use this to annotate the heatmap instead + of the data. Note that DataFrames will match on position, not index. + fmt : str, optional + String formatting code to use when adding annotations. + annot_kws : dict of key, value mappings, optional + Keyword arguments for :meth:`matplotlib.axes.Axes.text` when ``annot`` + is True. + linewidths : float, optional + Width of the lines that will divide each cell. + linecolor : color, optional + Color of the lines that will divide each cell. + cbar : bool, optional + Whether to draw a colorbar. + cbar_kws : dict of key, value mappings, optional + Keyword arguments for :meth:`matplotlib.figure.Figure.colorbar`. + cbar_ax : matplotlib Axes, optional + Axes in which to draw the colorbar, otherwise take space from the + main Axes. + square : bool, optional + If True, set the Axes aspect to "equal" so each cell will be + square-shaped. + xticklabels, yticklabels : "auto", bool, list-like, or int, optional + If True, plot the column names of the dataframe. If False, don't plot + the column names. If list-like, plot these alternate labels as the + xticklabels. If an integer, use the column names but plot only every + n label. If "auto", try to densely plot non-overlapping labels. + mask : bool array or DataFrame, optional + If passed, data will not be shown in cells where ``mask`` is True. + Cells with missing values are automatically masked. + ax : matplotlib Axes, optional + Axes in which to draw the plot, otherwise use the currently-active + Axes. + kwargs : other keyword arguments + All other keyword arguments are passed to + :meth:`matplotlib.axes.Axes.pcolormesh`. + + Returns + ------- + ax : matplotlib Axes + Axes object with the heatmap. + + See Also + -------- + clustermap : Plot a matrix using hierarchical clustering to arrange the + rows and columns. + + Examples + -------- + + .. include:: ../docstrings/heatmap.rst + + """ + # Initialize the plotter object + plotter = _HeatMapper(data, vmin, vmax, cmap, center, robust, annot, fmt, + annot_kws, cbar, cbar_kws, xticklabels, + yticklabels, mask) + + # Add the pcolormesh kwargs here + kwargs["linewidths"] = linewidths + kwargs["edgecolor"] = linecolor + + # Draw the plot and return the Axes + if ax is None: + ax = plt.gca() + if square: + ax.set_aspect("equal") + plotter.plot(ax, cbar_ax, kwargs) + return ax + + +class _DendrogramPlotter: + """Object for drawing tree of similarities between data rows/columns""" + + def __init__(self, data, linkage, metric, method, axis, label, rotate): + """Plot a dendrogram of the relationships between the columns of data + + Parameters + ---------- + data : pandas.DataFrame + Rectangular data + """ + self.axis = axis + if self.axis == 1: + data = data.T + + if isinstance(data, pd.DataFrame): + array = data.values + else: + array = np.asarray(data) + data = pd.DataFrame(array) + + self.array = array + self.data = data + + self.shape = self.data.shape + self.metric = metric + self.method = method + self.axis = axis + self.label = label + self.rotate = rotate + + if linkage is None: + self.linkage = self.calculated_linkage + else: + self.linkage = linkage + self.dendrogram = self.calculate_dendrogram() + + # Dendrogram ends are always at multiples of 5, who knows why + ticks = 10 * np.arange(self.data.shape[0]) + 5 + + if self.label: + ticklabels = _index_to_ticklabels(self.data.index) + ticklabels = [ticklabels[i] for i in self.reordered_ind] + if self.rotate: + self.xticks = [] + self.yticks = ticks + self.xticklabels = [] + + self.yticklabels = ticklabels + self.ylabel = _index_to_label(self.data.index) + self.xlabel = '' + else: + self.xticks = ticks + self.yticks = [] + self.xticklabels = ticklabels + self.yticklabels = [] + self.ylabel = '' + self.xlabel = _index_to_label(self.data.index) + else: + self.xticks, self.yticks = [], [] + self.yticklabels, self.xticklabels = [], [] + self.xlabel, self.ylabel = '', '' + + self.dependent_coord = self.dendrogram['dcoord'] + self.independent_coord = self.dendrogram['icoord'] + + def _calculate_linkage_scipy(self): + linkage = hierarchy.linkage(self.array, method=self.method, + metric=self.metric) + return linkage + + def _calculate_linkage_fastcluster(self): + import fastcluster + # Fastcluster has a memory-saving vectorized version, but only + # with certain linkage methods, and mostly with euclidean metric + # vector_methods = ('single', 'centroid', 'median', 'ward') + euclidean_methods = ('centroid', 'median', 'ward') + euclidean = self.metric == 'euclidean' and self.method in \ + euclidean_methods + if euclidean or self.method == 'single': + return fastcluster.linkage_vector(self.array, + method=self.method, + metric=self.metric) + else: + linkage = fastcluster.linkage(self.array, method=self.method, + metric=self.metric) + return linkage + + @property + def calculated_linkage(self): + + try: + return self._calculate_linkage_fastcluster() + except ImportError: + if np.product(self.shape) >= 10000: + msg = ("Clustering large matrix with scipy. Installing " + "`fastcluster` may give better performance.") + warnings.warn(msg) + + return self._calculate_linkage_scipy() + + def calculate_dendrogram(self): + """Calculates a dendrogram based on the linkage matrix + + Made a separate function, not a property because don't want to + recalculate the dendrogram every time it is accessed. + + Returns + ------- + dendrogram : dict + Dendrogram dictionary as returned by scipy.cluster.hierarchy + .dendrogram. The important key-value pairing is + "reordered_ind" which indicates the re-ordering of the matrix + """ + return hierarchy.dendrogram(self.linkage, no_plot=True, + color_threshold=-np.inf) + + @property + def reordered_ind(self): + """Indices of the matrix, reordered by the dendrogram""" + return self.dendrogram['leaves'] + + def plot(self, ax, tree_kws): + """Plots a dendrogram of the similarities between data on the axes + + Parameters + ---------- + ax : matplotlib.axes.Axes + Axes object upon which the dendrogram is plotted + + """ + tree_kws = {} if tree_kws is None else tree_kws.copy() + tree_kws.setdefault("linewidths", .5) + tree_kws.setdefault("colors", tree_kws.pop("color", (.2, .2, .2))) + + if self.rotate and self.axis == 0: + coords = zip(self.dependent_coord, self.independent_coord) + else: + coords = zip(self.independent_coord, self.dependent_coord) + lines = LineCollection([list(zip(x, y)) for x, y in coords], + **tree_kws) + + ax.add_collection(lines) + number_of_leaves = len(self.reordered_ind) + max_dependent_coord = max(map(max, self.dependent_coord)) + + if self.rotate: + ax.yaxis.set_ticks_position('right') + + # Constants 10 and 1.05 come from + # `scipy.cluster.hierarchy._plot_dendrogram` + ax.set_ylim(0, number_of_leaves * 10) + ax.set_xlim(0, max_dependent_coord * 1.05) + + ax.invert_xaxis() + ax.invert_yaxis() + else: + # Constants 10 and 1.05 come from + # `scipy.cluster.hierarchy._plot_dendrogram` + ax.set_xlim(0, number_of_leaves * 10) + ax.set_ylim(0, max_dependent_coord * 1.05) + + despine(ax=ax, bottom=True, left=True) + + ax.set(xticks=self.xticks, yticks=self.yticks, + xlabel=self.xlabel, ylabel=self.ylabel) + xtl = ax.set_xticklabels(self.xticklabels) + ytl = ax.set_yticklabels(self.yticklabels, rotation='vertical') + + # Force a draw of the plot to avoid matplotlib window error + _draw_figure(ax.figure) + + if len(ytl) > 0 and axis_ticklabels_overlap(ytl): + plt.setp(ytl, rotation="horizontal") + if len(xtl) > 0 and axis_ticklabels_overlap(xtl): + plt.setp(xtl, rotation="vertical") + return self + + +def dendrogram( + data, *, + linkage=None, axis=1, label=True, metric='euclidean', + method='average', rotate=False, tree_kws=None, ax=None +): + """Draw a tree diagram of relationships within a matrix + + Parameters + ---------- + data : pandas.DataFrame + Rectangular data + linkage : numpy.array, optional + Linkage matrix + axis : int, optional + Which axis to use to calculate linkage. 0 is rows, 1 is columns. + label : bool, optional + If True, label the dendrogram at leaves with column or row names + metric : str, optional + Distance metric. Anything valid for scipy.spatial.distance.pdist + method : str, optional + Linkage method to use. Anything valid for + scipy.cluster.hierarchy.linkage + rotate : bool, optional + When plotting the matrix, whether to rotate it 90 degrees + counter-clockwise, so the leaves face right + tree_kws : dict, optional + Keyword arguments for the ``matplotlib.collections.LineCollection`` + that is used for plotting the lines of the dendrogram tree. + ax : matplotlib axis, optional + Axis to plot on, otherwise uses current axis + + Returns + ------- + dendrogramplotter : _DendrogramPlotter + A Dendrogram plotter object. + + Notes + ----- + Access the reordered dendrogram indices with + dendrogramplotter.reordered_ind + + """ + if _no_scipy: + raise RuntimeError("dendrogram requires scipy to be installed") + + plotter = _DendrogramPlotter(data, linkage=linkage, axis=axis, + metric=metric, method=method, + label=label, rotate=rotate) + if ax is None: + ax = plt.gca() + + return plotter.plot(ax=ax, tree_kws=tree_kws) + + +class ClusterGrid(Grid): + + def __init__(self, data, pivot_kws=None, z_score=None, standard_scale=None, + figsize=None, row_colors=None, col_colors=None, mask=None, + dendrogram_ratio=None, colors_ratio=None, cbar_pos=None): + """Grid object for organizing clustered heatmap input on to axes""" + if _no_scipy: + raise RuntimeError("ClusterGrid requires scipy to be available") + + if isinstance(data, pd.DataFrame): + self.data = data + else: + self.data = pd.DataFrame(data) + + self.data2d = self.format_data(self.data, pivot_kws, z_score, + standard_scale) + + self.mask = _matrix_mask(self.data2d, mask) + + self._figure = plt.figure(figsize=figsize) + + self.row_colors, self.row_color_labels = \ + self._preprocess_colors(data, row_colors, axis=0) + self.col_colors, self.col_color_labels = \ + self._preprocess_colors(data, col_colors, axis=1) + + try: + row_dendrogram_ratio, col_dendrogram_ratio = dendrogram_ratio + except TypeError: + row_dendrogram_ratio = col_dendrogram_ratio = dendrogram_ratio + + try: + row_colors_ratio, col_colors_ratio = colors_ratio + except TypeError: + row_colors_ratio = col_colors_ratio = colors_ratio + + width_ratios = self.dim_ratios(self.row_colors, + row_dendrogram_ratio, + row_colors_ratio) + height_ratios = self.dim_ratios(self.col_colors, + col_dendrogram_ratio, + col_colors_ratio) + + nrows = 2 if self.col_colors is None else 3 + ncols = 2 if self.row_colors is None else 3 + + self.gs = gridspec.GridSpec(nrows, ncols, + width_ratios=width_ratios, + height_ratios=height_ratios) + + self.ax_row_dendrogram = self._figure.add_subplot(self.gs[-1, 0]) + self.ax_col_dendrogram = self._figure.add_subplot(self.gs[0, -1]) + self.ax_row_dendrogram.set_axis_off() + self.ax_col_dendrogram.set_axis_off() + + self.ax_row_colors = None + self.ax_col_colors = None + + if self.row_colors is not None: + self.ax_row_colors = self._figure.add_subplot( + self.gs[-1, 1]) + if self.col_colors is not None: + self.ax_col_colors = self._figure.add_subplot( + self.gs[1, -1]) + + self.ax_heatmap = self._figure.add_subplot(self.gs[-1, -1]) + if cbar_pos is None: + self.ax_cbar = self.cax = None + else: + # Initialize the colorbar axes in the gridspec so that tight_layout + # works. We will move it where it belongs later. This is a hack. + self.ax_cbar = self._figure.add_subplot(self.gs[0, 0]) + self.cax = self.ax_cbar # Backwards compatibility + self.cbar_pos = cbar_pos + + self.dendrogram_row = None + self.dendrogram_col = None + + def _preprocess_colors(self, data, colors, axis): + """Preprocess {row/col}_colors to extract labels and convert colors.""" + labels = None + + if colors is not None: + if isinstance(colors, (pd.DataFrame, pd.Series)): + + # If data is unindexed, raise + if (not hasattr(data, "index") and axis == 0) or ( + not hasattr(data, "columns") and axis == 1 + ): + axis_name = "col" if axis else "row" + msg = (f"{axis_name}_colors indices can't be matched with data " + f"indices. Provide {axis_name}_colors as a non-indexed " + "datatype, e.g. by using `.to_numpy()``") + raise TypeError(msg) + + # Ensure colors match data indices + if axis == 0: + colors = colors.reindex(data.index) + else: + colors = colors.reindex(data.columns) + + # Replace na's with white color + # TODO We should set these to transparent instead + colors = colors.astype(object).fillna('white') + + # Extract color values and labels from frame/series + if isinstance(colors, pd.DataFrame): + labels = list(colors.columns) + colors = colors.T.values + else: + if colors.name is None: + labels = [""] + else: + labels = [colors.name] + colors = colors.values + + colors = _convert_colors(colors) + + return colors, labels + + def format_data(self, data, pivot_kws, z_score=None, + standard_scale=None): + """Extract variables from data or use directly.""" + + # Either the data is already in 2d matrix format, or need to do a pivot + if pivot_kws is not None: + data2d = data.pivot(**pivot_kws) + else: + data2d = data + + if z_score is not None and standard_scale is not None: + raise ValueError( + 'Cannot perform both z-scoring and standard-scaling on data') + + if z_score is not None: + data2d = self.z_score(data2d, z_score) + if standard_scale is not None: + data2d = self.standard_scale(data2d, standard_scale) + return data2d + + @staticmethod + def z_score(data2d, axis=1): + """Standarize the mean and variance of the data axis + + Parameters + ---------- + data2d : pandas.DataFrame + Data to normalize + axis : int + Which axis to normalize across. If 0, normalize across rows, if 1, + normalize across columns. + + Returns + ------- + normalized : pandas.DataFrame + Noramlized data with a mean of 0 and variance of 1 across the + specified axis. + """ + if axis == 1: + z_scored = data2d + else: + z_scored = data2d.T + + z_scored = (z_scored - z_scored.mean()) / z_scored.std() + + if axis == 1: + return z_scored + else: + return z_scored.T + + @staticmethod + def standard_scale(data2d, axis=1): + """Divide the data by the difference between the max and min + + Parameters + ---------- + data2d : pandas.DataFrame + Data to normalize + axis : int + Which axis to normalize across. If 0, normalize across rows, if 1, + normalize across columns. + + Returns + ------- + standardized : pandas.DataFrame + Noramlized data with a mean of 0 and variance of 1 across the + specified axis. + + """ + # Normalize these values to range from 0 to 1 + if axis == 1: + standardized = data2d + else: + standardized = data2d.T + + subtract = standardized.min() + standardized = (standardized - subtract) / ( + standardized.max() - standardized.min()) + + if axis == 1: + return standardized + else: + return standardized.T + + def dim_ratios(self, colors, dendrogram_ratio, colors_ratio): + """Get the proportions of the figure taken up by each axes.""" + ratios = [dendrogram_ratio] + + if colors is not None: + # Colors are encoded as rgb, so there is an extra dimension + if np.ndim(colors) > 2: + n_colors = len(colors) + else: + n_colors = 1 + + ratios += [n_colors * colors_ratio] + + # Add the ratio for the heatmap itself + ratios.append(1 - sum(ratios)) + + return ratios + + @staticmethod + def color_list_to_matrix_and_cmap(colors, ind, axis=0): + """Turns a list of colors into a numpy matrix and matplotlib colormap + + These arguments can now be plotted using heatmap(matrix, cmap) + and the provided colors will be plotted. + + Parameters + ---------- + colors : list of matplotlib colors + Colors to label the rows or columns of a dataframe. + ind : list of ints + Ordering of the rows or columns, to reorder the original colors + by the clustered dendrogram order + axis : int + Which axis this is labeling + + Returns + ------- + matrix : numpy.array + A numpy array of integer values, where each indexes into the cmap + cmap : matplotlib.colors.ListedColormap + + """ + try: + mpl.colors.to_rgb(colors[0]) + except ValueError: + # We have a 2D color structure + m, n = len(colors), len(colors[0]) + if not all(len(c) == n for c in colors[1:]): + raise ValueError("Multiple side color vectors must have same size") + else: + # We have one vector of colors + m, n = 1, len(colors) + colors = [colors] + + # Map from unique colors to colormap index value + unique_colors = {} + matrix = np.zeros((m, n), int) + for i, inner in enumerate(colors): + for j, color in enumerate(inner): + idx = unique_colors.setdefault(color, len(unique_colors)) + matrix[i, j] = idx + + # Reorder for clustering and transpose for axis + matrix = matrix[:, ind] + if axis == 0: + matrix = matrix.T + + cmap = mpl.colors.ListedColormap(list(unique_colors)) + return matrix, cmap + + def plot_dendrograms(self, row_cluster, col_cluster, metric, method, + row_linkage, col_linkage, tree_kws): + # Plot the row dendrogram + if row_cluster: + self.dendrogram_row = dendrogram( + self.data2d, metric=metric, method=method, label=False, axis=0, + ax=self.ax_row_dendrogram, rotate=True, linkage=row_linkage, + tree_kws=tree_kws + ) + else: + self.ax_row_dendrogram.set_xticks([]) + self.ax_row_dendrogram.set_yticks([]) + # PLot the column dendrogram + if col_cluster: + self.dendrogram_col = dendrogram( + self.data2d, metric=metric, method=method, label=False, + axis=1, ax=self.ax_col_dendrogram, linkage=col_linkage, + tree_kws=tree_kws + ) + else: + self.ax_col_dendrogram.set_xticks([]) + self.ax_col_dendrogram.set_yticks([]) + despine(ax=self.ax_row_dendrogram, bottom=True, left=True) + despine(ax=self.ax_col_dendrogram, bottom=True, left=True) + + def plot_colors(self, xind, yind, **kws): + """Plots color labels between the dendrogram and the heatmap + + Parameters + ---------- + heatmap_kws : dict + Keyword arguments heatmap + + """ + # Remove any custom colormap and centering + # TODO this code has consistently caused problems when we + # have missed kwargs that need to be excluded that it might + # be better to rewrite *in*clusively. + kws = kws.copy() + kws.pop('cmap', None) + kws.pop('norm', None) + kws.pop('center', None) + kws.pop('annot', None) + kws.pop('vmin', None) + kws.pop('vmax', None) + kws.pop('robust', None) + kws.pop('xticklabels', None) + kws.pop('yticklabels', None) + + # Plot the row colors + if self.row_colors is not None: + matrix, cmap = self.color_list_to_matrix_and_cmap( + self.row_colors, yind, axis=0) + + # Get row_color labels + if self.row_color_labels is not None: + row_color_labels = self.row_color_labels + else: + row_color_labels = False + + heatmap(matrix, cmap=cmap, cbar=False, ax=self.ax_row_colors, + xticklabels=row_color_labels, yticklabels=False, **kws) + + # Adjust rotation of labels + if row_color_labels is not False: + plt.setp(self.ax_row_colors.get_xticklabels(), rotation=90) + else: + despine(self.ax_row_colors, left=True, bottom=True) + + # Plot the column colors + if self.col_colors is not None: + matrix, cmap = self.color_list_to_matrix_and_cmap( + self.col_colors, xind, axis=1) + + # Get col_color labels + if self.col_color_labels is not None: + col_color_labels = self.col_color_labels + else: + col_color_labels = False + + heatmap(matrix, cmap=cmap, cbar=False, ax=self.ax_col_colors, + xticklabels=False, yticklabels=col_color_labels, **kws) + + # Adjust rotation of labels, place on right side + if col_color_labels is not False: + self.ax_col_colors.yaxis.tick_right() + plt.setp(self.ax_col_colors.get_yticklabels(), rotation=0) + else: + despine(self.ax_col_colors, left=True, bottom=True) + + def plot_matrix(self, colorbar_kws, xind, yind, **kws): + self.data2d = self.data2d.iloc[yind, xind] + self.mask = self.mask.iloc[yind, xind] + + # Try to reorganize specified tick labels, if provided + xtl = kws.pop("xticklabels", "auto") + try: + xtl = np.asarray(xtl)[xind] + except (TypeError, IndexError): + pass + ytl = kws.pop("yticklabels", "auto") + try: + ytl = np.asarray(ytl)[yind] + except (TypeError, IndexError): + pass + + # Reorganize the annotations to match the heatmap + annot = kws.pop("annot", None) + if annot is None or annot is False: + pass + else: + if isinstance(annot, bool): + annot_data = self.data2d + else: + annot_data = np.asarray(annot) + if annot_data.shape != self.data2d.shape: + err = "`data` and `annot` must have same shape." + raise ValueError(err) + annot_data = annot_data[yind][:, xind] + annot = annot_data + + # Setting ax_cbar=None in clustermap call implies no colorbar + kws.setdefault("cbar", self.ax_cbar is not None) + heatmap(self.data2d, ax=self.ax_heatmap, cbar_ax=self.ax_cbar, + cbar_kws=colorbar_kws, mask=self.mask, + xticklabels=xtl, yticklabels=ytl, annot=annot, **kws) + + ytl = self.ax_heatmap.get_yticklabels() + ytl_rot = None if not ytl else ytl[0].get_rotation() + self.ax_heatmap.yaxis.set_ticks_position('right') + self.ax_heatmap.yaxis.set_label_position('right') + if ytl_rot is not None: + ytl = self.ax_heatmap.get_yticklabels() + plt.setp(ytl, rotation=ytl_rot) + + tight_params = dict(h_pad=.02, w_pad=.02) + if self.ax_cbar is None: + self._figure.tight_layout(**tight_params) + else: + # Turn the colorbar axes off for tight layout so that its + # ticks don't interfere with the rest of the plot layout. + # Then move it. + self.ax_cbar.set_axis_off() + self._figure.tight_layout(**tight_params) + self.ax_cbar.set_axis_on() + self.ax_cbar.set_position(self.cbar_pos) + + def plot(self, metric, method, colorbar_kws, row_cluster, col_cluster, + row_linkage, col_linkage, tree_kws, **kws): + + # heatmap square=True sets the aspect ratio on the axes, but that is + # not compatible with the multi-axes layout of clustergrid + if kws.get("square", False): + msg = "``square=True`` ignored in clustermap" + warnings.warn(msg) + kws.pop("square") + + colorbar_kws = {} if colorbar_kws is None else colorbar_kws + + self.plot_dendrograms(row_cluster, col_cluster, metric, method, + row_linkage=row_linkage, col_linkage=col_linkage, + tree_kws=tree_kws) + try: + xind = self.dendrogram_col.reordered_ind + except AttributeError: + xind = np.arange(self.data2d.shape[1]) + try: + yind = self.dendrogram_row.reordered_ind + except AttributeError: + yind = np.arange(self.data2d.shape[0]) + + self.plot_colors(xind, yind, **kws) + self.plot_matrix(colorbar_kws, xind, yind, **kws) + return self + + +def clustermap( + data, *, + pivot_kws=None, method='average', metric='euclidean', + z_score=None, standard_scale=None, figsize=(10, 10), + cbar_kws=None, row_cluster=True, col_cluster=True, + row_linkage=None, col_linkage=None, + row_colors=None, col_colors=None, mask=None, + dendrogram_ratio=.2, colors_ratio=0.03, + cbar_pos=(.02, .8, .05, .18), tree_kws=None, + **kwargs +): + """ + Plot a matrix dataset as a hierarchically-clustered heatmap. + + This function requires scipy to be available. + + Parameters + ---------- + data : 2D array-like + Rectangular data for clustering. Cannot contain NAs. + pivot_kws : dict, optional + If `data` is a tidy dataframe, can provide keyword arguments for + pivot to create a rectangular dataframe. + method : str, optional + Linkage method to use for calculating clusters. See + :func:`scipy.cluster.hierarchy.linkage` documentation for more + information. + metric : str, optional + Distance metric to use for the data. See + :func:`scipy.spatial.distance.pdist` documentation for more options. + To use different metrics (or methods) for rows and columns, you may + construct each linkage matrix yourself and provide them as + `{row,col}_linkage`. + z_score : int or None, optional + Either 0 (rows) or 1 (columns). Whether or not to calculate z-scores + for the rows or the columns. Z scores are: z = (x - mean)/std, so + values in each row (column) will get the mean of the row (column) + subtracted, then divided by the standard deviation of the row (column). + This ensures that each row (column) has mean of 0 and variance of 1. + standard_scale : int or None, optional + Either 0 (rows) or 1 (columns). Whether or not to standardize that + dimension, meaning for each row or column, subtract the minimum and + divide each by its maximum. + figsize : tuple of (width, height), optional + Overall size of the figure. + cbar_kws : dict, optional + Keyword arguments to pass to `cbar_kws` in :func:`heatmap`, e.g. to + add a label to the colorbar. + {row,col}_cluster : bool, optional + If ``True``, cluster the {rows, columns}. + {row,col}_linkage : :class:`numpy.ndarray`, optional + Precomputed linkage matrix for the rows or columns. See + :func:`scipy.cluster.hierarchy.linkage` for specific formats. + {row,col}_colors : list-like or pandas DataFrame/Series, optional + List of colors to label for either the rows or columns. Useful to evaluate + whether samples within a group are clustered together. Can use nested lists or + DataFrame for multiple color levels of labeling. If given as a + :class:`pandas.DataFrame` or :class:`pandas.Series`, labels for the colors are + extracted from the DataFrames column names or from the name of the Series. + DataFrame/Series colors are also matched to the data by their index, ensuring + colors are drawn in the correct order. + mask : bool array or DataFrame, optional + If passed, data will not be shown in cells where `mask` is True. + Cells with missing values are automatically masked. Only used for + visualizing, not for calculating. + {dendrogram,colors}_ratio : float, or pair of floats, optional + Proportion of the figure size devoted to the two marginal elements. If + a pair is given, they correspond to (row, col) ratios. + cbar_pos : tuple of (left, bottom, width, height), optional + Position of the colorbar axes in the figure. Setting to ``None`` will + disable the colorbar. + tree_kws : dict, optional + Parameters for the :class:`matplotlib.collections.LineCollection` + that is used to plot the lines of the dendrogram tree. + kwargs : other keyword arguments + All other keyword arguments are passed to :func:`heatmap`. + + Returns + ------- + :class:`ClusterGrid` + A :class:`ClusterGrid` instance. + + See Also + -------- + heatmap : Plot rectangular data as a color-encoded matrix. + + Notes + ----- + The returned object has a ``savefig`` method that should be used if you + want to save the figure object without clipping the dendrograms. + + To access the reordered row indices, use: + ``clustergrid.dendrogram_row.reordered_ind`` + + Column indices, use: + ``clustergrid.dendrogram_col.reordered_ind`` + + Examples + -------- + + .. include:: ../docstrings/clustermap.rst + + """ + if _no_scipy: + raise RuntimeError("clustermap requires scipy to be available") + + plotter = ClusterGrid(data, pivot_kws=pivot_kws, figsize=figsize, + row_colors=row_colors, col_colors=col_colors, + z_score=z_score, standard_scale=standard_scale, + mask=mask, dendrogram_ratio=dendrogram_ratio, + colors_ratio=colors_ratio, cbar_pos=cbar_pos) + + return plotter.plot(metric=metric, method=method, + colorbar_kws=cbar_kws, + row_cluster=row_cluster, col_cluster=col_cluster, + row_linkage=row_linkage, col_linkage=col_linkage, + tree_kws=tree_kws, **kwargs) diff --git a/lib/python3.10/site-packages/seaborn/miscplot.py b/lib/python3.10/site-packages/seaborn/miscplot.py new file mode 100644 index 0000000000000000000000000000000000000000..717c0ac40b07dafb60a1216be6f239f59b3ed524 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/miscplot.py @@ -0,0 +1,48 @@ +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +import matplotlib.ticker as ticker + +__all__ = ["palplot", "dogplot"] + + +def palplot(pal, size=1): + """Plot the values in a color palette as a horizontal array. + + Parameters + ---------- + pal : sequence of matplotlib colors + colors, i.e. as returned by seaborn.color_palette() + size : + scaling factor for size of plot + + """ + n = len(pal) + f, ax = plt.subplots(1, 1, figsize=(n * size, size)) + ax.imshow(np.arange(n).reshape(1, n), + cmap=mpl.colors.ListedColormap(list(pal)), + interpolation="nearest", aspect="auto") + ax.set_xticks(np.arange(n) - .5) + ax.set_yticks([-.5, .5]) + # Ensure nice border between colors + ax.set_xticklabels(["" for _ in range(n)]) + # The proper way to set no ticks + ax.yaxis.set_major_locator(ticker.NullLocator()) + + +def dogplot(*_, **__): + """Who's a good boy?""" + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + from io import BytesIO + + url = "https://github.com/mwaskom/seaborn-data/raw/master/png/img{}.png" + pic = np.random.randint(2, 7) + data = BytesIO(urlopen(url.format(pic)).read()) + img = plt.imread(data) + f, ax = plt.subplots(figsize=(5, 5), dpi=100) + f.subplots_adjust(0, 0, 1, 1) + ax.imshow(img) + ax.set_axis_off() diff --git a/lib/python3.10/site-packages/seaborn/objects.py b/lib/python3.10/site-packages/seaborn/objects.py new file mode 100644 index 0000000000000000000000000000000000000000..123e57f0a936e8e73c684dd647b9813da86a3f60 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/objects.py @@ -0,0 +1,49 @@ +""" +A declarative, object-oriented interface for creating statistical graphics. + +The seaborn.objects namespace contains a number of classes that can be composed +together to build a customized visualization. + +The main object is :class:`Plot`, which is the starting point for all figures. +Pass :class:`Plot` a dataset and specify assignments from its variables to +roles in the plot. Build up the visualization by calling its methods. + +There are four other general types of objects in this interface: + +- :class:`Mark` subclasses, which create matplotlib artists for visualization +- :class:`Stat` subclasses, which apply statistical transforms before plotting +- :class:`Move` subclasses, which make further adjustments to reduce overplotting + +These classes are passed to :meth:`Plot.add` to define a layer in the plot. +Each layer has a :class:`Mark` and optional :class:`Stat` and/or :class:`Move`. +Plots can have multiple layers. + +The other general type of object is a :class:`Scale` subclass, which provide an +interface for controlling the mappings between data values and visual properties. +Pass :class:`Scale` objects to :meth:`Plot.scale`. + +See the documentation for other :class:`Plot` methods to learn about the many +ways that a plot can be enhanced and customized. + +""" +from seaborn._core.plot import Plot # noqa: F401 + +from seaborn._marks.base import Mark # noqa: F401 +from seaborn._marks.area import Area, Band # noqa: F401 +from seaborn._marks.bar import Bar, Bars # noqa: F401 +from seaborn._marks.dot import Dot, Dots # noqa: F401 +from seaborn._marks.line import Dash, Line, Lines, Path, Paths, Range # noqa: F401 +from seaborn._marks.text import Text # noqa: F401 + +from seaborn._stats.base import Stat # noqa: F401 +from seaborn._stats.aggregation import Agg, Est # noqa: F401 +from seaborn._stats.counting import Count, Hist # noqa: F401 +from seaborn._stats.density import KDE # noqa: F401 +from seaborn._stats.order import Perc # noqa: F401 +from seaborn._stats.regression import PolyFit # noqa: F401 + +from seaborn._core.moves import Dodge, Jitter, Norm, Shift, Stack, Move # noqa: F401 + +from seaborn._core.scales import ( # noqa: F401 + Boolean, Continuous, Nominal, Temporal, Scale +) diff --git a/lib/python3.10/site-packages/seaborn/palettes.py b/lib/python3.10/site-packages/seaborn/palettes.py new file mode 100644 index 0000000000000000000000000000000000000000..f1214b2a0f8d22339c0acaa05aea6af27a2e080c --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/palettes.py @@ -0,0 +1,842 @@ +import colorsys +from itertools import cycle + +import numpy as np +import matplotlib as mpl + +from .external import husl + +from .utils import desaturate, get_color_cycle +from .colors import xkcd_rgb, crayons +from ._compat import get_colormap + + +__all__ = ["color_palette", "hls_palette", "husl_palette", "mpl_palette", + "dark_palette", "light_palette", "diverging_palette", + "blend_palette", "xkcd_palette", "crayon_palette", + "cubehelix_palette", "set_color_codes"] + + +SEABORN_PALETTES = dict( + deep=["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3", + "#937860", "#DA8BC3", "#8C8C8C", "#CCB974", "#64B5CD"], + deep6=["#4C72B0", "#55A868", "#C44E52", + "#8172B3", "#CCB974", "#64B5CD"], + muted=["#4878D0", "#EE854A", "#6ACC64", "#D65F5F", "#956CB4", + "#8C613C", "#DC7EC0", "#797979", "#D5BB67", "#82C6E2"], + muted6=["#4878D0", "#6ACC64", "#D65F5F", + "#956CB4", "#D5BB67", "#82C6E2"], + pastel=["#A1C9F4", "#FFB482", "#8DE5A1", "#FF9F9B", "#D0BBFF", + "#DEBB9B", "#FAB0E4", "#CFCFCF", "#FFFEA3", "#B9F2F0"], + pastel6=["#A1C9F4", "#8DE5A1", "#FF9F9B", + "#D0BBFF", "#FFFEA3", "#B9F2F0"], + bright=["#023EFF", "#FF7C00", "#1AC938", "#E8000B", "#8B2BE2", + "#9F4800", "#F14CC1", "#A3A3A3", "#FFC400", "#00D7FF"], + bright6=["#023EFF", "#1AC938", "#E8000B", + "#8B2BE2", "#FFC400", "#00D7FF"], + dark=["#001C7F", "#B1400D", "#12711C", "#8C0800", "#591E71", + "#592F0D", "#A23582", "#3C3C3C", "#B8850A", "#006374"], + dark6=["#001C7F", "#12711C", "#8C0800", + "#591E71", "#B8850A", "#006374"], + colorblind=["#0173B2", "#DE8F05", "#029E73", "#D55E00", "#CC78BC", + "#CA9161", "#FBAFE4", "#949494", "#ECE133", "#56B4E9"], + colorblind6=["#0173B2", "#029E73", "#D55E00", + "#CC78BC", "#ECE133", "#56B4E9"] +) + + +MPL_QUAL_PALS = { + "tab10": 10, "tab20": 20, "tab20b": 20, "tab20c": 20, + "Set1": 9, "Set2": 8, "Set3": 12, + "Accent": 8, "Paired": 12, + "Pastel1": 9, "Pastel2": 8, "Dark2": 8, +} + + +QUAL_PALETTE_SIZES = MPL_QUAL_PALS.copy() +QUAL_PALETTE_SIZES.update({k: len(v) for k, v in SEABORN_PALETTES.items()}) +QUAL_PALETTES = list(QUAL_PALETTE_SIZES.keys()) + + +class _ColorPalette(list): + """Set the color palette in a with statement, otherwise be a list.""" + def __enter__(self): + """Open the context.""" + from .rcmod import set_palette + self._orig_palette = color_palette() + set_palette(self) + return self + + def __exit__(self, *args): + """Close the context.""" + from .rcmod import set_palette + set_palette(self._orig_palette) + + def as_hex(self): + """Return a color palette with hex codes instead of RGB values.""" + hex = [mpl.colors.rgb2hex(rgb) for rgb in self] + return _ColorPalette(hex) + + def _repr_html_(self): + """Rich display of the color palette in an HTML frontend.""" + s = 55 + n = len(self) + html = f'' + for i, c in enumerate(self.as_hex()): + html += ( + f'' + ) + html += '' + return html + + +def _patch_colormap_display(): + """Simplify the rich display of matplotlib color maps in a notebook.""" + def _repr_png_(self): + """Generate a PNG representation of the Colormap.""" + import io + from PIL import Image + import numpy as np + IMAGE_SIZE = (400, 50) + X = np.tile(np.linspace(0, 1, IMAGE_SIZE[0]), (IMAGE_SIZE[1], 1)) + pixels = self(X, bytes=True) + png_bytes = io.BytesIO() + Image.fromarray(pixels).save(png_bytes, format='png') + return png_bytes.getvalue() + + def _repr_html_(self): + """Generate an HTML representation of the Colormap.""" + import base64 + png_bytes = self._repr_png_() + png_base64 = base64.b64encode(png_bytes).decode('ascii') + return ('') + + mpl.colors.Colormap._repr_png_ = _repr_png_ + mpl.colors.Colormap._repr_html_ = _repr_html_ + + +def color_palette(palette=None, n_colors=None, desat=None, as_cmap=False): + """Return a list of colors or continuous colormap defining a palette. + + Possible ``palette`` values include: + - Name of a seaborn palette (deep, muted, bright, pastel, dark, colorblind) + - Name of matplotlib colormap + - 'husl' or 'hls' + - 'ch:' + - 'light:', 'dark:', 'blend:,', + - A sequence of colors in any format matplotlib accepts + + Calling this function with ``palette=None`` will return the current + matplotlib color cycle. + + This function can also be used in a ``with`` statement to temporarily + set the color cycle for a plot or set of plots. + + See the :ref:`tutorial ` for more information. + + Parameters + ---------- + palette : None, string, or sequence, optional + Name of palette or None to return current palette. If a sequence, input + colors are used but possibly cycled and desaturated. + n_colors : int, optional + Number of colors in the palette. If ``None``, the default will depend + on how ``palette`` is specified. Named palettes default to 6 colors, + but grabbing the current palette or passing in a list of colors will + not change the number of colors unless this is specified. Asking for + more colors than exist in the palette will cause it to cycle. Ignored + when ``as_cmap`` is True. + desat : float, optional + Proportion to desaturate each color by. + as_cmap : bool + If True, return a :class:`matplotlib.colors.ListedColormap`. + + Returns + ------- + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + set_palette : Set the default color cycle for all plots. + set_color_codes : Reassign color codes like ``"b"``, ``"g"``, etc. to + colors from one of the seaborn palettes. + + Examples + -------- + + .. include:: ../docstrings/color_palette.rst + + """ + if palette is None: + palette = get_color_cycle() + if n_colors is None: + n_colors = len(palette) + + elif not isinstance(palette, str): + palette = palette + if n_colors is None: + n_colors = len(palette) + else: + + if n_colors is None: + # Use all colors in a qualitative palette or 6 of another kind + n_colors = QUAL_PALETTE_SIZES.get(palette, 6) + + if palette in SEABORN_PALETTES: + # Named "seaborn variant" of matplotlib default color cycle + palette = SEABORN_PALETTES[palette] + + elif palette == "hls": + # Evenly spaced colors in cylindrical RGB space + palette = hls_palette(n_colors, as_cmap=as_cmap) + + elif palette == "husl": + # Evenly spaced colors in cylindrical Lab space + palette = husl_palette(n_colors, as_cmap=as_cmap) + + elif palette.lower() == "jet": + # Paternalism + raise ValueError("No.") + + elif palette.startswith("ch:"): + # Cubehelix palette with params specified in string + args, kwargs = _parse_cubehelix_args(palette) + palette = cubehelix_palette(n_colors, *args, **kwargs, as_cmap=as_cmap) + + elif palette.startswith("light:"): + # light palette to color specified in string + _, color = palette.split(":") + reverse = color.endswith("_r") + if reverse: + color = color[:-2] + palette = light_palette(color, n_colors, reverse=reverse, as_cmap=as_cmap) + + elif palette.startswith("dark:"): + # light palette to color specified in string + _, color = palette.split(":") + reverse = color.endswith("_r") + if reverse: + color = color[:-2] + palette = dark_palette(color, n_colors, reverse=reverse, as_cmap=as_cmap) + + elif palette.startswith("blend:"): + # blend palette between colors specified in string + _, colors = palette.split(":") + colors = colors.split(",") + palette = blend_palette(colors, n_colors, as_cmap=as_cmap) + + else: + try: + # Perhaps a named matplotlib colormap? + palette = mpl_palette(palette, n_colors, as_cmap=as_cmap) + except (ValueError, KeyError): # Error class changed in mpl36 + raise ValueError(f"{palette!r} is not a valid palette name") + + if desat is not None: + palette = [desaturate(c, desat) for c in palette] + + if not as_cmap: + + # Always return as many colors as we asked for + pal_cycle = cycle(palette) + palette = [next(pal_cycle) for _ in range(n_colors)] + + # Always return in r, g, b tuple format + try: + palette = map(mpl.colors.colorConverter.to_rgb, palette) + palette = _ColorPalette(palette) + except ValueError: + raise ValueError(f"Could not generate a palette for {palette}") + + return palette + + +def hls_palette(n_colors=6, h=.01, l=.6, s=.65, as_cmap=False): # noqa + """ + Return hues with constant lightness and saturation in the HLS system. + + The hues are evenly sampled along a circular path. The resulting palette will be + appropriate for categorical or cyclical data. + + The `h`, `l`, and `s` values should be between 0 and 1. + + .. note:: + While the separation of the resulting colors will be mathematically + constant, the HLS system does not construct a perceptually-uniform space, + so their apparent intensity will vary. + + Parameters + ---------- + n_colors : int + Number of colors in the palette. + h : float + The value of the first hue. + l : float + The lightness value. + s : float + The saturation intensity. + as_cmap : bool + If True, return a matplotlib colormap object. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + husl_palette : Make a palette using evenly spaced hues in the HUSL system. + + Examples + -------- + .. include:: ../docstrings/hls_palette.rst + + """ + if as_cmap: + n_colors = 256 + hues = np.linspace(0, 1, int(n_colors) + 1)[:-1] + hues += h + hues %= 1 + hues -= hues.astype(int) + palette = [colorsys.hls_to_rgb(h_i, l, s) for h_i in hues] + if as_cmap: + return mpl.colors.ListedColormap(palette, "hls") + else: + return _ColorPalette(palette) + + +def husl_palette(n_colors=6, h=.01, s=.9, l=.65, as_cmap=False): # noqa + """ + Return hues with constant lightness and saturation in the HUSL system. + + The hues are evenly sampled along a circular path. The resulting palette will be + appropriate for categorical or cyclical data. + + The `h`, `l`, and `s` values should be between 0 and 1. + + This function is similar to :func:`hls_palette`, but it uses a nonlinear color + space that is more perceptually uniform. + + Parameters + ---------- + n_colors : int + Number of colors in the palette. + h : float + The value of the first hue. + l : float + The lightness value. + s : float + The saturation intensity. + as_cmap : bool + If True, return a matplotlib colormap object. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + hls_palette : Make a palette using evenly spaced hues in the HSL system. + + Examples + -------- + .. include:: ../docstrings/husl_palette.rst + + """ + if as_cmap: + n_colors = 256 + hues = np.linspace(0, 1, int(n_colors) + 1)[:-1] + hues += h + hues %= 1 + hues *= 359 + s *= 99 + l *= 99 # noqa + palette = [_color_to_rgb((h_i, s, l), input="husl") for h_i in hues] + if as_cmap: + return mpl.colors.ListedColormap(palette, "hsl") + else: + return _ColorPalette(palette) + + +def mpl_palette(name, n_colors=6, as_cmap=False): + """ + Return a palette or colormap from the matplotlib registry. + + For continuous palettes, evenly-spaced discrete samples are chosen while + excluding the minimum and maximum value in the colormap to provide better + contrast at the extremes. + + For qualitative palettes (e.g. those from colorbrewer), exact values are + indexed (rather than interpolated), but fewer than `n_colors` can be returned + if the palette does not define that many. + + Parameters + ---------- + name : string + Name of the palette. This should be a named matplotlib colormap. + n_colors : int + Number of discrete colors in the palette. + + Returns + ------- + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + Examples + -------- + .. include: ../docstrings/mpl_palette.rst + + """ + if name.endswith("_d"): + sub_name = name[:-2] + if sub_name.endswith("_r"): + reverse = True + sub_name = sub_name[:-2] + else: + reverse = False + pal = color_palette(sub_name, 2) + ["#333333"] + if reverse: + pal = pal[::-1] + cmap = blend_palette(pal, n_colors, as_cmap=True) + else: + cmap = get_colormap(name) + + if name in MPL_QUAL_PALS: + bins = np.linspace(0, 1, MPL_QUAL_PALS[name])[:n_colors] + else: + bins = np.linspace(0, 1, int(n_colors) + 2)[1:-1] + palette = list(map(tuple, cmap(bins)[:, :3])) + + if as_cmap: + return cmap + else: + return _ColorPalette(palette) + + +def _color_to_rgb(color, input): + """Add some more flexibility to color choices.""" + if input == "hls": + color = colorsys.hls_to_rgb(*color) + elif input == "husl": + color = husl.husl_to_rgb(*color) + color = tuple(np.clip(color, 0, 1)) + elif input == "xkcd": + color = xkcd_rgb[color] + + return mpl.colors.to_rgb(color) + + +def dark_palette(color, n_colors=6, reverse=False, as_cmap=False, input="rgb"): + """Make a sequential palette that blends from dark to ``color``. + + This kind of palette is good for data that range between relatively + uninteresting low values and interesting high values. + + The ``color`` parameter can be specified in a number of ways, including + all options for defining a color in matplotlib and several additional + color spaces that are handled by seaborn. You can also use the database + of named colors from the XKCD color survey. + + If you are using the IPython notebook, you can also choose this palette + interactively with the :func:`choose_dark_palette` function. + + Parameters + ---------- + color : base color for high values + hex, rgb-tuple, or html color name + n_colors : int, optional + number of colors in the palette + reverse : bool, optional + if True, reverse the direction of the blend + as_cmap : bool, optional + If True, return a :class:`matplotlib.colors.ListedColormap`. + input : {'rgb', 'hls', 'husl', xkcd'} + Color space to interpret the input color. The first three options + apply to tuple inputs and the latter applies to string inputs. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + light_palette : Create a sequential palette with bright low values. + diverging_palette : Create a diverging palette with two colors. + + Examples + -------- + .. include:: ../docstrings/dark_palette.rst + + """ + rgb = _color_to_rgb(color, input) + h, s, l = husl.rgb_to_husl(*rgb) + gray_s, gray_l = .15 * s, 15 + gray = _color_to_rgb((h, gray_s, gray_l), input="husl") + colors = [rgb, gray] if reverse else [gray, rgb] + return blend_palette(colors, n_colors, as_cmap) + + +def light_palette(color, n_colors=6, reverse=False, as_cmap=False, input="rgb"): + """Make a sequential palette that blends from light to ``color``. + + The ``color`` parameter can be specified in a number of ways, including + all options for defining a color in matplotlib and several additional + color spaces that are handled by seaborn. You can also use the database + of named colors from the XKCD color survey. + + If you are using a Jupyter notebook, you can also choose this palette + interactively with the :func:`choose_light_palette` function. + + Parameters + ---------- + color : base color for high values + hex code, html color name, or tuple in `input` space. + n_colors : int, optional + number of colors in the palette + reverse : bool, optional + if True, reverse the direction of the blend + as_cmap : bool, optional + If True, return a :class:`matplotlib.colors.ListedColormap`. + input : {'rgb', 'hls', 'husl', xkcd'} + Color space to interpret the input color. The first three options + apply to tuple inputs and the latter applies to string inputs. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + dark_palette : Create a sequential palette with dark low values. + diverging_palette : Create a diverging palette with two colors. + + Examples + -------- + .. include:: ../docstrings/light_palette.rst + + """ + rgb = _color_to_rgb(color, input) + h, s, l = husl.rgb_to_husl(*rgb) + gray_s, gray_l = .15 * s, 95 + gray = _color_to_rgb((h, gray_s, gray_l), input="husl") + colors = [rgb, gray] if reverse else [gray, rgb] + return blend_palette(colors, n_colors, as_cmap) + + +def diverging_palette(h_neg, h_pos, s=75, l=50, sep=1, n=6, # noqa + center="light", as_cmap=False): + """Make a diverging palette between two HUSL colors. + + If you are using the IPython notebook, you can also choose this palette + interactively with the :func:`choose_diverging_palette` function. + + Parameters + ---------- + h_neg, h_pos : float in [0, 359] + Anchor hues for negative and positive extents of the map. + s : float in [0, 100], optional + Anchor saturation for both extents of the map. + l : float in [0, 100], optional + Anchor lightness for both extents of the map. + sep : int, optional + Size of the intermediate region. + n : int, optional + Number of colors in the palette (if not returning a cmap) + center : {"light", "dark"}, optional + Whether the center of the palette is light or dark + as_cmap : bool, optional + If True, return a :class:`matplotlib.colors.ListedColormap`. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + dark_palette : Create a sequential palette with dark values. + light_palette : Create a sequential palette with light values. + + Examples + -------- + .. include: ../docstrings/diverging_palette.rst + + """ + palfunc = dict(dark=dark_palette, light=light_palette)[center] + n_half = int(128 - (sep // 2)) + neg = palfunc((h_neg, s, l), n_half, reverse=True, input="husl") + pos = palfunc((h_pos, s, l), n_half, input="husl") + midpoint = dict(light=[(.95, .95, .95)], dark=[(.133, .133, .133)])[center] + mid = midpoint * sep + pal = blend_palette(np.concatenate([neg, mid, pos]), n, as_cmap=as_cmap) + return pal + + +def blend_palette(colors, n_colors=6, as_cmap=False, input="rgb"): + """Make a palette that blends between a list of colors. + + Parameters + ---------- + colors : sequence of colors in various formats interpreted by `input` + hex code, html color name, or tuple in `input` space. + n_colors : int, optional + Number of colors in the palette. + as_cmap : bool, optional + If True, return a :class:`matplotlib.colors.ListedColormap`. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + Examples + -------- + .. include: ../docstrings/blend_palette.rst + + """ + colors = [_color_to_rgb(color, input) for color in colors] + name = "blend" + pal = mpl.colors.LinearSegmentedColormap.from_list(name, colors) + if not as_cmap: + rgb_array = pal(np.linspace(0, 1, int(n_colors)))[:, :3] # no alpha + pal = _ColorPalette(map(tuple, rgb_array)) + return pal + + +def xkcd_palette(colors): + """Make a palette with color names from the xkcd color survey. + + See xkcd for the full list of colors: https://xkcd.com/color/rgb/ + + This is just a simple wrapper around the `seaborn.xkcd_rgb` dictionary. + + Parameters + ---------- + colors : list of strings + List of keys in the `seaborn.xkcd_rgb` dictionary. + + Returns + ------- + palette + A list of colors as RGB tuples. + + See Also + -------- + crayon_palette : Make a palette with Crayola crayon colors. + + """ + palette = [xkcd_rgb[name] for name in colors] + return color_palette(palette, len(palette)) + + +def crayon_palette(colors): + """Make a palette with color names from Crayola crayons. + + Colors are taken from here: + https://en.wikipedia.org/wiki/List_of_Crayola_crayon_colors + + This is just a simple wrapper around the `seaborn.crayons` dictionary. + + Parameters + ---------- + colors : list of strings + List of keys in the `seaborn.crayons` dictionary. + + Returns + ------- + palette + A list of colors as RGB tuples. + + See Also + -------- + xkcd_palette : Make a palette with named colors from the XKCD color survey. + + """ + palette = [crayons[name] for name in colors] + return color_palette(palette, len(palette)) + + +def cubehelix_palette(n_colors=6, start=0, rot=.4, gamma=1.0, hue=0.8, + light=.85, dark=.15, reverse=False, as_cmap=False): + """Make a sequential palette from the cubehelix system. + + This produces a colormap with linearly-decreasing (or increasing) + brightness. That means that information will be preserved if printed to + black and white or viewed by someone who is colorblind. "cubehelix" is + also available as a matplotlib-based palette, but this function gives the + user more control over the look of the palette and has a different set of + defaults. + + In addition to using this function, it is also possible to generate a + cubehelix palette generally in seaborn using a string starting with + `ch:` and containing other parameters (e.g. `"ch:s=.25,r=-.5"`). + + Parameters + ---------- + n_colors : int + Number of colors in the palette. + start : float, 0 <= start <= 3 + The hue value at the start of the helix. + rot : float + Rotations around the hue wheel over the range of the palette. + gamma : float 0 <= gamma + Nonlinearity to emphasize dark (gamma < 1) or light (gamma > 1) colors. + hue : float, 0 <= hue <= 1 + Saturation of the colors. + dark : float 0 <= dark <= 1 + Intensity of the darkest color in the palette. + light : float 0 <= light <= 1 + Intensity of the lightest color in the palette. + reverse : bool + If True, the palette will go from dark to light. + as_cmap : bool + If True, return a :class:`matplotlib.colors.ListedColormap`. + + Returns + ------- + palette + list of RGB tuples or :class:`matplotlib.colors.ListedColormap` + + See Also + -------- + choose_cubehelix_palette : Launch an interactive widget to select cubehelix + palette parameters. + dark_palette : Create a sequential palette with dark low values. + light_palette : Create a sequential palette with bright low values. + + References + ---------- + Green, D. A. (2011). "A colour scheme for the display of astronomical + intensity images". Bulletin of the Astromical Society of India, Vol. 39, + p. 289-295. + + Examples + -------- + .. include:: ../docstrings/cubehelix_palette.rst + + """ + def get_color_function(p0, p1): + # Copied from matplotlib because it lives in private module + def color(x): + # Apply gamma factor to emphasise low or high intensity values + xg = x ** gamma + + # Calculate amplitude and angle of deviation from the black + # to white diagonal in the plane of constant + # perceived intensity. + a = hue * xg * (1 - xg) / 2 + + phi = 2 * np.pi * (start / 3 + rot * x) + + return xg + a * (p0 * np.cos(phi) + p1 * np.sin(phi)) + return color + + cdict = { + "red": get_color_function(-0.14861, 1.78277), + "green": get_color_function(-0.29227, -0.90649), + "blue": get_color_function(1.97294, 0.0), + } + + cmap = mpl.colors.LinearSegmentedColormap("cubehelix", cdict) + + x = np.linspace(light, dark, int(n_colors)) + pal = cmap(x)[:, :3].tolist() + if reverse: + pal = pal[::-1] + + if as_cmap: + x_256 = np.linspace(light, dark, 256) + if reverse: + x_256 = x_256[::-1] + pal_256 = cmap(x_256) + cmap = mpl.colors.ListedColormap(pal_256, "seaborn_cubehelix") + return cmap + else: + return _ColorPalette(pal) + + +def _parse_cubehelix_args(argstr): + """Turn stringified cubehelix params into args/kwargs.""" + + if argstr.startswith("ch:"): + argstr = argstr[3:] + + if argstr.endswith("_r"): + reverse = True + argstr = argstr[:-2] + else: + reverse = False + + if not argstr: + return [], {"reverse": reverse} + + all_args = argstr.split(",") + + args = [float(a.strip(" ")) for a in all_args if "=" not in a] + + kwargs = [a.split("=") for a in all_args if "=" in a] + kwargs = {k.strip(" "): float(v.strip(" ")) for k, v in kwargs} + + kwarg_map = dict( + s="start", r="rot", g="gamma", + h="hue", l="light", d="dark", # noqa: E741 + ) + + kwargs = {kwarg_map.get(k, k): v for k, v in kwargs.items()} + + if reverse: + kwargs["reverse"] = True + + return args, kwargs + + +def set_color_codes(palette="deep"): + """Change how matplotlib color shorthands are interpreted. + + Calling this will change how shorthand codes like "b" or "g" + are interpreted by matplotlib in subsequent plots. + + Parameters + ---------- + palette : {deep, muted, pastel, dark, bright, colorblind} + Named seaborn palette to use as the source of colors. + + See Also + -------- + set : Color codes can be set through the high-level seaborn style + manager. + set_palette : Color codes can also be set through the function that + sets the matplotlib color cycle. + + """ + if palette == "reset": + colors = [ + (0., 0., 1.), + (0., .5, 0.), + (1., 0., 0.), + (.75, 0., .75), + (.75, .75, 0.), + (0., .75, .75), + (0., 0., 0.) + ] + elif not isinstance(palette, str): + err = "set_color_codes requires a named seaborn palette" + raise TypeError(err) + elif palette in SEABORN_PALETTES: + if not palette.endswith("6"): + palette = palette + "6" + colors = SEABORN_PALETTES[palette] + [(.1, .1, .1)] + else: + err = f"Cannot set colors with palette '{palette}'" + raise ValueError(err) + + for code, color in zip("bgrmyck", colors): + rgb = mpl.colors.colorConverter.to_rgb(color) + mpl.colors.colorConverter.colors[code] = rgb + mpl.colors.colorConverter.cache[code] = rgb diff --git a/lib/python3.10/site-packages/seaborn/rcmod.py b/lib/python3.10/site-packages/seaborn/rcmod.py new file mode 100644 index 0000000000000000000000000000000000000000..ca70a44695b4f7573df1e23bec2b0be8bd8b5905 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/rcmod.py @@ -0,0 +1,534 @@ +"""Control plot style and scaling using the matplotlib rcParams interface.""" +import functools +import matplotlib as mpl +from cycler import cycler +from . import palettes + + +__all__ = ["set_theme", "set", "reset_defaults", "reset_orig", + "axes_style", "set_style", "plotting_context", "set_context", + "set_palette"] + + +_style_keys = [ + + "axes.facecolor", + "axes.edgecolor", + "axes.grid", + "axes.axisbelow", + "axes.labelcolor", + + "figure.facecolor", + + "grid.color", + "grid.linestyle", + + "text.color", + + "xtick.color", + "ytick.color", + "xtick.direction", + "ytick.direction", + "lines.solid_capstyle", + + "patch.edgecolor", + "patch.force_edgecolor", + + "image.cmap", + "font.family", + "font.sans-serif", + + "xtick.bottom", + "xtick.top", + "ytick.left", + "ytick.right", + + "axes.spines.left", + "axes.spines.bottom", + "axes.spines.right", + "axes.spines.top", + +] + +_context_keys = [ + + "font.size", + "axes.labelsize", + "axes.titlesize", + "xtick.labelsize", + "ytick.labelsize", + "legend.fontsize", + "legend.title_fontsize", + + "axes.linewidth", + "grid.linewidth", + "lines.linewidth", + "lines.markersize", + "patch.linewidth", + + "xtick.major.width", + "ytick.major.width", + "xtick.minor.width", + "ytick.minor.width", + + "xtick.major.size", + "ytick.major.size", + "xtick.minor.size", + "ytick.minor.size", + +] + + +def set_theme(context="notebook", style="darkgrid", palette="deep", + font="sans-serif", font_scale=1, color_codes=True, rc=None): + """ + Set aspects of the visual theme for all matplotlib and seaborn plots. + + This function changes the global defaults for all plots using the + matplotlib rcParams system. The themeing is decomposed into several distinct + sets of parameter values. + + The options are illustrated in the :doc:`aesthetics <../tutorial/aesthetics>` + and :doc:`color palette <../tutorial/color_palettes>` tutorials. + + Parameters + ---------- + context : string or dict + Scaling parameters, see :func:`plotting_context`. + style : string or dict + Axes style parameters, see :func:`axes_style`. + palette : string or sequence + Color palette, see :func:`color_palette`. + font : string + Font family, see matplotlib font manager. + font_scale : float, optional + Separate scaling factor to independently scale the size of the + font elements. + color_codes : bool + If ``True`` and ``palette`` is a seaborn palette, remap the shorthand + color codes (e.g. "b", "g", "r", etc.) to the colors from this palette. + rc : dict or None + Dictionary of rc parameter mappings to override the above. + + Examples + -------- + + .. include:: ../docstrings/set_theme.rst + + """ + set_context(context, font_scale) + set_style(style, rc={"font.family": font}) + set_palette(palette, color_codes=color_codes) + if rc is not None: + mpl.rcParams.update(rc) + + +def set(*args, **kwargs): + """ + Alias for :func:`set_theme`, which is the preferred interface. + + This function may be removed in the future. + """ + set_theme(*args, **kwargs) + + +def reset_defaults(): + """Restore all RC params to default settings.""" + mpl.rcParams.update(mpl.rcParamsDefault) + + +def reset_orig(): + """Restore all RC params to original settings (respects custom rc).""" + from . import _orig_rc_params + mpl.rcParams.update(_orig_rc_params) + + +def axes_style(style=None, rc=None): + """ + Get the parameters that control the general style of the plots. + + The style parameters control properties like the color of the background and + whether a grid is enabled by default. This is accomplished using the + matplotlib rcParams system. + + The options are illustrated in the + :doc:`aesthetics tutorial <../tutorial/aesthetics>`. + + This function can also be used as a context manager to temporarily + alter the global defaults. See :func:`set_theme` or :func:`set_style` + to modify the global defaults for all plots. + + Parameters + ---------- + style : None, dict, or one of {darkgrid, whitegrid, dark, white, ticks} + A dictionary of parameters or the name of a preconfigured style. + rc : dict, optional + Parameter mappings to override the values in the preset seaborn + style dictionaries. This only updates parameters that are + considered part of the style definition. + + Examples + -------- + + .. include:: ../docstrings/axes_style.rst + + """ + if style is None: + style_dict = {k: mpl.rcParams[k] for k in _style_keys} + + elif isinstance(style, dict): + style_dict = style + + else: + styles = ["white", "dark", "whitegrid", "darkgrid", "ticks"] + if style not in styles: + raise ValueError(f"style must be one of {', '.join(styles)}") + + # Define colors here + dark_gray = ".15" + light_gray = ".8" + + # Common parameters + style_dict = { + + "figure.facecolor": "white", + "axes.labelcolor": dark_gray, + + "xtick.direction": "out", + "ytick.direction": "out", + "xtick.color": dark_gray, + "ytick.color": dark_gray, + + "axes.axisbelow": True, + "grid.linestyle": "-", + + + "text.color": dark_gray, + "font.family": ["sans-serif"], + "font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", + "Bitstream Vera Sans", "sans-serif"], + + + "lines.solid_capstyle": "round", + "patch.edgecolor": "w", + "patch.force_edgecolor": True, + + "image.cmap": "rocket", + + "xtick.top": False, + "ytick.right": False, + + } + + # Set grid on or off + if "grid" in style: + style_dict.update({ + "axes.grid": True, + }) + else: + style_dict.update({ + "axes.grid": False, + }) + + # Set the color of the background, spines, and grids + if style.startswith("dark"): + style_dict.update({ + + "axes.facecolor": "#EAEAF2", + "axes.edgecolor": "white", + "grid.color": "white", + + "axes.spines.left": True, + "axes.spines.bottom": True, + "axes.spines.right": True, + "axes.spines.top": True, + + }) + + elif style == "whitegrid": + style_dict.update({ + + "axes.facecolor": "white", + "axes.edgecolor": light_gray, + "grid.color": light_gray, + + "axes.spines.left": True, + "axes.spines.bottom": True, + "axes.spines.right": True, + "axes.spines.top": True, + + }) + + elif style in ["white", "ticks"]: + style_dict.update({ + + "axes.facecolor": "white", + "axes.edgecolor": dark_gray, + "grid.color": light_gray, + + "axes.spines.left": True, + "axes.spines.bottom": True, + "axes.spines.right": True, + "axes.spines.top": True, + + }) + + # Show or hide the axes ticks + if style == "ticks": + style_dict.update({ + "xtick.bottom": True, + "ytick.left": True, + }) + else: + style_dict.update({ + "xtick.bottom": False, + "ytick.left": False, + }) + + # Remove entries that are not defined in the base list of valid keys + # This lets us handle matplotlib <=/> 2.0 + style_dict = {k: v for k, v in style_dict.items() if k in _style_keys} + + # Override these settings with the provided rc dictionary + if rc is not None: + rc = {k: v for k, v in rc.items() if k in _style_keys} + style_dict.update(rc) + + # Wrap in an _AxesStyle object so this can be used in a with statement + style_object = _AxesStyle(style_dict) + + return style_object + + +def set_style(style=None, rc=None): + """ + Set the parameters that control the general style of the plots. + + The style parameters control properties like the color of the background and + whether a grid is enabled by default. This is accomplished using the + matplotlib rcParams system. + + The options are illustrated in the + :doc:`aesthetics tutorial <../tutorial/aesthetics>`. + + See :func:`axes_style` to get the parameter values. + + Parameters + ---------- + style : dict, or one of {darkgrid, whitegrid, dark, white, ticks} + A dictionary of parameters or the name of a preconfigured style. + rc : dict, optional + Parameter mappings to override the values in the preset seaborn + style dictionaries. This only updates parameters that are + considered part of the style definition. + + Examples + -------- + + .. include:: ../docstrings/set_style.rst + + """ + style_object = axes_style(style, rc) + mpl.rcParams.update(style_object) + + +def plotting_context(context=None, font_scale=1, rc=None): + """ + Get the parameters that control the scaling of plot elements. + + This affects things like the size of the labels, lines, and other elements + of the plot, but not the overall style. This is accomplished using the + matplotlib rcParams system. + + The base context is "notebook", and the other contexts are "paper", "talk", + and "poster", which are version of the notebook parameters scaled by different + values. Font elements can also be scaled independently of (but relative to) + the other values. + + This function can also be used as a context manager to temporarily + alter the global defaults. See :func:`set_theme` or :func:`set_context` + to modify the global defaults for all plots. + + Parameters + ---------- + context : None, dict, or one of {paper, notebook, talk, poster} + A dictionary of parameters or the name of a preconfigured set. + font_scale : float, optional + Separate scaling factor to independently scale the size of the + font elements. + rc : dict, optional + Parameter mappings to override the values in the preset seaborn + context dictionaries. This only updates parameters that are + considered part of the context definition. + + Examples + -------- + + .. include:: ../docstrings/plotting_context.rst + + """ + if context is None: + context_dict = {k: mpl.rcParams[k] for k in _context_keys} + + elif isinstance(context, dict): + context_dict = context + + else: + + contexts = ["paper", "notebook", "talk", "poster"] + if context not in contexts: + raise ValueError(f"context must be in {', '.join(contexts)}") + + # Set up dictionary of default parameters + texts_base_context = { + + "font.size": 12, + "axes.labelsize": 12, + "axes.titlesize": 12, + "xtick.labelsize": 11, + "ytick.labelsize": 11, + "legend.fontsize": 11, + "legend.title_fontsize": 12, + + } + + base_context = { + + "axes.linewidth": 1.25, + "grid.linewidth": 1, + "lines.linewidth": 1.5, + "lines.markersize": 6, + "patch.linewidth": 1, + + "xtick.major.width": 1.25, + "ytick.major.width": 1.25, + "xtick.minor.width": 1, + "ytick.minor.width": 1, + + "xtick.major.size": 6, + "ytick.major.size": 6, + "xtick.minor.size": 4, + "ytick.minor.size": 4, + + } + base_context.update(texts_base_context) + + # Scale all the parameters by the same factor depending on the context + scaling = dict(paper=.8, notebook=1, talk=1.5, poster=2)[context] + context_dict = {k: v * scaling for k, v in base_context.items()} + + # Now independently scale the fonts + font_keys = texts_base_context.keys() + font_dict = {k: context_dict[k] * font_scale for k in font_keys} + context_dict.update(font_dict) + + # Override these settings with the provided rc dictionary + if rc is not None: + rc = {k: v for k, v in rc.items() if k in _context_keys} + context_dict.update(rc) + + # Wrap in a _PlottingContext object so this can be used in a with statement + context_object = _PlottingContext(context_dict) + + return context_object + + +def set_context(context=None, font_scale=1, rc=None): + """ + Set the parameters that control the scaling of plot elements. + + This affects things like the size of the labels, lines, and other elements + of the plot, but not the overall style. This is accomplished using the + matplotlib rcParams system. + + The base context is "notebook", and the other contexts are "paper", "talk", + and "poster", which are version of the notebook parameters scaled by different + values. Font elements can also be scaled independently of (but relative to) + the other values. + + See :func:`plotting_context` to get the parameter values. + + Parameters + ---------- + context : dict, or one of {paper, notebook, talk, poster} + A dictionary of parameters or the name of a preconfigured set. + font_scale : float, optional + Separate scaling factor to independently scale the size of the + font elements. + rc : dict, optional + Parameter mappings to override the values in the preset seaborn + context dictionaries. This only updates parameters that are + considered part of the context definition. + + Examples + -------- + + .. include:: ../docstrings/set_context.rst + + """ + context_object = plotting_context(context, font_scale, rc) + mpl.rcParams.update(context_object) + + +class _RCAesthetics(dict): + def __enter__(self): + rc = mpl.rcParams + self._orig = {k: rc[k] for k in self._keys} + self._set(self) + + def __exit__(self, exc_type, exc_value, exc_tb): + self._set(self._orig) + + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + with self: + return func(*args, **kwargs) + return wrapper + + +class _AxesStyle(_RCAesthetics): + """Light wrapper on a dict to set style temporarily.""" + _keys = _style_keys + _set = staticmethod(set_style) + + +class _PlottingContext(_RCAesthetics): + """Light wrapper on a dict to set context temporarily.""" + _keys = _context_keys + _set = staticmethod(set_context) + + +def set_palette(palette, n_colors=None, desat=None, color_codes=False): + """Set the matplotlib color cycle using a seaborn palette. + + Parameters + ---------- + palette : seaborn color paltte | matplotlib colormap | hls | husl + Palette definition. Should be something :func:`color_palette` can process. + n_colors : int + Number of colors in the cycle. The default number of colors will depend + on the format of ``palette``, see the :func:`color_palette` + documentation for more information. + desat : float + Proportion to desaturate each color by. + color_codes : bool + If ``True`` and ``palette`` is a seaborn palette, remap the shorthand + color codes (e.g. "b", "g", "r", etc.) to the colors from this palette. + + See Also + -------- + color_palette : build a color palette or set the color cycle temporarily + in a ``with`` statement. + set_context : set parameters to scale plot elements + set_style : set the default parameters for figure style + + """ + colors = palettes.color_palette(palette, n_colors, desat) + cyl = cycler('color', colors) + mpl.rcParams['axes.prop_cycle'] = cyl + if color_codes: + try: + palettes.set_color_codes(palette) + except (ValueError, TypeError): + pass diff --git a/lib/python3.10/site-packages/seaborn/regression.py b/lib/python3.10/site-packages/seaborn/regression.py new file mode 100644 index 0000000000000000000000000000000000000000..1c7d804e26228a8370ab4ffa107043b144c4fa1c --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/regression.py @@ -0,0 +1,924 @@ +"""Plotting functions for linear models (broadly construed).""" +import copy +from textwrap import dedent +import warnings +import numpy as np +import pandas as pd +import matplotlib as mpl +import matplotlib.pyplot as plt + +try: + import statsmodels + assert statsmodels + _has_statsmodels = True +except ImportError: + _has_statsmodels = False + +from . import utils +from . import algorithms as algo +from .axisgrid import FacetGrid, _facet_docs + + +__all__ = ["lmplot", "regplot", "residplot"] + + +class _LinearPlotter: + """Base class for plotting relational data in tidy format. + + To get anything useful done you'll have to inherit from this, but setup + code that can be abstracted out should be put here. + + """ + def establish_variables(self, data, **kws): + """Extract variables from data or use directly.""" + self.data = data + + # Validate the inputs + any_strings = any([isinstance(v, str) for v in kws.values()]) + if any_strings and data is None: + raise ValueError("Must pass `data` if using named variables.") + + # Set the variables + for var, val in kws.items(): + if isinstance(val, str): + vector = data[val] + elif isinstance(val, list): + vector = np.asarray(val) + else: + vector = val + if vector is not None and vector.shape != (1,): + vector = np.squeeze(vector) + if np.ndim(vector) > 1: + err = "regplot inputs must be 1d" + raise ValueError(err) + setattr(self, var, vector) + + def dropna(self, *vars): + """Remove observations with missing data.""" + vals = [getattr(self, var) for var in vars] + vals = [v for v in vals if v is not None] + not_na = np.all(np.column_stack([pd.notnull(v) for v in vals]), axis=1) + for var in vars: + val = getattr(self, var) + if val is not None: + setattr(self, var, val[not_na]) + + def plot(self, ax): + raise NotImplementedError + + +class _RegressionPlotter(_LinearPlotter): + """Plotter for numeric independent variables with regression model. + + This does the computations and drawing for the `regplot` function, and + is thus also used indirectly by `lmplot`. + """ + def __init__(self, x, y, data=None, x_estimator=None, x_bins=None, + x_ci="ci", scatter=True, fit_reg=True, ci=95, n_boot=1000, + units=None, seed=None, order=1, logistic=False, lowess=False, + robust=False, logx=False, x_partial=None, y_partial=None, + truncate=False, dropna=True, x_jitter=None, y_jitter=None, + color=None, label=None): + + # Set member attributes + self.x_estimator = x_estimator + self.ci = ci + self.x_ci = ci if x_ci == "ci" else x_ci + self.n_boot = n_boot + self.seed = seed + self.scatter = scatter + self.fit_reg = fit_reg + self.order = order + self.logistic = logistic + self.lowess = lowess + self.robust = robust + self.logx = logx + self.truncate = truncate + self.x_jitter = x_jitter + self.y_jitter = y_jitter + self.color = color + self.label = label + + # Validate the regression options: + if sum((order > 1, logistic, robust, lowess, logx)) > 1: + raise ValueError("Mutually exclusive regression options.") + + # Extract the data vals from the arguments or passed dataframe + self.establish_variables(data, x=x, y=y, units=units, + x_partial=x_partial, y_partial=y_partial) + + # Drop null observations + if dropna: + self.dropna("x", "y", "units", "x_partial", "y_partial") + + # Regress nuisance variables out of the data + if self.x_partial is not None: + self.x = self.regress_out(self.x, self.x_partial) + if self.y_partial is not None: + self.y = self.regress_out(self.y, self.y_partial) + + # Possibly bin the predictor variable, which implies a point estimate + if x_bins is not None: + self.x_estimator = np.mean if x_estimator is None else x_estimator + x_discrete, x_bins = self.bin_predictor(x_bins) + self.x_discrete = x_discrete + else: + self.x_discrete = self.x + + # Disable regression in case of singleton inputs + if len(self.x) <= 1: + self.fit_reg = False + + # Save the range of the x variable for the grid later + if self.fit_reg: + self.x_range = self.x.min(), self.x.max() + + @property + def scatter_data(self): + """Data where each observation is a point.""" + x_j = self.x_jitter + if x_j is None: + x = self.x + else: + x = self.x + np.random.uniform(-x_j, x_j, len(self.x)) + + y_j = self.y_jitter + if y_j is None: + y = self.y + else: + y = self.y + np.random.uniform(-y_j, y_j, len(self.y)) + + return x, y + + @property + def estimate_data(self): + """Data with a point estimate and CI for each discrete x value.""" + x, y = self.x_discrete, self.y + vals = sorted(np.unique(x)) + points, cis = [], [] + + for val in vals: + + # Get the point estimate of the y variable + _y = y[x == val] + est = self.x_estimator(_y) + points.append(est) + + # Compute the confidence interval for this estimate + if self.x_ci is None: + cis.append(None) + else: + units = None + if self.x_ci == "sd": + sd = np.std(_y) + _ci = est - sd, est + sd + else: + if self.units is not None: + units = self.units[x == val] + boots = algo.bootstrap(_y, + func=self.x_estimator, + n_boot=self.n_boot, + units=units, + seed=self.seed) + _ci = utils.ci(boots, self.x_ci) + cis.append(_ci) + + return vals, points, cis + + def fit_regression(self, ax=None, x_range=None, grid=None): + """Fit the regression model.""" + # Create the grid for the regression + if grid is None: + if self.truncate: + x_min, x_max = self.x_range + else: + if ax is None: + x_min, x_max = x_range + else: + x_min, x_max = ax.get_xlim() + grid = np.linspace(x_min, x_max, 100) + ci = self.ci + + # Fit the regression + if self.order > 1: + yhat, yhat_boots = self.fit_poly(grid, self.order) + elif self.logistic: + from statsmodels.genmod.generalized_linear_model import GLM + from statsmodels.genmod.families import Binomial + yhat, yhat_boots = self.fit_statsmodels(grid, GLM, + family=Binomial()) + elif self.lowess: + ci = None + grid, yhat = self.fit_lowess() + elif self.robust: + from statsmodels.robust.robust_linear_model import RLM + yhat, yhat_boots = self.fit_statsmodels(grid, RLM) + elif self.logx: + yhat, yhat_boots = self.fit_logx(grid) + else: + yhat, yhat_boots = self.fit_fast(grid) + + # Compute the confidence interval at each grid point + if ci is None: + err_bands = None + else: + err_bands = utils.ci(yhat_boots, ci, axis=0) + + return grid, yhat, err_bands + + def fit_fast(self, grid): + """Low-level regression and prediction using linear algebra.""" + def reg_func(_x, _y): + return np.linalg.pinv(_x).dot(_y) + + X, y = np.c_[np.ones(len(self.x)), self.x], self.y + grid = np.c_[np.ones(len(grid)), grid] + yhat = grid.dot(reg_func(X, y)) + if self.ci is None: + return yhat, None + + beta_boots = algo.bootstrap(X, y, + func=reg_func, + n_boot=self.n_boot, + units=self.units, + seed=self.seed).T + yhat_boots = grid.dot(beta_boots).T + return yhat, yhat_boots + + def fit_poly(self, grid, order): + """Regression using numpy polyfit for higher-order trends.""" + def reg_func(_x, _y): + return np.polyval(np.polyfit(_x, _y, order), grid) + + x, y = self.x, self.y + yhat = reg_func(x, y) + if self.ci is None: + return yhat, None + + yhat_boots = algo.bootstrap(x, y, + func=reg_func, + n_boot=self.n_boot, + units=self.units, + seed=self.seed) + return yhat, yhat_boots + + def fit_statsmodels(self, grid, model, **kwargs): + """More general regression function using statsmodels objects.""" + import statsmodels.genmod.generalized_linear_model as glm + X, y = np.c_[np.ones(len(self.x)), self.x], self.y + grid = np.c_[np.ones(len(grid)), grid] + + def reg_func(_x, _y): + try: + yhat = model(_y, _x, **kwargs).fit().predict(grid) + except glm.PerfectSeparationError: + yhat = np.empty(len(grid)) + yhat.fill(np.nan) + return yhat + + yhat = reg_func(X, y) + if self.ci is None: + return yhat, None + + yhat_boots = algo.bootstrap(X, y, + func=reg_func, + n_boot=self.n_boot, + units=self.units, + seed=self.seed) + return yhat, yhat_boots + + def fit_lowess(self): + """Fit a locally-weighted regression, which returns its own grid.""" + from statsmodels.nonparametric.smoothers_lowess import lowess + grid, yhat = lowess(self.y, self.x).T + return grid, yhat + + def fit_logx(self, grid): + """Fit the model in log-space.""" + X, y = np.c_[np.ones(len(self.x)), self.x], self.y + grid = np.c_[np.ones(len(grid)), np.log(grid)] + + def reg_func(_x, _y): + _x = np.c_[_x[:, 0], np.log(_x[:, 1])] + return np.linalg.pinv(_x).dot(_y) + + yhat = grid.dot(reg_func(X, y)) + if self.ci is None: + return yhat, None + + beta_boots = algo.bootstrap(X, y, + func=reg_func, + n_boot=self.n_boot, + units=self.units, + seed=self.seed).T + yhat_boots = grid.dot(beta_boots).T + return yhat, yhat_boots + + def bin_predictor(self, bins): + """Discretize a predictor by assigning value to closest bin.""" + x = np.asarray(self.x) + if np.isscalar(bins): + percentiles = np.linspace(0, 100, bins + 2)[1:-1] + bins = np.percentile(x, percentiles) + else: + bins = np.ravel(bins) + + dist = np.abs(np.subtract.outer(x, bins)) + x_binned = bins[np.argmin(dist, axis=1)].ravel() + + return x_binned, bins + + def regress_out(self, a, b): + """Regress b from a keeping a's original mean.""" + a_mean = a.mean() + a = a - a_mean + b = b - b.mean() + b = np.c_[b] + a_prime = a - b.dot(np.linalg.pinv(b).dot(a)) + return np.asarray(a_prime + a_mean).reshape(a.shape) + + def plot(self, ax, scatter_kws, line_kws): + """Draw the full plot.""" + # Insert the plot label into the correct set of keyword arguments + if self.scatter: + scatter_kws["label"] = self.label + else: + line_kws["label"] = self.label + + # Use the current color cycle state as a default + if self.color is None: + lines, = ax.plot([], []) + color = lines.get_color() + lines.remove() + else: + color = self.color + + # Ensure that color is hex to avoid matplotlib weirdness + color = mpl.colors.rgb2hex(mpl.colors.colorConverter.to_rgb(color)) + + # Let color in keyword arguments override overall plot color + scatter_kws.setdefault("color", color) + line_kws.setdefault("color", color) + + # Draw the constituent plots + if self.scatter: + self.scatterplot(ax, scatter_kws) + + if self.fit_reg: + self.lineplot(ax, line_kws) + + # Label the axes + if hasattr(self.x, "name"): + ax.set_xlabel(self.x.name) + if hasattr(self.y, "name"): + ax.set_ylabel(self.y.name) + + def scatterplot(self, ax, kws): + """Draw the data.""" + # Treat the line-based markers specially, explicitly setting larger + # linewidth than is provided by the seaborn style defaults. + # This would ideally be handled better in matplotlib (i.e., distinguish + # between edgewidth for solid glyphs and linewidth for line glyphs + # but this should do for now. + line_markers = ["1", "2", "3", "4", "+", "x", "|", "_"] + if self.x_estimator is None: + if "marker" in kws and kws["marker"] in line_markers: + lw = mpl.rcParams["lines.linewidth"] + else: + lw = mpl.rcParams["lines.markeredgewidth"] + kws.setdefault("linewidths", lw) + + if not hasattr(kws['color'], 'shape') or kws['color'].shape[1] < 4: + kws.setdefault("alpha", .8) + + x, y = self.scatter_data + ax.scatter(x, y, **kws) + else: + # TODO abstraction + ci_kws = {"color": kws["color"]} + if "alpha" in kws: + ci_kws["alpha"] = kws["alpha"] + ci_kws["linewidth"] = mpl.rcParams["lines.linewidth"] * 1.75 + kws.setdefault("s", 50) + + xs, ys, cis = self.estimate_data + if [ci for ci in cis if ci is not None]: + for x, ci in zip(xs, cis): + ax.plot([x, x], ci, **ci_kws) + ax.scatter(xs, ys, **kws) + + def lineplot(self, ax, kws): + """Draw the model.""" + # Fit the regression model + grid, yhat, err_bands = self.fit_regression(ax) + edges = grid[0], grid[-1] + + # Get set default aesthetics + fill_color = kws["color"] + lw = kws.pop("lw", mpl.rcParams["lines.linewidth"] * 1.5) + kws.setdefault("linewidth", lw) + + # Draw the regression line and confidence interval + line, = ax.plot(grid, yhat, **kws) + if not self.truncate: + line.sticky_edges.x[:] = edges # Prevent mpl from adding margin + if err_bands is not None: + ax.fill_between(grid, *err_bands, facecolor=fill_color, alpha=.15) + + +_regression_docs = dict( + + model_api=dedent("""\ + There are a number of mutually exclusive options for estimating the + regression model. See the :ref:`tutorial ` for more + information.\ + """), + regplot_vs_lmplot=dedent("""\ + The :func:`regplot` and :func:`lmplot` functions are closely related, but + the former is an axes-level function while the latter is a figure-level + function that combines :func:`regplot` and :class:`FacetGrid`.\ + """), + x_estimator=dedent("""\ + x_estimator : callable that maps vector -> scalar, optional + Apply this function to each unique value of ``x`` and plot the + resulting estimate. This is useful when ``x`` is a discrete variable. + If ``x_ci`` is given, this estimate will be bootstrapped and a + confidence interval will be drawn.\ + """), + x_bins=dedent("""\ + x_bins : int or vector, optional + Bin the ``x`` variable into discrete bins and then estimate the central + tendency and a confidence interval. This binning only influences how + the scatterplot is drawn; the regression is still fit to the original + data. This parameter is interpreted either as the number of + evenly-sized (not necessary spaced) bins or the positions of the bin + centers. When this parameter is used, it implies that the default of + ``x_estimator`` is ``numpy.mean``.\ + """), + x_ci=dedent("""\ + x_ci : "ci", "sd", int in [0, 100] or None, optional + Size of the confidence interval used when plotting a central tendency + for discrete values of ``x``. If ``"ci"``, defer to the value of the + ``ci`` parameter. If ``"sd"``, skip bootstrapping and show the + standard deviation of the observations in each bin.\ + """), + scatter=dedent("""\ + scatter : bool, optional + If ``True``, draw a scatterplot with the underlying observations (or + the ``x_estimator`` values).\ + """), + fit_reg=dedent("""\ + fit_reg : bool, optional + If ``True``, estimate and plot a regression model relating the ``x`` + and ``y`` variables.\ + """), + ci=dedent("""\ + ci : int in [0, 100] or None, optional + Size of the confidence interval for the regression estimate. This will + be drawn using translucent bands around the regression line. The + confidence interval is estimated using a bootstrap; for large + datasets, it may be advisable to avoid that computation by setting + this parameter to None.\ + """), + n_boot=dedent("""\ + n_boot : int, optional + Number of bootstrap resamples used to estimate the ``ci``. The default + value attempts to balance time and stability; you may want to increase + this value for "final" versions of plots.\ + """), + units=dedent("""\ + units : variable name in ``data``, optional + If the ``x`` and ``y`` observations are nested within sampling units, + those can be specified here. This will be taken into account when + computing the confidence intervals by performing a multilevel bootstrap + that resamples both units and observations (within unit). This does not + otherwise influence how the regression is estimated or drawn.\ + """), + seed=dedent("""\ + seed : int, numpy.random.Generator, or numpy.random.RandomState, optional + Seed or random number generator for reproducible bootstrapping.\ + """), + order=dedent("""\ + order : int, optional + If ``order`` is greater than 1, use ``numpy.polyfit`` to estimate a + polynomial regression.\ + """), + logistic=dedent("""\ + logistic : bool, optional + If ``True``, assume that ``y`` is a binary variable and use + ``statsmodels`` to estimate a logistic regression model. Note that this + is substantially more computationally intensive than linear regression, + so you may wish to decrease the number of bootstrap resamples + (``n_boot``) or set ``ci`` to None.\ + """), + lowess=dedent("""\ + lowess : bool, optional + If ``True``, use ``statsmodels`` to estimate a nonparametric lowess + model (locally weighted linear regression). Note that confidence + intervals cannot currently be drawn for this kind of model.\ + """), + robust=dedent("""\ + robust : bool, optional + If ``True``, use ``statsmodels`` to estimate a robust regression. This + will de-weight outliers. Note that this is substantially more + computationally intensive than standard linear regression, so you may + wish to decrease the number of bootstrap resamples (``n_boot``) or set + ``ci`` to None.\ + """), + logx=dedent("""\ + logx : bool, optional + If ``True``, estimate a linear regression of the form y ~ log(x), but + plot the scatterplot and regression model in the input space. Note that + ``x`` must be positive for this to work.\ + """), + xy_partial=dedent("""\ + {x,y}_partial : strings in ``data`` or matrices + Confounding variables to regress out of the ``x`` or ``y`` variables + before plotting.\ + """), + truncate=dedent("""\ + truncate : bool, optional + If ``True``, the regression line is bounded by the data limits. If + ``False``, it extends to the ``x`` axis limits. + """), + xy_jitter=dedent("""\ + {x,y}_jitter : floats, optional + Add uniform random noise of this size to either the ``x`` or ``y`` + variables. The noise is added to a copy of the data after fitting the + regression, and only influences the look of the scatterplot. This can + be helpful when plotting variables that take discrete values.\ + """), + scatter_line_kws=dedent("""\ + {scatter,line}_kws : dictionaries + Additional keyword arguments to pass to ``plt.scatter`` and + ``plt.plot``.\ + """), +) +_regression_docs.update(_facet_docs) + + +def lmplot( + data=None, *, + x=None, y=None, hue=None, col=None, row=None, + palette=None, col_wrap=None, height=5, aspect=1, markers="o", + sharex=None, sharey=None, hue_order=None, col_order=None, row_order=None, + legend=True, legend_out=None, x_estimator=None, x_bins=None, + x_ci="ci", scatter=True, fit_reg=True, ci=95, n_boot=1000, + units=None, seed=None, order=1, logistic=False, lowess=False, + robust=False, logx=False, x_partial=None, y_partial=None, + truncate=True, x_jitter=None, y_jitter=None, scatter_kws=None, + line_kws=None, facet_kws=None, +): + + if facet_kws is None: + facet_kws = {} + + def facet_kw_deprecation(key, val): + msg = ( + f"{key} is deprecated from the `lmplot` function signature. " + "Please update your code to pass it using `facet_kws`." + ) + if val is not None: + warnings.warn(msg, UserWarning) + facet_kws[key] = val + + facet_kw_deprecation("sharex", sharex) + facet_kw_deprecation("sharey", sharey) + facet_kw_deprecation("legend_out", legend_out) + + if data is None: + raise TypeError("Missing required keyword argument `data`.") + + # Reduce the dataframe to only needed columns + need_cols = [x, y, hue, col, row, units, x_partial, y_partial] + cols = np.unique([a for a in need_cols if a is not None]).tolist() + data = data[cols] + + # Initialize the grid + facets = FacetGrid( + data, row=row, col=col, hue=hue, + palette=palette, + row_order=row_order, col_order=col_order, hue_order=hue_order, + height=height, aspect=aspect, col_wrap=col_wrap, + **facet_kws, + ) + + # Add the markers here as FacetGrid has figured out how many levels of the + # hue variable are needed and we don't want to duplicate that process + if facets.hue_names is None: + n_markers = 1 + else: + n_markers = len(facets.hue_names) + if not isinstance(markers, list): + markers = [markers] * n_markers + if len(markers) != n_markers: + raise ValueError("markers must be a singleton or a list of markers " + "for each level of the hue variable") + facets.hue_kws = {"marker": markers} + + def update_datalim(data, x, y, ax, **kws): + xys = data[[x, y]].to_numpy().astype(float) + ax.update_datalim(xys, updatey=False) + ax.autoscale_view(scaley=False) + + facets.map_dataframe(update_datalim, x=x, y=y) + + # Draw the regression plot on each facet + regplot_kws = dict( + x_estimator=x_estimator, x_bins=x_bins, x_ci=x_ci, + scatter=scatter, fit_reg=fit_reg, ci=ci, n_boot=n_boot, units=units, + seed=seed, order=order, logistic=logistic, lowess=lowess, + robust=robust, logx=logx, x_partial=x_partial, y_partial=y_partial, + truncate=truncate, x_jitter=x_jitter, y_jitter=y_jitter, + scatter_kws=scatter_kws, line_kws=line_kws, + ) + facets.map_dataframe(regplot, x=x, y=y, **regplot_kws) + facets.set_axis_labels(x, y) + + # Add a legend + if legend and (hue is not None) and (hue not in [col, row]): + facets.add_legend() + return facets + + +lmplot.__doc__ = dedent("""\ + Plot data and regression model fits across a FacetGrid. + + This function combines :func:`regplot` and :class:`FacetGrid`. It is + intended as a convenient interface to fit regression models across + conditional subsets of a dataset. + + When thinking about how to assign variables to different facets, a general + rule is that it makes sense to use ``hue`` for the most important + comparison, followed by ``col`` and ``row``. However, always think about + your particular dataset and the goals of the visualization you are + creating. + + {model_api} + + The parameters to this function span most of the options in + :class:`FacetGrid`, although there may be occasional cases where you will + want to use that class and :func:`regplot` directly. + + Parameters + ---------- + {data} + x, y : strings, optional + Input variables; these should be column names in ``data``. + hue, col, row : strings + Variables that define subsets of the data, which will be drawn on + separate facets in the grid. See the ``*_order`` parameters to control + the order of levels of this variable. + {palette} + {col_wrap} + {height} + {aspect} + markers : matplotlib marker code or list of marker codes, optional + Markers for the scatterplot. If a list, each marker in the list will be + used for each level of the ``hue`` variable. + {share_xy} + + .. deprecated:: 0.12.0 + Pass using the `facet_kws` dictionary. + + {{hue,col,row}}_order : lists, optional + Order for the levels of the faceting variables. By default, this will + be the order that the levels appear in ``data`` or, if the variables + are pandas categoricals, the category order. + legend : bool, optional + If ``True`` and there is a ``hue`` variable, add a legend. + {legend_out} + + .. deprecated:: 0.12.0 + Pass using the `facet_kws` dictionary. + + {x_estimator} + {x_bins} + {x_ci} + {scatter} + {fit_reg} + {ci} + {n_boot} + {units} + {seed} + {order} + {logistic} + {lowess} + {robust} + {logx} + {xy_partial} + {truncate} + {xy_jitter} + {scatter_line_kws} + facet_kws : dict + Dictionary of keyword arguments for :class:`FacetGrid`. + + See Also + -------- + regplot : Plot data and a conditional model fit. + FacetGrid : Subplot grid for plotting conditional relationships. + pairplot : Combine :func:`regplot` and :class:`PairGrid` (when used with + ``kind="reg"``). + + Notes + ----- + + {regplot_vs_lmplot} + + Examples + -------- + + .. include:: ../docstrings/lmplot.rst + + """).format(**_regression_docs) + + +def regplot( + data=None, *, x=None, y=None, + x_estimator=None, x_bins=None, x_ci="ci", + scatter=True, fit_reg=True, ci=95, n_boot=1000, units=None, + seed=None, order=1, logistic=False, lowess=False, robust=False, + logx=False, x_partial=None, y_partial=None, + truncate=True, dropna=True, x_jitter=None, y_jitter=None, + label=None, color=None, marker="o", + scatter_kws=None, line_kws=None, ax=None +): + + plotter = _RegressionPlotter(x, y, data, x_estimator, x_bins, x_ci, + scatter, fit_reg, ci, n_boot, units, seed, + order, logistic, lowess, robust, logx, + x_partial, y_partial, truncate, dropna, + x_jitter, y_jitter, color, label) + + if ax is None: + ax = plt.gca() + + scatter_kws = {} if scatter_kws is None else copy.copy(scatter_kws) + scatter_kws["marker"] = marker + line_kws = {} if line_kws is None else copy.copy(line_kws) + plotter.plot(ax, scatter_kws, line_kws) + return ax + + +regplot.__doc__ = dedent("""\ + Plot data and a linear regression model fit. + + {model_api} + + Parameters + ---------- + x, y: string, series, or vector array + Input variables. If strings, these should correspond with column names + in ``data``. When pandas objects are used, axes will be labeled with + the series name. + {data} + {x_estimator} + {x_bins} + {x_ci} + {scatter} + {fit_reg} + {ci} + {n_boot} + {units} + {seed} + {order} + {logistic} + {lowess} + {robust} + {logx} + {xy_partial} + {truncate} + {xy_jitter} + label : string + Label to apply to either the scatterplot or regression line (if + ``scatter`` is ``False``) for use in a legend. + color : matplotlib color + Color to apply to all plot elements; will be superseded by colors + passed in ``scatter_kws`` or ``line_kws``. + marker : matplotlib marker code + Marker to use for the scatterplot glyphs. + {scatter_line_kws} + ax : matplotlib Axes, optional + Axes object to draw the plot onto, otherwise uses the current Axes. + + Returns + ------- + ax : matplotlib Axes + The Axes object containing the plot. + + See Also + -------- + lmplot : Combine :func:`regplot` and :class:`FacetGrid` to plot multiple + linear relationships in a dataset. + jointplot : Combine :func:`regplot` and :class:`JointGrid` (when used with + ``kind="reg"``). + pairplot : Combine :func:`regplot` and :class:`PairGrid` (when used with + ``kind="reg"``). + residplot : Plot the residuals of a linear regression model. + + Notes + ----- + + {regplot_vs_lmplot} + + + It's also easy to combine :func:`regplot` and :class:`JointGrid` or + :class:`PairGrid` through the :func:`jointplot` and :func:`pairplot` + functions, although these do not directly accept all of :func:`regplot`'s + parameters. + + Examples + -------- + + .. include: ../docstrings/regplot.rst + + """).format(**_regression_docs) + + +def residplot( + data=None, *, x=None, y=None, + x_partial=None, y_partial=None, lowess=False, + order=1, robust=False, dropna=True, label=None, color=None, + scatter_kws=None, line_kws=None, ax=None +): + """Plot the residuals of a linear regression. + + This function will regress y on x (possibly as a robust or polynomial + regression) and then draw a scatterplot of the residuals. You can + optionally fit a lowess smoother to the residual plot, which can + help in determining if there is structure to the residuals. + + Parameters + ---------- + data : DataFrame, optional + DataFrame to use if `x` and `y` are column names. + x : vector or string + Data or column name in `data` for the predictor variable. + y : vector or string + Data or column name in `data` for the response variable. + {x, y}_partial : vectors or string(s) , optional + These variables are treated as confounding and are removed from + the `x` or `y` variables before plotting. + lowess : boolean, optional + Fit a lowess smoother to the residual scatterplot. + order : int, optional + Order of the polynomial to fit when calculating the residuals. + robust : boolean, optional + Fit a robust linear regression when calculating the residuals. + dropna : boolean, optional + If True, ignore observations with missing data when fitting and + plotting. + label : string, optional + Label that will be used in any plot legends. + color : matplotlib color, optional + Color to use for all elements of the plot. + {scatter, line}_kws : dictionaries, optional + Additional keyword arguments passed to scatter() and plot() for drawing + the components of the plot. + ax : matplotlib axis, optional + Plot into this axis, otherwise grab the current axis or make a new + one if not existing. + + Returns + ------- + ax: matplotlib axes + Axes with the regression plot. + + See Also + -------- + regplot : Plot a simple linear regression model. + jointplot : Draw a :func:`residplot` with univariate marginal distributions + (when used with ``kind="resid"``). + + Examples + -------- + + .. include:: ../docstrings/residplot.rst + + """ + plotter = _RegressionPlotter(x, y, data, ci=None, + order=order, robust=robust, + x_partial=x_partial, y_partial=y_partial, + dropna=dropna, color=color, label=label) + + if ax is None: + ax = plt.gca() + + # Calculate the residual from a linear regression + _, yhat, _ = plotter.fit_regression(grid=plotter.x) + plotter.y = plotter.y - yhat + + # Set the regression option on the plotter + if lowess: + plotter.lowess = True + else: + plotter.fit_reg = False + + # Plot a horizontal line at 0 + ax.axhline(0, ls=":", c=".2") + + # Draw the scatterplot + scatter_kws = {} if scatter_kws is None else scatter_kws.copy() + line_kws = {} if line_kws is None else line_kws.copy() + plotter.plot(ax, scatter_kws, line_kws) + return ax diff --git a/lib/python3.10/site-packages/seaborn/relational.py b/lib/python3.10/site-packages/seaborn/relational.py new file mode 100644 index 0000000000000000000000000000000000000000..18e18bb64ca8dc6a51ab8e554a31cf4cfccdbef5 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/relational.py @@ -0,0 +1,1071 @@ +import warnings + +import numpy as np +import pandas as pd +import matplotlib as mpl +import matplotlib.pyplot as plt + +from ._oldcore import ( + VectorPlotter, +) +from .utils import ( + locator_to_legend_entries, + adjust_legend_subtitles, + _default_color, + _deprecate_ci, +) +from ._statistics import EstimateAggregator +from .axisgrid import FacetGrid, _facet_docs +from ._docstrings import DocstringComponents, _core_docs + + +__all__ = ["relplot", "scatterplot", "lineplot"] + + +_relational_narrative = DocstringComponents(dict( + + # --- Introductory prose + main_api=""" +The relationship between `x` and `y` can be shown for different subsets +of the data using the `hue`, `size`, and `style` parameters. These +parameters control what visual semantics are used to identify the different +subsets. It is possible to show up to three dimensions independently by +using all three semantic types, but this style of plot can be hard to +interpret and is often ineffective. Using redundant semantics (i.e. both +`hue` and `style` for the same variable) can be helpful for making +graphics more accessible. + +See the :ref:`tutorial ` for more information. + """, + + relational_semantic=""" +The default treatment of the `hue` (and to a lesser extent, `size`) +semantic, if present, depends on whether the variable is inferred to +represent "numeric" or "categorical" data. In particular, numeric variables +are represented with a sequential colormap by default, and the legend +entries show regular "ticks" with values that may or may not exist in the +data. This behavior can be controlled through various parameters, as +described and illustrated below. + """, +)) + +_relational_docs = dict( + + # --- Shared function parameters + data_vars=""" +x, y : names of variables in `data` or vector data + Input data variables; must be numeric. Can pass data directly or + reference columns in `data`. + """, + data=""" +data : DataFrame, array, or list of arrays + Input data structure. If `x` and `y` are specified as names, this + should be a "long-form" DataFrame containing those columns. Otherwise + it is treated as "wide-form" data and grouping variables are ignored. + See the examples for the various ways this parameter can be specified + and the different effects of each. + """, + palette=""" +palette : string, list, dict, or matplotlib colormap + An object that determines how colors are chosen when `hue` is used. + It can be the name of a seaborn palette or matplotlib colormap, a list + of colors (anything matplotlib understands), a dict mapping levels + of the `hue` variable to colors, or a matplotlib colormap object. + """, + hue_order=""" +hue_order : list + Specified order for the appearance of the `hue` variable levels, + otherwise they are determined from the data. Not relevant when the + `hue` variable is numeric. + """, + hue_norm=""" +hue_norm : tuple or :class:`matplotlib.colors.Normalize` object + Normalization in data units for colormap applied to the `hue` + variable when it is numeric. Not relevant if `hue` is categorical. + """, + sizes=""" +sizes : list, dict, or tuple + An object that determines how sizes are chosen when `size` is used. + List or dict arguments should provide a size for each unique data value, + which forces a categorical interpretation. The argument may also be a + min, max tuple. + """, + size_order=""" +size_order : list + Specified order for appearance of the `size` variable levels, + otherwise they are determined from the data. Not relevant when the + `size` variable is numeric. + """, + size_norm=""" +size_norm : tuple or Normalize object + Normalization in data units for scaling plot objects when the + `size` variable is numeric. + """, + dashes=""" +dashes : boolean, list, or dictionary + Object determining how to draw the lines for different levels of the + `style` variable. Setting to `True` will use default dash codes, or + you can pass a list of dash codes or a dictionary mapping levels of the + `style` variable to dash codes. Setting to `False` will use solid + lines for all subsets. Dashes are specified as in matplotlib: a tuple + of `(segment, gap)` lengths, or an empty string to draw a solid line. + """, + markers=""" +markers : boolean, list, or dictionary + Object determining how to draw the markers for different levels of the + `style` variable. Setting to `True` will use default markers, or + you can pass a list of markers or a dictionary mapping levels of the + `style` variable to markers. Setting to `False` will draw + marker-less lines. Markers are specified as in matplotlib. + """, + style_order=""" +style_order : list + Specified order for appearance of the `style` variable levels + otherwise they are determined from the data. Not relevant when the + `style` variable is numeric. + """, + units=""" +units : vector or key in `data` + Grouping variable identifying sampling units. When used, a separate + line will be drawn for each unit with appropriate semantics, but no + legend entry will be added. Useful for showing distribution of + experimental replicates when exact identities are not needed. + """, + estimator=""" +estimator : name of pandas method or callable or None + Method for aggregating across multiple observations of the `y` + variable at the same `x` level. If `None`, all observations will + be drawn. + """, + ci=""" +ci : int or "sd" or None + Size of the confidence interval to draw when aggregating. + + .. deprecated:: 0.12.0 + Use the new `errorbar` parameter for more flexibility. + + """, + n_boot=""" +n_boot : int + Number of bootstraps to use for computing the confidence interval. + """, + seed=""" +seed : int, numpy.random.Generator, or numpy.random.RandomState + Seed or random number generator for reproducible bootstrapping. + """, + legend=""" +legend : "auto", "brief", "full", or False + How to draw the legend. If "brief", numeric `hue` and `size` + variables will be represented with a sample of evenly spaced values. + If "full", every group will get an entry in the legend. If "auto", + choose between brief or full representation based on number of levels. + If `False`, no legend data is added and no legend is drawn. + """, + ax_in=""" +ax : matplotlib Axes + Axes object to draw the plot onto, otherwise uses the current Axes. + """, + ax_out=""" +ax : matplotlib Axes + Returns the Axes object with the plot drawn onto it. + """, + +) + + +_param_docs = DocstringComponents.from_nested_components( + core=_core_docs["params"], + facets=DocstringComponents(_facet_docs), + rel=DocstringComponents(_relational_docs), + stat=DocstringComponents.from_function_params(EstimateAggregator.__init__), +) + + +class _RelationalPlotter(VectorPlotter): + + wide_structure = { + "x": "@index", "y": "@values", "hue": "@columns", "style": "@columns", + } + + # TODO where best to define default parameters? + sort = True + + def add_legend_data(self, ax): + """Add labeled artists to represent the different plot semantics.""" + verbosity = self.legend + if isinstance(verbosity, str) and verbosity not in ["auto", "brief", "full"]: + err = "`legend` must be 'auto', 'brief', 'full', or a boolean." + raise ValueError(err) + elif verbosity is True: + verbosity = "auto" + + legend_kwargs = {} + keys = [] + + # Assign a legend title if there is only going to be one sub-legend, + # otherwise, subtitles will be inserted into the texts list with an + # invisible handle (which is a hack) + titles = { + title for title in + (self.variables.get(v, None) for v in ["hue", "size", "style"]) + if title is not None + } + if len(titles) == 1: + legend_title = titles.pop() + else: + legend_title = "" + + title_kws = dict( + visible=False, color="w", s=0, linewidth=0, marker="", dashes="" + ) + + def update(var_name, val_name, **kws): + + key = var_name, val_name + if key in legend_kwargs: + legend_kwargs[key].update(**kws) + else: + keys.append(key) + + legend_kwargs[key] = dict(**kws) + + # Define the maximum number of ticks to use for "brief" legends + brief_ticks = 6 + + # -- Add a legend for hue semantics + brief_hue = self._hue_map.map_type == "numeric" and ( + verbosity == "brief" + or (verbosity == "auto" and len(self._hue_map.levels) > brief_ticks) + ) + if brief_hue: + if isinstance(self._hue_map.norm, mpl.colors.LogNorm): + locator = mpl.ticker.LogLocator(numticks=brief_ticks) + else: + locator = mpl.ticker.MaxNLocator(nbins=brief_ticks) + limits = min(self._hue_map.levels), max(self._hue_map.levels) + hue_levels, hue_formatted_levels = locator_to_legend_entries( + locator, limits, self.plot_data["hue"].infer_objects().dtype + ) + elif self._hue_map.levels is None: + hue_levels = hue_formatted_levels = [] + else: + hue_levels = hue_formatted_levels = self._hue_map.levels + + # Add the hue semantic subtitle + if not legend_title and self.variables.get("hue", None) is not None: + update((self.variables["hue"], "title"), + self.variables["hue"], **title_kws) + + # Add the hue semantic labels + for level, formatted_level in zip(hue_levels, hue_formatted_levels): + if level is not None: + color = self._hue_map(level) + update(self.variables["hue"], formatted_level, color=color) + + # -- Add a legend for size semantics + brief_size = self._size_map.map_type == "numeric" and ( + verbosity == "brief" + or (verbosity == "auto" and len(self._size_map.levels) > brief_ticks) + ) + if brief_size: + # Define how ticks will interpolate between the min/max data values + if isinstance(self._size_map.norm, mpl.colors.LogNorm): + locator = mpl.ticker.LogLocator(numticks=brief_ticks) + else: + locator = mpl.ticker.MaxNLocator(nbins=brief_ticks) + # Define the min/max data values + limits = min(self._size_map.levels), max(self._size_map.levels) + size_levels, size_formatted_levels = locator_to_legend_entries( + locator, limits, self.plot_data["size"].infer_objects().dtype + ) + elif self._size_map.levels is None: + size_levels = size_formatted_levels = [] + else: + size_levels = size_formatted_levels = self._size_map.levels + + # Add the size semantic subtitle + if not legend_title and self.variables.get("size", None) is not None: + update((self.variables["size"], "title"), + self.variables["size"], **title_kws) + + # Add the size semantic labels + for level, formatted_level in zip(size_levels, size_formatted_levels): + if level is not None: + size = self._size_map(level) + update( + self.variables["size"], + formatted_level, + linewidth=size, + s=size, + ) + + # -- Add a legend for style semantics + + # Add the style semantic title + if not legend_title and self.variables.get("style", None) is not None: + update((self.variables["style"], "title"), + self.variables["style"], **title_kws) + + # Add the style semantic labels + if self._style_map.levels is not None: + for level in self._style_map.levels: + if level is not None: + attrs = self._style_map(level) + update( + self.variables["style"], + level, + marker=attrs.get("marker", ""), + dashes=attrs.get("dashes", ""), + ) + + func = getattr(ax, self._legend_func) + + legend_data = {} + legend_order = [] + + for key in keys: + + _, label = key + kws = legend_kwargs[key] + kws.setdefault("color", ".2") + use_kws = {} + for attr in self._legend_attributes + ["visible"]: + if attr in kws: + use_kws[attr] = kws[attr] + artist = func([], [], label=label, **use_kws) + if self._legend_func == "plot": + artist = artist[0] + legend_data[key] = artist + legend_order.append(key) + + self.legend_title = legend_title + self.legend_data = legend_data + self.legend_order = legend_order + + +class _LinePlotter(_RelationalPlotter): + + _legend_attributes = ["color", "linewidth", "marker", "dashes"] + _legend_func = "plot" + + def __init__( + self, *, + data=None, variables={}, + estimator=None, n_boot=None, seed=None, errorbar=None, + sort=True, orient="x", err_style=None, err_kws=None, legend=None + ): + + # TODO this is messy, we want the mapping to be agnostic about + # the kind of plot to draw, but for the time being we need to set + # this information so the SizeMapping can use it + self._default_size_range = ( + np.r_[.5, 2] * mpl.rcParams["lines.linewidth"] + ) + + super().__init__(data=data, variables=variables) + + self.estimator = estimator + self.errorbar = errorbar + self.n_boot = n_boot + self.seed = seed + self.sort = sort + self.orient = orient + self.err_style = err_style + self.err_kws = {} if err_kws is None else err_kws + + self.legend = legend + + def plot(self, ax, kws): + """Draw the plot onto an axes, passing matplotlib kwargs.""" + + # Draw a test plot, using the passed in kwargs. The goal here is to + # honor both (a) the current state of the plot cycler and (b) the + # specified kwargs on all the lines we will draw, overriding when + # relevant with the data semantics. Note that we won't cycle + # internally; in other words, if `hue` is not used, all elements will + # have the same color, but they will have the color that you would have + # gotten from the corresponding matplotlib function, and calling the + # function will advance the axes property cycle. + + kws.setdefault("markeredgewidth", kws.pop("mew", .75)) + kws.setdefault("markeredgecolor", kws.pop("mec", "w")) + + # Set default error kwargs + err_kws = self.err_kws.copy() + if self.err_style == "band": + err_kws.setdefault("alpha", .2) + elif self.err_style == "bars": + pass + elif self.err_style is not None: + err = "`err_style` must be 'band' or 'bars', not {}" + raise ValueError(err.format(self.err_style)) + + # Initialize the aggregation object + agg = EstimateAggregator( + self.estimator, self.errorbar, n_boot=self.n_boot, seed=self.seed, + ) + + # TODO abstract variable to aggregate over here-ish. Better name? + orient = self.orient + if orient not in {"x", "y"}: + err = f"`orient` must be either 'x' or 'y', not {orient!r}." + raise ValueError(err) + other = {"x": "y", "y": "x"}[orient] + + # TODO How to handle NA? We don't want NA to propagate through to the + # estimate/CI when some values are present, but we would also like + # matplotlib to show "gaps" in the line when all values are missing. + # This is straightforward absent aggregation, but complicated with it. + # If we want to use nas, we need to conditionalize dropna in iter_data. + + # Loop over the semantic subsets and add to the plot + grouping_vars = "hue", "size", "style" + for sub_vars, sub_data in self.iter_data(grouping_vars, from_comp_data=True): + + if self.sort: + sort_vars = ["units", orient, other] + sort_cols = [var for var in sort_vars if var in self.variables] + sub_data = sub_data.sort_values(sort_cols) + + if ( + self.estimator is not None + and sub_data[orient].value_counts().max() > 1 + ): + if "units" in self.variables: + # TODO eventually relax this constraint + err = "estimator must be None when specifying units" + raise ValueError(err) + grouped = sub_data.groupby(orient, sort=self.sort) + # Could pass as_index=False instead of reset_index, + # but that fails on a corner case with older pandas. + sub_data = grouped.apply(agg, other).reset_index() + else: + sub_data[f"{other}min"] = np.nan + sub_data[f"{other}max"] = np.nan + + # TODO this is pretty ad hoc ; see GH2409 + for var in "xy": + if self._log_scaled(var): + for col in sub_data.filter(regex=f"^{var}"): + sub_data[col] = np.power(10, sub_data[col]) + + # --- Draw the main line(s) + + if "units" in self.variables: # XXX why not add to grouping variables? + lines = [] + for _, unit_data in sub_data.groupby("units"): + lines.extend(ax.plot(unit_data["x"], unit_data["y"], **kws)) + else: + lines = ax.plot(sub_data["x"], sub_data["y"], **kws) + + for line in lines: + + if "hue" in sub_vars: + line.set_color(self._hue_map(sub_vars["hue"])) + + if "size" in sub_vars: + line.set_linewidth(self._size_map(sub_vars["size"])) + + if "style" in sub_vars: + attributes = self._style_map(sub_vars["style"]) + if "dashes" in attributes: + line.set_dashes(attributes["dashes"]) + if "marker" in attributes: + line.set_marker(attributes["marker"]) + + line_color = line.get_color() + line_alpha = line.get_alpha() + line_capstyle = line.get_solid_capstyle() + + # --- Draw the confidence intervals + + if self.estimator is not None and self.errorbar is not None: + + # TODO handling of orientation will need to happen here + + if self.err_style == "band": + + func = {"x": ax.fill_between, "y": ax.fill_betweenx}[orient] + func( + sub_data[orient], + sub_data[f"{other}min"], sub_data[f"{other}max"], + color=line_color, **err_kws + ) + + elif self.err_style == "bars": + + error_param = { + f"{other}err": ( + sub_data[other] - sub_data[f"{other}min"], + sub_data[f"{other}max"] - sub_data[other], + ) + } + ebars = ax.errorbar( + sub_data["x"], sub_data["y"], **error_param, + linestyle="", color=line_color, alpha=line_alpha, + **err_kws + ) + + # Set the capstyle properly on the error bars + for obj in ebars.get_children(): + if isinstance(obj, mpl.collections.LineCollection): + obj.set_capstyle(line_capstyle) + + # Finalize the axes details + self._add_axis_labels(ax) + if self.legend: + self.add_legend_data(ax) + handles, _ = ax.get_legend_handles_labels() + if handles: + legend = ax.legend(title=self.legend_title) + adjust_legend_subtitles(legend) + + +class _ScatterPlotter(_RelationalPlotter): + + _legend_attributes = ["color", "s", "marker"] + _legend_func = "scatter" + + def __init__(self, *, data=None, variables={}, legend=None): + + # TODO this is messy, we want the mapping to be agnostic about + # the kind of plot to draw, but for the time being we need to set + # this information so the SizeMapping can use it + self._default_size_range = ( + np.r_[.5, 2] * np.square(mpl.rcParams["lines.markersize"]) + ) + + super().__init__(data=data, variables=variables) + + self.legend = legend + + def plot(self, ax, kws): + + # --- Determine the visual attributes of the plot + + data = self.plot_data.dropna() + if data.empty: + return + + # Define the vectors of x and y positions + empty = np.full(len(data), np.nan) + x = data.get("x", empty) + y = data.get("y", empty) + + if "style" in self.variables: + # Use a representative marker so scatter sets the edgecolor + # properly for line art markers. We currently enforce either + # all or none line art so this works. + example_level = self._style_map.levels[0] + example_marker = self._style_map(example_level, "marker") + kws.setdefault("marker", example_marker) + + # Conditionally set the marker edgecolor based on whether the marker is "filled" + # See https://github.com/matplotlib/matplotlib/issues/17849 for context + m = kws.get("marker", mpl.rcParams.get("marker", "o")) + if not isinstance(m, mpl.markers.MarkerStyle): + # TODO in more recent matplotlib (which?) can pass a MarkerStyle here + m = mpl.markers.MarkerStyle(m) + if m.is_filled(): + kws.setdefault("edgecolor", "w") + + # Draw the scatter plot + points = ax.scatter(x=x, y=y, **kws) + + # Apply the mapping from semantic variables to artist attributes + + if "hue" in self.variables: + points.set_facecolors(self._hue_map(data["hue"])) + + if "size" in self.variables: + points.set_sizes(self._size_map(data["size"])) + + if "style" in self.variables: + p = [self._style_map(val, "path") for val in data["style"]] + points.set_paths(p) + + # Apply dependent default attributes + + if "linewidth" not in kws: + sizes = points.get_sizes() + points.set_linewidths(.08 * np.sqrt(np.percentile(sizes, 10))) + + # Finalize the axes details + self._add_axis_labels(ax) + if self.legend: + self.add_legend_data(ax) + handles, _ = ax.get_legend_handles_labels() + if handles: + legend = ax.legend(title=self.legend_title) + adjust_legend_subtitles(legend) + + +def lineplot( + data=None, *, + x=None, y=None, hue=None, size=None, style=None, units=None, + palette=None, hue_order=None, hue_norm=None, + sizes=None, size_order=None, size_norm=None, + dashes=True, markers=None, style_order=None, + estimator="mean", errorbar=("ci", 95), n_boot=1000, seed=None, + orient="x", sort=True, err_style="band", err_kws=None, + legend="auto", ci="deprecated", ax=None, **kwargs +): + + # Handle deprecation of ci parameter + errorbar = _deprecate_ci(errorbar, ci) + + variables = _LinePlotter.get_semantics(locals()) + p = _LinePlotter( + data=data, variables=variables, + estimator=estimator, n_boot=n_boot, seed=seed, errorbar=errorbar, + sort=sort, orient=orient, err_style=err_style, err_kws=err_kws, + legend=legend, + ) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + p.map_size(sizes=sizes, order=size_order, norm=size_norm) + p.map_style(markers=markers, dashes=dashes, order=style_order) + + if ax is None: + ax = plt.gca() + + if style is None and not {"ls", "linestyle"} & set(kwargs): # XXX + kwargs["dashes"] = "" if dashes is None or isinstance(dashes, bool) else dashes + + if not p.has_xy_data: + return ax + + p._attach(ax) + + # Other functions have color as an explicit param, + # and we should probably do that here too + color = kwargs.pop("color", kwargs.pop("c", None)) + kwargs["color"] = _default_color(ax.plot, hue, color, kwargs) + + p.plot(ax, kwargs) + return ax + + +lineplot.__doc__ = """\ +Draw a line plot with possibility of several semantic groupings. + +{narrative.main_api} + +{narrative.relational_semantic} + +By default, the plot aggregates over multiple `y` values at each value of +`x` and shows an estimate of the central tendency and a confidence +interval for that estimate. + +Parameters +---------- +{params.core.data} +{params.core.xy} +hue : vector or key in `data` + Grouping variable that will produce lines with different colors. + Can be either categorical or numeric, although color mapping will + behave differently in latter case. +size : vector or key in `data` + Grouping variable that will produce lines with different widths. + Can be either categorical or numeric, although size mapping will + behave differently in latter case. +style : vector or key in `data` + Grouping variable that will produce lines with different dashes + and/or markers. Can have a numeric dtype but will always be treated + as categorical. +{params.rel.units} +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.rel.sizes} +{params.rel.size_order} +{params.rel.size_norm} +{params.rel.dashes} +{params.rel.markers} +{params.rel.style_order} +{params.rel.estimator} +{params.stat.errorbar} +{params.rel.n_boot} +{params.rel.seed} +orient : "x" or "y" + Dimension along which the data are sorted / aggregated. Equivalently, + the "independent variable" of the resulting function. +sort : boolean + If True, the data will be sorted by the x and y variables, otherwise + lines will connect points in the order they appear in the dataset. +err_style : "band" or "bars" + Whether to draw the confidence intervals with translucent error bands + or discrete error bars. +err_kws : dict of keyword arguments + Additional parameters to control the aesthetics of the error bars. The + kwargs are passed either to :meth:`matplotlib.axes.Axes.fill_between` + or :meth:`matplotlib.axes.Axes.errorbar`, depending on `err_style`. +{params.rel.legend} +{params.rel.ci} +{params.core.ax} +kwargs : key, value mappings + Other keyword arguments are passed down to + :meth:`matplotlib.axes.Axes.plot`. + +Returns +------- +{returns.ax} + +See Also +-------- +{seealso.scatterplot} +{seealso.pointplot} + +Examples +-------- + +.. include:: ../docstrings/lineplot.rst + +""".format( + narrative=_relational_narrative, + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def scatterplot( + data=None, *, + x=None, y=None, hue=None, size=None, style=None, + palette=None, hue_order=None, hue_norm=None, + sizes=None, size_order=None, size_norm=None, + markers=True, style_order=None, legend="auto", ax=None, + **kwargs +): + + variables = _ScatterPlotter.get_semantics(locals()) + p = _ScatterPlotter(data=data, variables=variables, legend=legend) + + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + p.map_size(sizes=sizes, order=size_order, norm=size_norm) + p.map_style(markers=markers, order=style_order) + + if ax is None: + ax = plt.gca() + + if not p.has_xy_data: + return ax + + p._attach(ax) + + # Other functions have color as an explicit param, + # and we should probably do that here too + color = kwargs.pop("color", None) + kwargs["color"] = _default_color(ax.scatter, hue, color, kwargs) + + p.plot(ax, kwargs) + + return ax + + +scatterplot.__doc__ = """\ +Draw a scatter plot with possibility of several semantic groupings. + +{narrative.main_api} + +{narrative.relational_semantic} + +Parameters +---------- +{params.core.data} +{params.core.xy} +hue : vector or key in `data` + Grouping variable that will produce points with different colors. + Can be either categorical or numeric, although color mapping will + behave differently in latter case. +size : vector or key in `data` + Grouping variable that will produce points with different sizes. + Can be either categorical or numeric, although size mapping will + behave differently in latter case. +style : vector or key in `data` + Grouping variable that will produce points with different markers. + Can have a numeric dtype but will always be treated as categorical. +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.rel.sizes} +{params.rel.size_order} +{params.rel.size_norm} +{params.rel.markers} +{params.rel.style_order} +{params.rel.legend} +{params.core.ax} +kwargs : key, value mappings + Other keyword arguments are passed down to + :meth:`matplotlib.axes.Axes.scatter`. + +Returns +------- +{returns.ax} + +See Also +-------- +{seealso.lineplot} +{seealso.stripplot} +{seealso.swarmplot} + +Examples +-------- + +.. include:: ../docstrings/scatterplot.rst + +""".format( + narrative=_relational_narrative, + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) + + +def relplot( + data=None, *, + x=None, y=None, hue=None, size=None, style=None, units=None, + row=None, col=None, col_wrap=None, row_order=None, col_order=None, + palette=None, hue_order=None, hue_norm=None, + sizes=None, size_order=None, size_norm=None, + markers=None, dashes=None, style_order=None, + legend="auto", kind="scatter", height=5, aspect=1, facet_kws=None, + **kwargs +): + + if kind == "scatter": + + plotter = _ScatterPlotter + func = scatterplot + markers = True if markers is None else markers + + elif kind == "line": + + plotter = _LinePlotter + func = lineplot + dashes = True if dashes is None else dashes + + else: + err = f"Plot kind {kind} not recognized" + raise ValueError(err) + + # Check for attempt to plot onto specific axes and warn + if "ax" in kwargs: + msg = ( + "relplot is a figure-level function and does not accept " + "the `ax` parameter. You may wish to try {}".format(kind + "plot") + ) + warnings.warn(msg, UserWarning) + kwargs.pop("ax") + + # Use the full dataset to map the semantics + p = plotter( + data=data, + variables=plotter.get_semantics(locals()), + legend=legend, + ) + p.map_hue(palette=palette, order=hue_order, norm=hue_norm) + p.map_size(sizes=sizes, order=size_order, norm=size_norm) + p.map_style(markers=markers, dashes=dashes, order=style_order) + + # Extract the semantic mappings + if "hue" in p.variables: + palette = p._hue_map.lookup_table + hue_order = p._hue_map.levels + hue_norm = p._hue_map.norm + else: + palette = hue_order = hue_norm = None + + if "size" in p.variables: + sizes = p._size_map.lookup_table + size_order = p._size_map.levels + size_norm = p._size_map.norm + + if "style" in p.variables: + style_order = p._style_map.levels + if markers: + markers = {k: p._style_map(k, "marker") for k in style_order} + else: + markers = None + if dashes: + dashes = {k: p._style_map(k, "dashes") for k in style_order} + else: + dashes = None + else: + markers = dashes = style_order = None + + # Now extract the data that would be used to draw a single plot + variables = p.variables + plot_data = p.plot_data + plot_semantics = p.semantics + + # Define the common plotting parameters + plot_kws = dict( + palette=palette, hue_order=hue_order, hue_norm=hue_norm, + sizes=sizes, size_order=size_order, size_norm=size_norm, + markers=markers, dashes=dashes, style_order=style_order, + legend=False, + ) + plot_kws.update(kwargs) + if kind == "scatter": + plot_kws.pop("dashes") + + # Add the grid semantics onto the plotter + grid_semantics = "row", "col" + p.semantics = plot_semantics + grid_semantics + p.assign_variables( + data=data, + variables=dict( + x=x, y=y, + hue=hue, size=size, style=style, units=units, + row=row, col=col, + ), + ) + + # Define the named variables for plotting on each facet + # Rename the variables with a leading underscore to avoid + # collisions with faceting variable names + plot_variables = {v: f"_{v}" for v in variables} + plot_kws.update(plot_variables) + + # Pass the row/col variables to FacetGrid with their original + # names so that the axes titles render correctly + for var in ["row", "col"]: + # Handle faceting variables that lack name information + if var in p.variables and p.variables[var] is None: + p.variables[var] = f"_{var}_" + grid_kws = {v: p.variables.get(v) for v in grid_semantics} + + # Rename the columns of the plot_data structure appropriately + new_cols = plot_variables.copy() + new_cols.update(grid_kws) + full_data = p.plot_data.rename(columns=new_cols) + + # Set up the FacetGrid object + facet_kws = {} if facet_kws is None else facet_kws.copy() + g = FacetGrid( + data=full_data.dropna(axis=1, how="all"), + **grid_kws, + col_wrap=col_wrap, row_order=row_order, col_order=col_order, + height=height, aspect=aspect, dropna=False, + **facet_kws + ) + + # Draw the plot + g.map_dataframe(func, **plot_kws) + + # Label the axes, using the original variables + # Pass "" when the variable name is None to overwrite internal variables + g.set_axis_labels(variables.get("x") or "", variables.get("y") or "") + + # Show the legend + if legend: + # Replace the original plot data so the legend uses + # numeric data with the correct type + p.plot_data = plot_data + p.add_legend_data(g.axes.flat[0]) + if p.legend_data: + g.add_legend(legend_data=p.legend_data, + label_order=p.legend_order, + title=p.legend_title, + adjust_subtitles=True) + + # Rename the columns of the FacetGrid's `data` attribute + # to match the original column names + orig_cols = { + f"_{k}": f"_{k}_" if v is None else v for k, v in variables.items() + } + grid_data = g.data.rename(columns=orig_cols) + if data is not None and (x is not None or y is not None): + if not isinstance(data, pd.DataFrame): + data = pd.DataFrame(data) + g.data = pd.merge( + data, + grid_data[grid_data.columns.difference(data.columns)], + left_index=True, + right_index=True, + ) + else: + g.data = grid_data + + return g + + +relplot.__doc__ = """\ +Figure-level interface for drawing relational plots onto a FacetGrid. + +This function provides access to several different axes-level functions +that show the relationship between two variables with semantic mappings +of subsets. The `kind` parameter selects the underlying axes-level +function to use: + +- :func:`scatterplot` (with `kind="scatter"`; the default) +- :func:`lineplot` (with `kind="line"`) + +Extra keyword arguments are passed to the underlying function, so you +should refer to the documentation for each to see kind-specific options. + +{narrative.main_api} + +{narrative.relational_semantic} + +After plotting, the :class:`FacetGrid` with the plot is returned and can +be used directly to tweak supporting plot details or add other layers. + +Parameters +---------- +{params.core.data} +{params.core.xy} +hue : vector or key in `data` + Grouping variable that will produce elements with different colors. + Can be either categorical or numeric, although color mapping will + behave differently in latter case. +size : vector or key in `data` + Grouping variable that will produce elements with different sizes. + Can be either categorical or numeric, although size mapping will + behave differently in latter case. +style : vector or key in `data` + Grouping variable that will produce elements with different styles. + Can have a numeric dtype but will always be treated as categorical. +{params.rel.units} +{params.facets.rowcol} +{params.facets.col_wrap} +row_order, col_order : lists of strings + Order to organize the rows and/or columns of the grid in, otherwise the + orders are inferred from the data objects. +{params.core.palette} +{params.core.hue_order} +{params.core.hue_norm} +{params.rel.sizes} +{params.rel.size_order} +{params.rel.size_norm} +{params.rel.style_order} +{params.rel.dashes} +{params.rel.markers} +{params.rel.legend} +kind : string + Kind of plot to draw, corresponding to a seaborn relational plot. + Options are `"scatter"` or `"line"`. +{params.facets.height} +{params.facets.aspect} +facet_kws : dict + Dictionary of other keyword arguments to pass to :class:`FacetGrid`. +kwargs : key, value pairings + Other keyword arguments are passed through to the underlying plotting + function. + +Returns +------- +{returns.facetgrid} + +Examples +-------- + +.. include:: ../docstrings/relplot.rst + +""".format( + narrative=_relational_narrative, + params=_param_docs, + returns=_core_docs["returns"], + seealso=_core_docs["seealso"], +) diff --git a/lib/python3.10/site-packages/seaborn/utils.py b/lib/python3.10/site-packages/seaborn/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..3cf01755a40e1f929fb22e4cd1e16015ca584986 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/utils.py @@ -0,0 +1,876 @@ +"""Utility functions, mostly for internal use.""" +import os +import re +import inspect +import warnings +import colorsys +from contextlib import contextmanager +from urllib.request import urlopen, urlretrieve + +import numpy as np +import pandas as pd +import matplotlib as mpl +from matplotlib.colors import to_rgb +import matplotlib.pyplot as plt +from matplotlib.cbook import normalize_kwargs + +from .external.version import Version +from .external.appdirs import user_cache_dir + +__all__ = ["desaturate", "saturate", "set_hls_values", "move_legend", + "despine", "get_dataset_names", "get_data_home", "load_dataset"] + + +def ci_to_errsize(cis, heights): + """Convert intervals to error arguments relative to plot heights. + + Parameters + ---------- + cis : 2 x n sequence + sequence of confidence interval limits + heights : n sequence + sequence of plot heights + + Returns + ------- + errsize : 2 x n array + sequence of error size relative to height values in correct + format as argument for plt.bar + + """ + cis = np.atleast_2d(cis).reshape(2, -1) + heights = np.atleast_1d(heights) + errsize = [] + for i, (low, high) in enumerate(np.transpose(cis)): + h = heights[i] + elow = h - low + ehigh = high - h + errsize.append([elow, ehigh]) + + errsize = np.asarray(errsize).T + return errsize + + +def _normal_quantile_func(q): + """ + Compute the quantile function of the standard normal distribution. + + This wrapper exists because we are dropping scipy as a mandatory dependency + but statistics.NormalDist was added to the standard library in 3.8. + + """ + try: + from statistics import NormalDist + qf = np.vectorize(NormalDist().inv_cdf) + except ImportError: + try: + from scipy.stats import norm + qf = norm.ppf + except ImportError: + msg = ( + "Standard normal quantile functions require either Python>=3.8 or scipy" + ) + raise RuntimeError(msg) + return qf(q) + + +def _draw_figure(fig): + """Force draw of a matplotlib figure, accounting for back-compat.""" + # See https://github.com/matplotlib/matplotlib/issues/19197 for context + fig.canvas.draw() + if fig.stale: + try: + fig.draw(fig.canvas.get_renderer()) + except AttributeError: + pass + + +def _default_color(method, hue, color, kws): + """If needed, get a default color by using the matplotlib property cycle.""" + + if hue is not None: + # This warning is probably user-friendly, but it's currently triggered + # in a FacetGrid context and I don't want to mess with that logic right now + # if color is not None: + # msg = "`color` is ignored when `hue` is assigned." + # warnings.warn(msg) + return None + + kws = kws.copy() + kws.pop("label", None) + + if color is not None: + return color + + elif method.__name__ == "plot": + + color = _normalize_kwargs(kws, mpl.lines.Line2D).get("color") + scout, = method([], [], scalex=False, scaley=False, color=color) + color = scout.get_color() + scout.remove() + + elif method.__name__ == "scatter": + + # Matplotlib will raise if the size of x/y don't match s/c, + # and the latter might be in the kws dict + scout_size = max( + np.atleast_1d(kws.get(key, [])).shape[0] + for key in ["s", "c", "fc", "facecolor", "facecolors"] + ) + scout_x = scout_y = np.full(scout_size, np.nan) + + scout = method(scout_x, scout_y, **kws) + facecolors = scout.get_facecolors() + + if not len(facecolors): + # Handle bug in matplotlib <= 3.2 (I think) + # This will limit the ability to use non color= kwargs to specify + # a color in versions of matplotlib with the bug, but trying to + # work out what the user wanted by re-implementing the broken logic + # of inspecting the kwargs is probably too brittle. + single_color = False + else: + single_color = np.unique(facecolors, axis=0).shape[0] == 1 + + # Allow the user to specify an array of colors through various kwargs + if "c" not in kws and single_color: + color = to_rgb(facecolors[0]) + + scout.remove() + + elif method.__name__ == "bar": + + # bar() needs masked, not empty data, to generate a patch + scout, = method([np.nan], [np.nan], **kws) + color = to_rgb(scout.get_facecolor()) + scout.remove() + + elif method.__name__ == "fill_between": + + # There is a bug on matplotlib < 3.3 where fill_between with + # datetime units and empty data will set incorrect autoscale limits + # To workaround it, we'll always return the first color in the cycle. + # https://github.com/matplotlib/matplotlib/issues/17586 + ax = method.__self__ + datetime_axis = any([ + isinstance(ax.xaxis.converter, mpl.dates.DateConverter), + isinstance(ax.yaxis.converter, mpl.dates.DateConverter), + ]) + if Version(mpl.__version__) < Version("3.3") and datetime_axis: + return "C0" + + kws = _normalize_kwargs(kws, mpl.collections.PolyCollection) + + scout = method([], [], **kws) + facecolor = scout.get_facecolor() + color = to_rgb(facecolor[0]) + scout.remove() + + return color + + +def desaturate(color, prop): + """Decrease the saturation channel of a color by some percent. + + Parameters + ---------- + color : matplotlib color + hex, rgb-tuple, or html color name + prop : float + saturation channel of color will be multiplied by this value + + Returns + ------- + new_color : rgb tuple + desaturated color code in RGB tuple representation + + """ + # Check inputs + if not 0 <= prop <= 1: + raise ValueError("prop must be between 0 and 1") + + # Get rgb tuple rep + rgb = to_rgb(color) + + # Convert to hls + h, l, s = colorsys.rgb_to_hls(*rgb) + + # Desaturate the saturation channel + s *= prop + + # Convert back to rgb + new_color = colorsys.hls_to_rgb(h, l, s) + + return new_color + + +def saturate(color): + """Return a fully saturated color with the same hue. + + Parameters + ---------- + color : matplotlib color + hex, rgb-tuple, or html color name + + Returns + ------- + new_color : rgb tuple + saturated color code in RGB tuple representation + + """ + return set_hls_values(color, s=1) + + +def set_hls_values(color, h=None, l=None, s=None): # noqa + """Independently manipulate the h, l, or s channels of a color. + + Parameters + ---------- + color : matplotlib color + hex, rgb-tuple, or html color name + h, l, s : floats between 0 and 1, or None + new values for each channel in hls space + + Returns + ------- + new_color : rgb tuple + new color code in RGB tuple representation + + """ + # Get an RGB tuple representation + rgb = to_rgb(color) + vals = list(colorsys.rgb_to_hls(*rgb)) + for i, val in enumerate([h, l, s]): + if val is not None: + vals[i] = val + + rgb = colorsys.hls_to_rgb(*vals) + return rgb + + +def axlabel(xlabel, ylabel, **kwargs): + """Grab current axis and label it. + + DEPRECATED: will be removed in a future version. + + """ + msg = "This function is deprecated and will be removed in a future version" + warnings.warn(msg, FutureWarning) + ax = plt.gca() + ax.set_xlabel(xlabel, **kwargs) + ax.set_ylabel(ylabel, **kwargs) + + +def remove_na(vector): + """Helper method for removing null values from data vectors. + + Parameters + ---------- + vector : vector object + Must implement boolean masking with [] subscript syntax. + + Returns + ------- + clean_clean : same type as ``vector`` + Vector of data with null values removed. May be a copy or a view. + + """ + return vector[pd.notnull(vector)] + + +def get_color_cycle(): + """Return the list of colors in the current matplotlib color cycle + + Parameters + ---------- + None + + Returns + ------- + colors : list + List of matplotlib colors in the current cycle, or dark gray if + the current color cycle is empty. + """ + cycler = mpl.rcParams['axes.prop_cycle'] + return cycler.by_key()['color'] if 'color' in cycler.keys else [".15"] + + +def despine(fig=None, ax=None, top=True, right=True, left=False, + bottom=False, offset=None, trim=False): + """Remove the top and right spines from plot(s). + + fig : matplotlib figure, optional + Figure to despine all axes of, defaults to the current figure. + ax : matplotlib axes, optional + Specific axes object to despine. Ignored if fig is provided. + top, right, left, bottom : boolean, optional + If True, remove that spine. + offset : int or dict, optional + Absolute distance, in points, spines should be moved away + from the axes (negative values move spines inward). A single value + applies to all spines; a dict can be used to set offset values per + side. + trim : bool, optional + If True, limit spines to the smallest and largest major tick + on each non-despined axis. + + Returns + ------- + None + + """ + # Get references to the axes we want + if fig is None and ax is None: + axes = plt.gcf().axes + elif fig is not None: + axes = fig.axes + elif ax is not None: + axes = [ax] + + for ax_i in axes: + for side in ["top", "right", "left", "bottom"]: + # Toggle the spine objects + is_visible = not locals()[side] + ax_i.spines[side].set_visible(is_visible) + if offset is not None and is_visible: + try: + val = offset.get(side, 0) + except AttributeError: + val = offset + ax_i.spines[side].set_position(('outward', val)) + + # Potentially move the ticks + if left and not right: + maj_on = any( + t.tick1line.get_visible() + for t in ax_i.yaxis.majorTicks + ) + min_on = any( + t.tick1line.get_visible() + for t in ax_i.yaxis.minorTicks + ) + ax_i.yaxis.set_ticks_position("right") + for t in ax_i.yaxis.majorTicks: + t.tick2line.set_visible(maj_on) + for t in ax_i.yaxis.minorTicks: + t.tick2line.set_visible(min_on) + + if bottom and not top: + maj_on = any( + t.tick1line.get_visible() + for t in ax_i.xaxis.majorTicks + ) + min_on = any( + t.tick1line.get_visible() + for t in ax_i.xaxis.minorTicks + ) + ax_i.xaxis.set_ticks_position("top") + for t in ax_i.xaxis.majorTicks: + t.tick2line.set_visible(maj_on) + for t in ax_i.xaxis.minorTicks: + t.tick2line.set_visible(min_on) + + if trim: + # clip off the parts of the spines that extend past major ticks + xticks = np.asarray(ax_i.get_xticks()) + if xticks.size: + firsttick = np.compress(xticks >= min(ax_i.get_xlim()), + xticks)[0] + lasttick = np.compress(xticks <= max(ax_i.get_xlim()), + xticks)[-1] + ax_i.spines['bottom'].set_bounds(firsttick, lasttick) + ax_i.spines['top'].set_bounds(firsttick, lasttick) + newticks = xticks.compress(xticks <= lasttick) + newticks = newticks.compress(newticks >= firsttick) + ax_i.set_xticks(newticks) + + yticks = np.asarray(ax_i.get_yticks()) + if yticks.size: + firsttick = np.compress(yticks >= min(ax_i.get_ylim()), + yticks)[0] + lasttick = np.compress(yticks <= max(ax_i.get_ylim()), + yticks)[-1] + ax_i.spines['left'].set_bounds(firsttick, lasttick) + ax_i.spines['right'].set_bounds(firsttick, lasttick) + newticks = yticks.compress(yticks <= lasttick) + newticks = newticks.compress(newticks >= firsttick) + ax_i.set_yticks(newticks) + + +def move_legend(obj, loc, **kwargs): + """ + Recreate a plot's legend at a new location. + + The name is a slight misnomer. Matplotlib legends do not expose public + control over their position parameters. So this function creates a new legend, + copying over the data from the original object, which is then removed. + + Parameters + ---------- + obj : the object with the plot + This argument can be either a seaborn or matplotlib object: + + - :class:`seaborn.FacetGrid` or :class:`seaborn.PairGrid` + - :class:`matplotlib.axes.Axes` or :class:`matplotlib.figure.Figure` + + loc : str or int + Location argument, as in :meth:`matplotlib.axes.Axes.legend`. + + kwargs + Other keyword arguments are passed to :meth:`matplotlib.axes.Axes.legend`. + + Examples + -------- + + .. include:: ../docstrings/move_legend.rst + + """ + # This is a somewhat hackish solution that will hopefully be obviated by + # upstream improvements to matplotlib legends that make them easier to + # modify after creation. + + from seaborn.axisgrid import Grid # Avoid circular import + + # Locate the legend object and a method to recreate the legend + if isinstance(obj, Grid): + old_legend = obj.legend + legend_func = obj.figure.legend + elif isinstance(obj, mpl.axes.Axes): + old_legend = obj.legend_ + legend_func = obj.legend + elif isinstance(obj, mpl.figure.Figure): + if obj.legends: + old_legend = obj.legends[-1] + else: + old_legend = None + legend_func = obj.legend + else: + err = "`obj` must be a seaborn Grid or matplotlib Axes or Figure instance." + raise TypeError(err) + + if old_legend is None: + err = f"{obj} has no legend attached." + raise ValueError(err) + + # Extract the components of the legend we need to reuse + handles = old_legend.legendHandles + labels = [t.get_text() for t in old_legend.get_texts()] + + # Extract legend properties that can be passed to the recreation method + # (Vexingly, these don't all round-trip) + legend_kws = inspect.signature(mpl.legend.Legend).parameters + props = {k: v for k, v in old_legend.properties().items() if k in legend_kws} + + # Delegate default bbox_to_anchor rules to matplotlib + props.pop("bbox_to_anchor") + + # Try to propagate the existing title and font properties; respect new ones too + title = props.pop("title") + if "title" in kwargs: + title.set_text(kwargs.pop("title")) + title_kwargs = {k: v for k, v in kwargs.items() if k.startswith("title_")} + for key, val in title_kwargs.items(): + title.set(**{key[6:]: val}) + kwargs.pop(key) + + # Try to respect the frame visibility + kwargs.setdefault("frameon", old_legend.legendPatch.get_visible()) + + # Remove the old legend and create the new one + props.update(kwargs) + old_legend.remove() + new_legend = legend_func(handles, labels, loc=loc, **props) + new_legend.set_title(title.get_text(), title.get_fontproperties()) + + # Let the Grid object continue to track the correct legend object + if isinstance(obj, Grid): + obj._legend = new_legend + + +def _kde_support(data, bw, gridsize, cut, clip): + """Establish support for a kernel density estimate.""" + support_min = max(data.min() - bw * cut, clip[0]) + support_max = min(data.max() + bw * cut, clip[1]) + support = np.linspace(support_min, support_max, gridsize) + + return support + + +def ci(a, which=95, axis=None): + """Return a percentile range from an array of values.""" + p = 50 - which / 2, 50 + which / 2 + return np.nanpercentile(a, p, axis) + + +def get_dataset_names(): + """Report available example datasets, useful for reporting issues. + + Requires an internet connection. + + """ + url = "https://github.com/mwaskom/seaborn-data" + with urlopen(url) as resp: + html = resp.read() + + pat = r"/mwaskom/seaborn-data/blob/master/(\w*).csv" + datasets = re.findall(pat, html.decode()) + return datasets + + +def get_data_home(data_home=None): + """Return a path to the cache directory for example datasets. + + This directory is used by :func:`load_dataset`. + + If the ``data_home`` argument is not provided, it will use a directory + specified by the `SEABORN_DATA` environment variable (if it exists) + or otherwise default to an OS-appropriate user cache location. + + """ + if data_home is None: + data_home = os.environ.get("SEABORN_DATA", user_cache_dir("seaborn")) + data_home = os.path.expanduser(data_home) + if not os.path.exists(data_home): + os.makedirs(data_home) + return data_home + + +def load_dataset(name, cache=True, data_home=None, **kws): + """Load an example dataset from the online repository (requires internet). + + This function provides quick access to a small number of example datasets + that are useful for documenting seaborn or generating reproducible examples + for bug reports. It is not necessary for normal usage. + + Note that some of the datasets have a small amount of preprocessing applied + to define a proper ordering for categorical variables. + + Use :func:`get_dataset_names` to see a list of available datasets. + + Parameters + ---------- + name : str + Name of the dataset (``{name}.csv`` on + https://github.com/mwaskom/seaborn-data). + cache : boolean, optional + If True, try to load from the local cache first, and save to the cache + if a download is required. + data_home : string, optional + The directory in which to cache data; see :func:`get_data_home`. + kws : keys and values, optional + Additional keyword arguments are passed to passed through to + :func:`pandas.read_csv`. + + Returns + ------- + df : :class:`pandas.DataFrame` + Tabular data, possibly with some preprocessing applied. + + """ + # A common beginner mistake is to assume that one's personal data needs + # to be passed through this function to be usable with seaborn. + # Let's provide a more helpful error than you would otherwise get. + if isinstance(name, pd.DataFrame): + err = ( + "This function accepts only strings (the name of an example dataset). " + "You passed a pandas DataFrame. If you have your own dataset, " + "it is not necessary to use this function before plotting." + ) + raise TypeError(err) + + url = f"https://raw.githubusercontent.com/mwaskom/seaborn-data/master/{name}.csv" + + if cache: + cache_path = os.path.join(get_data_home(data_home), os.path.basename(url)) + if not os.path.exists(cache_path): + if name not in get_dataset_names(): + raise ValueError(f"'{name}' is not one of the example datasets.") + urlretrieve(url, cache_path) + full_path = cache_path + else: + full_path = url + + df = pd.read_csv(full_path, **kws) + + if df.iloc[-1].isnull().all(): + df = df.iloc[:-1] + + # Set some columns as a categorical type with ordered levels + + if name == "tips": + df["day"] = pd.Categorical(df["day"], ["Thur", "Fri", "Sat", "Sun"]) + df["sex"] = pd.Categorical(df["sex"], ["Male", "Female"]) + df["time"] = pd.Categorical(df["time"], ["Lunch", "Dinner"]) + df["smoker"] = pd.Categorical(df["smoker"], ["Yes", "No"]) + + elif name == "flights": + months = df["month"].str[:3] + df["month"] = pd.Categorical(months, months.unique()) + + elif name == "exercise": + df["time"] = pd.Categorical(df["time"], ["1 min", "15 min", "30 min"]) + df["kind"] = pd.Categorical(df["kind"], ["rest", "walking", "running"]) + df["diet"] = pd.Categorical(df["diet"], ["no fat", "low fat"]) + + elif name == "titanic": + df["class"] = pd.Categorical(df["class"], ["First", "Second", "Third"]) + df["deck"] = pd.Categorical(df["deck"], list("ABCDEFG")) + + elif name == "penguins": + df["sex"] = df["sex"].str.title() + + elif name == "diamonds": + df["color"] = pd.Categorical( + df["color"], ["D", "E", "F", "G", "H", "I", "J"], + ) + df["clarity"] = pd.Categorical( + df["clarity"], ["IF", "VVS1", "VVS2", "VS1", "VS2", "SI1", "SI2", "I1"], + ) + df["cut"] = pd.Categorical( + df["cut"], ["Ideal", "Premium", "Very Good", "Good", "Fair"], + ) + + elif name == "taxis": + df["pickup"] = pd.to_datetime(df["pickup"]) + df["dropoff"] = pd.to_datetime(df["dropoff"]) + + elif name == "seaice": + df["Date"] = pd.to_datetime(df["Date"]) + + elif name == "dowjones": + df["Date"] = pd.to_datetime(df["Date"]) + + return df + + +def axis_ticklabels_overlap(labels): + """Return a boolean for whether the list of ticklabels have overlaps. + + Parameters + ---------- + labels : list of matplotlib ticklabels + + Returns + ------- + overlap : boolean + True if any of the labels overlap. + + """ + if not labels: + return False + try: + bboxes = [l.get_window_extent() for l in labels] + overlaps = [b.count_overlaps(bboxes) for b in bboxes] + return max(overlaps) > 1 + except RuntimeError: + # Issue on macos backend raises an error in the above code + return False + + +def axes_ticklabels_overlap(ax): + """Return booleans for whether the x and y ticklabels on an Axes overlap. + + Parameters + ---------- + ax : matplotlib Axes + + Returns + ------- + x_overlap, y_overlap : booleans + True when the labels on that axis overlap. + + """ + return (axis_ticklabels_overlap(ax.get_xticklabels()), + axis_ticklabels_overlap(ax.get_yticklabels())) + + +def locator_to_legend_entries(locator, limits, dtype): + """Return levels and formatted levels for brief numeric legends.""" + raw_levels = locator.tick_values(*limits).astype(dtype) + + # The locator can return ticks outside the limits, clip them here + raw_levels = [l for l in raw_levels if l >= limits[0] and l <= limits[1]] + + class dummy_axis: + def get_view_interval(self): + return limits + + if isinstance(locator, mpl.ticker.LogLocator): + formatter = mpl.ticker.LogFormatter() + else: + formatter = mpl.ticker.ScalarFormatter() + # Avoid having an offset/scientific notation which we don't currently + # have any way of representing in the legend + formatter.set_useOffset(False) + formatter.set_scientific(False) + formatter.axis = dummy_axis() + + # TODO: The following two lines should be replaced + # once pinned matplotlib>=3.1.0 with: + # formatted_levels = formatter.format_ticks(raw_levels) + formatter.set_locs(raw_levels) + formatted_levels = [formatter(x) for x in raw_levels] + + return raw_levels, formatted_levels + + +def relative_luminance(color): + """Calculate the relative luminance of a color according to W3C standards + + Parameters + ---------- + color : matplotlib color or sequence of matplotlib colors + Hex code, rgb-tuple, or html color name. + + Returns + ------- + luminance : float(s) between 0 and 1 + + """ + rgb = mpl.colors.colorConverter.to_rgba_array(color)[:, :3] + rgb = np.where(rgb <= .03928, rgb / 12.92, ((rgb + .055) / 1.055) ** 2.4) + lum = rgb.dot([.2126, .7152, .0722]) + try: + return lum.item() + except ValueError: + return lum + + +def to_utf8(obj): + """Return a string representing a Python object. + + Strings (i.e. type ``str``) are returned unchanged. + + Byte strings (i.e. type ``bytes``) are returned as UTF-8-decoded strings. + + For other objects, the method ``__str__()`` is called, and the result is + returned as a string. + + Parameters + ---------- + obj : object + Any Python object + + Returns + ------- + s : str + UTF-8-decoded string representation of ``obj`` + + """ + if isinstance(obj, str): + return obj + try: + return obj.decode(encoding="utf-8") + except AttributeError: # obj is not bytes-like + return str(obj) + + +def _normalize_kwargs(kws, artist): + """Wrapper for mpl.cbook.normalize_kwargs that supports <= 3.2.1.""" + _alias_map = { + 'color': ['c'], + 'linewidth': ['lw'], + 'linestyle': ['ls'], + 'facecolor': ['fc'], + 'edgecolor': ['ec'], + 'markerfacecolor': ['mfc'], + 'markeredgecolor': ['mec'], + 'markeredgewidth': ['mew'], + 'markersize': ['ms'] + } + try: + kws = normalize_kwargs(kws, artist) + except AttributeError: + kws = normalize_kwargs(kws, _alias_map) + return kws + + +def _check_argument(param, options, value): + """Raise if value for param is not in options.""" + if value not in options: + raise ValueError( + f"`{param}` must be one of {options}, but {repr(value)} was passed." + ) + + +def _assign_default_kwargs(kws, call_func, source_func): + """Assign default kwargs for call_func using values from source_func.""" + # This exists so that axes-level functions and figure-level functions can + # both call a Plotter method while having the default kwargs be defined in + # the signature of the axes-level function. + # An alternative would be to have a decorator on the method that sets its + # defaults based on those defined in the axes-level function. + # Then the figure-level function would not need to worry about defaults. + # I am not sure which is better. + needed = inspect.signature(call_func).parameters + defaults = inspect.signature(source_func).parameters + + for param in needed: + if param in defaults and param not in kws: + kws[param] = defaults[param].default + + return kws + + +def adjust_legend_subtitles(legend): + """ + Make invisible-handle "subtitles" entries look more like titles. + + Note: This function is not part of the public API and may be changed or removed. + + """ + # Legend title not in rcParams until 3.0 + font_size = plt.rcParams.get("legend.title_fontsize", None) + hpackers = legend.findobj(mpl.offsetbox.VPacker)[0].get_children() + for hpack in hpackers: + draw_area, text_area = hpack.get_children() + handles = draw_area.get_children() + if not all(artist.get_visible() for artist in handles): + draw_area.set_width(0) + for text in text_area.get_children(): + if font_size is not None: + text.set_size(font_size) + + +def _deprecate_ci(errorbar, ci): + """ + Warn on usage of ci= and convert to appropriate errorbar= arg. + + ci was deprecated when errorbar was added in 0.12. It should not be removed + completely for some time, but it can be moved out of function definitions + (and extracted from kwargs) after one cycle. + + """ + if ci != "deprecated": + if ci is None: + errorbar = None + elif ci == "sd": + errorbar = "sd" + else: + errorbar = ("ci", ci) + msg = ( + "\n\nThe `ci` parameter is deprecated. " + f"Use `errorbar={repr(errorbar)}` for the same effect.\n" + ) + warnings.warn(msg, FutureWarning, stacklevel=3) + + return errorbar + + +@contextmanager +def _disable_autolayout(): + """Context manager for preventing rc-controlled auto-layout behavior.""" + # This is a workaround for an issue in matplotlib, for details see + # https://github.com/mwaskom/seaborn/issues/2914 + # The only affect of this rcParam is to set the default value for + # layout= in plt.figure, so we could just do that instead. + # But then we would need to own the complexity of the transition + # from tight_layout=True -> layout="tight". This seems easier, + # but can be removed when (if) that is simpler on the matplotlib side, + # or if the layout algorithms are improved to handle figure legends. + orig_val = mpl.rcParams["figure.autolayout"] + try: + mpl.rcParams["figure.autolayout"] = False + yield + finally: + mpl.rcParams["figure.autolayout"] = orig_val diff --git a/lib/python3.10/site-packages/seaborn/widgets.py b/lib/python3.10/site-packages/seaborn/widgets.py new file mode 100644 index 0000000000000000000000000000000000000000..502812af57f5fa2c7e8163c33f472b594f506c79 --- /dev/null +++ b/lib/python3.10/site-packages/seaborn/widgets.py @@ -0,0 +1,426 @@ +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import LinearSegmentedColormap + +try: + from ipywidgets import interact, FloatSlider, IntSlider +except ImportError: + def interact(f): + msg = "Interactive palettes require `ipywidgets`, which is not installed." + raise ImportError(msg) + +from .miscplot import palplot +from .palettes import (color_palette, dark_palette, light_palette, + diverging_palette, cubehelix_palette) + + +__all__ = ["choose_colorbrewer_palette", "choose_cubehelix_palette", + "choose_dark_palette", "choose_light_palette", + "choose_diverging_palette"] + + +def _init_mutable_colormap(): + """Create a matplotlib colormap that will be updated by the widgets.""" + greys = color_palette("Greys", 256) + cmap = LinearSegmentedColormap.from_list("interactive", greys) + cmap._init() + cmap._set_extremes() + return cmap + + +def _update_lut(cmap, colors): + """Change the LUT values in a matplotlib colormap in-place.""" + cmap._lut[:256] = colors + cmap._set_extremes() + + +def _show_cmap(cmap): + """Show a continuous matplotlib colormap.""" + from .rcmod import axes_style # Avoid circular import + with axes_style("white"): + f, ax = plt.subplots(figsize=(8.25, .75)) + ax.set(xticks=[], yticks=[]) + x = np.linspace(0, 1, 256)[np.newaxis, :] + ax.pcolormesh(x, cmap=cmap) + + +def choose_colorbrewer_palette(data_type, as_cmap=False): + """Select a palette from the ColorBrewer set. + + These palettes are built into matplotlib and can be used by name in + many seaborn functions, or by passing the object returned by this function. + + Parameters + ---------- + data_type : {'sequential', 'diverging', 'qualitative'} + This describes the kind of data you want to visualize. See the seaborn + color palette docs for more information about how to choose this value. + Note that you can pass substrings (e.g. 'q' for 'qualitative. + + as_cmap : bool + If True, the return value is a matplotlib colormap rather than a + list of discrete colors. + + Returns + ------- + pal or cmap : list of colors or matplotlib colormap + Object that can be passed to plotting functions. + + See Also + -------- + dark_palette : Create a sequential palette with dark low values. + light_palette : Create a sequential palette with bright low values. + diverging_palette : Create a diverging palette from selected colors. + cubehelix_palette : Create a sequential palette or colormap using the + cubehelix system. + + + """ + if data_type.startswith("q") and as_cmap: + raise ValueError("Qualitative palettes cannot be colormaps.") + + pal = [] + if as_cmap: + cmap = _init_mutable_colormap() + + if data_type.startswith("s"): + opts = ["Greys", "Reds", "Greens", "Blues", "Oranges", "Purples", + "BuGn", "BuPu", "GnBu", "OrRd", "PuBu", "PuRd", "RdPu", "YlGn", + "PuBuGn", "YlGnBu", "YlOrBr", "YlOrRd"] + variants = ["regular", "reverse", "dark"] + + @interact + def choose_sequential(name=opts, n=(2, 18), + desat=FloatSlider(min=0, max=1, value=1), + variant=variants): + if variant == "reverse": + name += "_r" + elif variant == "dark": + name += "_d" + + if as_cmap: + colors = color_palette(name, 256, desat) + _update_lut(cmap, np.c_[colors, np.ones(256)]) + _show_cmap(cmap) + else: + pal[:] = color_palette(name, n, desat) + palplot(pal) + + elif data_type.startswith("d"): + opts = ["RdBu", "RdGy", "PRGn", "PiYG", "BrBG", + "RdYlBu", "RdYlGn", "Spectral"] + variants = ["regular", "reverse"] + + @interact + def choose_diverging(name=opts, n=(2, 16), + desat=FloatSlider(min=0, max=1, value=1), + variant=variants): + if variant == "reverse": + name += "_r" + if as_cmap: + colors = color_palette(name, 256, desat) + _update_lut(cmap, np.c_[colors, np.ones(256)]) + _show_cmap(cmap) + else: + pal[:] = color_palette(name, n, desat) + palplot(pal) + + elif data_type.startswith("q"): + opts = ["Set1", "Set2", "Set3", "Paired", "Accent", + "Pastel1", "Pastel2", "Dark2"] + + @interact + def choose_qualitative(name=opts, n=(2, 16), + desat=FloatSlider(min=0, max=1, value=1)): + pal[:] = color_palette(name, n, desat) + palplot(pal) + + if as_cmap: + return cmap + return pal + + +def choose_dark_palette(input="husl", as_cmap=False): + """Launch an interactive widget to create a dark sequential palette. + + This corresponds with the :func:`dark_palette` function. This kind + of palette is good for data that range between relatively uninteresting + low values and interesting high values. + + Requires IPython 2+ and must be used in the notebook. + + Parameters + ---------- + input : {'husl', 'hls', 'rgb'} + Color space for defining the seed value. Note that the default is + different than the default input for :func:`dark_palette`. + as_cmap : bool + If True, the return value is a matplotlib colormap rather than a + list of discrete colors. + + Returns + ------- + pal or cmap : list of colors or matplotlib colormap + Object that can be passed to plotting functions. + + See Also + -------- + dark_palette : Create a sequential palette with dark low values. + light_palette : Create a sequential palette with bright low values. + cubehelix_palette : Create a sequential palette or colormap using the + cubehelix system. + + """ + pal = [] + if as_cmap: + cmap = _init_mutable_colormap() + + if input == "rgb": + @interact + def choose_dark_palette_rgb(r=(0., 1.), + g=(0., 1.), + b=(0., 1.), + n=(3, 17)): + color = r, g, b + if as_cmap: + colors = dark_palette(color, 256, input="rgb") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = dark_palette(color, n, input="rgb") + palplot(pal) + + elif input == "hls": + @interact + def choose_dark_palette_hls(h=(0., 1.), + l=(0., 1.), # noqa: E741 + s=(0., 1.), + n=(3, 17)): + color = h, l, s + if as_cmap: + colors = dark_palette(color, 256, input="hls") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = dark_palette(color, n, input="hls") + palplot(pal) + + elif input == "husl": + @interact + def choose_dark_palette_husl(h=(0, 359), + s=(0, 99), + l=(0, 99), # noqa: E741 + n=(3, 17)): + color = h, s, l + if as_cmap: + colors = dark_palette(color, 256, input="husl") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = dark_palette(color, n, input="husl") + palplot(pal) + + if as_cmap: + return cmap + return pal + + +def choose_light_palette(input="husl", as_cmap=False): + """Launch an interactive widget to create a light sequential palette. + + This corresponds with the :func:`light_palette` function. This kind + of palette is good for data that range between relatively uninteresting + low values and interesting high values. + + Requires IPython 2+ and must be used in the notebook. + + Parameters + ---------- + input : {'husl', 'hls', 'rgb'} + Color space for defining the seed value. Note that the default is + different than the default input for :func:`light_palette`. + as_cmap : bool + If True, the return value is a matplotlib colormap rather than a + list of discrete colors. + + Returns + ------- + pal or cmap : list of colors or matplotlib colormap + Object that can be passed to plotting functions. + + See Also + -------- + light_palette : Create a sequential palette with bright low values. + dark_palette : Create a sequential palette with dark low values. + cubehelix_palette : Create a sequential palette or colormap using the + cubehelix system. + + """ + pal = [] + if as_cmap: + cmap = _init_mutable_colormap() + + if input == "rgb": + @interact + def choose_light_palette_rgb(r=(0., 1.), + g=(0., 1.), + b=(0., 1.), + n=(3, 17)): + color = r, g, b + if as_cmap: + colors = light_palette(color, 256, input="rgb") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = light_palette(color, n, input="rgb") + palplot(pal) + + elif input == "hls": + @interact + def choose_light_palette_hls(h=(0., 1.), + l=(0., 1.), # noqa: E741 + s=(0., 1.), + n=(3, 17)): + color = h, l, s + if as_cmap: + colors = light_palette(color, 256, input="hls") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = light_palette(color, n, input="hls") + palplot(pal) + + elif input == "husl": + @interact + def choose_light_palette_husl(h=(0, 359), + s=(0, 99), + l=(0, 99), # noqa: E741 + n=(3, 17)): + color = h, s, l + if as_cmap: + colors = light_palette(color, 256, input="husl") + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = light_palette(color, n, input="husl") + palplot(pal) + + if as_cmap: + return cmap + return pal + + +def choose_diverging_palette(as_cmap=False): + """Launch an interactive widget to choose a diverging color palette. + + This corresponds with the :func:`diverging_palette` function. This kind + of palette is good for data that range between interesting low values + and interesting high values with a meaningful midpoint. (For example, + change scores relative to some baseline value). + + Requires IPython 2+ and must be used in the notebook. + + Parameters + ---------- + as_cmap : bool + If True, the return value is a matplotlib colormap rather than a + list of discrete colors. + + Returns + ------- + pal or cmap : list of colors or matplotlib colormap + Object that can be passed to plotting functions. + + See Also + -------- + diverging_palette : Create a diverging color palette or colormap. + choose_colorbrewer_palette : Interactively choose palettes from the + colorbrewer set, including diverging palettes. + + """ + pal = [] + if as_cmap: + cmap = _init_mutable_colormap() + + @interact + def choose_diverging_palette( + h_neg=IntSlider(min=0, + max=359, + value=220), + h_pos=IntSlider(min=0, + max=359, + value=10), + s=IntSlider(min=0, max=99, value=74), + l=IntSlider(min=0, max=99, value=50), # noqa: E741 + sep=IntSlider(min=1, max=50, value=10), + n=(2, 16), + center=["light", "dark"] + ): + if as_cmap: + colors = diverging_palette(h_neg, h_pos, s, l, sep, 256, center) + _update_lut(cmap, colors) + _show_cmap(cmap) + else: + pal[:] = diverging_palette(h_neg, h_pos, s, l, sep, n, center) + palplot(pal) + + if as_cmap: + return cmap + return pal + + +def choose_cubehelix_palette(as_cmap=False): + """Launch an interactive widget to create a sequential cubehelix palette. + + This corresponds with the :func:`cubehelix_palette` function. This kind + of palette is good for data that range between relatively uninteresting + low values and interesting high values. The cubehelix system allows the + palette to have more hue variance across the range, which can be helpful + for distinguishing a wider range of values. + + Requires IPython 2+ and must be used in the notebook. + + Parameters + ---------- + as_cmap : bool + If True, the return value is a matplotlib colormap rather than a + list of discrete colors. + + Returns + ------- + pal or cmap : list of colors or matplotlib colormap + Object that can be passed to plotting functions. + + See Also + -------- + cubehelix_palette : Create a sequential palette or colormap using the + cubehelix system. + + """ + pal = [] + if as_cmap: + cmap = _init_mutable_colormap() + + @interact + def choose_cubehelix(n_colors=IntSlider(min=2, max=16, value=9), + start=FloatSlider(min=0, max=3, value=0), + rot=FloatSlider(min=-1, max=1, value=.4), + gamma=FloatSlider(min=0, max=5, value=1), + hue=FloatSlider(min=0, max=1, value=.8), + light=FloatSlider(min=0, max=1, value=.85), + dark=FloatSlider(min=0, max=1, value=.15), + reverse=False): + + if as_cmap: + colors = cubehelix_palette(256, start, rot, gamma, + hue, light, dark, reverse) + _update_lut(cmap, np.c_[colors, np.ones(256)]) + _show_cmap(cmap) + else: + pal[:] = cubehelix_palette(n_colors, start, rot, gamma, + hue, light, dark, reverse) + palplot(pal) + + if as_cmap: + return cmap + return pal diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/PKG-INFO b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/PKG-INFO new file mode 100644 index 0000000000000000000000000000000000000000..aac6c29862fd359ebb21a092c5d10934fb917898 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/PKG-INFO @@ -0,0 +1,140 @@ +Metadata-Version: 2.4 +Name: setuptools +Version: 78.1.1 +Summary: Easily download, build, install, upgrade, and uninstall Python packages +Author-email: Python Packaging Authority +Project-URL: Source, https://github.com/pypa/setuptools +Project-URL: Documentation, https://setuptools.pypa.io/ +Project-URL: Changelog, https://setuptools.pypa.io/en/stable/history.html +Keywords: CPAN PyPI distutils eggs package management +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: test +Requires-Dist: pytest!=8.1.*,>=6; extra == "test" +Requires-Dist: virtualenv>=13.0.0; extra == "test" +Requires-Dist: wheel>=0.44.0; extra == "test" +Requires-Dist: pip>=19.1; extra == "test" +Requires-Dist: packaging>=24.2; extra == "test" +Requires-Dist: jaraco.envs>=2.2; extra == "test" +Requires-Dist: pytest-xdist>=3; extra == "test" +Requires-Dist: jaraco.path>=3.7.2; extra == "test" +Requires-Dist: build[virtualenv]>=1.0.3; extra == "test" +Requires-Dist: filelock>=3.4.0; extra == "test" +Requires-Dist: ini2toml[lite]>=0.14; extra == "test" +Requires-Dist: tomli-w>=1.0.0; extra == "test" +Requires-Dist: pytest-timeout; extra == "test" +Requires-Dist: pytest-perf; sys_platform != "cygwin" and extra == "test" +Requires-Dist: jaraco.develop>=7.21; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test" +Requires-Dist: pytest-home>=0.5; extra == "test" +Requires-Dist: pytest-subprocess; extra == "test" +Requires-Dist: pyproject-hooks!=1.1; extra == "test" +Requires-Dist: jaraco.test>=5.5; extra == "test" +Provides-Extra: doc +Requires-Dist: sphinx>=3.5; extra == "doc" +Requires-Dist: jaraco.packaging>=9.3; extra == "doc" +Requires-Dist: rst.linker>=1.9; extra == "doc" +Requires-Dist: furo; extra == "doc" +Requires-Dist: sphinx-lint; extra == "doc" +Requires-Dist: jaraco.tidelift>=1.4; extra == "doc" +Requires-Dist: pygments-github-lexers==0.0.5; extra == "doc" +Requires-Dist: sphinx-favicon; extra == "doc" +Requires-Dist: sphinx-inline-tabs; extra == "doc" +Requires-Dist: sphinx-reredirects; extra == "doc" +Requires-Dist: sphinxcontrib-towncrier; extra == "doc" +Requires-Dist: sphinx-notfound-page<2,>=1; extra == "doc" +Requires-Dist: pyproject-hooks!=1.1; extra == "doc" +Requires-Dist: towncrier<24.7; extra == "doc" +Provides-Extra: ssl +Provides-Extra: certs +Provides-Extra: core +Requires-Dist: packaging>=24.2; extra == "core" +Requires-Dist: more_itertools>=8.8; extra == "core" +Requires-Dist: jaraco.text>=3.7; extra == "core" +Requires-Dist: importlib_metadata>=6; python_version < "3.10" and extra == "core" +Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "core" +Requires-Dist: wheel>=0.43.0; extra == "core" +Requires-Dist: platformdirs>=4.2.2; extra == "core" +Requires-Dist: jaraco.functools>=4; extra == "core" +Requires-Dist: more_itertools; extra == "core" +Provides-Extra: check +Requires-Dist: pytest-checkdocs>=2.4; extra == "check" +Requires-Dist: pytest-ruff>=0.2.1; sys_platform != "cygwin" and extra == "check" +Requires-Dist: ruff>=0.8.0; sys_platform != "cygwin" and extra == "check" +Provides-Extra: cover +Requires-Dist: pytest-cov; extra == "cover" +Provides-Extra: enabler +Requires-Dist: pytest-enabler>=2.2; extra == "enabler" +Provides-Extra: type +Requires-Dist: pytest-mypy; extra == "type" +Requires-Dist: mypy==1.14.*; extra == "type" +Requires-Dist: importlib_metadata>=7.0.2; python_version < "3.10" and extra == "type" +Requires-Dist: jaraco.develop>=7.21; sys_platform != "cygwin" and extra == "type" +Dynamic: license-file + +.. |pypi-version| image:: https://img.shields.io/pypi/v/setuptools.svg + :target: https://pypi.org/project/setuptools + +.. |py-version| image:: https://img.shields.io/pypi/pyversions/setuptools.svg + +.. |test-badge| image:: https://github.com/pypa/setuptools/actions/workflows/main.yml/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. |ruff-badge| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. |docs-badge| image:: https://img.shields.io/readthedocs/setuptools/latest.svg + :target: https://setuptools.pypa.io + +.. |skeleton-badge| image:: https://img.shields.io/badge/skeleton-2025-informational + :target: https://blog.jaraco.com/skeleton + +.. |codecov-badge| image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white + :target: https://codecov.io/gh/pypa/setuptools + +.. |tidelift-badge| image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme + +.. |discord-badge| image:: https://img.shields.io/discord/803025117553754132 + :target: https://discord.com/channels/803025117553754132/815945031150993468 + :alt: Discord + +|pypi-version| |py-version| |test-badge| |ruff-badge| |docs-badge| |skeleton-badge| |codecov-badge| |discord-badge| + +See the `Quickstart `_ +and the `User's Guide `_ for +instructions on how to use Setuptools. + +Questions and comments should be directed to `GitHub Discussions +`_. +Bug reports and especially tested patches may be +submitted directly to the `bug tracker +`_. + + +Code of Conduct +=============== + +Everyone interacting in the setuptools project's codebases, issue trackers, +chat rooms, and fora is expected to follow the +`PSF Code of Conduct `_. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +Setuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/SOURCES.txt b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/SOURCES.txt new file mode 100644 index 0000000000000000000000000000000000000000..5b103154f708b35c78f72b04cd387616bb122021 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/SOURCES.txt @@ -0,0 +1,582 @@ +LICENSE +MANIFEST.in +NEWS.rst +README.rst +conftest.py +exercises.py +launcher.c +mypy.ini +pyproject.toml +pytest.ini +setup.cfg +setup.py +tox.ini +_distutils_hack/__init__.py +_distutils_hack/override.py +docs/artwork.rst +docs/build_meta.rst +docs/conf.py +docs/history.rst +docs/index.rst +docs/pkg_resources.rst +docs/python 2 sunset.rst +docs/roadmap.rst +docs/setuptools.rst +docs/deprecated/changed_keywords.rst +docs/deprecated/commands.rst +docs/deprecated/dependency_links.rst +docs/deprecated/distutils-legacy.rst +docs/deprecated/easy_install.rst +docs/deprecated/functionalities.rst +docs/deprecated/index.rst +docs/deprecated/python_eggs.rst +docs/deprecated/resource_extraction.rst +docs/deprecated/zip_safe.rst +docs/deprecated/distutils/_setuptools_disclaimer.rst +docs/deprecated/distutils/apiref.rst +docs/deprecated/distutils/builtdist.rst +docs/deprecated/distutils/commandref.rst +docs/deprecated/distutils/configfile.rst +docs/deprecated/distutils/examples.rst +docs/deprecated/distutils/extending.rst +docs/deprecated/distutils/index.rst +docs/deprecated/distutils/introduction.rst +docs/deprecated/distutils/packageindex.rst +docs/deprecated/distutils/setupscript.rst +docs/deprecated/distutils/sourcedist.rst +docs/deprecated/distutils/uploading.rst +docs/development/developer-guide.rst +docs/development/index.rst +docs/development/releases.rst +docs/references/keywords.rst +docs/userguide/datafiles.rst +docs/userguide/declarative_config.rst +docs/userguide/dependency_management.rst +docs/userguide/development_mode.rst +docs/userguide/distribution.rst +docs/userguide/entry_point.rst +docs/userguide/ext_modules.rst +docs/userguide/extension.rst +docs/userguide/index.rst +docs/userguide/miscellaneous.rst +docs/userguide/package_discovery.rst +docs/userguide/pyproject_config.rst +docs/userguide/quickstart.rst +newsfragments/.gitignore +newsfragments/README.rst +pkg_resources/__init__.py +pkg_resources/api_tests.txt +pkg_resources/py.typed +pkg_resources/tests/__init__.py +pkg_resources/tests/test_find_distributions.py +pkg_resources/tests/test_integration_zope_interface.py +pkg_resources/tests/test_markers.py +pkg_resources/tests/test_pkg_resources.py +pkg_resources/tests/test_resources.py +pkg_resources/tests/test_working_set.py +pkg_resources/tests/data/my-test-package-source/setup.cfg +pkg_resources/tests/data/my-test-package-source/setup.py +pkg_resources/tests/data/my-test-package-zip/my-test-package.zip +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/PKG-INFO +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/SOURCES.txt +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/dependency_links.txt +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/top_level.txt +pkg_resources/tests/data/my-test-package_unpacked-egg/my_test_package-1.0-py3.7.egg/EGG-INFO/zip-safe +pkg_resources/tests/data/my-test-package_zipped-egg/my_test_package-1.0-py3.7.egg +setuptools/__init__.py +setuptools/_core_metadata.py +setuptools/_entry_points.py +setuptools/_imp.py +setuptools/_importlib.py +setuptools/_itertools.py +setuptools/_normalization.py +setuptools/_path.py +setuptools/_reqs.py +setuptools/_shutil.py +setuptools/_static.py +setuptools/archive_util.py +setuptools/build_meta.py +setuptools/cli-32.exe +setuptools/cli-64.exe +setuptools/cli-arm64.exe +setuptools/cli.exe +setuptools/depends.py +setuptools/discovery.py +setuptools/dist.py +setuptools/errors.py +setuptools/extension.py +setuptools/glob.py +setuptools/gui-32.exe +setuptools/gui-64.exe +setuptools/gui-arm64.exe +setuptools/gui.exe +setuptools/installer.py +setuptools/launch.py +setuptools/logging.py +setuptools/modified.py +setuptools/monkey.py +setuptools/msvc.py +setuptools/namespaces.py +setuptools/package_index.py +setuptools/sandbox.py +setuptools/script (dev).tmpl +setuptools/script.tmpl +setuptools/unicode_utils.py +setuptools/version.py +setuptools/warnings.py +setuptools/wheel.py +setuptools/windows_support.py +setuptools.egg-info/PKG-INFO +setuptools.egg-info/SOURCES.txt +setuptools.egg-info/dependency_links.txt +setuptools.egg-info/entry_points.txt +setuptools.egg-info/requires.txt +setuptools.egg-info/top_level.txt +setuptools/_distutils/__init__.py +setuptools/_distutils/_log.py +setuptools/_distutils/_macos_compat.py +setuptools/_distutils/_modified.py +setuptools/_distutils/_msvccompiler.py +setuptools/_distutils/archive_util.py +setuptools/_distutils/ccompiler.py +setuptools/_distutils/cmd.py +setuptools/_distutils/core.py +setuptools/_distutils/cygwinccompiler.py +setuptools/_distutils/debug.py +setuptools/_distutils/dep_util.py +setuptools/_distutils/dir_util.py +setuptools/_distutils/dist.py +setuptools/_distutils/errors.py +setuptools/_distutils/extension.py +setuptools/_distutils/fancy_getopt.py +setuptools/_distutils/file_util.py +setuptools/_distutils/filelist.py +setuptools/_distutils/log.py +setuptools/_distutils/spawn.py +setuptools/_distutils/sysconfig.py +setuptools/_distutils/text_file.py +setuptools/_distutils/unixccompiler.py +setuptools/_distutils/util.py +setuptools/_distutils/version.py +setuptools/_distutils/versionpredicate.py +setuptools/_distutils/zosccompiler.py +setuptools/_distutils/command/__init__.py +setuptools/_distutils/command/_framework_compat.py +setuptools/_distutils/command/bdist.py +setuptools/_distutils/command/bdist_dumb.py +setuptools/_distutils/command/bdist_rpm.py +setuptools/_distutils/command/build.py +setuptools/_distutils/command/build_clib.py +setuptools/_distutils/command/build_ext.py +setuptools/_distutils/command/build_py.py +setuptools/_distutils/command/build_scripts.py +setuptools/_distutils/command/check.py +setuptools/_distutils/command/clean.py +setuptools/_distutils/command/config.py +setuptools/_distutils/command/install.py +setuptools/_distutils/command/install_data.py +setuptools/_distutils/command/install_egg_info.py +setuptools/_distutils/command/install_headers.py +setuptools/_distutils/command/install_lib.py +setuptools/_distutils/command/install_scripts.py +setuptools/_distutils/command/sdist.py +setuptools/_distutils/compat/__init__.py +setuptools/_distutils/compat/numpy.py +setuptools/_distutils/compat/py39.py +setuptools/_distutils/compilers/C/base.py +setuptools/_distutils/compilers/C/cygwin.py +setuptools/_distutils/compilers/C/errors.py +setuptools/_distutils/compilers/C/msvc.py +setuptools/_distutils/compilers/C/unix.py +setuptools/_distutils/compilers/C/zos.py +setuptools/_distutils/compilers/C/tests/test_base.py +setuptools/_distutils/compilers/C/tests/test_cygwin.py +setuptools/_distutils/compilers/C/tests/test_mingw.py +setuptools/_distutils/compilers/C/tests/test_msvc.py +setuptools/_distutils/compilers/C/tests/test_unix.py +setuptools/_distutils/tests/__init__.py +setuptools/_distutils/tests/support.py +setuptools/_distutils/tests/test_archive_util.py +setuptools/_distutils/tests/test_bdist.py +setuptools/_distutils/tests/test_bdist_dumb.py +setuptools/_distutils/tests/test_bdist_rpm.py +setuptools/_distutils/tests/test_build.py +setuptools/_distutils/tests/test_build_clib.py +setuptools/_distutils/tests/test_build_ext.py +setuptools/_distutils/tests/test_build_py.py +setuptools/_distutils/tests/test_build_scripts.py +setuptools/_distutils/tests/test_check.py +setuptools/_distutils/tests/test_clean.py +setuptools/_distutils/tests/test_cmd.py +setuptools/_distutils/tests/test_config_cmd.py +setuptools/_distutils/tests/test_core.py +setuptools/_distutils/tests/test_dir_util.py +setuptools/_distutils/tests/test_dist.py +setuptools/_distutils/tests/test_extension.py +setuptools/_distutils/tests/test_file_util.py +setuptools/_distutils/tests/test_filelist.py +setuptools/_distutils/tests/test_install.py +setuptools/_distutils/tests/test_install_data.py +setuptools/_distutils/tests/test_install_headers.py +setuptools/_distutils/tests/test_install_lib.py +setuptools/_distutils/tests/test_install_scripts.py +setuptools/_distutils/tests/test_log.py +setuptools/_distutils/tests/test_modified.py +setuptools/_distutils/tests/test_sdist.py +setuptools/_distutils/tests/test_spawn.py +setuptools/_distutils/tests/test_sysconfig.py +setuptools/_distutils/tests/test_text_file.py +setuptools/_distutils/tests/test_util.py +setuptools/_distutils/tests/test_version.py +setuptools/_distutils/tests/test_versionpredicate.py +setuptools/_distutils/tests/unix_compat.py +setuptools/_distutils/tests/compat/__init__.py +setuptools/_distutils/tests/compat/py39.py +setuptools/_vendor/ruff.toml +setuptools/_vendor/typing_extensions.py +setuptools/_vendor/autocommand/__init__.py +setuptools/_vendor/autocommand/autoasync.py +setuptools/_vendor/autocommand/autocommand.py +setuptools/_vendor/autocommand/automain.py +setuptools/_vendor/autocommand/autoparse.py +setuptools/_vendor/autocommand/errors.py +setuptools/_vendor/autocommand-2.2.2.dist-info/INSTALLER +setuptools/_vendor/autocommand-2.2.2.dist-info/LICENSE +setuptools/_vendor/autocommand-2.2.2.dist-info/METADATA +setuptools/_vendor/autocommand-2.2.2.dist-info/RECORD +setuptools/_vendor/autocommand-2.2.2.dist-info/WHEEL +setuptools/_vendor/autocommand-2.2.2.dist-info/top_level.txt +setuptools/_vendor/backports/__init__.py +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/INSTALLER +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/LICENSE +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/METADATA +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/RECORD +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/REQUESTED +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/WHEEL +setuptools/_vendor/backports.tarfile-1.2.0.dist-info/top_level.txt +setuptools/_vendor/backports/tarfile/__init__.py +setuptools/_vendor/backports/tarfile/__main__.py +setuptools/_vendor/backports/tarfile/compat/__init__.py +setuptools/_vendor/backports/tarfile/compat/py38.py +setuptools/_vendor/importlib_metadata/__init__.py +setuptools/_vendor/importlib_metadata/_adapters.py +setuptools/_vendor/importlib_metadata/_collections.py +setuptools/_vendor/importlib_metadata/_compat.py +setuptools/_vendor/importlib_metadata/_functools.py +setuptools/_vendor/importlib_metadata/_itertools.py +setuptools/_vendor/importlib_metadata/_meta.py +setuptools/_vendor/importlib_metadata/_text.py +setuptools/_vendor/importlib_metadata/diagnose.py +setuptools/_vendor/importlib_metadata/py.typed +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/INSTALLER +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/LICENSE +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/METADATA +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/RECORD +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/REQUESTED +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/WHEEL +setuptools/_vendor/importlib_metadata-8.0.0.dist-info/top_level.txt +setuptools/_vendor/importlib_metadata/compat/__init__.py +setuptools/_vendor/importlib_metadata/compat/py311.py +setuptools/_vendor/importlib_metadata/compat/py39.py +setuptools/_vendor/inflect/__init__.py +setuptools/_vendor/inflect/py.typed +setuptools/_vendor/inflect-7.3.1.dist-info/INSTALLER +setuptools/_vendor/inflect-7.3.1.dist-info/LICENSE +setuptools/_vendor/inflect-7.3.1.dist-info/METADATA +setuptools/_vendor/inflect-7.3.1.dist-info/RECORD +setuptools/_vendor/inflect-7.3.1.dist-info/WHEEL +setuptools/_vendor/inflect-7.3.1.dist-info/top_level.txt +setuptools/_vendor/inflect/compat/__init__.py +setuptools/_vendor/inflect/compat/py38.py +setuptools/_vendor/jaraco/context.py +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/INSTALLER +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/LICENSE +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/METADATA +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/RECORD +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/REQUESTED +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/WHEEL +setuptools/_vendor/jaraco.collections-5.1.0.dist-info/top_level.txt +setuptools/_vendor/jaraco.context-5.3.0.dist-info/INSTALLER +setuptools/_vendor/jaraco.context-5.3.0.dist-info/LICENSE +setuptools/_vendor/jaraco.context-5.3.0.dist-info/METADATA +setuptools/_vendor/jaraco.context-5.3.0.dist-info/RECORD +setuptools/_vendor/jaraco.context-5.3.0.dist-info/WHEEL +setuptools/_vendor/jaraco.context-5.3.0.dist-info/top_level.txt +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/INSTALLER +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/LICENSE +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/METADATA +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/RECORD +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/WHEEL +setuptools/_vendor/jaraco.functools-4.0.1.dist-info/top_level.txt +setuptools/_vendor/jaraco.text-3.12.1.dist-info/INSTALLER +setuptools/_vendor/jaraco.text-3.12.1.dist-info/LICENSE +setuptools/_vendor/jaraco.text-3.12.1.dist-info/METADATA +setuptools/_vendor/jaraco.text-3.12.1.dist-info/RECORD +setuptools/_vendor/jaraco.text-3.12.1.dist-info/REQUESTED +setuptools/_vendor/jaraco.text-3.12.1.dist-info/WHEEL +setuptools/_vendor/jaraco.text-3.12.1.dist-info/top_level.txt +setuptools/_vendor/jaraco/collections/__init__.py +setuptools/_vendor/jaraco/collections/py.typed +setuptools/_vendor/jaraco/functools/__init__.py +setuptools/_vendor/jaraco/functools/__init__.pyi +setuptools/_vendor/jaraco/functools/py.typed +setuptools/_vendor/jaraco/text/Lorem ipsum.txt +setuptools/_vendor/jaraco/text/__init__.py +setuptools/_vendor/jaraco/text/layouts.py +setuptools/_vendor/jaraco/text/show-newlines.py +setuptools/_vendor/jaraco/text/strip-prefix.py +setuptools/_vendor/jaraco/text/to-dvorak.py +setuptools/_vendor/jaraco/text/to-qwerty.py +setuptools/_vendor/more_itertools/__init__.py +setuptools/_vendor/more_itertools/__init__.pyi +setuptools/_vendor/more_itertools/more.py +setuptools/_vendor/more_itertools/more.pyi +setuptools/_vendor/more_itertools/py.typed +setuptools/_vendor/more_itertools/recipes.py +setuptools/_vendor/more_itertools/recipes.pyi +setuptools/_vendor/more_itertools-10.3.0.dist-info/INSTALLER +setuptools/_vendor/more_itertools-10.3.0.dist-info/LICENSE +setuptools/_vendor/more_itertools-10.3.0.dist-info/METADATA +setuptools/_vendor/more_itertools-10.3.0.dist-info/RECORD +setuptools/_vendor/more_itertools-10.3.0.dist-info/REQUESTED +setuptools/_vendor/more_itertools-10.3.0.dist-info/WHEEL +setuptools/_vendor/packaging/__init__.py +setuptools/_vendor/packaging/_elffile.py +setuptools/_vendor/packaging/_manylinux.py +setuptools/_vendor/packaging/_musllinux.py +setuptools/_vendor/packaging/_parser.py +setuptools/_vendor/packaging/_structures.py +setuptools/_vendor/packaging/_tokenizer.py +setuptools/_vendor/packaging/markers.py +setuptools/_vendor/packaging/metadata.py +setuptools/_vendor/packaging/py.typed +setuptools/_vendor/packaging/requirements.py +setuptools/_vendor/packaging/specifiers.py +setuptools/_vendor/packaging/tags.py +setuptools/_vendor/packaging/utils.py +setuptools/_vendor/packaging/version.py +setuptools/_vendor/packaging-24.2.dist-info/INSTALLER +setuptools/_vendor/packaging-24.2.dist-info/LICENSE +setuptools/_vendor/packaging-24.2.dist-info/LICENSE.APACHE +setuptools/_vendor/packaging-24.2.dist-info/LICENSE.BSD +setuptools/_vendor/packaging-24.2.dist-info/METADATA +setuptools/_vendor/packaging-24.2.dist-info/RECORD +setuptools/_vendor/packaging-24.2.dist-info/REQUESTED +setuptools/_vendor/packaging-24.2.dist-info/WHEEL +setuptools/_vendor/packaging/licenses/__init__.py +setuptools/_vendor/packaging/licenses/_spdx.py +setuptools/_vendor/platformdirs/__init__.py +setuptools/_vendor/platformdirs/__main__.py +setuptools/_vendor/platformdirs/android.py +setuptools/_vendor/platformdirs/api.py +setuptools/_vendor/platformdirs/macos.py +setuptools/_vendor/platformdirs/py.typed +setuptools/_vendor/platformdirs/unix.py +setuptools/_vendor/platformdirs/version.py +setuptools/_vendor/platformdirs/windows.py +setuptools/_vendor/platformdirs-4.2.2.dist-info/INSTALLER +setuptools/_vendor/platformdirs-4.2.2.dist-info/METADATA +setuptools/_vendor/platformdirs-4.2.2.dist-info/RECORD +setuptools/_vendor/platformdirs-4.2.2.dist-info/REQUESTED +setuptools/_vendor/platformdirs-4.2.2.dist-info/WHEEL +setuptools/_vendor/platformdirs-4.2.2.dist-info/licenses/LICENSE +setuptools/_vendor/tomli/__init__.py +setuptools/_vendor/tomli/_parser.py +setuptools/_vendor/tomli/_re.py +setuptools/_vendor/tomli/_types.py +setuptools/_vendor/tomli/py.typed +setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER +setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE +setuptools/_vendor/tomli-2.0.1.dist-info/METADATA +setuptools/_vendor/tomli-2.0.1.dist-info/RECORD +setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED +setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL +setuptools/_vendor/typeguard/__init__.py +setuptools/_vendor/typeguard/_checkers.py +setuptools/_vendor/typeguard/_config.py +setuptools/_vendor/typeguard/_decorators.py +setuptools/_vendor/typeguard/_exceptions.py +setuptools/_vendor/typeguard/_functions.py +setuptools/_vendor/typeguard/_importhook.py +setuptools/_vendor/typeguard/_memo.py +setuptools/_vendor/typeguard/_pytest_plugin.py +setuptools/_vendor/typeguard/_suppression.py +setuptools/_vendor/typeguard/_transformer.py +setuptools/_vendor/typeguard/_union_transformer.py +setuptools/_vendor/typeguard/_utils.py +setuptools/_vendor/typeguard/py.typed +setuptools/_vendor/typeguard-4.3.0.dist-info/INSTALLER +setuptools/_vendor/typeguard-4.3.0.dist-info/LICENSE +setuptools/_vendor/typeguard-4.3.0.dist-info/METADATA +setuptools/_vendor/typeguard-4.3.0.dist-info/RECORD +setuptools/_vendor/typeguard-4.3.0.dist-info/WHEEL +setuptools/_vendor/typeguard-4.3.0.dist-info/entry_points.txt +setuptools/_vendor/typeguard-4.3.0.dist-info/top_level.txt +setuptools/_vendor/typing_extensions-4.12.2.dist-info/INSTALLER +setuptools/_vendor/typing_extensions-4.12.2.dist-info/LICENSE +setuptools/_vendor/typing_extensions-4.12.2.dist-info/METADATA +setuptools/_vendor/typing_extensions-4.12.2.dist-info/RECORD +setuptools/_vendor/typing_extensions-4.12.2.dist-info/WHEEL +setuptools/_vendor/wheel/__init__.py +setuptools/_vendor/wheel/__main__.py +setuptools/_vendor/wheel/_bdist_wheel.py +setuptools/_vendor/wheel/_setuptools_logging.py +setuptools/_vendor/wheel/bdist_wheel.py +setuptools/_vendor/wheel/macosx_libfile.py +setuptools/_vendor/wheel/metadata.py +setuptools/_vendor/wheel/util.py +setuptools/_vendor/wheel/wheelfile.py +setuptools/_vendor/wheel-0.45.1.dist-info/INSTALLER +setuptools/_vendor/wheel-0.45.1.dist-info/LICENSE.txt +setuptools/_vendor/wheel-0.45.1.dist-info/METADATA +setuptools/_vendor/wheel-0.45.1.dist-info/RECORD +setuptools/_vendor/wheel-0.45.1.dist-info/REQUESTED +setuptools/_vendor/wheel-0.45.1.dist-info/WHEEL +setuptools/_vendor/wheel-0.45.1.dist-info/entry_points.txt +setuptools/_vendor/wheel/cli/__init__.py +setuptools/_vendor/wheel/cli/convert.py +setuptools/_vendor/wheel/cli/pack.py +setuptools/_vendor/wheel/cli/tags.py +setuptools/_vendor/wheel/cli/unpack.py +setuptools/_vendor/wheel/vendored/__init__.py +setuptools/_vendor/wheel/vendored/vendor.txt +setuptools/_vendor/wheel/vendored/packaging/LICENSE +setuptools/_vendor/wheel/vendored/packaging/LICENSE.APACHE +setuptools/_vendor/wheel/vendored/packaging/LICENSE.BSD +setuptools/_vendor/wheel/vendored/packaging/__init__.py +setuptools/_vendor/wheel/vendored/packaging/_elffile.py +setuptools/_vendor/wheel/vendored/packaging/_manylinux.py +setuptools/_vendor/wheel/vendored/packaging/_musllinux.py +setuptools/_vendor/wheel/vendored/packaging/_parser.py +setuptools/_vendor/wheel/vendored/packaging/_structures.py +setuptools/_vendor/wheel/vendored/packaging/_tokenizer.py +setuptools/_vendor/wheel/vendored/packaging/markers.py +setuptools/_vendor/wheel/vendored/packaging/requirements.py +setuptools/_vendor/wheel/vendored/packaging/specifiers.py +setuptools/_vendor/wheel/vendored/packaging/tags.py +setuptools/_vendor/wheel/vendored/packaging/utils.py +setuptools/_vendor/wheel/vendored/packaging/version.py +setuptools/_vendor/zipp/__init__.py +setuptools/_vendor/zipp/glob.py +setuptools/_vendor/zipp-3.19.2.dist-info/INSTALLER +setuptools/_vendor/zipp-3.19.2.dist-info/LICENSE +setuptools/_vendor/zipp-3.19.2.dist-info/METADATA +setuptools/_vendor/zipp-3.19.2.dist-info/RECORD +setuptools/_vendor/zipp-3.19.2.dist-info/REQUESTED +setuptools/_vendor/zipp-3.19.2.dist-info/WHEEL +setuptools/_vendor/zipp-3.19.2.dist-info/top_level.txt +setuptools/_vendor/zipp/compat/__init__.py +setuptools/_vendor/zipp/compat/py310.py +setuptools/command/__init__.py +setuptools/command/_requirestxt.py +setuptools/command/alias.py +setuptools/command/bdist_egg.py +setuptools/command/bdist_rpm.py +setuptools/command/bdist_wheel.py +setuptools/command/build.py +setuptools/command/build_clib.py +setuptools/command/build_ext.py +setuptools/command/build_py.py +setuptools/command/develop.py +setuptools/command/dist_info.py +setuptools/command/easy_install.py +setuptools/command/editable_wheel.py +setuptools/command/egg_info.py +setuptools/command/install.py +setuptools/command/install_egg_info.py +setuptools/command/install_lib.py +setuptools/command/install_scripts.py +setuptools/command/launcher manifest.xml +setuptools/command/rotate.py +setuptools/command/saveopts.py +setuptools/command/sdist.py +setuptools/command/setopt.py +setuptools/command/test.py +setuptools/compat/__init__.py +setuptools/compat/py310.py +setuptools/compat/py311.py +setuptools/compat/py312.py +setuptools/compat/py39.py +setuptools/config/NOTICE +setuptools/config/__init__.py +setuptools/config/_apply_pyprojecttoml.py +setuptools/config/distutils.schema.json +setuptools/config/expand.py +setuptools/config/pyprojecttoml.py +setuptools/config/setupcfg.py +setuptools/config/setuptools.schema.json +setuptools/config/_validate_pyproject/NOTICE +setuptools/config/_validate_pyproject/__init__.py +setuptools/config/_validate_pyproject/error_reporting.py +setuptools/config/_validate_pyproject/extra_validations.py +setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py +setuptools/config/_validate_pyproject/fastjsonschema_validations.py +setuptools/config/_validate_pyproject/formats.py +setuptools/tests/__init__.py +setuptools/tests/contexts.py +setuptools/tests/environment.py +setuptools/tests/fixtures.py +setuptools/tests/mod_with_constant.py +setuptools/tests/namespaces.py +setuptools/tests/script-with-bom.py +setuptools/tests/server.py +setuptools/tests/test_archive_util.py +setuptools/tests/test_bdist_deprecations.py +setuptools/tests/test_bdist_egg.py +setuptools/tests/test_bdist_wheel.py +setuptools/tests/test_build.py +setuptools/tests/test_build_clib.py +setuptools/tests/test_build_ext.py +setuptools/tests/test_build_meta.py +setuptools/tests/test_build_py.py +setuptools/tests/test_config_discovery.py +setuptools/tests/test_core_metadata.py +setuptools/tests/test_depends.py +setuptools/tests/test_develop.py +setuptools/tests/test_dist.py +setuptools/tests/test_dist_info.py +setuptools/tests/test_distutils_adoption.py +setuptools/tests/test_easy_install.py +setuptools/tests/test_editable_install.py +setuptools/tests/test_egg_info.py +setuptools/tests/test_extern.py +setuptools/tests/test_find_packages.py +setuptools/tests/test_find_py_modules.py +setuptools/tests/test_glob.py +setuptools/tests/test_install_scripts.py +setuptools/tests/test_logging.py +setuptools/tests/test_manifest.py +setuptools/tests/test_namespaces.py +setuptools/tests/test_packageindex.py +setuptools/tests/test_sandbox.py +setuptools/tests/test_sdist.py +setuptools/tests/test_setopt.py +setuptools/tests/test_setuptools.py +setuptools/tests/test_shutil_wrapper.py +setuptools/tests/test_unicode_utils.py +setuptools/tests/test_virtualenv.py +setuptools/tests/test_warnings.py +setuptools/tests/test_wheel.py +setuptools/tests/test_windows_wrappers.py +setuptools/tests/text.py +setuptools/tests/textwrap.py +setuptools/tests/compat/__init__.py +setuptools/tests/compat/py39.py +setuptools/tests/config/__init__.py +setuptools/tests/config/setupcfg_examples.txt +setuptools/tests/config/test_apply_pyprojecttoml.py +setuptools/tests/config/test_expand.py +setuptools/tests/config/test_pyprojecttoml.py +setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py +setuptools/tests/config/test_setupcfg.py +setuptools/tests/config/downloads/__init__.py +setuptools/tests/config/downloads/preload.py +setuptools/tests/indexes/test_links_priority/external.html +setuptools/tests/indexes/test_links_priority/simple/foobar/index.html +setuptools/tests/integration/__init__.py +setuptools/tests/integration/helpers.py +setuptools/tests/integration/test_pip_install_sdist.py +tools/build_launchers.py +tools/finalize.py +tools/generate_validation_code.py +tools/vendored.py \ No newline at end of file diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/dependency_links.txt b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/dependency_links.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/entry_points.txt b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..0db0a6c8f1b8d9c0ad4a25db6892e29f8988fcf2 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/entry_points.txt @@ -0,0 +1,51 @@ +[distutils.commands] +alias = setuptools.command.alias:alias +bdist_egg = setuptools.command.bdist_egg:bdist_egg +bdist_rpm = setuptools.command.bdist_rpm:bdist_rpm +bdist_wheel = setuptools.command.bdist_wheel:bdist_wheel +build = setuptools.command.build:build +build_clib = setuptools.command.build_clib:build_clib +build_ext = setuptools.command.build_ext:build_ext +build_py = setuptools.command.build_py:build_py +develop = setuptools.command.develop:develop +dist_info = setuptools.command.dist_info:dist_info +easy_install = setuptools.command.easy_install:easy_install +editable_wheel = setuptools.command.editable_wheel:editable_wheel +egg_info = setuptools.command.egg_info:egg_info +install = setuptools.command.install:install +install_egg_info = setuptools.command.install_egg_info:install_egg_info +install_lib = setuptools.command.install_lib:install_lib +install_scripts = setuptools.command.install_scripts:install_scripts +rotate = setuptools.command.rotate:rotate +saveopts = setuptools.command.saveopts:saveopts +sdist = setuptools.command.sdist:sdist +setopt = setuptools.command.setopt:setopt + +[distutils.setup_keywords] +dependency_links = setuptools.dist:assert_string_list +eager_resources = setuptools.dist:assert_string_list +entry_points = setuptools.dist:check_entry_points +exclude_package_data = setuptools.dist:check_package_data +extras_require = setuptools.dist:check_extras +include_package_data = setuptools.dist:assert_bool +install_requires = setuptools.dist:check_requirements +namespace_packages = setuptools.dist:check_nsp +package_data = setuptools.dist:check_package_data +packages = setuptools.dist:check_packages +python_requires = setuptools.dist:check_specifier +setup_requires = setuptools.dist:check_requirements +use_2to3 = setuptools.dist:invalid_unless_false +zip_safe = setuptools.dist:assert_bool + +[egg_info.writers] +PKG-INFO = setuptools.command.egg_info:write_pkg_info +dependency_links.txt = setuptools.command.egg_info:overwrite_arg +eager_resources.txt = setuptools.command.egg_info:overwrite_arg +entry_points.txt = setuptools.command.egg_info:write_entries +namespace_packages.txt = setuptools.command.egg_info:overwrite_arg +requires.txt = setuptools.command.egg_info:write_requirements +top_level.txt = setuptools.command.egg_info:write_toplevel_names + +[setuptools.finalize_distribution_options] +keywords = setuptools.dist:Distribution._finalize_setup_keywords +parent_finalize = setuptools.dist:_Distribution.finalize_options diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/requires.txt b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/requires.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc4015ad09eda715d3bafd8dda0968f2d1c8f1ab --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/requires.txt @@ -0,0 +1,83 @@ + +[certs] + +[check] +pytest-checkdocs>=2.4 + +[check:sys_platform != "cygwin"] +pytest-ruff>=0.2.1 +ruff>=0.8.0 + +[core] +packaging>=24.2 +more_itertools>=8.8 +jaraco.text>=3.7 +wheel>=0.43.0 +platformdirs>=4.2.2 +jaraco.functools>=4 +more_itertools + +[core:python_version < "3.10"] +importlib_metadata>=6 + +[core:python_version < "3.11"] +tomli>=2.0.1 + +[cover] +pytest-cov + +[doc] +sphinx>=3.5 +jaraco.packaging>=9.3 +rst.linker>=1.9 +furo +sphinx-lint +jaraco.tidelift>=1.4 +pygments-github-lexers==0.0.5 +sphinx-favicon +sphinx-inline-tabs +sphinx-reredirects +sphinxcontrib-towncrier +sphinx-notfound-page<2,>=1 +pyproject-hooks!=1.1 +towncrier<24.7 + +[enabler] +pytest-enabler>=2.2 + +[ssl] + +[test] +pytest!=8.1.*,>=6 +virtualenv>=13.0.0 +wheel>=0.44.0 +pip>=19.1 +packaging>=24.2 +jaraco.envs>=2.2 +pytest-xdist>=3 +jaraco.path>=3.7.2 +build[virtualenv]>=1.0.3 +filelock>=3.4.0 +ini2toml[lite]>=0.14 +tomli-w>=1.0.0 +pytest-timeout +pytest-home>=0.5 +pytest-subprocess +pyproject-hooks!=1.1 +jaraco.test>=5.5 + +[test:python_version >= "3.9" and sys_platform != "cygwin"] +jaraco.develop>=7.21 + +[test:sys_platform != "cygwin"] +pytest-perf + +[type] +pytest-mypy +mypy==1.14.* + +[type:python_version < "3.10"] +importlib_metadata>=7.0.2 + +[type:sys_platform != "cygwin"] +jaraco.develop>=7.21 diff --git a/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/top_level.txt b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..b5ac1070294b478b7cc2ce677207ee08813bfa37 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools-78.1.1-py3.10.egg-info/top_level.txt @@ -0,0 +1,3 @@ +_distutils_hack +pkg_resources +setuptools diff --git a/lib/python3.10/site-packages/setuptools/__init__.py b/lib/python3.10/site-packages/setuptools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..64464dfaa38a82e9647a095263d70f12961a1f92 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/__init__.py @@ -0,0 +1,286 @@ +"""Extensions to the 'distutils' for large or complex distributions""" +# mypy: disable_error_code=override +# Command.reinitialize_command has an extra **kw param that distutils doesn't have +# Can't disable on the exact line because distutils doesn't exists on Python 3.12 +# and mypy isn't aware of distutils_hack, causing distutils.core.Command to be Any, +# and a [unused-ignore] to be raised on 3.12+ + +from __future__ import annotations + +import functools +import os +import re +import sys +from abc import abstractmethod +from collections.abc import Mapping +from typing import TYPE_CHECKING, TypeVar, overload + +sys.path.extend(((vendor_path := os.path.join(os.path.dirname(os.path.dirname(__file__)), 'setuptools', '_vendor')) not in sys.path) * [vendor_path]) # fmt: skip +# workaround for #4476 +sys.modules.pop('backports', None) + +import _distutils_hack.override # noqa: F401 + +from . import logging, monkey +from .depends import Require +from .discovery import PackageFinder, PEP420PackageFinder +from .dist import Distribution +from .extension import Extension +from .version import __version__ as __version__ +from .warnings import SetuptoolsDeprecationWarning + +import distutils.core +from distutils.errors import DistutilsOptionError + +__all__ = [ + 'setup', + 'Distribution', + 'Command', + 'Extension', + 'Require', + 'SetuptoolsDeprecationWarning', + 'find_packages', + 'find_namespace_packages', +] + +_CommandT = TypeVar("_CommandT", bound="_Command") + +bootstrap_install_from = None + +find_packages = PackageFinder.find +find_namespace_packages = PEP420PackageFinder.find + + +def _install_setup_requires(attrs): + # Note: do not use `setuptools.Distribution` directly, as + # our PEP 517 backend patch `distutils.core.Distribution`. + class MinimalDistribution(distutils.core.Distribution): + """ + A minimal version of a distribution for supporting the + fetch_build_eggs interface. + """ + + def __init__(self, attrs: Mapping[str, object]) -> None: + _incl = 'dependency_links', 'setup_requires' + filtered = {k: attrs[k] for k in set(_incl) & set(attrs)} + super().__init__(filtered) + # Prevent accidentally triggering discovery with incomplete set of attrs + self.set_defaults._disable() + + def _get_project_config_files(self, filenames=None): + """Ignore ``pyproject.toml``, they are not related to setup_requires""" + try: + cfg, _toml = super()._split_standard_project_metadata(filenames) + except Exception: + return filenames, () + return cfg, () + + def finalize_options(self): + """ + Disable finalize_options to avoid building the working set. + Ref #2158. + """ + + dist = MinimalDistribution(attrs) + + # Honor setup.cfg's options. + dist.parse_config_files(ignore_option_errors=True) + if dist.setup_requires: + _fetch_build_eggs(dist) + + +def _fetch_build_eggs(dist: Distribution): + try: + dist.fetch_build_eggs(dist.setup_requires) + except Exception as ex: + msg = """ + It is possible a package already installed in your system + contains an version that is invalid according to PEP 440. + You can try `pip install --use-pep517` as a workaround for this problem, + or rely on a new virtual environment. + + If the problem refers to a package that is not installed yet, + please contact that package's maintainers or distributors. + """ + if "InvalidVersion" in ex.__class__.__name__: + if hasattr(ex, "add_note"): + ex.add_note(msg) # PEP 678 + else: + dist.announce(f"\n{msg}\n") + raise + + +def setup(**attrs): + logging.configure() + # Make sure we have any requirements needed to interpret 'attrs'. + _install_setup_requires(attrs) + return distutils.core.setup(**attrs) + + +setup.__doc__ = distutils.core.setup.__doc__ + +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + from distutils.core import Command as _Command +else: + _Command = monkey.get_unpatched(distutils.core.Command) + + +class Command(_Command): + """ + Setuptools internal actions are organized using a *command design pattern*. + This means that each action (or group of closely related actions) executed during + the build should be implemented as a ``Command`` subclass. + + These commands are abstractions and do not necessarily correspond to a command that + can (or should) be executed via a terminal, in a CLI fashion (although historically + they would). + + When creating a new command from scratch, custom defined classes **SHOULD** inherit + from ``setuptools.Command`` and implement a few mandatory methods. + Between these mandatory methods, are listed: + :meth:`initialize_options`, :meth:`finalize_options` and :meth:`run`. + + A useful analogy for command classes is to think of them as subroutines with local + variables called "options". The options are "declared" in :meth:`initialize_options` + and "defined" (given their final values, aka "finalized") in :meth:`finalize_options`, + both of which must be defined by every command class. The "body" of the subroutine, + (where it does all the work) is the :meth:`run` method. + Between :meth:`initialize_options` and :meth:`finalize_options`, ``setuptools`` may set + the values for options/attributes based on user's input (or circumstance), + which means that the implementation should be careful to not overwrite values in + :meth:`finalize_options` unless necessary. + + Please note that other commands (or other parts of setuptools) may also overwrite + the values of the command's options/attributes multiple times during the build + process. + Therefore it is important to consistently implement :meth:`initialize_options` and + :meth:`finalize_options`. For example, all derived attributes (or attributes that + depend on the value of other attributes) **SHOULD** be recomputed in + :meth:`finalize_options`. + + When overwriting existing commands, custom defined classes **MUST** abide by the + same APIs implemented by the original class. They also **SHOULD** inherit from the + original class. + """ + + command_consumes_arguments = False + distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution + + def __init__(self, dist: Distribution, **kw) -> None: + """ + Construct the command for dist, updating + vars(self) with any keyword parameters. + """ + super().__init__(dist) + vars(self).update(kw) + + def _ensure_stringlike(self, option, what, default=None): + val = getattr(self, option) + if val is None: + setattr(self, option, default) + return default + elif not isinstance(val, str): + raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)") + return val + + def ensure_string_list(self, option: str) -> None: + r"""Ensure that 'option' is a list of strings. If 'option' is + currently a string, we split it either on /,\s*/ or /\s+/, so + "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become + ["foo", "bar", "baz"]. + + .. + TODO: This method seems to be similar to the one in ``distutils.cmd`` + Probably it is just here for backward compatibility with old Python versions? + + :meta private: + """ + val = getattr(self, option) + if val is None: + return + elif isinstance(val, str): + setattr(self, option, re.split(r',\s*|\s+', val)) + else: + if isinstance(val, list): + ok = all(isinstance(v, str) for v in val) + else: + ok = False + if not ok: + raise DistutilsOptionError( + f"'{option}' must be a list of strings (got {val!r})" + ) + + @overload + def reinitialize_command( + self, command: str, reinit_subcommands: bool = False, **kw + ) -> _Command: ... + @overload + def reinitialize_command( + self, command: _CommandT, reinit_subcommands: bool = False, **kw + ) -> _CommandT: ... + def reinitialize_command( + self, command: str | _Command, reinit_subcommands: bool = False, **kw + ) -> _Command: + cmd = _Command.reinitialize_command(self, command, reinit_subcommands) + vars(cmd).update(kw) + return cmd # pyright: ignore[reportReturnType] # pypa/distutils#307 + + @abstractmethod + def initialize_options(self) -> None: + """ + Set or (reset) all options/attributes/caches used by the command + to their default values. Note that these values may be overwritten during + the build. + """ + raise NotImplementedError + + @abstractmethod + def finalize_options(self) -> None: + """ + Set final values for all options/attributes used by the command. + Most of the time, each option/attribute/cache should only be set if it does not + have any value yet (e.g. ``if self.attr is None: self.attr = val``). + """ + raise NotImplementedError + + @abstractmethod + def run(self) -> None: + """ + Execute the actions intended by the command. + (Side effects **SHOULD** only take place when :meth:`run` is executed, + for example, creating new files or writing to the terminal output). + """ + raise NotImplementedError + + +def _find_all_simple(path): + """ + Find all files under 'path' + """ + results = ( + os.path.join(base, file) + for base, dirs, files in os.walk(path, followlinks=True) + for file in files + ) + return filter(os.path.isfile, results) + + +def findall(dir=os.curdir): + """ + Find all files under 'dir' and return the list of full filenames. + Unless dir is '.', return full filenames with dir prepended. + """ + files = _find_all_simple(dir) + if dir == os.curdir: + make_rel = functools.partial(os.path.relpath, start=dir) + files = map(make_rel, files) + return list(files) + + +class sic(str): + """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)""" + + +# Apply monkey patches +monkey.patch_all() diff --git a/lib/python3.10/site-packages/setuptools/_core_metadata.py b/lib/python3.10/site-packages/setuptools/_core_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..a52d5cf755cdd6a502e752c2f7a3afa3b25897d5 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_core_metadata.py @@ -0,0 +1,337 @@ +""" +Handling of Core Metadata for Python packages (including reading and writing). + +See: https://packaging.python.org/en/latest/specifications/core-metadata/ +""" + +from __future__ import annotations + +import os +import stat +import textwrap +from email import message_from_file +from email.message import Message +from tempfile import NamedTemporaryFile + +from packaging.markers import Marker +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name, canonicalize_version +from packaging.version import Version + +from . import _normalization, _reqs +from ._static import is_static +from .warnings import SetuptoolsDeprecationWarning + +from distutils.util import rfc822_escape + + +def get_metadata_version(self): + mv = getattr(self, 'metadata_version', None) + if mv is None: + mv = Version('2.4') + self.metadata_version = mv + return mv + + +def rfc822_unescape(content: str) -> str: + """Reverse RFC-822 escaping by removing leading whitespaces from content.""" + lines = content.splitlines() + if len(lines) == 1: + return lines[0].lstrip() + return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:])))) + + +def _read_field_from_msg(msg: Message, field: str) -> str | None: + """Read Message header field.""" + value = msg[field] + if value == 'UNKNOWN': + return None + return value + + +def _read_field_unescaped_from_msg(msg: Message, field: str) -> str | None: + """Read Message header field and apply rfc822_unescape.""" + value = _read_field_from_msg(msg, field) + if value is None: + return value + return rfc822_unescape(value) + + +def _read_list_from_msg(msg: Message, field: str) -> list[str] | None: + """Read Message header field and return all results as list.""" + values = msg.get_all(field, None) + if values == []: + return None + return values + + +def _read_payload_from_msg(msg: Message) -> str | None: + value = str(msg.get_payload()).strip() + if value == 'UNKNOWN' or not value: + return None + return value + + +def read_pkg_file(self, file): + """Reads the metadata values from a file object.""" + msg = message_from_file(file) + + self.metadata_version = Version(msg['metadata-version']) + self.name = _read_field_from_msg(msg, 'name') + self.version = _read_field_from_msg(msg, 'version') + self.description = _read_field_from_msg(msg, 'summary') + # we are filling author only. + self.author = _read_field_from_msg(msg, 'author') + self.maintainer = None + self.author_email = _read_field_from_msg(msg, 'author-email') + self.maintainer_email = None + self.url = _read_field_from_msg(msg, 'home-page') + self.download_url = _read_field_from_msg(msg, 'download-url') + self.license = _read_field_unescaped_from_msg(msg, 'license') + self.license_expression = _read_field_unescaped_from_msg(msg, 'license-expression') + + self.long_description = _read_field_unescaped_from_msg(msg, 'description') + if self.long_description is None and self.metadata_version >= Version('2.1'): + self.long_description = _read_payload_from_msg(msg) + self.description = _read_field_from_msg(msg, 'summary') + + if 'keywords' in msg: + self.keywords = _read_field_from_msg(msg, 'keywords').split(',') + + self.platforms = _read_list_from_msg(msg, 'platform') + self.classifiers = _read_list_from_msg(msg, 'classifier') + + # PEP 314 - these fields only exist in 1.1 + if self.metadata_version == Version('1.1'): + self.requires = _read_list_from_msg(msg, 'requires') + self.provides = _read_list_from_msg(msg, 'provides') + self.obsoletes = _read_list_from_msg(msg, 'obsoletes') + else: + self.requires = None + self.provides = None + self.obsoletes = None + + self.license_files = _read_list_from_msg(msg, 'license-file') + + +def single_line(val): + """ + Quick and dirty validation for Summary pypa/setuptools#1390. + """ + if '\n' in val: + # TODO: Replace with `raise ValueError("newlines not allowed")` + # after reviewing #2893. + msg = "newlines are not allowed in `summary` and will break in the future" + SetuptoolsDeprecationWarning.emit("Invalid config.", msg) + # due_date is undefined. Controversial change, there was a lot of push back. + val = val.strip().split('\n')[0] + return val + + +def write_pkg_info(self, base_dir): + """Write the PKG-INFO file into the release tree.""" + temp = "" + final = os.path.join(base_dir, 'PKG-INFO') + try: + # Use a temporary file while writing to avoid race conditions + # (e.g. `importlib.metadata` reading `.egg-info/PKG-INFO`): + with NamedTemporaryFile("w", encoding="utf-8", dir=base_dir, delete=False) as f: + temp = f.name + self.write_pkg_file(f) + permissions = stat.S_IMODE(os.lstat(temp).st_mode) + os.chmod(temp, permissions | stat.S_IRGRP | stat.S_IROTH) + os.replace(temp, final) # atomic operation. + finally: + if temp and os.path.exists(temp): + os.remove(temp) + + +# Based on Python 3.5 version +def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME + """Write the PKG-INFO format data to a file object.""" + version = self.get_metadata_version() + + def write_field(key, value): + file.write(f"{key}: {value}\n") + + write_field('Metadata-Version', str(version)) + write_field('Name', self.get_name()) + write_field('Version', self.get_version()) + + summary = self.get_description() + if summary: + write_field('Summary', single_line(summary)) + + optional_fields = ( + ('Home-page', 'url'), + ('Download-URL', 'download_url'), + ('Author', 'author'), + ('Author-email', 'author_email'), + ('Maintainer', 'maintainer'), + ('Maintainer-email', 'maintainer_email'), + ) + + for field, attr in optional_fields: + attr_val = getattr(self, attr, None) + if attr_val is not None: + write_field(field, attr_val) + + if license_expression := self.license_expression: + write_field('License-Expression', license_expression) + elif license := self.get_license(): + write_field('License', rfc822_escape(license)) + + for label, url in self.project_urls.items(): + write_field('Project-URL', f'{label}, {url}') + + keywords = ','.join(self.get_keywords()) + if keywords: + write_field('Keywords', keywords) + + platforms = self.get_platforms() or [] + for platform in platforms: + write_field('Platform', platform) + + self._write_list(file, 'Classifier', self.get_classifiers()) + + # PEP 314 + self._write_list(file, 'Requires', self.get_requires()) + self._write_list(file, 'Provides', self.get_provides()) + self._write_list(file, 'Obsoletes', self.get_obsoletes()) + + # Setuptools specific for PEP 345 + if hasattr(self, 'python_requires'): + write_field('Requires-Python', self.python_requires) + + # PEP 566 + if self.long_description_content_type: + write_field('Description-Content-Type', self.long_description_content_type) + + safe_license_files = map(_safe_license_file, self.license_files or []) + self._write_list(file, 'License-File', safe_license_files) + _write_requirements(self, file) + + for field, attr in _POSSIBLE_DYNAMIC_FIELDS.items(): + if (val := getattr(self, attr, None)) and not is_static(val): + write_field('Dynamic', field) + + long_description = self.get_long_description() + if long_description: + file.write(f"\n{long_description}") + if not long_description.endswith("\n"): + file.write("\n") + + +def _write_requirements(self, file): + for req in _reqs.parse(self.install_requires): + file.write(f"Requires-Dist: {req}\n") + + processed_extras = {} + for augmented_extra, reqs in self.extras_require.items(): + # Historically, setuptools allows "augmented extras": `:` + unsafe_extra, _, condition = augmented_extra.partition(":") + unsafe_extra = unsafe_extra.strip() + extra = _normalization.safe_extra(unsafe_extra) + + if extra: + _write_provides_extra(file, processed_extras, extra, unsafe_extra) + for req in _reqs.parse_strings(reqs): + r = _include_extra(req, extra, condition.strip()) + file.write(f"Requires-Dist: {r}\n") + + return processed_extras + + +def _include_extra(req: str, extra: str, condition: str) -> Requirement: + r = Requirement(req) # create a fresh object that can be modified + parts = ( + f"({r.marker})" if r.marker else None, + f"({condition})" if condition else None, + f"extra == {extra!r}" if extra else None, + ) + r.marker = Marker(" and ".join(x for x in parts if x)) + return r + + +def _write_provides_extra(file, processed_extras, safe, unsafe): + previous = processed_extras.get(safe) + if previous == unsafe: + SetuptoolsDeprecationWarning.emit( + 'Ambiguity during "extra" normalization for dependencies.', + f""" + {previous!r} and {unsafe!r} normalize to the same value:\n + {safe!r}\n + In future versions, setuptools might halt the build process. + """, + see_url="https://peps.python.org/pep-0685/", + ) + else: + processed_extras[safe] = unsafe + file.write(f"Provides-Extra: {safe}\n") + + +# from pypa/distutils#244; needed only until that logic is always available +def get_fullname(self): + return _distribution_fullname(self.get_name(), self.get_version()) + + +def _distribution_fullname(name: str, version: str) -> str: + """ + >>> _distribution_fullname('setup.tools', '1.0-2') + 'setup_tools-1.0.post2' + >>> _distribution_fullname('setup-tools', '1.2post2') + 'setup_tools-1.2.post2' + >>> _distribution_fullname('setup-tools', '1.0-r2') + 'setup_tools-1.0.post2' + >>> _distribution_fullname('setup.tools', '1.0.post') + 'setup_tools-1.0.post0' + >>> _distribution_fullname('setup.tools', '1.0+ubuntu-1') + 'setup_tools-1.0+ubuntu.1' + """ + return "{}-{}".format( + canonicalize_name(name).replace('-', '_'), + canonicalize_version(version, strip_trailing_zero=False), + ) + + +def _safe_license_file(file): + # XXX: Do we need this after the deprecation discussed in #4892, #4896?? + normalized = os.path.normpath(file).replace(os.sep, "/") + if "../" in normalized: + return os.path.basename(normalized) # Temporarily restore pre PEP639 behaviour + return normalized + + +_POSSIBLE_DYNAMIC_FIELDS = { + # Core Metadata Field x related Distribution attribute + "author": "author", + "author-email": "author_email", + "classifier": "classifiers", + "description": "long_description", + "description-content-type": "long_description_content_type", + "download-url": "download_url", + "home-page": "url", + "keywords": "keywords", + "license": "license", + # XXX: License-File is complicated because the user gives globs that are expanded + # during the build. Without special handling it is likely always + # marked as Dynamic, which is an acceptable outcome according to: + # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677 + "license-file": "license_files", + "license-expression": "license_expression", # PEP 639 + "maintainer": "maintainer", + "maintainer-email": "maintainer_email", + "obsoletes": "obsoletes", + # "obsoletes-dist": "obsoletes_dist", # NOT USED + "platform": "platforms", + "project-url": "project_urls", + "provides": "provides", + # "provides-dist": "provides_dist", # NOT USED + "provides-extra": "extras_require", + "requires": "requires", + "requires-dist": "install_requires", + # "requires-external": "requires_external", # NOT USED + "requires-python": "python_requires", + "summary": "description", + # "supported-platform": "supported_platforms", # NOT USED +} diff --git a/lib/python3.10/site-packages/setuptools/_entry_points.py b/lib/python3.10/site-packages/setuptools/_entry_points.py new file mode 100644 index 0000000000000000000000000000000000000000..e785fc7df8d51241a38675902fe5e5b78a0cc29c --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_entry_points.py @@ -0,0 +1,90 @@ +import functools +import itertools +import operator + +from jaraco.functools import pass_none +from jaraco.text import yield_lines +from more_itertools import consume + +from ._importlib import metadata +from ._itertools import ensure_unique +from .errors import OptionError + + +def ensure_valid(ep): + """ + Exercise one of the dynamic properties to trigger + the pattern match. + """ + try: + ep.extras + except (AttributeError, AssertionError) as ex: + # Why both? See https://github.com/python/importlib_metadata/issues/488 + msg = ( + f"Problems to parse {ep}.\nPlease ensure entry-point follows the spec: " + "https://packaging.python.org/en/latest/specifications/entry-points/" + ) + raise OptionError(msg) from ex + + +def load_group(value, group): + """ + Given a value of an entry point or series of entry points, + return each as an EntryPoint. + """ + # normalize to a single sequence of lines + lines = yield_lines(value) + text = f'[{group}]\n' + '\n'.join(lines) + return metadata.EntryPoints._from_text(text) + + +def by_group_and_name(ep): + return ep.group, ep.name + + +def validate(eps: metadata.EntryPoints): + """ + Ensure entry points are unique by group and name and validate each. + """ + consume(map(ensure_valid, ensure_unique(eps, key=by_group_and_name))) + return eps + + +@functools.singledispatch +def load(eps): + """ + Given a Distribution.entry_points, produce EntryPoints. + """ + groups = itertools.chain.from_iterable( + load_group(value, group) for group, value in eps.items() + ) + return validate(metadata.EntryPoints(groups)) + + +@load.register(str) +def _(eps): + r""" + >>> ep, = load('[console_scripts]\nfoo=bar') + >>> ep.group + 'console_scripts' + >>> ep.name + 'foo' + >>> ep.value + 'bar' + """ + return validate(metadata.EntryPoints(metadata.EntryPoints._from_text(eps))) + + +load.register(type(None), lambda x: x) + + +@pass_none +def render(eps: metadata.EntryPoints): + by_group = operator.attrgetter('group') + groups = itertools.groupby(sorted(eps, key=by_group), by_group) + + return '\n'.join(f'[{group}]\n{render_items(items)}\n' for group, items in groups) + + +def render_items(eps): + return '\n'.join(f'{ep.name} = {ep.value}' for ep in sorted(eps)) diff --git a/lib/python3.10/site-packages/setuptools/_imp.py b/lib/python3.10/site-packages/setuptools/_imp.py new file mode 100644 index 0000000000000000000000000000000000000000..f1d9f29218987d4f830f2d57aca9e3f74d00a095 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_imp.py @@ -0,0 +1,87 @@ +""" +Re-implementation of find_module and get_frozen_object +from the deprecated imp module. +""" + +import importlib.machinery +import importlib.util +import os +import tokenize +from importlib.util import module_from_spec + +PY_SOURCE = 1 +PY_COMPILED = 2 +C_EXTENSION = 3 +C_BUILTIN = 6 +PY_FROZEN = 7 + + +def find_spec(module, paths): + finder = ( + importlib.machinery.PathFinder().find_spec + if isinstance(paths, list) + else importlib.util.find_spec + ) + return finder(module, paths) + + +def find_module(module, paths=None): + """Just like 'imp.find_module()', but with package support""" + spec = find_spec(module, paths) + if spec is None: + raise ImportError(f"Can't find {module}") + if not spec.has_location and hasattr(spec, 'submodule_search_locations'): + spec = importlib.util.spec_from_loader('__init__.py', spec.loader) + + kind = -1 + file = None + static = isinstance(spec.loader, type) + if ( + spec.origin == 'frozen' + or static + and issubclass(spec.loader, importlib.machinery.FrozenImporter) + ): + kind = PY_FROZEN + path = None # imp compabilty + suffix = mode = '' # imp compatibility + elif ( + spec.origin == 'built-in' + or static + and issubclass(spec.loader, importlib.machinery.BuiltinImporter) + ): + kind = C_BUILTIN + path = None # imp compabilty + suffix = mode = '' # imp compatibility + elif spec.has_location: + path = spec.origin + suffix = os.path.splitext(path)[1] + mode = 'r' if suffix in importlib.machinery.SOURCE_SUFFIXES else 'rb' + + if suffix in importlib.machinery.SOURCE_SUFFIXES: + kind = PY_SOURCE + file = tokenize.open(path) + elif suffix in importlib.machinery.BYTECODE_SUFFIXES: + kind = PY_COMPILED + file = open(path, 'rb') + elif suffix in importlib.machinery.EXTENSION_SUFFIXES: + kind = C_EXTENSION + + else: + path = None + suffix = mode = '' + + return file, path, (suffix, mode, kind) + + +def get_frozen_object(module, paths=None): + spec = find_spec(module, paths) + if not spec: + raise ImportError(f"Can't find {module}") + return spec.loader.get_code(module) + + +def get_module(module, paths, info): + spec = find_spec(module, paths) + if not spec: + raise ImportError(f"Can't find {module}") + return module_from_spec(spec) diff --git a/lib/python3.10/site-packages/setuptools/_importlib.py b/lib/python3.10/site-packages/setuptools/_importlib.py new file mode 100644 index 0000000000000000000000000000000000000000..ce0fd52653b56c9c2cb2b2c7bfb35e3ec3c61408 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_importlib.py @@ -0,0 +1,9 @@ +import sys + +if sys.version_info < (3, 10): + import importlib_metadata as metadata # pragma: no cover +else: + import importlib.metadata as metadata # noqa: F401 + + +import importlib.resources as resources # noqa: F401 diff --git a/lib/python3.10/site-packages/setuptools/_itertools.py b/lib/python3.10/site-packages/setuptools/_itertools.py new file mode 100644 index 0000000000000000000000000000000000000000..d6ca841353ce39ac4361013f5c8160d69028d0d8 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_itertools.py @@ -0,0 +1,23 @@ +from more_itertools import consume # noqa: F401 + + +# copied from jaraco.itertools 6.1 +def ensure_unique(iterable, key=lambda x: x): + """ + Wrap an iterable to raise a ValueError if non-unique values are encountered. + + >>> list(ensure_unique('abc')) + ['a', 'b', 'c'] + >>> consume(ensure_unique('abca')) + Traceback (most recent call last): + ... + ValueError: Duplicate element 'a' encountered. + """ + seen = set() + seen_add = seen.add + for element in iterable: + k = key(element) + if k in seen: + raise ValueError(f"Duplicate element {element!r} encountered.") + seen_add(k) + yield element diff --git a/lib/python3.10/site-packages/setuptools/_normalization.py b/lib/python3.10/site-packages/setuptools/_normalization.py new file mode 100644 index 0000000000000000000000000000000000000000..0937a4faf8cf2358ed1ec8a4f12b8260e493fd24 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_normalization.py @@ -0,0 +1,179 @@ +""" +Helpers for normalization as expected in wheel/sdist/module file names +and core metadata +""" + +import re +from typing import TYPE_CHECKING + +import packaging + +# https://packaging.python.org/en/latest/specifications/core-metadata/#name +_VALID_NAME = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.I) +_UNSAFE_NAME_CHARS = re.compile(r"[^A-Z0-9._-]+", re.I) +_NON_ALPHANUMERIC = re.compile(r"[^A-Z0-9]+", re.I) +_PEP440_FALLBACK = re.compile(r"^v?(?P(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I) + + +def safe_identifier(name: str) -> str: + """Make a string safe to be used as Python identifier. + >>> safe_identifier("12abc") + '_12abc' + >>> safe_identifier("__editable__.myns.pkg-78.9.3_local") + '__editable___myns_pkg_78_9_3_local' + """ + safe = re.sub(r'\W|^(?=\d)', '_', name) + assert safe.isidentifier() + return safe + + +def safe_name(component: str) -> str: + """Escape a component used as a project name according to Core Metadata. + >>> safe_name("hello world") + 'hello-world' + >>> safe_name("hello?world") + 'hello-world' + >>> safe_name("hello_world") + 'hello_world' + """ + # See pkg_resources.safe_name + return _UNSAFE_NAME_CHARS.sub("-", component) + + +def safe_version(version: str) -> str: + """Convert an arbitrary string into a valid version string. + Can still raise an ``InvalidVersion`` exception. + To avoid exceptions use ``best_effort_version``. + >>> safe_version("1988 12 25") + '1988.12.25' + >>> safe_version("v0.2.1") + '0.2.1' + >>> safe_version("v0.2?beta") + '0.2b0' + >>> safe_version("v0.2 beta") + '0.2b0' + >>> safe_version("ubuntu lts") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'ubuntu.lts' + """ + v = version.replace(' ', '.') + try: + return str(packaging.version.Version(v)) + except packaging.version.InvalidVersion: + attempt = _UNSAFE_NAME_CHARS.sub("-", v) + return str(packaging.version.Version(attempt)) + + +def best_effort_version(version: str) -> str: + """Convert an arbitrary string into a version-like string. + Fallback when ``safe_version`` is not safe enough. + >>> best_effort_version("v0.2 beta") + '0.2b0' + >>> best_effort_version("ubuntu lts") + '0.dev0+sanitized.ubuntu.lts' + >>> best_effort_version("0.23ubuntu1") + '0.23.dev0+sanitized.ubuntu1' + >>> best_effort_version("0.23-") + '0.23.dev0+sanitized' + >>> best_effort_version("0.-_") + '0.dev0+sanitized' + >>> best_effort_version("42.+?1") + '42.dev0+sanitized.1' + """ + # See pkg_resources._forgiving_version + try: + return safe_version(version) + except packaging.version.InvalidVersion: + v = version.replace(' ', '.') + match = _PEP440_FALLBACK.search(v) + if match: + safe = match["safe"] + rest = v[len(safe) :] + else: + safe = "0" + rest = version + safe_rest = _NON_ALPHANUMERIC.sub(".", rest).strip(".") + local = f"sanitized.{safe_rest}".strip(".") + return safe_version(f"{safe}.dev0+{local}") + + +def safe_extra(extra: str) -> str: + """Normalize extra name according to PEP 685 + >>> safe_extra("_FrIeNdLy-._.-bArD") + 'friendly-bard' + >>> safe_extra("FrIeNdLy-._.-bArD__._-") + 'friendly-bard' + """ + return _NON_ALPHANUMERIC.sub("-", extra).strip("-").lower() + + +def filename_component(value: str) -> str: + """Normalize each component of a filename (e.g. distribution/version part of wheel) + Note: ``value`` needs to be already normalized. + >>> filename_component("my-pkg") + 'my_pkg' + """ + return value.replace("-", "_").strip("_") + + +def filename_component_broken(value: str) -> str: + """ + Produce the incorrect filename component for compatibility. + + See pypa/setuptools#4167 for detailed analysis. + + TODO: replace this with filename_component after pip 24 is + nearly-ubiquitous. + + >>> filename_component_broken('foo_bar-baz') + 'foo-bar-baz' + """ + return value.replace('_', '-') + + +def safer_name(value: str) -> str: + """Like ``safe_name`` but can be used as filename component for wheel""" + # See bdist_wheel.safer_name + return ( + # Per https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization + re.sub(r"[-_.]+", "-", safe_name(value)) + .lower() + # Per https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + .replace("-", "_") + ) + + +def safer_best_effort_version(value: str) -> str: + """Like ``best_effort_version`` but can be used as filename component for wheel""" + # See bdist_wheel.safer_verion + # TODO: Replace with only safe_version in the future (no need for best effort) + return filename_component(best_effort_version(value)) + + +def _missing_canonicalize_license_expression(expression: str) -> str: + """ + Defer import error to affect only users that actually use it + https://github.com/pypa/setuptools/issues/4894 + >>> _missing_canonicalize_license_expression("a OR b") + Traceback (most recent call last): + ... + ImportError: ...Cannot import `packaging.licenses`... + """ + raise ImportError( + "Cannot import `packaging.licenses`." + """ + Setuptools>=77.0.0 requires "packaging>=24.2" to work properly. + Please make sure you have a suitable version installed. + """ + ) + + +try: + from packaging.licenses import ( + canonicalize_license_expression as _canonicalize_license_expression, + ) +except ImportError: # pragma: nocover + if not TYPE_CHECKING: + # XXX: pyright is still upset even with # pyright: ignore[reportAssignmentType] + _canonicalize_license_expression = _missing_canonicalize_license_expression diff --git a/lib/python3.10/site-packages/setuptools/_path.py b/lib/python3.10/site-packages/setuptools/_path.py new file mode 100644 index 0000000000000000000000000000000000000000..0d99b0f539ff5f819b167013c48726180cd83d49 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_path.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import contextlib +import os +import sys +from typing import TYPE_CHECKING, TypeVar, Union + +from more_itertools import unique_everseen + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath +StrPathT = TypeVar("StrPathT", bound=Union[str, os.PathLike[str]]) + + +def ensure_directory(path): + """Ensure that the parent directory of `path` exists""" + dirname = os.path.dirname(path) + os.makedirs(dirname, exist_ok=True) + + +def same_path(p1: StrPath, p2: StrPath) -> bool: + """Differs from os.path.samefile because it does not require paths to exist. + Purely string based (no comparison between i-nodes). + >>> same_path("a/b", "./a/b") + True + >>> same_path("a/b", "a/./b") + True + >>> same_path("a/b", "././a/b") + True + >>> same_path("a/b", "./a/b/c/..") + True + >>> same_path("a/b", "../a/b/c") + False + >>> same_path("a", "a/b") + False + """ + return normpath(p1) == normpath(p2) + + +def normpath(filename: StrPath) -> str: + """Normalize a file/dir name for comparison purposes.""" + # See pkg_resources.normalize_path for notes about cygwin + file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename + return os.path.normcase(os.path.realpath(os.path.normpath(file))) + + +@contextlib.contextmanager +def paths_on_pythonpath(paths): + """ + Add the indicated paths to the head of the PYTHONPATH environment + variable so that subprocesses will also see the packages at + these paths. + + Do this in a context that restores the value on exit. + + >>> getfixture('monkeypatch').setenv('PYTHONPATH', 'anything') + >>> with paths_on_pythonpath(['foo', 'bar']): + ... assert 'foo' in os.environ['PYTHONPATH'] + ... assert 'anything' in os.environ['PYTHONPATH'] + >>> os.environ['PYTHONPATH'] + 'anything' + + >>> getfixture('monkeypatch').delenv('PYTHONPATH') + >>> with paths_on_pythonpath(['foo', 'bar']): + ... assert 'foo' in os.environ['PYTHONPATH'] + >>> os.environ.get('PYTHONPATH') + """ + nothing = object() + orig_pythonpath = os.environ.get('PYTHONPATH', nothing) + current_pythonpath = os.environ.get('PYTHONPATH', '') + try: + prefix = os.pathsep.join(unique_everseen(paths)) + to_join = filter(None, [prefix, current_pythonpath]) + new_path = os.pathsep.join(to_join) + if new_path: + os.environ['PYTHONPATH'] = new_path + yield + finally: + if orig_pythonpath is nothing: + os.environ.pop('PYTHONPATH', None) + else: + os.environ['PYTHONPATH'] = orig_pythonpath diff --git a/lib/python3.10/site-packages/setuptools/_reqs.py b/lib/python3.10/site-packages/setuptools/_reqs.py new file mode 100644 index 0000000000000000000000000000000000000000..c793be4d6eb3991d7b4ada615201a01cbfbbefd5 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_reqs.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from functools import lru_cache +from typing import TYPE_CHECKING, Callable, TypeVar, Union, overload + +import jaraco.text as text +from packaging.requirements import Requirement + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +_T = TypeVar("_T") +_StrOrIter: TypeAlias = Union[str, Iterable[str]] + + +parse_req: Callable[[str], Requirement] = lru_cache()(Requirement) +# Setuptools parses the same requirement many times +# (e.g. first for validation than for normalisation), +# so it might be worth to cache. + + +def parse_strings(strs: _StrOrIter) -> Iterator[str]: + """ + Yield requirement strings for each specification in `strs`. + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + return text.join_continuation(map(text.drop_comment, text.yield_lines(strs))) + + +# These overloads are only needed because of a mypy false-positive, pyright gets it right +# https://github.com/python/mypy/issues/3737 +@overload +def parse(strs: _StrOrIter) -> Iterator[Requirement]: ... +@overload +def parse(strs: _StrOrIter, parser: Callable[[str], _T]) -> Iterator[_T]: ... +def parse(strs: _StrOrIter, parser: Callable[[str], _T] = parse_req) -> Iterator[_T]: # type: ignore[assignment] + """ + Replacement for ``pkg_resources.parse_requirements`` that uses ``packaging``. + """ + return map(parser, parse_strings(strs)) diff --git a/lib/python3.10/site-packages/setuptools/_shutil.py b/lib/python3.10/site-packages/setuptools/_shutil.py new file mode 100644 index 0000000000000000000000000000000000000000..6acbb4281fc986587f52a83395dc63912a863caf --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_shutil.py @@ -0,0 +1,53 @@ +"""Convenience layer on top of stdlib's shutil and os""" + +import os +import stat +from typing import Callable, TypeVar + +from .compat import py311 + +from distutils import log + +try: + from os import chmod # pyright: ignore[reportAssignmentType] + # Losing type-safety w/ pyright, but that's ok +except ImportError: # pragma: no cover + # Jython compatibility + def chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy reuses the imported definition anyway + pass + + +_T = TypeVar("_T") + + +def attempt_chmod_verbose(path, mode): + log.debug("changing mode of %s to %o", path, mode) + try: + chmod(path, mode) + except OSError as e: # pragma: no cover + log.debug("chmod failed: %s", e) + + +# Must match shutil._OnExcCallback +def _auto_chmod( + func: Callable[..., _T], arg: str, exc: BaseException +) -> _T: # pragma: no cover + """shutils onexc callback to automatically call chmod for certain functions.""" + # Only retry for scenarios known to have an issue + if func in [os.unlink, os.remove] and os.name == 'nt': + attempt_chmod_verbose(arg, stat.S_IWRITE) + return func(arg) + raise exc + + +def rmtree(path, ignore_errors=False, onexc=_auto_chmod): + """ + Similar to ``shutil.rmtree`` but automatically executes ``chmod`` + for well know Windows failure scenarios. + """ + return py311.shutil_rmtree(path, ignore_errors, onexc) + + +def rmdir(path, **opts): + if os.path.isdir(path): + rmtree(path, **opts) diff --git a/lib/python3.10/site-packages/setuptools/_static.py b/lib/python3.10/site-packages/setuptools/_static.py new file mode 100644 index 0000000000000000000000000000000000000000..af35862cf8b759a0e60110ce9e92bfdb1b49bc5f --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/_static.py @@ -0,0 +1,188 @@ +from functools import wraps +from typing import TypeVar + +import packaging.specifiers + +from .warnings import SetuptoolsDeprecationWarning + + +class Static: + """ + Wrapper for built-in object types that are allow setuptools to identify + static core metadata (in opposition to ``Dynamic``, as defined :pep:`643`). + + The trick is to mark values with :class:`Static` when they come from + ``pyproject.toml`` or ``setup.cfg``, so if any plugin overwrite the value + with a built-in, setuptools will be able to recognise the change. + + We inherit from built-in classes, so that we don't need to change the existing + code base to deal with the new types. + We also should strive for immutability objects to avoid changes after the + initial parsing. + """ + + _mutated_: bool = False # TODO: Remove after deprecation warning is solved + + +def _prevent_modification(target: type, method: str, copying: str) -> None: + """ + Because setuptools is very flexible we cannot fully prevent + plugins and user customizations from modifying static values that were + parsed from config files. + But we can attempt to block "in-place" mutations and identify when they + were done. + """ + fn = getattr(target, method, None) + if fn is None: + return + + @wraps(fn) + def _replacement(self: Static, *args, **kwargs): + # TODO: After deprecation period raise NotImplementedError instead of warning + # which obviated the existence and checks of the `_mutated_` attribute. + self._mutated_ = True + SetuptoolsDeprecationWarning.emit( + "Direct modification of value will be disallowed", + f""" + In an effort to implement PEP 643, direct/in-place changes of static values + that come from configuration files are deprecated. + If you need to modify this value, please first create a copy with {copying} + and make sure conform to all relevant standards when overriding setuptools + functionality (https://packaging.python.org/en/latest/specifications/). + """, + due_date=(2025, 10, 10), # Initially introduced in 2024-09-06 + ) + return fn(self, *args, **kwargs) + + _replacement.__doc__ = "" # otherwise doctest may fail. + setattr(target, method, _replacement) + + +class Str(str, Static): + pass + + +class Tuple(tuple, Static): + pass + + +class List(list, Static): + """ + :meta private: + >>> x = List([1, 2, 3]) + >>> is_static(x) + True + >>> x += [0] # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + SetuptoolsDeprecationWarning: Direct modification ... + >>> is_static(x) # no longer static after modification + False + >>> y = list(x) + >>> y.clear() + >>> y + [] + >>> y == x + False + >>> is_static(List(y)) + True + """ + + +# Make `List` immutable-ish +# (certain places of setuptools/distutils issue a warn if we use tuple instead of list) +for _method in ( + '__delitem__', + '__iadd__', + '__setitem__', + 'append', + 'clear', + 'extend', + 'insert', + 'remove', + 'reverse', + 'pop', +): + _prevent_modification(List, _method, "`list(value)`") + + +class Dict(dict, Static): + """ + :meta private: + >>> x = Dict({'a': 1, 'b': 2}) + >>> is_static(x) + True + >>> x['c'] = 0 # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + SetuptoolsDeprecationWarning: Direct modification ... + >>> x._mutated_ + True + >>> is_static(x) # no longer static after modification + False + >>> y = dict(x) + >>> y.popitem() + ('b', 2) + >>> y == x + False + >>> is_static(Dict(y)) + True + """ + + +# Make `Dict` immutable-ish (we cannot inherit from types.MappingProxyType): +for _method in ( + '__delitem__', + '__ior__', + '__setitem__', + 'clear', + 'pop', + 'popitem', + 'setdefault', + 'update', +): + _prevent_modification(Dict, _method, "`dict(value)`") + + +class SpecifierSet(packaging.specifiers.SpecifierSet, Static): + """Not exactly a built-in type but useful for ``requires-python``""" + + +T = TypeVar("T") + + +def noop(value: T) -> T: + """ + >>> noop(42) + 42 + """ + return value + + +_CONVERSIONS = {str: Str, tuple: Tuple, list: List, dict: Dict} + + +def attempt_conversion(value: T) -> T: + """ + >>> is_static(attempt_conversion("hello")) + True + >>> is_static(object()) + False + """ + return _CONVERSIONS.get(type(value), noop)(value) # type: ignore[call-overload] + + +def is_static(value: object) -> bool: + """ + >>> is_static(a := Dict({'a': 1})) + True + >>> is_static(dict(a)) + False + >>> is_static(b := List([1, 2, 3])) + True + >>> is_static(list(b)) + False + """ + return isinstance(value, Static) and not value._mutated_ + + +EMPTY_LIST = List() +EMPTY_DICT = Dict() diff --git a/lib/python3.10/site-packages/setuptools/archive_util.py b/lib/python3.10/site-packages/setuptools/archive_util.py new file mode 100644 index 0000000000000000000000000000000000000000..1a02010bb2af2be0487730d6a32080877b9ac220 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/archive_util.py @@ -0,0 +1,219 @@ +"""Utilities for extracting common archive formats""" + +import contextlib +import os +import posixpath +import shutil +import tarfile +import zipfile + +from ._path import ensure_directory + +from distutils.errors import DistutilsError + +__all__ = [ + "unpack_archive", + "unpack_zipfile", + "unpack_tarfile", + "default_filter", + "UnrecognizedFormat", + "extraction_drivers", + "unpack_directory", +] + + +class UnrecognizedFormat(DistutilsError): + """Couldn't recognize the archive type""" + + +def default_filter(src, dst): + """The default progress/filter callback; returns True for all files""" + return dst + + +def unpack_archive( + filename, extract_dir, progress_filter=default_filter, drivers=None +) -> None: + """Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat`` + + `progress_filter` is a function taking two arguments: a source path + internal to the archive ('/'-separated), and a filesystem path where it + will be extracted. The callback must return the desired extract path + (which may be the same as the one passed in), or else ``None`` to skip + that file or directory. The callback can thus be used to report on the + progress of the extraction, as well as to filter the items extracted or + alter their extraction paths. + + `drivers`, if supplied, must be a non-empty sequence of functions with the + same signature as this function (minus the `drivers` argument), that raise + ``UnrecognizedFormat`` if they do not support extracting the designated + archive type. The `drivers` are tried in sequence until one is found that + does not raise an error, or until all are exhausted (in which case + ``UnrecognizedFormat`` is raised). If you do not supply a sequence of + drivers, the module's ``extraction_drivers`` constant will be used, which + means that ``unpack_zipfile`` and ``unpack_tarfile`` will be tried, in that + order. + """ + for driver in drivers or extraction_drivers: + try: + driver(filename, extract_dir, progress_filter) + except UnrecognizedFormat: + continue + else: + return + else: + raise UnrecognizedFormat(f"Not a recognized archive type: {filename}") + + +def unpack_directory(filename, extract_dir, progress_filter=default_filter) -> None: + """ "Unpack" a directory, using the same interface as for archives + + Raises ``UnrecognizedFormat`` if `filename` is not a directory + """ + if not os.path.isdir(filename): + raise UnrecognizedFormat(f"{filename} is not a directory") + + paths = { + filename: ('', extract_dir), + } + for base, dirs, files in os.walk(filename): + src, dst = paths[base] + for d in dirs: + paths[os.path.join(base, d)] = src + d + '/', os.path.join(dst, d) + for f in files: + target = os.path.join(dst, f) + target = progress_filter(src + f, target) + if not target: + # skip non-files + continue + ensure_directory(target) + f = os.path.join(base, f) + shutil.copyfile(f, target) + shutil.copystat(f, target) + + +def unpack_zipfile(filename, extract_dir, progress_filter=default_filter) -> None: + """Unpack zip `filename` to `extract_dir` + + Raises ``UnrecognizedFormat`` if `filename` is not a zipfile (as determined + by ``zipfile.is_zipfile()``). See ``unpack_archive()`` for an explanation + of the `progress_filter` argument. + """ + + if not zipfile.is_zipfile(filename): + raise UnrecognizedFormat(f"{filename} is not a zip file") + + with zipfile.ZipFile(filename) as z: + _unpack_zipfile_obj(z, extract_dir, progress_filter) + + +def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter): + """Internal/private API used by other parts of setuptools. + Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile` + object instead of a filename. + """ + for info in zipfile_obj.infolist(): + name = info.filename + + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name.split('/'): + continue + + target = os.path.join(extract_dir, *name.split('/')) + target = progress_filter(name, target) + if not target: + continue + if name.endswith('/'): + # directory + ensure_directory(target) + else: + # file + ensure_directory(target) + data = zipfile_obj.read(info.filename) + with open(target, 'wb') as f: + f.write(data) + unix_attributes = info.external_attr >> 16 + if unix_attributes: + os.chmod(target, unix_attributes) + + +def _resolve_tar_file_or_dir(tar_obj, tar_member_obj): + """Resolve any links and extract link targets as normal files.""" + while tar_member_obj is not None and ( + tar_member_obj.islnk() or tar_member_obj.issym() + ): + linkpath = tar_member_obj.linkname + if tar_member_obj.issym(): + base = posixpath.dirname(tar_member_obj.name) + linkpath = posixpath.join(base, linkpath) + linkpath = posixpath.normpath(linkpath) + tar_member_obj = tar_obj._getmember(linkpath) + + is_file_or_dir = tar_member_obj is not None and ( + tar_member_obj.isfile() or tar_member_obj.isdir() + ) + if is_file_or_dir: + return tar_member_obj + + raise LookupError('Got unknown file type') + + +def _iter_open_tar(tar_obj, extract_dir, progress_filter): + """Emit member-destination pairs from a tar archive.""" + # don't do any chowning! + tar_obj.chown = lambda *args: None + + with contextlib.closing(tar_obj): + for member in tar_obj: + name = member.name + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name.split('/'): + continue + + prelim_dst = os.path.join(extract_dir, *name.split('/')) + + try: + member = _resolve_tar_file_or_dir(tar_obj, member) + except LookupError: + continue + + final_dst = progress_filter(name, prelim_dst) + if not final_dst: + continue + + if final_dst.endswith(os.sep): + final_dst = final_dst[:-1] + + yield member, final_dst + + +def unpack_tarfile(filename, extract_dir, progress_filter=default_filter) -> bool: + """Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir` + + Raises ``UnrecognizedFormat`` if `filename` is not a tarfile (as determined + by ``tarfile.open()``). See ``unpack_archive()`` for an explanation + of the `progress_filter` argument. + """ + try: + tarobj = tarfile.open(filename) + except tarfile.TarError as e: + raise UnrecognizedFormat( + f"{filename} is not a compressed or uncompressed tar file" + ) from e + + for member, final_dst in _iter_open_tar( + tarobj, + extract_dir, + progress_filter, + ): + try: + # XXX Ugh + tarobj._extract_member(member, final_dst) + except tarfile.ExtractError: + # chown/chmod/mkfifo/mknode/makedev failed + pass + + return True + + +extraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile diff --git a/lib/python3.10/site-packages/setuptools/build_meta.py b/lib/python3.10/site-packages/setuptools/build_meta.py new file mode 100644 index 0000000000000000000000000000000000000000..00fa5e1f7048c8781cfc4838f705e122fd547825 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/build_meta.py @@ -0,0 +1,560 @@ +"""A PEP 517 interface to setuptools + +Previously, when a user or a command line tool (let's call it a "frontend") +needed to make a request of setuptools to take a certain action, for +example, generating a list of installation requirements, the frontend +would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line. + +PEP 517 defines a different method of interfacing with setuptools. Rather +than calling "setup.py" directly, the frontend should: + + 1. Set the current directory to the directory with a setup.py file + 2. Import this module into a safe python interpreter (one in which + setuptools can potentially set global variables or crash hard). + 3. Call one of the functions defined in PEP 517. + +What each function does is defined in PEP 517. However, here is a "casual" +definition of the functions (this definition should not be relied on for +bug reports or API stability): + + - `build_wheel`: build a wheel in the folder and return the basename + - `get_requires_for_build_wheel`: get the `setup_requires` to build + - `prepare_metadata_for_build_wheel`: get the `install_requires` + - `build_sdist`: build an sdist in the folder and return the basename + - `get_requires_for_build_sdist`: get the `setup_requires` to build + +Again, this is not a formal definition! Just a "taste" of the module. +""" + +from __future__ import annotations + +import contextlib +import io +import os +import shlex +import shutil +import sys +import tempfile +import tokenize +import warnings +from collections.abc import Iterable, Iterator, Mapping +from pathlib import Path +from typing import TYPE_CHECKING, Union + +import setuptools + +from . import errors +from ._path import StrPath, same_path +from ._reqs import parse_strings +from .warnings import SetuptoolsDeprecationWarning + +import distutils +from distutils.util import strtobool + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +__all__ = [ + 'get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + 'build_wheel', + 'build_sdist', + 'get_requires_for_build_editable', + 'prepare_metadata_for_build_editable', + 'build_editable', + '__legacy__', + 'SetupRequirementsError', +] + +SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower() +LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-") + + +class SetupRequirementsError(BaseException): + def __init__(self, specifiers) -> None: + self.specifiers = specifiers + + +class Distribution(setuptools.dist.Distribution): + def fetch_build_eggs(self, specifiers): + specifier_list = list(parse_strings(specifiers)) + + raise SetupRequirementsError(specifier_list) + + @classmethod + @contextlib.contextmanager + def patch(cls): + """ + Replace + distutils.dist.Distribution with this class + for the duration of this context. + """ + orig = distutils.core.Distribution + distutils.core.Distribution = cls # type: ignore[misc] # monkeypatching + try: + yield + finally: + distutils.core.Distribution = orig # type: ignore[misc] # monkeypatching + + +@contextlib.contextmanager +def no_install_setup_requires(): + """Temporarily disable installing setup_requires + + Under PEP 517, the backend reports build dependencies to the frontend, + and the frontend is responsible for ensuring they're installed. + So setuptools (acting as a backend) should not try to install them. + """ + orig = setuptools._install_setup_requires + setuptools._install_setup_requires = lambda attrs: None + try: + yield + finally: + setuptools._install_setup_requires = orig + + +def _get_immediate_subdirectories(a_dir): + return [ + name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name)) + ] + + +def _file_with_extension(directory: StrPath, extension: str | tuple[str, ...]): + matching = (f for f in os.listdir(directory) if f.endswith(extension)) + try: + (file,) = matching + except ValueError: + raise ValueError( + 'No distribution was found. Ensure that `setup.py` ' + 'is not empty and that it calls `setup()`.' + ) from None + return file + + +def _open_setup_script(setup_script): + if not os.path.exists(setup_script): + # Supply a default setup.py + return io.StringIO("from setuptools import setup; setup()") + + return tokenize.open(setup_script) + + +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'setup.py install is deprecated') + yield + + +_ConfigSettings: TypeAlias = Union[Mapping[str, Union[str, list[str], None]], None] +""" +Currently the user can run:: + + pip install -e . --config-settings key=value + python -m build -C--key=value -C key=value + +- pip will pass both key and value as strings and overwriting repeated keys + (pypa/pip#11059). +- build will accumulate values associated with repeated keys in a list. + It will also accept keys with no associated value. + This means that an option passed by build can be ``str | list[str] | None``. +- PEP 517 specifies that ``config_settings`` is an optional dict. +""" + + +class _ConfigSettingsTranslator: + """Translate ``config_settings`` into distutils-style command arguments. + Only a limited number of options is currently supported. + """ + + # See pypa/setuptools#1928 pypa/setuptools#2491 + + def _get_config(self, key: str, config_settings: _ConfigSettings) -> list[str]: + """ + Get the value of a specific key in ``config_settings`` as a list of strings. + + >>> fn = _ConfigSettingsTranslator()._get_config + >>> fn("--global-option", None) + [] + >>> fn("--global-option", {}) + [] + >>> fn("--global-option", {'--global-option': 'foo'}) + ['foo'] + >>> fn("--global-option", {'--global-option': ['foo']}) + ['foo'] + >>> fn("--global-option", {'--global-option': 'foo'}) + ['foo'] + >>> fn("--global-option", {'--global-option': 'foo bar'}) + ['foo', 'bar'] + """ + cfg = config_settings or {} + opts = cfg.get(key) or [] + return shlex.split(opts) if isinstance(opts, str) else opts + + def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + Let the user specify ``verbose`` or ``quiet`` + escape hatch via + ``--global-option``. + Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools, + so we just have to cover the basic scenario ``-v``. + + >>> fn = _ConfigSettingsTranslator()._global_args + >>> list(fn(None)) + [] + >>> list(fn({"verbose": "False"})) + ['-q'] + >>> list(fn({"verbose": "1"})) + ['-v'] + >>> list(fn({"--verbose": None})) + ['-v'] + >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"})) + ['-v', '-q', '--no-user-cfg'] + >>> list(fn({"--quiet": None})) + ['-q'] + """ + cfg = config_settings or {} + falsey = {"false", "no", "0", "off"} + if "verbose" in cfg or "--verbose" in cfg: + level = str(cfg.get("verbose") or cfg.get("--verbose") or "1") + yield ("-q" if level.lower() in falsey else "-v") + if "quiet" in cfg or "--quiet" in cfg: + level = str(cfg.get("quiet") or cfg.get("--quiet") or "1") + yield ("-v" if level.lower() in falsey else "-q") + + yield from self._get_config("--global-option", config_settings) + + def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + The ``dist_info`` command accepts ``tag-date`` and ``tag-build``. + + .. warning:: + We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel`` + commands run in ``build_sdist`` and ``build_wheel`` to reuse the egg-info + directory created in ``prepare_metadata_for_build_wheel``. + + >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args + >>> list(fn(None)) + [] + >>> list(fn({"tag-date": "False"})) + ['--no-date'] + >>> list(fn({"tag-date": None})) + ['--no-date'] + >>> list(fn({"tag-date": "true", "tag-build": ".a"})) + ['--tag-date', '--tag-build', '.a'] + """ + cfg = config_settings or {} + if "tag-date" in cfg: + val = strtobool(str(cfg["tag-date"] or "false")) + yield ("--tag-date" if val else "--no-date") + if "tag-build" in cfg: + yield from ["--tag-build", str(cfg["tag-build"])] + + def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + The ``editable_wheel`` command accepts ``editable-mode=strict``. + + >>> fn = _ConfigSettingsTranslator()._editable_args + >>> list(fn(None)) + [] + >>> list(fn({"editable-mode": "strict"})) + ['--mode', 'strict'] + """ + cfg = config_settings or {} + mode = cfg.get("editable-mode") or cfg.get("editable_mode") + if not mode: + return + yield from ["--mode", str(mode)] + + def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + Users may expect to pass arbitrary lists of arguments to a command + via "--global-option" (example provided in PEP 517 of a "escape hatch"). + + >>> fn = _ConfigSettingsTranslator()._arbitrary_args + >>> list(fn(None)) + [] + >>> list(fn({})) + [] + >>> list(fn({'--build-option': 'foo'})) + ['foo'] + >>> list(fn({'--build-option': ['foo']})) + ['foo'] + >>> list(fn({'--build-option': 'foo'})) + ['foo'] + >>> list(fn({'--build-option': 'foo bar'})) + ['foo', 'bar'] + >>> list(fn({'--global-option': 'foo'})) + [] + """ + yield from self._get_config("--build-option", config_settings) + + +class _BuildMetaBackend(_ConfigSettingsTranslator): + def _get_build_requires( + self, config_settings: _ConfigSettings, requirements: list[str] + ): + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + "egg_info", + ] + try: + with Distribution.patch(): + self.run_setup() + except SetupRequirementsError as e: + requirements += e.specifiers + + return requirements + + def run_setup(self, setup_script: str = 'setup.py'): + # Note that we can reuse our build directory between calls + # Correctness comes first, then optimization later + __file__ = os.path.abspath(setup_script) + __name__ = '__main__' + + with _open_setup_script(__file__) as f: + code = f.read().replace(r'\r\n', r'\n') + + try: + exec(code, locals()) + except SystemExit as e: + if e.code: + raise + # We ignore exit code indicating success + SetuptoolsDeprecationWarning.emit( + "Running `setup.py` directly as CLI tool is deprecated.", + "Please avoid using `sys.exit(0)` or similar statements " + "that don't fit in the paradigm of a configuration file.", + see_url="https://blog.ganssle.io/articles/2021/10/" + "setup-py-deprecated.html", + ) + + def get_requires_for_build_wheel(self, config_settings: _ConfigSettings = None): + return self._get_build_requires(config_settings, requirements=[]) + + def get_requires_for_build_sdist(self, config_settings: _ConfigSettings = None): + return self._get_build_requires(config_settings, requirements=[]) + + def _bubble_up_info_directory( + self, metadata_directory: StrPath, suffix: str + ) -> str: + """ + PEP 517 requires that the .dist-info directory be placed in the + metadata_directory. To comply, we MUST copy the directory to the root. + + Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`. + """ + info_dir = self._find_info_directory(metadata_directory, suffix) + if not same_path(info_dir.parent, metadata_directory): + shutil.move(str(info_dir), metadata_directory) + # PEP 517 allow other files and dirs to exist in metadata_directory + return info_dir.name + + def _find_info_directory(self, metadata_directory: StrPath, suffix: str) -> Path: + for parent, dirs, _ in os.walk(metadata_directory): + candidates = [f for f in dirs if f.endswith(suffix)] + + if len(candidates) != 0 or len(dirs) != 1: + assert len(candidates) == 1, f"Multiple {suffix} directories found" + return Path(parent, candidates[0]) + + msg = f"No {suffix} directory found in {metadata_directory}" + raise errors.InternalError(msg) + + def prepare_metadata_for_build_wheel( + self, metadata_directory: StrPath, config_settings: _ConfigSettings = None + ): + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + "dist_info", + "--output-dir", + str(metadata_directory), + "--keep-egg-info", + ] + with no_install_setup_requires(): + self.run_setup() + + self._bubble_up_info_directory(metadata_directory, ".egg-info") + return self._bubble_up_info_directory(metadata_directory, ".dist-info") + + def _build_with_temp_dir( + self, + setup_command: Iterable[str], + result_extension: str | tuple[str, ...], + result_directory: StrPath, + config_settings: _ConfigSettings, + arbitrary_args: Iterable[str] = (), + ): + result_directory = os.path.abspath(result_directory) + + # Build in a temporary directory, then copy to the target. + os.makedirs(result_directory, exist_ok=True) + + with tempfile.TemporaryDirectory( + prefix=".tmp-", dir=result_directory + ) as tmp_dist_dir: + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + *setup_command, + "--dist-dir", + tmp_dist_dir, + *arbitrary_args, + ] + with no_install_setup_requires(): + self.run_setup() + + result_basename = _file_with_extension(tmp_dist_dir, result_extension) + result_path = os.path.join(result_directory, result_basename) + if os.path.exists(result_path): + # os.rename will fail overwriting on non-Unix. + os.remove(result_path) + os.rename(os.path.join(tmp_dist_dir, result_basename), result_path) + + return result_basename + + def build_wheel( + self, + wheel_directory: StrPath, + config_settings: _ConfigSettings = None, + metadata_directory: StrPath | None = None, + ): + def _build(cmd: list[str]): + with suppress_known_deprecation(): + return self._build_with_temp_dir( + cmd, + '.whl', + wheel_directory, + config_settings, + self._arbitrary_args(config_settings), + ) + + if metadata_directory is None: + return _build(['bdist_wheel']) + + try: + return _build(['bdist_wheel', '--dist-info-dir', str(metadata_directory)]) + except SystemExit as ex: # pragma: nocover + # pypa/setuptools#4683 + if "--dist-info-dir not recognized" not in str(ex): + raise + _IncompatibleBdistWheel.emit() + return _build(['bdist_wheel']) + + def build_sdist( + self, sdist_directory: StrPath, config_settings: _ConfigSettings = None + ): + return self._build_with_temp_dir( + ['sdist', '--formats', 'gztar'], '.tar.gz', sdist_directory, config_settings + ) + + def _get_dist_info_dir(self, metadata_directory: StrPath | None) -> str | None: + if not metadata_directory: + return None + dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info")) + assert len(dist_info_candidates) <= 1 + return str(dist_info_candidates[0]) if dist_info_candidates else None + + if not LEGACY_EDITABLE: + # PEP660 hooks: + # build_editable + # get_requires_for_build_editable + # prepare_metadata_for_build_editable + def build_editable( + self, + wheel_directory: StrPath, + config_settings: _ConfigSettings = None, + metadata_directory: StrPath | None = None, + ): + # XXX can or should we hide our editable_wheel command normally? + info_dir = self._get_dist_info_dir(metadata_directory) + opts = ["--dist-info-dir", info_dir] if info_dir else [] + cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)] + with suppress_known_deprecation(): + return self._build_with_temp_dir( + cmd, ".whl", wheel_directory, config_settings + ) + + def get_requires_for_build_editable( + self, config_settings: _ConfigSettings = None + ): + return self.get_requires_for_build_wheel(config_settings) + + def prepare_metadata_for_build_editable( + self, metadata_directory: StrPath, config_settings: _ConfigSettings = None + ): + return self.prepare_metadata_for_build_wheel( + metadata_directory, config_settings + ) + + +class _BuildMetaLegacyBackend(_BuildMetaBackend): + """Compatibility backend for setuptools + + This is a version of setuptools.build_meta that endeavors + to maintain backwards + compatibility with pre-PEP 517 modes of invocation. It + exists as a temporary + bridge between the old packaging mechanism and the new + packaging mechanism, + and will eventually be removed. + """ + + def run_setup(self, setup_script: str = 'setup.py'): + # In order to maintain compatibility with scripts assuming that + # the setup.py script is in a directory on the PYTHONPATH, inject + # '' into sys.path. (pypa/setuptools#1642) + sys_path = list(sys.path) # Save the original path + + script_dir = os.path.dirname(os.path.abspath(setup_script)) + if script_dir not in sys.path: + sys.path.insert(0, script_dir) + + # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to + # get the directory of the source code. They expect it to refer to the + # setup.py script. + sys_argv_0 = sys.argv[0] + sys.argv[0] = setup_script + + try: + super().run_setup(setup_script=setup_script) + finally: + # While PEP 517 frontends should be calling each hook in a fresh + # subprocess according to the standard (and thus it should not be + # strictly necessary to restore the old sys.path), we'll restore + # the original path so that the path manipulation does not persist + # within the hook after run_setup is called. + sys.path[:] = sys_path + sys.argv[0] = sys_argv_0 + + +class _IncompatibleBdistWheel(SetuptoolsDeprecationWarning): + _SUMMARY = "wheel.bdist_wheel is deprecated, please import it from setuptools" + _DETAILS = """ + Ensure that any custom bdist_wheel implementation is a subclass of + setuptools.command.bdist_wheel.bdist_wheel. + """ + _DUE_DATE = (2025, 10, 15) + # Initially introduced in 2024/10/15, but maybe too disruptive to be enforced? + _SEE_URL = "https://github.com/pypa/wheel/pull/631" + + +# The primary backend +_BACKEND = _BuildMetaBackend() + +get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel +get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist +prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel +build_wheel = _BACKEND.build_wheel +build_sdist = _BACKEND.build_sdist + +if not LEGACY_EDITABLE: + get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable + prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable + build_editable = _BACKEND.build_editable + + +# The legacy backend +__legacy__ = _BuildMetaLegacyBackend() diff --git a/lib/python3.10/site-packages/setuptools/cli-32.exe b/lib/python3.10/site-packages/setuptools/cli-32.exe new file mode 100644 index 0000000000000000000000000000000000000000..65c3cd99cc7433f271a5b9387abdd1ddb949d1a6 Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/cli-32.exe differ diff --git a/lib/python3.10/site-packages/setuptools/cli-64.exe b/lib/python3.10/site-packages/setuptools/cli-64.exe new file mode 100644 index 0000000000000000000000000000000000000000..3ea50eebfe3f0113b231a318cc1ad6e238afd60d Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/cli-64.exe differ diff --git a/lib/python3.10/site-packages/setuptools/cli-arm64.exe b/lib/python3.10/site-packages/setuptools/cli-arm64.exe new file mode 100644 index 0000000000000000000000000000000000000000..da96455a07a0bad4cde5dc5626544325f82c722b Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/cli-arm64.exe differ diff --git a/lib/python3.10/site-packages/setuptools/cli.exe b/lib/python3.10/site-packages/setuptools/cli.exe new file mode 100644 index 0000000000000000000000000000000000000000..65c3cd99cc7433f271a5b9387abdd1ddb949d1a6 Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/cli.exe differ diff --git a/lib/python3.10/site-packages/setuptools/depends.py b/lib/python3.10/site-packages/setuptools/depends.py new file mode 100644 index 0000000000000000000000000000000000000000..e5223b79561c36d9b6c45ead78288098e1cb0f1d --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/depends.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import contextlib +import dis +import marshal +import sys +from types import CodeType +from typing import Any, Literal, TypeVar + +from packaging.version import Version + +from . import _imp +from ._imp import PY_COMPILED, PY_FROZEN, PY_SOURCE, find_module + +_T = TypeVar("_T") + +__all__ = ['Require', 'find_module'] + + +class Require: + """A prerequisite to building or installing a distribution""" + + def __init__( + self, + name, + requested_version, + module, + homepage: str = '', + attribute=None, + format=None, + ) -> None: + if format is None and requested_version is not None: + format = Version + + if format is not None: + requested_version = format(requested_version) + if attribute is None: + attribute = '__version__' + + self.__dict__.update(locals()) + del self.self + + def full_name(self): + """Return full package/distribution name, w/version""" + if self.requested_version is not None: + return f'{self.name}-{self.requested_version}' + return self.name + + def version_ok(self, version): + """Is 'version' sufficiently up-to-date?""" + return ( + self.attribute is None + or self.format is None + or str(version) != "unknown" + and self.format(version) >= self.requested_version + ) + + def get_version( + self, paths=None, default: _T | Literal["unknown"] = "unknown" + ) -> _T | Literal["unknown"] | None | Any: + """Get version number of installed module, 'None', or 'default' + + Search 'paths' for module. If not found, return 'None'. If found, + return the extracted version attribute, or 'default' if no version + attribute was specified, or the value cannot be determined without + importing the module. The version is formatted according to the + requirement's version format (if any), unless it is 'None' or the + supplied 'default'. + """ + + if self.attribute is None: + try: + f, _p, _i = find_module(self.module, paths) + except ImportError: + return None + if f: + f.close() + return default + + v = get_module_constant(self.module, self.attribute, default, paths) + + if v is not None and v is not default and self.format is not None: + return self.format(v) + + return v + + def is_present(self, paths=None): + """Return true if dependency is present on 'paths'""" + return self.get_version(paths) is not None + + def is_current(self, paths=None): + """Return true if dependency is present and up-to-date on 'paths'""" + version = self.get_version(paths) + if version is None: + return False + return self.version_ok(str(version)) + + +def maybe_close(f): + @contextlib.contextmanager + def empty(): + yield + return + + if not f: + return empty() + + return contextlib.closing(f) + + +# Some objects are not available on some platforms. +# XXX it'd be better to test assertions about bytecode instead. +if not sys.platform.startswith('java') and sys.platform != 'cli': + + def get_module_constant( + module, symbol, default: _T | int = -1, paths=None + ) -> _T | int | None | Any: + """Find 'module' by searching 'paths', and extract 'symbol' + + Return 'None' if 'module' does not exist on 'paths', or it does not define + 'symbol'. If the module defines 'symbol' as a constant, return the + constant. Otherwise, return 'default'.""" + + try: + f, path, (_suffix, _mode, kind) = info = find_module(module, paths) + except ImportError: + # Module doesn't exist + return None + + with maybe_close(f): + if kind == PY_COMPILED: + f.read(8) # skip magic & date + code = marshal.load(f) + elif kind == PY_FROZEN: + code = _imp.get_frozen_object(module, paths) + elif kind == PY_SOURCE: + code = compile(f.read(), path, 'exec') + else: + # Not something we can parse; we'll have to import it. :( + imported = _imp.get_module(module, paths, info) + return getattr(imported, symbol, None) + + return extract_constant(code, symbol, default) + + def extract_constant( + code: CodeType, symbol: str, default: _T | int = -1 + ) -> _T | int | None | Any: + """Extract the constant value of 'symbol' from 'code' + + If the name 'symbol' is bound to a constant value by the Python code + object 'code', return that value. If 'symbol' is bound to an expression, + return 'default'. Otherwise, return 'None'. + + Return value is based on the first assignment to 'symbol'. 'symbol' must + be a global, or at least a non-"fast" local in the code block. That is, + only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol' + must be present in 'code.co_names'. + """ + if symbol not in code.co_names: + # name's not there, can't possibly be an assignment + return None + + name_idx = list(code.co_names).index(symbol) + + STORE_NAME = dis.opmap['STORE_NAME'] + STORE_GLOBAL = dis.opmap['STORE_GLOBAL'] + LOAD_CONST = dis.opmap['LOAD_CONST'] + + const = default + + for byte_code in dis.Bytecode(code): + op = byte_code.opcode + arg = byte_code.arg + + if op == LOAD_CONST: + assert arg is not None + const = code.co_consts[arg] + elif arg == name_idx and (op == STORE_NAME or op == STORE_GLOBAL): + return const + else: + const = default + + return None + + __all__ += ['get_module_constant', 'extract_constant'] diff --git a/lib/python3.10/site-packages/setuptools/discovery.py b/lib/python3.10/site-packages/setuptools/discovery.py new file mode 100644 index 0000000000000000000000000000000000000000..c88839918562bad12f1a2e72309f1eacfe23349c --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/discovery.py @@ -0,0 +1,614 @@ +"""Automatic discovery of Python modules and packages (for inclusion in the +distribution) and other config values. + +For the purposes of this module, the following nomenclature is used: + +- "src-layout": a directory representing a Python project that contains a "src" + folder. Everything under the "src" folder is meant to be included in the + distribution when packaging the project. Example:: + + . + ├── tox.ini + ├── pyproject.toml + └── src/ + └── mypkg/ + ├── __init__.py + ├── mymodule.py + └── my_data_file.txt + +- "flat-layout": a Python project that does not use "src-layout" but instead + have a directory under the project root for each package:: + + . + ├── tox.ini + ├── pyproject.toml + └── mypkg/ + ├── __init__.py + ├── mymodule.py + └── my_data_file.txt + +- "single-module": a project that contains a single Python script direct under + the project root (no directory used):: + + . + ├── tox.ini + ├── pyproject.toml + └── mymodule.py + +""" + +from __future__ import annotations + +import itertools +import os +from collections.abc import Iterable, Iterator, Mapping +from fnmatch import fnmatchcase +from glob import glob +from pathlib import Path +from typing import TYPE_CHECKING, ClassVar + +import _distutils_hack.override # noqa: F401 + +from ._path import StrPath + +from distutils import log +from distutils.util import convert_path + +if TYPE_CHECKING: + from setuptools import Distribution + +chain_iter = itertools.chain.from_iterable + + +def _valid_name(path: StrPath) -> bool: + # Ignore invalid names that cannot be imported directly + return os.path.basename(path).isidentifier() + + +class _Filter: + """ + Given a list of patterns, create a callable that will be true only if + the input matches at least one of the patterns. + """ + + def __init__(self, *patterns: str) -> None: + self._patterns = dict.fromkeys(patterns) + + def __call__(self, item: str) -> bool: + return any(fnmatchcase(item, pat) for pat in self._patterns) + + def __contains__(self, item: str) -> bool: + return item in self._patterns + + +class _Finder: + """Base class that exposes functionality for module/package finders""" + + ALWAYS_EXCLUDE: ClassVar[tuple[str, ...]] = () + DEFAULT_EXCLUDE: ClassVar[tuple[str, ...]] = () + + @classmethod + def find( + cls, + where: StrPath = '.', + exclude: Iterable[str] = (), + include: Iterable[str] = ('*',), + ) -> list[str]: + """Return a list of all Python items (packages or modules, depending on + the finder implementation) found within directory 'where'. + + 'where' is the root directory which will be searched. + It should be supplied as a "cross-platform" (i.e. URL-style) path; + it will be converted to the appropriate local path syntax. + + 'exclude' is a sequence of names to exclude; '*' can be used + as a wildcard in the names. + When finding packages, 'foo.*' will exclude all subpackages of 'foo' + (but not 'foo' itself). + + 'include' is a sequence of names to include. + If it's specified, only the named items will be included. + If it's not specified, all found items will be included. + 'include' can contain shell style wildcard patterns just like + 'exclude'. + """ + + exclude = exclude or cls.DEFAULT_EXCLUDE + return list( + cls._find_iter( + convert_path(str(where)), + _Filter(*cls.ALWAYS_EXCLUDE, *exclude), + _Filter(*include), + ) + ) + + @classmethod + def _find_iter( + cls, where: StrPath, exclude: _Filter, include: _Filter + ) -> Iterator[str]: + raise NotImplementedError + + +class PackageFinder(_Finder): + """ + Generate a list of all Python packages found within a directory + """ + + ALWAYS_EXCLUDE = ("ez_setup", "*__pycache__") + + @classmethod + def _find_iter( + cls, where: StrPath, exclude: _Filter, include: _Filter + ) -> Iterator[str]: + """ + All the packages found in 'where' that pass the 'include' filter, but + not the 'exclude' filter. + """ + for root, dirs, files in os.walk(str(where), followlinks=True): + # Copy dirs to iterate over it, then empty dirs. + all_dirs = dirs[:] + dirs[:] = [] + + for dir in all_dirs: + full_path = os.path.join(root, dir) + rel_path = os.path.relpath(full_path, where) + package = rel_path.replace(os.path.sep, '.') + + # Skip directory trees that are not valid packages + if '.' in dir or not cls._looks_like_package(full_path, package): + continue + + # Should this package be included? + if include(package) and not exclude(package): + yield package + + # Early pruning if there is nothing else to be scanned + if f"{package}*" in exclude or f"{package}.*" in exclude: + continue + + # Keep searching subdirectories, as there may be more packages + # down there, even if the parent was excluded. + dirs.append(dir) + + @staticmethod + def _looks_like_package(path: StrPath, _package_name: str) -> bool: + """Does a directory look like a package?""" + return os.path.isfile(os.path.join(path, '__init__.py')) + + +class PEP420PackageFinder(PackageFinder): + @staticmethod + def _looks_like_package(_path: StrPath, _package_name: str) -> bool: + return True + + +class ModuleFinder(_Finder): + """Find isolated Python modules. + This function will **not** recurse subdirectories. + """ + + @classmethod + def _find_iter( + cls, where: StrPath, exclude: _Filter, include: _Filter + ) -> Iterator[str]: + for file in glob(os.path.join(where, "*.py")): + module, _ext = os.path.splitext(os.path.basename(file)) + + if not cls._looks_like_module(module): + continue + + if include(module) and not exclude(module): + yield module + + _looks_like_module = staticmethod(_valid_name) + + +# We have to be extra careful in the case of flat layout to not include files +# and directories not meant for distribution (e.g. tool-related) + + +class FlatLayoutPackageFinder(PEP420PackageFinder): + _EXCLUDE = ( + "ci", + "bin", + "debian", + "doc", + "docs", + "documentation", + "manpages", + "news", + "newsfragments", + "changelog", + "test", + "tests", + "unit_test", + "unit_tests", + "example", + "examples", + "scripts", + "tools", + "util", + "utils", + "python", + "build", + "dist", + "venv", + "env", + "requirements", + # ---- Task runners / Build tools ---- + "tasks", # invoke + "fabfile", # fabric + "site_scons", # SCons + # ---- Other tools ---- + "benchmark", + "benchmarks", + "exercise", + "exercises", + "htmlcov", # Coverage.py + # ---- Hidden directories/Private packages ---- + "[._]*", + ) + + DEFAULT_EXCLUDE = tuple(chain_iter((p, f"{p}.*") for p in _EXCLUDE)) + """Reserved package names""" + + @staticmethod + def _looks_like_package(_path: StrPath, package_name: str) -> bool: + names = package_name.split('.') + # Consider PEP 561 + root_pkg_is_valid = names[0].isidentifier() or names[0].endswith("-stubs") + return root_pkg_is_valid and all(name.isidentifier() for name in names[1:]) + + +class FlatLayoutModuleFinder(ModuleFinder): + DEFAULT_EXCLUDE = ( + "setup", + "conftest", + "test", + "tests", + "example", + "examples", + "build", + # ---- Task runners ---- + "toxfile", + "noxfile", + "pavement", + "dodo", + "tasks", + "fabfile", + # ---- Other tools ---- + "[Ss][Cc]onstruct", # SCons + "conanfile", # Connan: C/C++ build tool + "manage", # Django + "benchmark", + "benchmarks", + "exercise", + "exercises", + # ---- Hidden files/Private modules ---- + "[._]*", + ) + """Reserved top-level module names""" + + +def _find_packages_within(root_pkg: str, pkg_dir: StrPath) -> list[str]: + nested = PEP420PackageFinder.find(pkg_dir) + return [root_pkg] + [".".join((root_pkg, n)) for n in nested] + + +class ConfigDiscovery: + """Fill-in metadata and options that can be automatically derived + (from other metadata/options, the file system or conventions) + """ + + def __init__(self, distribution: Distribution) -> None: + self.dist = distribution + self._called = False + self._disabled = False + self._skip_ext_modules = False + + def _disable(self): + """Internal API to disable automatic discovery""" + self._disabled = True + + def _ignore_ext_modules(self): + """Internal API to disregard ext_modules. + + Normally auto-discovery would not be triggered if ``ext_modules`` are set + (this is done for backward compatibility with existing packages relying on + ``setup.py`` or ``setup.cfg``). However, ``setuptools`` can call this function + to ignore given ``ext_modules`` and proceed with the auto-discovery if + ``packages`` and ``py_modules`` are not given (e.g. when using pyproject.toml + metadata). + """ + self._skip_ext_modules = True + + @property + def _root_dir(self) -> StrPath: + # The best is to wait until `src_root` is set in dist, before using _root_dir. + return self.dist.src_root or os.curdir + + @property + def _package_dir(self) -> dict[str, str]: + if self.dist.package_dir is None: + return {} + return self.dist.package_dir + + def __call__( + self, force: bool = False, name: bool = True, ignore_ext_modules: bool = False + ): + """Automatically discover missing configuration fields + and modifies the given ``distribution`` object in-place. + + Note that by default this will only have an effect the first time the + ``ConfigDiscovery`` object is called. + + To repeatedly invoke automatic discovery (e.g. when the project + directory changes), please use ``force=True`` (or create a new + ``ConfigDiscovery`` instance). + """ + if force is False and (self._called or self._disabled): + # Avoid overhead of multiple calls + return + + self._analyse_package_layout(ignore_ext_modules) + if name: + self.analyse_name() # depends on ``packages`` and ``py_modules`` + + self._called = True + + def _explicitly_specified(self, ignore_ext_modules: bool) -> bool: + """``True`` if the user has specified some form of package/module listing""" + ignore_ext_modules = ignore_ext_modules or self._skip_ext_modules + ext_modules = not (self.dist.ext_modules is None or ignore_ext_modules) + return ( + self.dist.packages is not None + or self.dist.py_modules is not None + or ext_modules + or hasattr(self.dist, "configuration") + and self.dist.configuration + # ^ Some projects use numpy.distutils.misc_util.Configuration + ) + + def _analyse_package_layout(self, ignore_ext_modules: bool) -> bool: + if self._explicitly_specified(ignore_ext_modules): + # For backward compatibility, just try to find modules/packages + # when nothing is given + return True + + log.debug( + "No `packages` or `py_modules` configuration, performing " + "automatic discovery." + ) + + return ( + self._analyse_explicit_layout() + or self._analyse_src_layout() + # flat-layout is the trickiest for discovery so it should be last + or self._analyse_flat_layout() + ) + + def _analyse_explicit_layout(self) -> bool: + """The user can explicitly give a package layout via ``package_dir``""" + package_dir = self._package_dir.copy() # don't modify directly + package_dir.pop("", None) # This falls under the "src-layout" umbrella + root_dir = self._root_dir + + if not package_dir: + return False + + log.debug(f"`explicit-layout` detected -- analysing {package_dir}") + pkgs = chain_iter( + _find_packages_within(pkg, os.path.join(root_dir, parent_dir)) + for pkg, parent_dir in package_dir.items() + ) + self.dist.packages = list(pkgs) + log.debug(f"discovered packages -- {self.dist.packages}") + return True + + def _analyse_src_layout(self) -> bool: + """Try to find all packages or modules under the ``src`` directory + (or anything pointed by ``package_dir[""]``). + + The "src-layout" is relatively safe for automatic discovery. + We assume that everything within is meant to be included in the + distribution. + + If ``package_dir[""]`` is not given, but the ``src`` directory exists, + this function will set ``package_dir[""] = "src"``. + """ + package_dir = self._package_dir + src_dir = os.path.join(self._root_dir, package_dir.get("", "src")) + if not os.path.isdir(src_dir): + return False + + log.debug(f"`src-layout` detected -- analysing {src_dir}") + package_dir.setdefault("", os.path.basename(src_dir)) + self.dist.package_dir = package_dir # persist eventual modifications + self.dist.packages = PEP420PackageFinder.find(src_dir) + self.dist.py_modules = ModuleFinder.find(src_dir) + log.debug(f"discovered packages -- {self.dist.packages}") + log.debug(f"discovered py_modules -- {self.dist.py_modules}") + return True + + def _analyse_flat_layout(self) -> bool: + """Try to find all packages and modules under the project root. + + Since the ``flat-layout`` is more dangerous in terms of accidentally including + extra files/directories, this function is more conservative and will raise an + error if multiple packages or modules are found. + + This assumes that multi-package dists are uncommon and refuse to support that + use case in order to be able to prevent unintended errors. + """ + log.debug(f"`flat-layout` detected -- analysing {self._root_dir}") + return self._analyse_flat_packages() or self._analyse_flat_modules() + + def _analyse_flat_packages(self) -> bool: + self.dist.packages = FlatLayoutPackageFinder.find(self._root_dir) + top_level = remove_nested_packages(remove_stubs(self.dist.packages)) + log.debug(f"discovered packages -- {self.dist.packages}") + self._ensure_no_accidental_inclusion(top_level, "packages") + return bool(top_level) + + def _analyse_flat_modules(self) -> bool: + self.dist.py_modules = FlatLayoutModuleFinder.find(self._root_dir) + log.debug(f"discovered py_modules -- {self.dist.py_modules}") + self._ensure_no_accidental_inclusion(self.dist.py_modules, "modules") + return bool(self.dist.py_modules) + + def _ensure_no_accidental_inclusion(self, detected: list[str], kind: str): + if len(detected) > 1: + from inspect import cleandoc + + from setuptools.errors import PackageDiscoveryError + + msg = f"""Multiple top-level {kind} discovered in a flat-layout: {detected}. + + To avoid accidental inclusion of unwanted files or directories, + setuptools will not proceed with this build. + + If you are trying to create a single distribution with multiple {kind} + on purpose, you should not rely on automatic discovery. + Instead, consider the following options: + + 1. set up custom discovery (`find` directive with `include` or `exclude`) + 2. use a `src-layout` + 3. explicitly set `py_modules` or `packages` with a list of names + + To find more information, look for "package discovery" on setuptools docs. + """ + raise PackageDiscoveryError(cleandoc(msg)) + + def analyse_name(self) -> None: + """The packages/modules are the essential contribution of the author. + Therefore the name of the distribution can be derived from them. + """ + if self.dist.metadata.name or self.dist.name: + # get_name() is not reliable (can return "UNKNOWN") + return + + log.debug("No `name` configuration, performing automatic discovery") + + name = ( + self._find_name_single_package_or_module() + or self._find_name_from_packages() + ) + if name: + self.dist.metadata.name = name + + def _find_name_single_package_or_module(self) -> str | None: + """Exactly one module or package""" + for field in ('packages', 'py_modules'): + items = getattr(self.dist, field, None) or [] + if items and len(items) == 1: + log.debug(f"Single module/package detected, name: {items[0]}") + return items[0] + + return None + + def _find_name_from_packages(self) -> str | None: + """Try to find the root package that is not a PEP 420 namespace""" + if not self.dist.packages: + return None + + packages = remove_stubs(sorted(self.dist.packages, key=len)) + package_dir = self.dist.package_dir or {} + + parent_pkg = find_parent_package(packages, package_dir, self._root_dir) + if parent_pkg: + log.debug(f"Common parent package detected, name: {parent_pkg}") + return parent_pkg + + log.warn("No parent package detected, impossible to derive `name`") + return None + + +def remove_nested_packages(packages: list[str]) -> list[str]: + """Remove nested packages from a list of packages. + + >>> remove_nested_packages(["a", "a.b1", "a.b2", "a.b1.c1"]) + ['a'] + >>> remove_nested_packages(["a", "b", "c.d", "c.d.e.f", "g.h", "a.a1"]) + ['a', 'b', 'c.d', 'g.h'] + """ + pkgs = sorted(packages, key=len) + top_level = pkgs[:] + size = len(pkgs) + for i, name in enumerate(reversed(pkgs)): + if any(name.startswith(f"{other}.") for other in top_level): + top_level.pop(size - i - 1) + + return top_level + + +def remove_stubs(packages: list[str]) -> list[str]: + """Remove type stubs (:pep:`561`) from a list of packages. + + >>> remove_stubs(["a", "a.b", "a-stubs", "a-stubs.b.c", "b", "c-stubs"]) + ['a', 'a.b', 'b'] + """ + return [pkg for pkg in packages if not pkg.split(".")[0].endswith("-stubs")] + + +def find_parent_package( + packages: list[str], package_dir: Mapping[str, str], root_dir: StrPath +) -> str | None: + """Find the parent package that is not a namespace.""" + packages = sorted(packages, key=len) + common_ancestors = [] + for i, name in enumerate(packages): + if not all(n.startswith(f"{name}.") for n in packages[i + 1 :]): + # Since packages are sorted by length, this condition is able + # to find a list of all common ancestors. + # When there is divergence (e.g. multiple root packages) + # the list will be empty + break + common_ancestors.append(name) + + for name in common_ancestors: + pkg_path = find_package_path(name, package_dir, root_dir) + init = os.path.join(pkg_path, "__init__.py") + if os.path.isfile(init): + return name + + return None + + +def find_package_path( + name: str, package_dir: Mapping[str, str], root_dir: StrPath +) -> str: + """Given a package name, return the path where it should be found on + disk, considering the ``package_dir`` option. + + >>> path = find_package_path("my.pkg", {"": "root/is/nested"}, ".") + >>> path.replace(os.sep, "/") + './root/is/nested/my/pkg' + + >>> path = find_package_path("my.pkg", {"my": "root/is/nested"}, ".") + >>> path.replace(os.sep, "/") + './root/is/nested/pkg' + + >>> path = find_package_path("my.pkg", {"my.pkg": "root/is/nested"}, ".") + >>> path.replace(os.sep, "/") + './root/is/nested' + + >>> path = find_package_path("other.pkg", {"my.pkg": "root/is/nested"}, ".") + >>> path.replace(os.sep, "/") + './other/pkg' + """ + parts = name.split(".") + for i in range(len(parts), 0, -1): + # Look backwards, the most specific package_dir first + partial_name = ".".join(parts[:i]) + if partial_name in package_dir: + parent = package_dir[partial_name] + return os.path.join(root_dir, parent, *parts[i:]) + + parent = package_dir.get("") or "" + return os.path.join(root_dir, *parent.split("/"), *parts) + + +def construct_package_dir(packages: list[str], package_path: StrPath) -> dict[str, str]: + parent_pkgs = remove_nested_packages(packages) + prefix = Path(package_path).parts + return {pkg: "/".join([*prefix, *pkg.split(".")]) for pkg in parent_pkgs} diff --git a/lib/python3.10/site-packages/setuptools/dist.py b/lib/python3.10/site-packages/setuptools/dist.py new file mode 100644 index 0000000000000000000000000000000000000000..8d972cc49bda84edee28c90be172b17107f00b07 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/dist.py @@ -0,0 +1,1121 @@ +from __future__ import annotations + +import functools +import io +import itertools +import numbers +import os +import re +import sys +from collections.abc import Iterable, Iterator, MutableMapping, Sequence +from glob import glob +from pathlib import Path +from typing import TYPE_CHECKING, Any, Union + +from more_itertools import partition, unique_everseen +from packaging.markers import InvalidMarker, Marker +from packaging.specifiers import InvalidSpecifier, SpecifierSet +from packaging.version import Version + +from . import ( + _entry_points, + _reqs, + _static, + command as _, # noqa: F401 # imported for side-effects +) +from ._importlib import metadata +from ._normalization import _canonicalize_license_expression +from ._path import StrPath +from ._reqs import _StrOrIter +from .config import pyprojecttoml, setupcfg +from .discovery import ConfigDiscovery +from .errors import InvalidConfigError +from .monkey import get_unpatched +from .warnings import InformationOnly, SetuptoolsDeprecationWarning + +import distutils.cmd +import distutils.command +import distutils.core +import distutils.dist +import distutils.log +from distutils.debug import DEBUG +from distutils.errors import DistutilsOptionError, DistutilsSetupError +from distutils.fancy_getopt import translate_longopt +from distutils.util import strtobool + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + + from pkg_resources import Distribution as _pkg_resources_Distribution + + +__all__ = ['Distribution'] + +_sequence = tuple, list +""" +:meta private: + +Supported iterable types that are known to be: +- ordered (which `set` isn't) +- not match a str (which `Sequence[str]` does) +- not imply a nested type (like `dict`) +for use with `isinstance`. +""" +_Sequence: TypeAlias = Union[tuple[str, ...], list[str]] +# This is how stringifying _Sequence would look in Python 3.10 +_sequence_type_repr = "tuple[str, ...] | list[str]" +_OrderedStrSequence: TypeAlias = Union[str, dict[str, Any], Sequence[str]] +""" +:meta private: +Avoid single-use iterable. Disallow sets. +A poor approximation of an OrderedSequence (dict doesn't match a Sequence). +""" + + +def __getattr__(name: str) -> Any: # pragma: no cover + if name == "sequence": + SetuptoolsDeprecationWarning.emit( + "`setuptools.dist.sequence` is an internal implementation detail.", + "Please define your own `sequence = tuple, list` instead.", + due_date=(2025, 8, 28), # Originally added on 2024-08-27 + ) + return _sequence + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def check_importable(dist, attr, value): + try: + ep = metadata.EntryPoint(value=value, name=None, group=None) + assert not ep.extras + except (TypeError, ValueError, AttributeError, AssertionError) as e: + raise DistutilsSetupError( + f"{attr!r} must be importable 'module:attrs' string (got {value!r})" + ) from e + + +def assert_string_list(dist, attr: str, value: _Sequence) -> None: + """Verify that value is a string list""" + try: + # verify that value is a list or tuple to exclude unordered + # or single-use iterables + assert isinstance(value, _sequence) + # verify that elements of value are strings + assert ''.join(value) != value + except (TypeError, ValueError, AttributeError, AssertionError) as e: + raise DistutilsSetupError( + f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})" + ) from e + + +def check_nsp(dist, attr, value): + """Verify that namespace packages are valid""" + ns_packages = value + assert_string_list(dist, attr, ns_packages) + for nsp in ns_packages: + if not dist.has_contents_for(nsp): + raise DistutilsSetupError( + f"Distribution contains no modules or packages for namespace package {nsp!r}" + ) + parent, _sep, _child = nsp.rpartition('.') + if parent and parent not in ns_packages: + distutils.log.warn( + "WARNING: %r is declared as a package namespace, but %r" + " is not: please correct this in setup.py", + nsp, + parent, + ) + SetuptoolsDeprecationWarning.emit( + "The namespace_packages parameter is deprecated.", + "Please replace its usage with implicit namespaces (PEP 420).", + see_docs="references/keywords.html#keyword-namespace-packages", + # TODO: define due_date, it may break old packages that are no longer + # maintained (e.g. sphinxcontrib extensions) when installed from source. + # Warning officially introduced in May 2022, however the deprecation + # was mentioned much earlier in the docs (May 2020, see #2149). + ) + + +def check_extras(dist, attr, value): + """Verify that extras_require mapping is valid""" + try: + list(itertools.starmap(_check_extra, value.items())) + except (TypeError, ValueError, AttributeError) as e: + raise DistutilsSetupError( + "'extras_require' must be a dictionary whose values are " + "strings or lists of strings containing valid project/version " + "requirement specifiers." + ) from e + + +def _check_extra(extra, reqs): + _name, _sep, marker = extra.partition(':') + try: + _check_marker(marker) + except InvalidMarker: + msg = f"Invalid environment marker: {marker} ({extra!r})" + raise DistutilsSetupError(msg) from None + list(_reqs.parse(reqs)) + + +def _check_marker(marker): + if not marker: + return + m = Marker(marker) + m.evaluate() + + +def assert_bool(dist, attr, value): + """Verify that value is True, False, 0, or 1""" + if bool(value) != value: + raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})") + + +def invalid_unless_false(dist, attr, value): + if not value: + DistDeprecationWarning.emit(f"{attr} is ignored.") + # TODO: should there be a `due_date` here? + return + raise DistutilsSetupError(f"{attr} is invalid.") + + +def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None: + """Verify that install_requires is a valid requirements list""" + try: + list(_reqs.parse(value)) + if isinstance(value, set): + raise TypeError("Unordered types are not allowed") + except (TypeError, ValueError) as error: + msg = ( + f"{attr!r} must be a string or iterable of strings " + f"containing valid project/version requirement specifiers; {error}" + ) + raise DistutilsSetupError(msg) from error + + +def check_specifier(dist, attr, value): + """Verify that value is a valid version specifier""" + try: + SpecifierSet(value) + except (InvalidSpecifier, AttributeError) as error: + msg = f"{attr!r} must be a string containing valid version specifiers; {error}" + raise DistutilsSetupError(msg) from error + + +def check_entry_points(dist, attr, value): + """Verify that entry_points map is parseable""" + try: + _entry_points.load(value) + except Exception as e: + raise DistutilsSetupError(e) from e + + +def check_package_data(dist, attr, value): + """Verify that value is a dictionary of package names to glob lists""" + if not isinstance(value, dict): + raise DistutilsSetupError( + f"{attr!r} must be a dictionary mapping package names to lists of " + "string wildcard patterns" + ) + for k, v in value.items(): + if not isinstance(k, str): + raise DistutilsSetupError( + f"keys of {attr!r} dict must be strings (got {k!r})" + ) + assert_string_list(dist, f'values of {attr!r} dict', v) + + +def check_packages(dist, attr, value): + for pkgname in value: + if not re.match(r'\w+(\.\w+)*', pkgname): + distutils.log.warn( + "WARNING: %r not a valid package name; please use only " + ".-separated package names in setup.py", + pkgname, + ) + + +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + from distutils.core import Distribution as _Distribution +else: + _Distribution = get_unpatched(distutils.core.Distribution) + + +class Distribution(_Distribution): + """Distribution with support for tests and package data + + This is an enhanced version of 'distutils.dist.Distribution' that + effectively adds the following new optional keyword arguments to 'setup()': + + 'install_requires' -- a string or sequence of strings specifying project + versions that the distribution requires when installed, in the format + used by 'pkg_resources.require()'. They will be installed + automatically when the package is installed. If you wish to use + packages that are not available in PyPI, or want to give your users an + alternate download location, you can add a 'find_links' option to the + '[easy_install]' section of your project's 'setup.cfg' file, and then + setuptools will scan the listed web pages for links that satisfy the + requirements. + + 'extras_require' -- a dictionary mapping names of optional "extras" to the + additional requirement(s) that using those extras incurs. For example, + this:: + + extras_require = dict(reST = ["docutils>=0.3", "reSTedit"]) + + indicates that the distribution can optionally provide an extra + capability called "reST", but it can only be used if docutils and + reSTedit are installed. If the user installs your package using + EasyInstall and requests one of your extras, the corresponding + additional requirements will be installed if needed. + + 'package_data' -- a dictionary mapping package names to lists of filenames + or globs to use to find data files contained in the named packages. + If the dictionary has filenames or globs listed under '""' (the empty + string), those names will be searched for in every package, in addition + to any names for the specific package. Data files found using these + names/globs will be installed along with the package, in the same + location as the package. Note that globs are allowed to reference + the contents of non-package subdirectories, as long as you use '/' as + a path separator. (Globs are automatically converted to + platform-specific paths at runtime.) + + In addition to these new keywords, this class also has several new methods + for manipulating the distribution's contents. For example, the 'include()' + and 'exclude()' methods can be thought of as in-place add and subtract + commands that add or remove packages, modules, extensions, and so on from + the distribution. + """ + + _DISTUTILS_UNSUPPORTED_METADATA = { + 'long_description_content_type': lambda: None, + 'project_urls': dict, + 'provides_extras': dict, # behaves like an ordered set + 'license_expression': lambda: None, + 'license_file': lambda: None, + 'license_files': lambda: None, + 'install_requires': list, + 'extras_require': dict, + } + + # Used by build_py, editable_wheel and install_lib commands for legacy namespaces + namespace_packages: list[str] #: :meta private: DEPRECATED + + # Any: Dynamic assignment results in Incompatible types in assignment + def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: + have_package_data = hasattr(self, "package_data") + if not have_package_data: + self.package_data: dict[str, list[str]] = {} + attrs = attrs or {} + self.dist_files: list[tuple[str, str, str]] = [] + self.include_package_data: bool | None = None + self.exclude_package_data: dict[str, list[str]] | None = None + # Filter-out setuptools' specific options. + self.src_root: str | None = attrs.pop("src_root", None) + self.dependency_links: list[str] = attrs.pop('dependency_links', []) + self.setup_requires: list[str] = attrs.pop('setup_requires', []) + for ep in metadata.entry_points(group='distutils.setup_keywords'): + vars(self).setdefault(ep.name, None) + + metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA) + metadata_only -= {"install_requires", "extras_require"} + dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} + _Distribution.__init__(self, dist_attrs) + + # Private API (setuptools-use only, not restricted to Distribution) + # Stores files that are referenced by the configuration and need to be in the + # sdist (e.g. `version = file: VERSION.txt`) + self._referenced_files = set[str]() + + self.set_defaults = ConfigDiscovery(self) + + self._set_metadata_defaults(attrs) + + self.metadata.version = self._normalize_version(self.metadata.version) + self._finalize_requires() + + def _validate_metadata(self): + required = {"name"} + provided = { + key + for key in vars(self.metadata) + if getattr(self.metadata, key, None) is not None + } + missing = required - provided + + if missing: + msg = f"Required package metadata is missing: {missing}" + raise DistutilsSetupError(msg) + + def _set_metadata_defaults(self, attrs): + """ + Fill-in missing metadata fields not supported by distutils. + Some fields may have been set by other tools (e.g. pbr). + Those fields (vars(self.metadata)) take precedence to + supplied attrs. + """ + for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): + vars(self.metadata).setdefault(option, attrs.get(option, default())) + + @staticmethod + def _normalize_version(version): + from . import sic + + if isinstance(version, numbers.Number): + # Some people apparently take "version number" too literally :) + version = str(version) + elif isinstance(version, sic) or version is None: + return version + + normalized = str(Version(version)) + if version != normalized: + InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'") + return normalized + return version + + def _finalize_requires(self): + """ + Set `metadata.python_requires` and fix environment markers + in `install_requires` and `extras_require`. + """ + if getattr(self, 'python_requires', None): + self.metadata.python_requires = self.python_requires + + self._normalize_requires() + self.metadata.install_requires = self.install_requires + self.metadata.extras_require = self.extras_require + + if self.extras_require: + for extra in self.extras_require.keys(): + # Setuptools allows a weird ": syntax for extras + extra = extra.split(':')[0] + if extra: + self.metadata.provides_extras.setdefault(extra) + + def _normalize_requires(self): + """Make sure requirement-related attributes exist and are normalized""" + install_requires = getattr(self, "install_requires", None) or [] + extras_require = getattr(self, "extras_require", None) or {} + + # Preserve the "static"-ness of values parsed from config files + list_ = _static.List if _static.is_static(install_requires) else list + self.install_requires = list_(map(str, _reqs.parse(install_requires))) + + dict_ = _static.Dict if _static.is_static(extras_require) else dict + self.extras_require = dict_( + (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items() + ) + + def _finalize_license_expression(self) -> None: + """ + Normalize license and license_expression. + >>> dist = Distribution({"license_expression": _static.Str("mit aNd gpl-3.0-OR-later")}) + >>> _static.is_static(dist.metadata.license_expression) + True + >>> dist._finalize_license_expression() + >>> _static.is_static(dist.metadata.license_expression) # preserve "static-ness" + True + >>> print(dist.metadata.license_expression) + MIT AND GPL-3.0-or-later + """ + classifiers = self.metadata.get_classifiers() + license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] + + license_expr = self.metadata.license_expression + if license_expr: + str_ = _static.Str if _static.is_static(license_expr) else str + normalized = str_(_canonicalize_license_expression(license_expr)) + if license_expr != normalized: + InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") + self.metadata.license_expression = normalized + if license_classifiers: + raise InvalidConfigError( + "License classifiers have been superseded by license expressions " + "(see https://peps.python.org/pep-0639/). Please remove:\n\n" + + "\n".join(license_classifiers), + ) + elif license_classifiers: + pypa_guides = "guides/writing-pyproject-toml/#license" + SetuptoolsDeprecationWarning.emit( + "License classifiers are deprecated.", + "Please consider removing the following classifiers in favor of a " + "SPDX license expression:\n\n" + "\n".join(license_classifiers), + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + # Warning introduced on 2025-02-17 + # TODO: Should we add a due date? It may affect old/unmaintained + # packages in the ecosystem and cause problems... + ) + + def _finalize_license_files(self) -> None: + """Compute names of all license files which should be included.""" + license_files: list[str] | None = self.metadata.license_files + patterns = license_files or [] + + license_file: str | None = self.metadata.license_file + if license_file and license_file not in patterns: + patterns.append(license_file) + + if license_files is None and license_file is None: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] + files = self._expand_patterns(patterns, enforce_match=False) + else: # Patterns explicitly given by the user + files = self._expand_patterns(patterns, enforce_match=True) + + self.metadata.license_files = list(unique_everseen(files)) + + @classmethod + def _expand_patterns( + cls, patterns: list[str], enforce_match: bool = True + ) -> Iterator[str]: + """ + >>> list(Distribution._expand_patterns(['LICENSE'])) + ['LICENSE'] + >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*'])) + ['pyproject.toml', 'LICENSE'] + >>> list(Distribution._expand_patterns(['setuptools/**/pyprojecttoml.py'])) + ['setuptools/config/pyprojecttoml.py'] + """ + return ( + path.replace(os.sep, "/") + for pattern in patterns + for path in sorted(cls._find_pattern(pattern, enforce_match)) + if not path.endswith('~') and os.path.isfile(path) + ) + + @staticmethod + def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]: + r""" + >>> Distribution._find_pattern("LICENSE") + ['LICENSE'] + >>> Distribution._find_pattern("/LICENSE.MIT") + Traceback (most recent call last): + ... + setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative... + >>> Distribution._find_pattern("../LICENSE.MIT") + Traceback (most recent call last): + ... + setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern '../LICENSE.MIT' cannot contain '..'... + >>> Distribution._find_pattern("LICEN{CSE*") + Traceback (most recent call last): + ... + setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters... + """ + pypa_guides = "specifications/glob-patterns/" + if ".." in pattern: + SetuptoolsDeprecationWarning.emit( + f"Pattern {pattern!r} cannot contain '..'", + """ + Please ensure the files specified are contained by the root + of the Python package (normally marked by `pyproject.toml`). + """, + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + due_date=(2026, 3, 20), # Introduced in 2025-03-20 + # Replace with InvalidConfigError after deprecation + ) + if pattern.startswith((os.sep, "/")) or ":\\" in pattern: + raise InvalidConfigError( + f"Pattern {pattern!r} should be relative and must not start with '/'" + ) + if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None: + SetuptoolsDeprecationWarning.emit( + "Please provide a valid glob pattern.", + "Pattern {pattern!r} contains invalid characters.", + pattern=pattern, + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + due_date=(2026, 3, 20), # Introduced in 2025-02-20 + ) + + found = glob(pattern, recursive=True) + + if enforce_match and not found: + SetuptoolsDeprecationWarning.emit( + "Cannot find any files for the given pattern.", + "Pattern {pattern!r} did not match any files.", + pattern=pattern, + due_date=(2026, 3, 20), # Introduced in 2025-02-20 + # PEP 639 requires us to error, but as a transition period + # we will only issue a warning to give people time to prepare. + # After the transition, this should raise an InvalidConfigError. + ) + return found + + # FIXME: 'Distribution._parse_config_files' is too complex (14) + def _parse_config_files(self, filenames=None): # noqa: C901 + """ + Adapted from distutils.dist.Distribution.parse_config_files, + this method provides the same functionality in subtly-improved + ways. + """ + from configparser import ConfigParser + + # Ignore install directory options if we have a venv + ignore_options = ( + [] + if sys.prefix == sys.base_prefix + else [ + 'install-base', + 'install-platbase', + 'install-lib', + 'install-platlib', + 'install-purelib', + 'install-headers', + 'install-scripts', + 'install-data', + 'prefix', + 'exec-prefix', + 'home', + 'user', + 'root', + ] + ) + + ignore_options = frozenset(ignore_options) + + if filenames is None: + filenames = self.find_config_files() + + if DEBUG: + self.announce("Distribution.parse_config_files():") + + parser = ConfigParser() + parser.optionxform = str + for filename in filenames: + with open(filename, encoding='utf-8') as reader: + if DEBUG: + self.announce(" reading {filename}".format(**locals())) + parser.read_file(reader) + for section in parser.sections(): + options = parser.options(section) + opt_dict = self.get_option_dict(section) + + for opt in options: + if opt == '__name__' or opt in ignore_options: + continue + + val = parser.get(section, opt) + opt = self._enforce_underscore(opt, section) + opt = self._enforce_option_lowercase(opt, section) + opt_dict[opt] = (filename, val) + + # Make the ConfigParser forget everything (so we retain + # the original filenames that options come from) + parser.__init__() + + if 'global' not in self.command_options: + return + + # If there was a "global" section in the config file, use it + # to set Distribution options. + + for opt, (src, val) in self.command_options['global'].items(): + alias = self.negative_opt.get(opt) + if alias: + val = not strtobool(val) + elif opt in ('verbose', 'dry_run'): # ugh! + val = strtobool(val) + + try: + setattr(self, alias or opt, val) + except ValueError as e: + raise DistutilsOptionError(e) from e + + def _enforce_underscore(self, opt: str, section: str) -> str: + if "-" not in opt or self._skip_setupcfg_normalization(section): + return opt + + underscore_opt = opt.replace('-', '_') + affected = f"(Affected: {self.metadata.name})." if self.metadata.name else "" + SetuptoolsDeprecationWarning.emit( + f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), " + f"please use the underscore name {underscore_opt!r} instead.", + f""" + Usage of dash-separated {opt!r} will not be supported in future + versions. Please use the underscore name {underscore_opt!r} instead. + {affected} + """, + see_docs="userguide/declarative_config.html", + due_date=(2026, 3, 3), + # Warning initially introduced in 3 Mar 2021 + ) + return underscore_opt + + def _enforce_option_lowercase(self, opt: str, section: str) -> str: + if opt.islower() or self._skip_setupcfg_normalization(section): + return opt + + lowercase_opt = opt.lower() + affected = f"(Affected: {self.metadata.name})." if self.metadata.name else "" + SetuptoolsDeprecationWarning.emit( + f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), " + f"please use lowercase {lowercase_opt!r} instead.", + f""" + Usage of uppercase key {opt!r} in {section!r} will not be supported in + future versions. Please use lowercase {lowercase_opt!r} instead. + {affected} + """, + see_docs="userguide/declarative_config.html", + due_date=(2026, 3, 3), + # Warning initially introduced in 6 Mar 2021 + ) + return lowercase_opt + + def _skip_setupcfg_normalization(self, section: str) -> bool: + skip = ( + 'options.extras_require', + 'options.data_files', + 'options.entry_points', + 'options.package_data', + 'options.exclude_package_data', + ) + return section in skip or not self._is_setuptools_section(section) + + def _is_setuptools_section(self, section: str) -> bool: + return ( + section == "metadata" + or section.startswith("options") + or section in _setuptools_commands() + ) + + # FIXME: 'Distribution._set_command_options' is too complex (14) + def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 + """ + Set the options for 'command_obj' from 'option_dict'. Basically + this means copying elements of a dictionary ('option_dict') to + attributes of an instance ('command'). + + 'command_obj' must be a Command instance. If 'option_dict' is not + supplied, uses the standard option dictionary for this command + (from 'self.command_options'). + + (Adopted from distutils.dist.Distribution._set_command_options) + """ + command_name = command_obj.get_command_name() + if option_dict is None: + option_dict = self.get_option_dict(command_name) + + if DEBUG: + self.announce(f" setting options for '{command_name}' command:") + for option, (source, value) in option_dict.items(): + if DEBUG: + self.announce(f" {option} = {value} (from {source})") + try: + bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] + except AttributeError: + bool_opts = [] + try: + neg_opt = command_obj.negative_opt + except AttributeError: + neg_opt = {} + + try: + is_string = isinstance(value, str) + if option in neg_opt and is_string: + setattr(command_obj, neg_opt[option], not strtobool(value)) + elif option in bool_opts and is_string: + setattr(command_obj, option, strtobool(value)) + elif hasattr(command_obj, option): + setattr(command_obj, option, value) + else: + raise DistutilsOptionError( + f"error in {source}: command '{command_name}' has no such option '{option}'" + ) + except ValueError as e: + raise DistutilsOptionError(e) from e + + def _get_project_config_files(self, filenames: Iterable[StrPath] | None): + """Add default file and split between INI and TOML""" + tomlfiles = [] + standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml") + if filenames is not None: + parts = partition(lambda f: Path(f).suffix == ".toml", filenames) + filenames = list(parts[0]) # 1st element => predicate is False + tomlfiles = list(parts[1]) # 2nd element => predicate is True + elif standard_project_metadata.exists(): + tomlfiles = [standard_project_metadata] + return filenames, tomlfiles + + def parse_config_files( + self, + filenames: Iterable[StrPath] | None = None, + ignore_option_errors: bool = False, + ) -> None: + """Parses configuration files from various levels + and loads configuration. + """ + inifiles, tomlfiles = self._get_project_config_files(filenames) + + self._parse_config_files(filenames=inifiles) + + setupcfg.parse_configuration( + self, self.command_options, ignore_option_errors=ignore_option_errors + ) + for filename in tomlfiles: + pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) + + self._finalize_requires() + self._finalize_license_expression() + self._finalize_license_files() + + def fetch_build_eggs( + self, requires: _StrOrIter + ) -> list[_pkg_resources_Distribution]: + """Resolve pre-setup requirements""" + from .installer import _fetch_build_eggs + + return _fetch_build_eggs(self, requires) + + def finalize_options(self) -> None: + """ + Allow plugins to apply arbitrary operations to the + distribution. Each hook may optionally define a 'order' + to influence the order of execution. Smaller numbers + go first and the default is 0. + """ + group = 'setuptools.finalize_distribution_options' + + def by_order(hook): + return getattr(hook, 'order', 0) + + defined = metadata.entry_points(group=group) + filtered = itertools.filterfalse(self._removed, defined) + loaded = map(lambda e: e.load(), filtered) + for ep in sorted(loaded, key=by_order): + ep(self) + + @staticmethod + def _removed(ep): + """ + When removing an entry point, if metadata is loaded + from an older version of Setuptools, that removed + entry point will attempt to be loaded and will fail. + See #2765 for more details. + """ + removed = { + # removed 2021-09-05 + '2to3_doctests', + } + return ep.name in removed + + def _finalize_setup_keywords(self): + for ep in metadata.entry_points(group='distutils.setup_keywords'): + value = getattr(self, ep.name, None) + if value is not None: + ep.load()(self, ep.name, value) + + def get_egg_cache_dir(self): + from . import windows_support + + egg_cache_dir = os.path.join(os.curdir, '.eggs') + if not os.path.exists(egg_cache_dir): + os.mkdir(egg_cache_dir) + windows_support.hide_file(egg_cache_dir) + readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt') + with open(readme_txt_filename, 'w', encoding="utf-8") as f: + f.write( + 'This directory contains eggs that were downloaded ' + 'by setuptools to build, test, and run plug-ins.\n\n' + ) + f.write( + 'This directory caches those eggs to prevent ' + 'repeated downloads.\n\n' + ) + f.write('However, it is safe to delete this directory.\n\n') + + return egg_cache_dir + + def fetch_build_egg(self, req): + """Fetch an egg needed for building""" + from .installer import fetch_build_egg + + return fetch_build_egg(self, req) + + def get_command_class(self, command: str) -> type[distutils.cmd.Command]: # type: ignore[override] # Not doing complex overrides yet + """Pluggable version of get_command_class()""" + if command in self.cmdclass: + return self.cmdclass[command] + + # Special case bdist_wheel so it's never loaded from "wheel" + if command == 'bdist_wheel': + from .command.bdist_wheel import bdist_wheel + + return bdist_wheel + + eps = metadata.entry_points(group='distutils.commands', name=command) + for ep in eps: + self.cmdclass[command] = cmdclass = ep.load() + return cmdclass + else: + return _Distribution.get_command_class(self, command) + + def print_commands(self): + for ep in metadata.entry_points(group='distutils.commands'): + if ep.name not in self.cmdclass: + cmdclass = ep.load() + self.cmdclass[ep.name] = cmdclass + return _Distribution.print_commands(self) + + def get_command_list(self): + for ep in metadata.entry_points(group='distutils.commands'): + if ep.name not in self.cmdclass: + cmdclass = ep.load() + self.cmdclass[ep.name] = cmdclass + return _Distribution.get_command_list(self) + + def include(self, **attrs) -> None: + """Add items to distribution that are named in keyword arguments + + For example, 'dist.include(py_modules=["x"])' would add 'x' to + the distribution's 'py_modules' attribute, if it was not already + there. + + Currently, this method only supports inclusion for attributes that are + lists or tuples. If you need to add support for adding to other + attributes in this or a subclass, you can add an '_include_X' method, + where 'X' is the name of the attribute. The method will be called with + the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})' + will try to call 'dist._include_foo({"bar":"baz"})', which can then + handle whatever special inclusion logic is needed. + """ + for k, v in attrs.items(): + include = getattr(self, '_include_' + k, None) + if include: + include(v) + else: + self._include_misc(k, v) + + def exclude_package(self, package: str) -> None: + """Remove packages, modules, and extensions in named package""" + + pfx = package + '.' + if self.packages: + self.packages = [ + p for p in self.packages if p != package and not p.startswith(pfx) + ] + + if self.py_modules: + self.py_modules = [ + p for p in self.py_modules if p != package and not p.startswith(pfx) + ] + + if self.ext_modules: + self.ext_modules = [ + p + for p in self.ext_modules + if p.name != package and not p.name.startswith(pfx) + ] + + def has_contents_for(self, package: str) -> bool: + """Return true if 'exclude_package(package)' would do something""" + + pfx = package + '.' + + for p in self.iter_distribution_names(): + if p == package or p.startswith(pfx): + return True + + return False + + def _exclude_misc(self, name: str, value: _Sequence) -> None: + """Handle 'exclude()' for list/tuple attrs without a special handler""" + if not isinstance(value, _sequence): + raise DistutilsSetupError( + f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" + ) + try: + old = getattr(self, name) + except AttributeError as e: + raise DistutilsSetupError(f"{name}: No such distribution setting") from e + if old is not None and not isinstance(old, _sequence): + raise DistutilsSetupError( + name + ": this setting cannot be changed via include/exclude" + ) + elif old: + setattr(self, name, [item for item in old if item not in value]) + + def _include_misc(self, name: str, value: _Sequence) -> None: + """Handle 'include()' for list/tuple attrs without a special handler""" + + if not isinstance(value, _sequence): + raise DistutilsSetupError( + f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" + ) + try: + old = getattr(self, name) + except AttributeError as e: + raise DistutilsSetupError(f"{name}: No such distribution setting") from e + if old is None: + setattr(self, name, value) + elif not isinstance(old, _sequence): + raise DistutilsSetupError( + name + ": this setting cannot be changed via include/exclude" + ) + else: + new = [item for item in value if item not in old] + setattr(self, name, list(old) + new) + + def exclude(self, **attrs) -> None: + """Remove items from distribution that are named in keyword arguments + + For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from + the distribution's 'py_modules' attribute. Excluding packages uses + the 'exclude_package()' method, so all of the package's contained + packages, modules, and extensions are also excluded. + + Currently, this method only supports exclusion from attributes that are + lists or tuples. If you need to add support for excluding from other + attributes in this or a subclass, you can add an '_exclude_X' method, + where 'X' is the name of the attribute. The method will be called with + the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})' + will try to call 'dist._exclude_foo({"bar":"baz"})', which can then + handle whatever special exclusion logic is needed. + """ + for k, v in attrs.items(): + exclude = getattr(self, '_exclude_' + k, None) + if exclude: + exclude(v) + else: + self._exclude_misc(k, v) + + def _exclude_packages(self, packages: _Sequence) -> None: + if not isinstance(packages, _sequence): + raise DistutilsSetupError( + f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})" + ) + list(map(self.exclude_package, packages)) + + def _parse_command_opts(self, parser, args): + # Remove --with-X/--without-X options when processing command args + self.global_options = self.__class__.global_options + self.negative_opt = self.__class__.negative_opt + + # First, expand any aliases + command = args[0] + aliases = self.get_option_dict('aliases') + while command in aliases: + _src, alias = aliases[command] + del aliases[command] # ensure each alias can expand only once! + import shlex + + args[:1] = shlex.split(alias, True) + command = args[0] + + nargs = _Distribution._parse_command_opts(self, parser, args) + + # Handle commands that want to consume all remaining arguments + cmd_class = self.get_command_class(command) + if getattr(cmd_class, 'command_consumes_arguments', None): + self.get_option_dict(command)['args'] = ("command line", nargs) + if nargs is not None: + return [] + + return nargs + + def get_cmdline_options(self) -> dict[str, dict[str, str | None]]: + """Return a '{cmd: {opt:val}}' map of all command-line options + + Option names are all long, but do not include the leading '--', and + contain dashes rather than underscores. If the option doesn't take + an argument (e.g. '--quiet'), the 'val' is 'None'. + + Note that options provided by config files are intentionally excluded. + """ + + d: dict[str, dict[str, str | None]] = {} + + for cmd, opts in self.command_options.items(): + val: str | None + for opt, (src, val) in opts.items(): + if src != "command line": + continue + + opt = opt.replace('_', '-') + + if val == 0: + cmdobj = self.get_command_obj(cmd) + neg_opt = self.negative_opt.copy() + neg_opt.update(getattr(cmdobj, 'negative_opt', {})) + for neg, pos in neg_opt.items(): + if pos == opt: + opt = neg + val = None + break + else: + raise AssertionError("Shouldn't be able to get here") + + elif val == 1: + val = None + + d.setdefault(cmd, {})[opt] = val + + return d + + def iter_distribution_names(self): + """Yield all packages, modules, and extension names in distribution""" + + yield from self.packages or () + + yield from self.py_modules or () + + for ext in self.ext_modules or (): + if isinstance(ext, tuple): + name, _buildinfo = ext + else: + name = ext.name + if name.endswith('module'): + name = name[:-6] + yield name + + def handle_display_options(self, option_order): + """If there were any non-global "display-only" options + (--help-commands or the metadata display options) on the command + line, display the requested info and return true; else return + false. + """ + import sys + + if self.help_commands: + return _Distribution.handle_display_options(self, option_order) + + # Stdout may be StringIO (e.g. in tests) + if not isinstance(sys.stdout, io.TextIOWrapper): + return _Distribution.handle_display_options(self, option_order) + + # Don't wrap stdout if utf-8 is already the encoding. Provides + # workaround for #334. + if sys.stdout.encoding.lower() in ('utf-8', 'utf8'): + return _Distribution.handle_display_options(self, option_order) + + # Print metadata in UTF-8 no matter the platform + encoding = sys.stdout.encoding + sys.stdout.reconfigure(encoding='utf-8') + try: + return _Distribution.handle_display_options(self, option_order) + finally: + sys.stdout.reconfigure(encoding=encoding) + + def run_command(self, command) -> None: + self.set_defaults() + # Postpone defaults until all explicit configuration is considered + # (setup() args, config files, command line and plugins) + + super().run_command(command) + + +@functools.cache +def _setuptools_commands() -> set[str]: + try: + # Use older API for importlib.metadata compatibility + entry_points = metadata.distribution('setuptools').entry_points + eps: Iterable[str] = (ep.name for ep in entry_points) + except metadata.PackageNotFoundError: + # during bootstrapping, distribution doesn't exist + eps = [] + return {*distutils.command.__all__, *eps} + + +class DistDeprecationWarning(SetuptoolsDeprecationWarning): + """Class for warning about deprecations in dist in + setuptools. Not ignored by default, unlike DeprecationWarning.""" diff --git a/lib/python3.10/site-packages/setuptools/errors.py b/lib/python3.10/site-packages/setuptools/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..990ecbf4e2f18eb188addc9e0466152a20193a90 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/errors.py @@ -0,0 +1,67 @@ +"""setuptools.errors + +Provides exceptions used by setuptools modules. +""" + +from __future__ import annotations + +from distutils import errors as _distutils_errors + +# Re-export errors from distutils to facilitate the migration to PEP632 + +ByteCompileError = _distutils_errors.DistutilsByteCompileError +CCompilerError = _distutils_errors.CCompilerError +ClassError = _distutils_errors.DistutilsClassError +CompileError = _distutils_errors.CompileError +ExecError = _distutils_errors.DistutilsExecError +FileError = _distutils_errors.DistutilsFileError +InternalError = _distutils_errors.DistutilsInternalError +LibError = _distutils_errors.LibError +LinkError = _distutils_errors.LinkError +ModuleError = _distutils_errors.DistutilsModuleError +OptionError = _distutils_errors.DistutilsOptionError +PlatformError = _distutils_errors.DistutilsPlatformError +PreprocessError = _distutils_errors.PreprocessError +SetupError = _distutils_errors.DistutilsSetupError +TemplateError = _distutils_errors.DistutilsTemplateError +UnknownFileError = _distutils_errors.UnknownFileError + +# The root error class in the hierarchy +BaseError = _distutils_errors.DistutilsError + + +class InvalidConfigError(OptionError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+ + """Error used for invalid configurations.""" + + +class RemovedConfigError(OptionError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+ + """Error used for configurations that were deprecated and removed.""" + + +class RemovedCommandError(BaseError, RuntimeError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+ + """Error used for commands that have been removed in setuptools. + + Since ``setuptools`` is built on ``distutils``, simply removing a command + from ``setuptools`` will make the behavior fall back to ``distutils``; this + error is raised if a command exists in ``distutils`` but has been actively + removed in ``setuptools``. + """ + + +class PackageDiscoveryError(BaseError, RuntimeError): # type: ignore[valid-type, misc] # distutils imports are `Any` on python 3.12+ + """Impossible to perform automatic discovery of packages and/or modules. + + The current project layout or given discovery options can lead to problems when + scanning the project directory. + + Setuptools might also refuse to complete auto-discovery if an error prone condition + is detected (e.g. when a project is organised as a flat-layout but contains + multiple directories that can be taken as top-level packages inside a single + distribution [*]_). In these situations the users are encouraged to be explicit + about which packages to include or to make the discovery parameters more specific. + + .. [*] Since multi-package distributions are uncommon it is very likely that the + developers did not intend for all the directories to be packaged, and are just + leaving auxiliary code in the repository top-level, such as maintenance-related + scripts. + """ diff --git a/lib/python3.10/site-packages/setuptools/extension.py b/lib/python3.10/site-packages/setuptools/extension.py new file mode 100644 index 0000000000000000000000000000000000000000..76e03d9d6bdcdbd72e443e90c85d34429c22c261 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/extension.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import functools +import re +from typing import TYPE_CHECKING + +from setuptools._path import StrPath + +from .monkey import get_unpatched + +import distutils.core +import distutils.errors +import distutils.extension + + +def _have_cython(): + """ + Return True if Cython can be imported. + """ + cython_impl = 'Cython.Distutils.build_ext' + try: + # from (cython_impl) import build_ext + __import__(cython_impl, fromlist=['build_ext']).build_ext + except Exception: + return False + return True + + +# for compatibility +have_pyrex = _have_cython +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + from distutils.core import Extension as _Extension +else: + _Extension = get_unpatched(distutils.core.Extension) + + +class Extension(_Extension): + """ + Describes a single extension module. + + This means that all source files will be compiled into a single binary file + ``.`` (with ```` derived from ``name`` and + ```` defined by one of the values in + ``importlib.machinery.EXTENSION_SUFFIXES``). + + In the case ``.pyx`` files are passed as ``sources and`` ``Cython`` is **not** + installed in the build environment, ``setuptools`` may also try to look for the + equivalent ``.cpp`` or ``.c`` files. + + :arg str name: + the full name of the extension, including any packages -- ie. + *not* a filename or pathname, but Python dotted name + + :arg list[str|os.PathLike[str]] sources: + list of source filenames, relative to the distribution root + (where the setup script lives), in Unix form (slash-separated) + for portability. Source files may be C, C++, SWIG (.i), + platform-specific resource files, or whatever else is recognized + by the "build_ext" command as source for a Python extension. + + :keyword list[str] include_dirs: + list of directories to search for C/C++ header files (in Unix + form for portability) + + :keyword list[tuple[str, str|None]] define_macros: + list of macros to define; each macro is defined using a 2-tuple: + the first item corresponding to the name of the macro and the second + item either a string with its value or None to + define it without a particular value (equivalent of "#define + FOO" in source or -DFOO on Unix C compiler command line) + + :keyword list[str] undef_macros: + list of macros to undefine explicitly + + :keyword list[str] library_dirs: + list of directories to search for C/C++ libraries at link time + + :keyword list[str] libraries: + list of library names (not filenames or paths) to link against + + :keyword list[str] runtime_library_dirs: + list of directories to search for C/C++ libraries at run time + (for shared extensions, this is when the extension is loaded). + Setting this will cause an exception during build on Windows + platforms. + + :keyword list[str] extra_objects: + list of extra files to link with (eg. object files not implied + by 'sources', static library that must be explicitly specified, + binary resource files, etc.) + + :keyword list[str] extra_compile_args: + any extra platform- and compiler-specific information to use + when compiling the source files in 'sources'. For platforms and + compilers where "command line" makes sense, this is typically a + list of command-line arguments, but for other platforms it could + be anything. + + :keyword list[str] extra_link_args: + any extra platform- and compiler-specific information to use + when linking object files together to create the extension (or + to create a new static Python interpreter). Similar + interpretation as for 'extra_compile_args'. + + :keyword list[str] export_symbols: + list of symbols to be exported from a shared extension. Not + used on all platforms, and not generally necessary for Python + extensions, which typically export exactly one symbol: "init" + + extension_name. + + :keyword list[str] swig_opts: + any extra options to pass to SWIG if a source file has the .i + extension. + + :keyword list[str] depends: + list of files that the extension depends on + + :keyword str language: + extension language (i.e. "c", "c++", "objc"). Will be detected + from the source extensions if not provided. + + :keyword bool optional: + specifies that a build failure in the extension should not abort the + build process, but simply not install the failing extension. + + :keyword bool py_limited_api: + opt-in flag for the usage of :doc:`Python's limited API `. + + :raises setuptools.errors.PlatformError: if ``runtime_library_dirs`` is + specified on Windows. (since v63) + """ + + # These 4 are set and used in setuptools/command/build_ext.py + # The lack of a default value and risk of `AttributeError` is purposeful + # to avoid people forgetting to call finalize_options if they modify the extension list. + # See example/rationale in https://github.com/pypa/setuptools/issues/4529. + _full_name: str #: Private API, internal use only. + _links_to_dynamic: bool #: Private API, internal use only. + _needs_stub: bool #: Private API, internal use only. + _file_name: str #: Private API, internal use only. + + def __init__( + self, + name: str, + sources: list[StrPath], + *args, + py_limited_api: bool = False, + **kw, + ) -> None: + # The *args is needed for compatibility as calls may use positional + # arguments. py_limited_api may be set only via keyword. + self.py_limited_api = py_limited_api + super().__init__( + name, + sources, # type: ignore[arg-type] # Vendored version of setuptools supports PathLike + *args, + **kw, + ) + + def _convert_pyx_sources_to_lang(self): + """ + Replace sources with .pyx extensions to sources with the target + language extension. This mechanism allows language authors to supply + pre-converted sources but to prefer the .pyx sources. + """ + if _have_cython(): + # the build has Cython, so allow it to compile the .pyx files + return + lang = self.language or '' + target_ext = '.cpp' if lang.lower() == 'c++' else '.c' + sub = functools.partial(re.sub, '.pyx$', target_ext) + self.sources = list(map(sub, self.sources)) + + +class Library(Extension): + """Just like a regular Extension, but built as a library instead""" diff --git a/lib/python3.10/site-packages/setuptools/gui-64.exe b/lib/python3.10/site-packages/setuptools/gui-64.exe new file mode 100644 index 0000000000000000000000000000000000000000..031cb77c17ba8d8a983448268851d612e05e80d1 Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/gui-64.exe differ diff --git a/lib/python3.10/site-packages/setuptools/gui-arm64.exe b/lib/python3.10/site-packages/setuptools/gui-arm64.exe new file mode 100644 index 0000000000000000000000000000000000000000..1e00ffacb182c2af206e5dd9d9fbc41d236da0d1 Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/gui-arm64.exe differ diff --git a/lib/python3.10/site-packages/setuptools/gui.exe b/lib/python3.10/site-packages/setuptools/gui.exe new file mode 100644 index 0000000000000000000000000000000000000000..1eb430c6d614a5daea4139badc09c222a4b0e72a Binary files /dev/null and b/lib/python3.10/site-packages/setuptools/gui.exe differ diff --git a/lib/python3.10/site-packages/setuptools/installer.py b/lib/python3.10/site-packages/setuptools/installer.py new file mode 100644 index 0000000000000000000000000000000000000000..64bc2def078bab5e4fb6aba26981e7b73bfc37a5 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/installer.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import glob +import os +import subprocess +import sys +import tempfile +from functools import partial + +from pkg_resources import Distribution + +from . import _reqs +from ._reqs import _StrOrIter +from .warnings import SetuptoolsDeprecationWarning +from .wheel import Wheel + +from distutils import log +from distutils.errors import DistutilsError + + +def _fixup_find_links(find_links): + """Ensure find-links option end-up being a list of strings.""" + if isinstance(find_links, str): + return find_links.split() + assert isinstance(find_links, (tuple, list)) + return find_links + + +def fetch_build_egg(dist, req): + """Fetch an egg needed for building. + + Use pip/wheel to fetch/build a wheel.""" + _DeprecatedInstaller.emit() + _warn_wheel_not_available(dist) + return _fetch_build_egg_no_warn(dist, req) + + +def _fetch_build_eggs(dist, requires: _StrOrIter) -> list[Distribution]: + import pkg_resources # Delay import to avoid unnecessary side-effects + + _DeprecatedInstaller.emit(stacklevel=3) + _warn_wheel_not_available(dist) + + resolved_dists = pkg_resources.working_set.resolve( + _reqs.parse(requires, pkg_resources.Requirement), # required for compatibility + installer=partial(_fetch_build_egg_no_warn, dist), # avoid warning twice + replace_conflicting=True, + ) + for dist in resolved_dists: + pkg_resources.working_set.add(dist, replace=True) + return resolved_dists + + +def _fetch_build_egg_no_warn(dist, req): # noqa: C901 # is too complex (16) # FIXME + import pkg_resources # Delay import to avoid unnecessary side-effects + + # Ignore environment markers; if supplied, it is required. + req = strip_marker(req) + # Take easy_install options into account, but do not override relevant + # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll + # take precedence. + opts = dist.get_option_dict('easy_install') + if 'allow_hosts' in opts: + raise DistutilsError( + 'the `allow-hosts` option is not supported ' + 'when using pip to install requirements.' + ) + quiet = 'PIP_QUIET' not in os.environ and 'PIP_VERBOSE' not in os.environ + if 'PIP_INDEX_URL' in os.environ: + index_url = None + elif 'index_url' in opts: + index_url = opts['index_url'][1] + else: + index_url = None + find_links = ( + _fixup_find_links(opts['find_links'][1])[:] if 'find_links' in opts else [] + ) + if dist.dependency_links: + find_links.extend(dist.dependency_links) + eggs_dir = os.path.realpath(dist.get_egg_cache_dir()) + environment = pkg_resources.Environment() + for egg_dist in pkg_resources.find_distributions(eggs_dir): + if egg_dist in req and environment.can_add(egg_dist): + return egg_dist + with tempfile.TemporaryDirectory() as tmpdir: + cmd = [ + sys.executable, + '-m', + 'pip', + '--disable-pip-version-check', + 'wheel', + '--no-deps', + '-w', + tmpdir, + ] + if quiet: + cmd.append('--quiet') + if index_url is not None: + cmd.extend(('--index-url', index_url)) + for link in find_links or []: + cmd.extend(('--find-links', link)) + # If requirement is a PEP 508 direct URL, directly pass + # the URL to pip, as `req @ url` does not work on the + # command line. + cmd.append(req.url or str(req)) + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + raise DistutilsError(str(e)) from e + wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0]) + dist_location = os.path.join(eggs_dir, wheel.egg_name()) + wheel.install_as_egg(dist_location) + dist_metadata = pkg_resources.PathMetadata( + dist_location, os.path.join(dist_location, 'EGG-INFO') + ) + return pkg_resources.Distribution.from_filename( + dist_location, metadata=dist_metadata + ) + + +def strip_marker(req): + """ + Return a new requirement without the environment marker to avoid + calling pip with something like `babel; extra == "i18n"`, which + would always be ignored. + """ + import pkg_resources # Delay import to avoid unnecessary side-effects + + # create a copy to avoid mutating the input + req = pkg_resources.Requirement.parse(str(req)) + req.marker = None + return req + + +def _warn_wheel_not_available(dist): + import pkg_resources # Delay import to avoid unnecessary side-effects + + try: + pkg_resources.get_distribution('wheel') + except pkg_resources.DistributionNotFound: + dist.announce('WARNING: The wheel package is not available.', log.WARN) + + +class _DeprecatedInstaller(SetuptoolsDeprecationWarning): + _SUMMARY = "setuptools.installer and fetch_build_eggs are deprecated." + _DETAILS = """ + Requirements should be satisfied by a PEP 517 installer. + If you are using pip, you can try `pip install --use-pep517`. + """ + # _DUE_DATE not decided yet diff --git a/lib/python3.10/site-packages/setuptools/launch.py b/lib/python3.10/site-packages/setuptools/launch.py new file mode 100644 index 0000000000000000000000000000000000000000..0d162647d55777d7afa1bf1e44a6c200a3f82419 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/launch.py @@ -0,0 +1,36 @@ +""" +Launch the Python script on the command line after +setuptools is bootstrapped via import. +""" + +# Note that setuptools gets imported implicitly by the +# invocation of this script using python -m setuptools.launch + +import sys +import tokenize + + +def run() -> None: + """ + Run the script in sys.argv[1] as if it had + been invoked naturally. + """ + __builtins__ + script_name = sys.argv[1] + namespace = dict( + __file__=script_name, + __name__='__main__', + __doc__=None, + ) + sys.argv[:] = sys.argv[1:] + + open_ = getattr(tokenize, 'open', open) + with open_(script_name) as fid: + script = fid.read() + norm_script = script.replace('\\r\\n', '\\n') + code = compile(norm_script, script_name, 'exec') + exec(code, namespace) + + +if __name__ == '__main__': + run() diff --git a/lib/python3.10/site-packages/setuptools/logging.py b/lib/python3.10/site-packages/setuptools/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..532da899f7dc02f9fea9a44c429086b98fe043d8 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/logging.py @@ -0,0 +1,40 @@ +import inspect +import logging +import sys + +from . import monkey + +import distutils.log + + +def _not_warning(record): + return record.levelno < logging.WARNING + + +def configure() -> None: + """ + Configure logging to emit warning and above to stderr + and everything else to stdout. This behavior is provided + for compatibility with distutils.log but may change in + the future. + """ + err_handler = logging.StreamHandler() + err_handler.setLevel(logging.WARNING) + out_handler = logging.StreamHandler(sys.stdout) + out_handler.addFilter(_not_warning) + handlers = err_handler, out_handler + logging.basicConfig( + format="{message}", style='{', handlers=handlers, level=logging.DEBUG + ) + if inspect.ismodule(distutils.dist.log): + monkey.patch_func(set_threshold, distutils.log, 'set_threshold') + # For some reason `distutils.log` module is getting cached in `distutils.dist` + # and then loaded again when patched, + # implying: id(distutils.log) != id(distutils.dist.log). + # Make sure the same module object is used everywhere: + distutils.dist.log = distutils.log + + +def set_threshold(level: int) -> int: + logging.root.setLevel(level * 10) + return set_threshold.unpatched(level) diff --git a/lib/python3.10/site-packages/setuptools/modified.py b/lib/python3.10/site-packages/setuptools/modified.py new file mode 100644 index 0000000000000000000000000000000000000000..6ba02fab68734e1e96fd50d7c4b6ffb1442717fb --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/modified.py @@ -0,0 +1,18 @@ +try: + # Ensure a DistutilsError raised by these methods is the same as distutils.errors.DistutilsError + from distutils._modified import ( + newer, + newer_group, + newer_pairwise, + newer_pairwise_group, + ) +except ImportError: + # fallback for SETUPTOOLS_USE_DISTUTILS=stdlib, because _modified never existed in stdlib + from ._distutils._modified import ( + newer, + newer_group, + newer_pairwise, + newer_pairwise_group, + ) + +__all__ = ['newer', 'newer_pairwise', 'newer_group', 'newer_pairwise_group'] diff --git a/lib/python3.10/site-packages/setuptools/monkey.py b/lib/python3.10/site-packages/setuptools/monkey.py new file mode 100644 index 0000000000000000000000000000000000000000..6ad1abac295c1df613942b8896edf089a103ae1f --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/monkey.py @@ -0,0 +1,126 @@ +""" +Monkey patching of distutils. +""" + +from __future__ import annotations + +import inspect +import platform +import sys +import types +from typing import TypeVar, cast, overload + +import distutils.filelist + +_T = TypeVar("_T") +_UnpatchT = TypeVar("_UnpatchT", type, types.FunctionType) + + +__all__: list[str] = [] +""" +Everything is private. Contact the project team +if you think you need this functionality. +""" + + +def _get_mro(cls): + """ + Returns the bases classes for cls sorted by the MRO. + + Works around an issue on Jython where inspect.getmro will not return all + base classes if multiple classes share the same name. Instead, this + function will return a tuple containing the class itself, and the contents + of cls.__bases__. See https://github.com/pypa/setuptools/issues/1024. + """ + if platform.python_implementation() == "Jython": + return (cls,) + cls.__bases__ + return inspect.getmro(cls) + + +@overload +def get_unpatched(item: _UnpatchT) -> _UnpatchT: ... +@overload +def get_unpatched(item: object) -> None: ... +def get_unpatched( + item: type | types.FunctionType | object, +) -> type | types.FunctionType | None: + if isinstance(item, type): + return get_unpatched_class(item) + if isinstance(item, types.FunctionType): + return get_unpatched_function(item) + return None + + +def get_unpatched_class(cls: type[_T]) -> type[_T]: + """Protect against re-patching the distutils if reloaded + + Also ensures that no other distutils extension monkeypatched the distutils + first. + """ + external_bases = ( + cast(type[_T], cls) + for cls in _get_mro(cls) + if not cls.__module__.startswith('setuptools') + ) + base = next(external_bases) + if not base.__module__.startswith('distutils'): + msg = f"distutils has already been patched by {cls!r}" + raise AssertionError(msg) + return base + + +def patch_all(): + import setuptools + + # we can't patch distutils.cmd, alas + distutils.core.Command = setuptools.Command # type: ignore[misc,assignment] # monkeypatching + + _patch_distribution_metadata() + + # Install Distribution throughout the distutils + for module in distutils.dist, distutils.core, distutils.cmd: + module.Distribution = setuptools.dist.Distribution + + # Install the patched Extension + distutils.core.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching + distutils.extension.Extension = setuptools.extension.Extension # type: ignore[misc,assignment] # monkeypatching + if 'distutils.command.build_ext' in sys.modules: + sys.modules[ + 'distutils.command.build_ext' + ].Extension = setuptools.extension.Extension + + +def _patch_distribution_metadata(): + from . import _core_metadata + + """Patch write_pkg_file and read_pkg_file for higher metadata standards""" + for attr in ( + 'write_pkg_info', + 'write_pkg_file', + 'read_pkg_file', + 'get_metadata_version', + 'get_fullname', + ): + new_val = getattr(_core_metadata, attr) + setattr(distutils.dist.DistributionMetadata, attr, new_val) + + +def patch_func(replacement, target_mod, func_name): + """ + Patch func_name in target_mod with replacement + + Important - original must be resolved by name to avoid + patching an already patched function. + """ + original = getattr(target_mod, func_name) + + # set the 'unpatched' attribute on the replacement to + # point to the original. + vars(replacement).setdefault('unpatched', original) + + # replace the function in the original module + setattr(target_mod, func_name, replacement) + + +def get_unpatched_function(candidate): + return candidate.unpatched diff --git a/lib/python3.10/site-packages/setuptools/msvc.py b/lib/python3.10/site-packages/setuptools/msvc.py new file mode 100644 index 0000000000000000000000000000000000000000..313a781ae095bbb20874795e5b87396790f0b9c9 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/msvc.py @@ -0,0 +1,1536 @@ +""" +Environment info about Microsoft Compilers. + +>>> getfixture('windows_only') +>>> ei = EnvironmentInfo('amd64') +""" + +from __future__ import annotations + +import contextlib +import itertools +import json +import os +import os.path +import platform +from typing import TYPE_CHECKING, TypedDict + +from more_itertools import unique_everseen + +import distutils.errors + +if TYPE_CHECKING: + from typing_extensions import LiteralString, NotRequired + +# https://github.com/python/mypy/issues/8166 +if not TYPE_CHECKING and platform.system() == 'Windows': + import winreg + from os import environ +else: + # Mock winreg and environ so the module can be imported on this platform. + + class winreg: + HKEY_USERS = None + HKEY_CURRENT_USER = None + HKEY_LOCAL_MACHINE = None + HKEY_CLASSES_ROOT = None + + environ: dict[str, str] = dict() + + +class PlatformInfo: + """ + Current and Target Architectures information. + + Parameters + ---------- + arch: str + Target architecture. + """ + + current_cpu = environ.get('processor_architecture', '').lower() + + def __init__(self, arch) -> None: + self.arch = arch.lower().replace('x64', 'amd64') + + @property + def target_cpu(self): + """ + Return Target CPU architecture. + + Return + ------ + str + Target CPU + """ + return self.arch[self.arch.find('_') + 1 :] + + def target_is_x86(self): + """ + Return True if target CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ + return self.target_cpu == 'x86' + + def current_is_x86(self): + """ + Return True if current CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ + return self.current_cpu == 'x86' + + def current_dir(self, hidex86=False, x64=False) -> str: + """ + Current platform specific subfolder. + + Parameters + ---------- + hidex86: bool + return '' and not '\x86' if architecture is x86. + x64: bool + return '\x64' and not '\amd64' if architecture is amd64. + + Return + ------ + str + subfolder: '\target', or '' (see hidex86 parameter) + """ + return ( + '' + if (self.current_cpu == 'x86' and hidex86) + else r'\x64' + if (self.current_cpu == 'amd64' and x64) + else rf'\{self.current_cpu}' + ) + + def target_dir(self, hidex86=False, x64=False) -> str: + r""" + Target platform specific subfolder. + + Parameters + ---------- + hidex86: bool + return '' and not '\x86' if architecture is x86. + x64: bool + return '\x64' and not '\amd64' if architecture is amd64. + + Return + ------ + str + subfolder: '\current', or '' (see hidex86 parameter) + """ + return ( + '' + if (self.target_cpu == 'x86' and hidex86) + else r'\x64' + if (self.target_cpu == 'amd64' and x64) + else rf'\{self.target_cpu}' + ) + + def cross_dir(self, forcex86=False): + r""" + Cross platform specific subfolder. + + Parameters + ---------- + forcex86: bool + Use 'x86' as current architecture even if current architecture is + not x86. + + Return + ------ + str + subfolder: '' if target architecture is current architecture, + '\current_target' if not. + """ + current = 'x86' if forcex86 else self.current_cpu + return ( + '' + if self.target_cpu == current + else self.target_dir().replace('\\', f'\\{current}_') + ) + + +class RegistryInfo: + """ + Microsoft Visual Studio related registry information. + + Parameters + ---------- + platform_info: PlatformInfo + "PlatformInfo" instance. + """ + + HKEYS = ( + winreg.HKEY_USERS, + winreg.HKEY_CURRENT_USER, + winreg.HKEY_LOCAL_MACHINE, + winreg.HKEY_CLASSES_ROOT, + ) + + def __init__(self, platform_info) -> None: + self.pi = platform_info + + @property + def visualstudio(self) -> str: + """ + Microsoft Visual Studio root registry key. + + Return + ------ + str + Registry key + """ + return 'VisualStudio' + + @property + def sxs(self): + """ + Microsoft Visual Studio SxS registry key. + + Return + ------ + str + Registry key + """ + return os.path.join(self.visualstudio, 'SxS') + + @property + def vc(self): + """ + Microsoft Visual C++ VC7 registry key. + + Return + ------ + str + Registry key + """ + return os.path.join(self.sxs, 'VC7') + + @property + def vs(self): + """ + Microsoft Visual Studio VS7 registry key. + + Return + ------ + str + Registry key + """ + return os.path.join(self.sxs, 'VS7') + + @property + def vc_for_python(self) -> str: + """ + Microsoft Visual C++ for Python registry key. + + Return + ------ + str + Registry key + """ + return r'DevDiv\VCForPython' + + @property + def microsoft_sdk(self) -> str: + """ + Microsoft SDK registry key. + + Return + ------ + str + Registry key + """ + return 'Microsoft SDKs' + + @property + def windows_sdk(self): + """ + Microsoft Windows/Platform SDK registry key. + + Return + ------ + str + Registry key + """ + return os.path.join(self.microsoft_sdk, 'Windows') + + @property + def netfx_sdk(self): + """ + Microsoft .NET Framework SDK registry key. + + Return + ------ + str + Registry key + """ + return os.path.join(self.microsoft_sdk, 'NETFXSDK') + + @property + def windows_kits_roots(self) -> str: + """ + Microsoft Windows Kits Roots registry key. + + Return + ------ + str + Registry key + """ + return r'Windows Kits\Installed Roots' + + def microsoft(self, key, x86=False): + """ + Return key in Microsoft software registry. + + Parameters + ---------- + key: str + Registry key path where look. + x86: str + Force x86 software registry. + + Return + ------ + str + Registry key + """ + node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node' + return os.path.join('Software', node64, 'Microsoft', key) + + def lookup(self, key, name): + """ + Look for values in registry in Microsoft software registry. + + Parameters + ---------- + key: str + Registry key path where look. + name: str + Value name to find. + + Return + ------ + str + value + """ + key_read = winreg.KEY_READ + openkey = winreg.OpenKey + closekey = winreg.CloseKey + ms = self.microsoft + for hkey in self.HKEYS: + bkey = None + try: + bkey = openkey(hkey, ms(key), 0, key_read) + except OSError: + if not self.pi.current_is_x86(): + try: + bkey = openkey(hkey, ms(key, True), 0, key_read) + except OSError: + continue + else: + continue + try: + return winreg.QueryValueEx(bkey, name)[0] + except OSError: + pass + finally: + if bkey: + closekey(bkey) + return None + + +class SystemInfo: + """ + Microsoft Windows and Visual Studio related system information. + + Parameters + ---------- + registry_info: RegistryInfo + "RegistryInfo" instance. + vc_ver: float + Required Microsoft Visual C++ version. + """ + + # Variables and properties in this class use originals CamelCase variables + # names from Microsoft source files for more easy comparison. + WinDir = environ.get('WinDir', '') + ProgramFiles = environ.get('ProgramFiles', '') + ProgramFilesx86 = environ.get('ProgramFiles(x86)', ProgramFiles) + + def __init__(self, registry_info, vc_ver=None) -> None: + self.ri = registry_info + self.pi = self.ri.pi + + self.known_vs_paths = self.find_programdata_vs_vers() + + # Except for VS15+, VC version is aligned with VS version + self.vs_ver = self.vc_ver = vc_ver or self._find_latest_available_vs_ver() + + def _find_latest_available_vs_ver(self): + """ + Find the latest VC version + + Return + ------ + float + version + """ + reg_vc_vers = self.find_reg_vs_vers() + + if not (reg_vc_vers or self.known_vs_paths): + raise distutils.errors.DistutilsPlatformError( + 'No Microsoft Visual C++ version found' + ) + + vc_vers = set(reg_vc_vers) + vc_vers.update(self.known_vs_paths) + return sorted(vc_vers)[-1] + + def find_reg_vs_vers(self): + """ + Find Microsoft Visual Studio versions available in registry. + + Return + ------ + list of float + Versions + """ + ms = self.ri.microsoft + vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) + vs_vers = [] + for hkey, key in itertools.product(self.ri.HKEYS, vckeys): + try: + bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) + except OSError: + continue + with bkey: + subkeys, values, _ = winreg.QueryInfoKey(bkey) + for i in range(values): + with contextlib.suppress(ValueError): + ver = float(winreg.EnumValue(bkey, i)[0]) + if ver not in vs_vers: + vs_vers.append(ver) + for i in range(subkeys): + with contextlib.suppress(ValueError): + ver = float(winreg.EnumKey(bkey, i)) + if ver not in vs_vers: + vs_vers.append(ver) + return sorted(vs_vers) + + def find_programdata_vs_vers(self) -> dict[float, str]: + r""" + Find Visual studio 2017+ versions from information in + "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances". + + Return + ------ + dict + float version as key, path as value. + """ + vs_versions: dict[float, str] = {} + instances_dir = r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' + + try: + hashed_names = os.listdir(instances_dir) + + except OSError: + # Directory not exists with all Visual Studio versions + return vs_versions + + for name in hashed_names: + try: + # Get VS installation path from "state.json" file + state_path = os.path.join(instances_dir, name, 'state.json') + with open(state_path, 'rt', encoding='utf-8') as state_file: + state = json.load(state_file) + vs_path = state['installationPath'] + + # Raises OSError if this VS installation does not contain VC + os.listdir(os.path.join(vs_path, r'VC\Tools\MSVC')) + + # Store version and path + vs_versions[self._as_float_version(state['installationVersion'])] = ( + vs_path + ) + + except (OSError, KeyError): + # Skip if "state.json" file is missing or bad format + continue + + return vs_versions + + @staticmethod + def _as_float_version(version): + """ + Return a string version as a simplified float version (major.minor) + + Parameters + ---------- + version: str + Version. + + Return + ------ + float + version + """ + return float('.'.join(version.split('.')[:2])) + + @property + def VSInstallDir(self): + """ + Microsoft Visual Studio directory. + + Return + ------ + str + path + """ + # Default path + default = os.path.join( + self.ProgramFilesx86, f'Microsoft Visual Studio {self.vs_ver:0.1f}' + ) + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vs, f'{self.vs_ver:0.1f}') or default + + @property + def VCInstallDir(self): + """ + Microsoft Visual C++ directory. + + Return + ------ + str + path + """ + path = self._guess_vc() or self._guess_vc_legacy() + + if not os.path.isdir(path): + msg = 'Microsoft Visual C++ directory not found' + raise distutils.errors.DistutilsPlatformError(msg) + + return path + + def _guess_vc(self): + """ + Locate Visual C++ for VS2017+. + + Return + ------ + str + path + """ + if self.vs_ver <= 14.0: + return '' + + try: + # First search in known VS paths + vs_dir = self.known_vs_paths[self.vs_ver] + except KeyError: + # Else, search with path from registry + vs_dir = self.VSInstallDir + + guess_vc = os.path.join(vs_dir, r'VC\Tools\MSVC') + + # Subdir with VC exact version as name + try: + # Update the VC version with real one instead of VS version + vc_ver = os.listdir(guess_vc)[-1] + self.vc_ver = self._as_float_version(vc_ver) + return os.path.join(guess_vc, vc_ver) + except (OSError, IndexError): + return '' + + def _guess_vc_legacy(self): + """ + Locate Visual C++ for versions prior to 2017. + + Return + ------ + str + path + """ + default = os.path.join( + self.ProgramFilesx86, + rf'Microsoft Visual Studio {self.vs_ver:0.1f}\VC', + ) + + # Try to get "VC++ for Python" path from registry as default path + reg_path = os.path.join(self.ri.vc_for_python, f'{self.vs_ver:0.1f}') + python_vc = self.ri.lookup(reg_path, 'installdir') + default_vc = os.path.join(python_vc, 'VC') if python_vc else default + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vc, f'{self.vs_ver:0.1f}') or default_vc + + @property + def WindowsSdkVersion(self) -> tuple[LiteralString, ...]: + """ + Microsoft Windows SDK versions for specified MSVC++ version. + + Return + ------ + tuple of str + versions + """ + if self.vs_ver <= 9.0: + return '7.0', '6.1', '6.0a' + elif self.vs_ver == 10.0: + return '7.1', '7.0a' + elif self.vs_ver == 11.0: + return '8.0', '8.0a' + elif self.vs_ver == 12.0: + return '8.1', '8.1a' + elif self.vs_ver >= 14.0: + return '10.0', '8.1' + return () + + @property + def WindowsSdkLastVersion(self): + """ + Microsoft Windows SDK last version. + + Return + ------ + str + version + """ + return self._use_last_dir_name(os.path.join(self.WindowsSdkDir, 'lib')) + + @property + def WindowsSdkDir(self) -> str | None: # noqa: C901 # is too complex (12) # FIXME + """ + Microsoft Windows SDK directory. + + Return + ------ + str + path + """ + sdkdir: str | None = '' + for ver in self.WindowsSdkVersion: + # Try to get it from registry + loc = os.path.join(self.ri.windows_sdk, f'v{ver}') + sdkdir = self.ri.lookup(loc, 'installationfolder') + if sdkdir: + break + if not sdkdir or not os.path.isdir(sdkdir): + # Try to get "VC++ for Python" version from registry + path = os.path.join(self.ri.vc_for_python, f'{self.vc_ver:0.1f}') + install_base = self.ri.lookup(path, 'installdir') + if install_base: + sdkdir = os.path.join(install_base, 'WinSDK') + if not sdkdir or not os.path.isdir(sdkdir): + # If fail, use default new path + for ver in self.WindowsSdkVersion: + intver = ver[: ver.rfind('.')] + path = rf'Microsoft SDKs\Windows Kits\{intver}' + d = os.path.join(self.ProgramFiles, path) + if os.path.isdir(d): + sdkdir = d + if not sdkdir or not os.path.isdir(sdkdir): + # If fail, use default old path + for ver in self.WindowsSdkVersion: + path = rf'Microsoft SDKs\Windows\v{ver}' + d = os.path.join(self.ProgramFiles, path) + if os.path.isdir(d): + sdkdir = d + if not sdkdir: + # If fail, use Platform SDK + sdkdir = os.path.join(self.VCInstallDir, 'PlatformSDK') + return sdkdir + + @property + def WindowsSDKExecutablePath(self): + """ + Microsoft Windows SDK executable directory. + + Return + ------ + str + path + """ + # Find WinSDK NetFx Tools registry dir name + if self.vs_ver <= 11.0: + netfxver = 35 + arch = '' + else: + netfxver = 40 + hidex86 = True if self.vs_ver <= 12.0 else False + arch = self.pi.current_dir(x64=True, hidex86=hidex86).replace('\\', '-') + fx = f'WinSDK-NetFx{netfxver}Tools{arch}' + + # list all possibles registry paths + regpaths = [] + if self.vs_ver >= 14.0: + for ver in self.NetFxSdkVersion: + regpaths += [os.path.join(self.ri.netfx_sdk, ver, fx)] + + for ver in self.WindowsSdkVersion: + regpaths += [os.path.join(self.ri.windows_sdk, f'v{ver}A', fx)] + + # Return installation folder from the more recent path + for path in regpaths: + execpath = self.ri.lookup(path, 'installationfolder') + if execpath: + return execpath + + return None + + @property + def FSharpInstallDir(self): + """ + Microsoft Visual F# directory. + + Return + ------ + str + path + """ + path = os.path.join(self.ri.visualstudio, rf'{self.vs_ver:0.1f}\Setup\F#') + return self.ri.lookup(path, 'productdir') or '' + + @property + def UniversalCRTSdkDir(self): + """ + Microsoft Universal CRT SDK directory. + + Return + ------ + str + path + """ + # Set Kit Roots versions for specified MSVC++ version + vers = ('10', '81') if self.vs_ver >= 14.0 else () + + # Find path of the more recent Kit + for ver in vers: + sdkdir = self.ri.lookup(self.ri.windows_kits_roots, f'kitsroot{ver}') + if sdkdir: + return sdkdir or '' + + return None + + @property + def UniversalCRTSdkLastVersion(self): + """ + Microsoft Universal C Runtime SDK last version. + + Return + ------ + str + version + """ + return self._use_last_dir_name(os.path.join(self.UniversalCRTSdkDir, 'lib')) + + @property + def NetFxSdkVersion(self): + """ + Microsoft .NET Framework SDK versions. + + Return + ------ + tuple of str + versions + """ + # Set FxSdk versions for specified VS version + return ( + ('4.7.2', '4.7.1', '4.7', '4.6.2', '4.6.1', '4.6', '4.5.2', '4.5.1', '4.5') + if self.vs_ver >= 14.0 + else () + ) + + @property + def NetFxSdkDir(self): + """ + Microsoft .NET Framework SDK directory. + + Return + ------ + str + path + """ + sdkdir = '' + for ver in self.NetFxSdkVersion: + loc = os.path.join(self.ri.netfx_sdk, ver) + sdkdir = self.ri.lookup(loc, 'kitsinstallationfolder') + if sdkdir: + break + return sdkdir + + @property + def FrameworkDir32(self): + """ + Microsoft .NET Framework 32bit directory. + + Return + ------ + str + path + """ + # Default path + guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework') + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vc, 'frameworkdir32') or guess_fw + + @property + def FrameworkDir64(self): + """ + Microsoft .NET Framework 64bit directory. + + Return + ------ + str + path + """ + # Default path + guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework64') + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw + + @property + def FrameworkVersion32(self) -> tuple[str, ...]: + """ + Microsoft .NET Framework 32bit versions. + + Return + ------ + tuple of str + versions + """ + return self._find_dot_net_versions(32) + + @property + def FrameworkVersion64(self) -> tuple[str, ...]: + """ + Microsoft .NET Framework 64bit versions. + + Return + ------ + tuple of str + versions + """ + return self._find_dot_net_versions(64) + + def _find_dot_net_versions(self, bits) -> tuple[str, ...]: + """ + Find Microsoft .NET Framework versions. + + Parameters + ---------- + bits: int + Platform number of bits: 32 or 64. + + Return + ------ + tuple of str + versions + """ + # Find actual .NET version in registry + reg_ver = self.ri.lookup(self.ri.vc, f'frameworkver{bits}') + dot_net_dir = getattr(self, f'FrameworkDir{bits}') + ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or '' + + # Set .NET versions for specified MSVC++ version + if self.vs_ver >= 12.0: + return ver, 'v4.0' + elif self.vs_ver >= 10.0: + return 'v4.0.30319' if ver.lower()[:2] != 'v4' else ver, 'v3.5' + elif self.vs_ver == 9.0: + return 'v3.5', 'v2.0.50727' + elif self.vs_ver == 8.0: + return 'v3.0', 'v2.0.50727' + return () + + @staticmethod + def _use_last_dir_name(path, prefix=''): + """ + Return name of the last dir in path or '' if no dir found. + + Parameters + ---------- + path: str + Use dirs in this path + prefix: str + Use only dirs starting by this prefix + + Return + ------ + str + name + """ + matching_dirs = ( + dir_name + for dir_name in reversed(os.listdir(path)) + if os.path.isdir(os.path.join(path, dir_name)) + and dir_name.startswith(prefix) + ) + return next(matching_dirs, None) or '' + + +class _EnvironmentDict(TypedDict): + include: str + lib: str + libpath: str + path: str + py_vcruntime_redist: NotRequired[str | None] + + +class EnvironmentInfo: + """ + Return environment variables for specified Microsoft Visual C++ version + and platform : Lib, Include, Path and libpath. + + This function is compatible with Microsoft Visual C++ 9.0 to 14.X. + + Script created by analysing Microsoft environment configuration files like + "vcvars[...].bat", "SetEnv.Cmd", "vcbuildtools.bat", ... + + Parameters + ---------- + arch: str + Target architecture. + vc_ver: float + Required Microsoft Visual C++ version. If not set, autodetect the last + version. + vc_min_ver: float + Minimum Microsoft Visual C++ version. + """ + + # Variables and properties in this class use originals CamelCase variables + # names from Microsoft source files for more easy comparison. + + def __init__(self, arch, vc_ver=None, vc_min_ver=0) -> None: + self.pi = PlatformInfo(arch) + self.ri = RegistryInfo(self.pi) + self.si = SystemInfo(self.ri, vc_ver) + + if self.vc_ver < vc_min_ver: + err = 'No suitable Microsoft Visual C++ version found' + raise distutils.errors.DistutilsPlatformError(err) + + @property + def vs_ver(self): + """ + Microsoft Visual Studio. + + Return + ------ + float + version + """ + return self.si.vs_ver + + @property + def vc_ver(self): + """ + Microsoft Visual C++ version. + + Return + ------ + float + version + """ + return self.si.vc_ver + + @property + def VSTools(self): + """ + Microsoft Visual Studio Tools. + + Return + ------ + list of str + paths + """ + paths = [r'Common7\IDE', r'Common7\Tools'] + + if self.vs_ver >= 14.0: + arch_subdir = self.pi.current_dir(hidex86=True, x64=True) + paths += [r'Common7\IDE\CommonExtensions\Microsoft\TestWindow'] + paths += [r'Team Tools\Performance Tools'] + paths += [rf'Team Tools\Performance Tools{arch_subdir}'] + + return [os.path.join(self.si.VSInstallDir, path) for path in paths] + + @property + def VCIncludes(self): + """ + Microsoft Visual C++ & Microsoft Foundation Class Includes. + + Return + ------ + list of str + paths + """ + return [ + os.path.join(self.si.VCInstallDir, 'Include'), + os.path.join(self.si.VCInstallDir, r'ATLMFC\Include'), + ] + + @property + def VCLibraries(self): + """ + Microsoft Visual C++ & Microsoft Foundation Class Libraries. + + Return + ------ + list of str + paths + """ + if self.vs_ver >= 15.0: + arch_subdir = self.pi.target_dir(x64=True) + else: + arch_subdir = self.pi.target_dir(hidex86=True) + paths = [f'Lib{arch_subdir}', rf'ATLMFC\Lib{arch_subdir}'] + + if self.vs_ver >= 14.0: + paths += [rf'Lib\store{arch_subdir}'] + + return [os.path.join(self.si.VCInstallDir, path) for path in paths] + + @property + def VCStoreRefs(self): + """ + Microsoft Visual C++ store references Libraries. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 14.0: + return [] + return [os.path.join(self.si.VCInstallDir, r'Lib\store\references')] + + @property + def VCTools(self): + """ + Microsoft Visual C++ Tools. + + Return + ------ + list of str + paths + + When host CPU is ARM, the tools should be found for ARM. + + >>> getfixture('windows_only') + >>> mp = getfixture('monkeypatch') + >>> mp.setattr(PlatformInfo, 'current_cpu', 'arm64') + >>> ei = EnvironmentInfo(arch='irrelevant') + >>> paths = ei.VCTools + >>> any('HostARM64' in path for path in paths) + True + """ + si = self.si + tools = [os.path.join(si.VCInstallDir, 'VCPackages')] + + forcex86 = True if self.vs_ver <= 10.0 else False + arch_subdir = self.pi.cross_dir(forcex86) + if arch_subdir: + tools += [os.path.join(si.VCInstallDir, f'Bin{arch_subdir}')] + + if self.vs_ver == 14.0: + path = f'Bin{self.pi.current_dir(hidex86=True)}' + tools += [os.path.join(si.VCInstallDir, path)] + + elif self.vs_ver >= 15.0: + host_id = self.pi.current_cpu.replace('amd64', 'x64').upper() + host_dir = os.path.join('bin', f'Host{host_id}%s') + tools += [ + os.path.join(si.VCInstallDir, host_dir % self.pi.target_dir(x64=True)) + ] + + if self.pi.current_cpu != self.pi.target_cpu: + tools += [ + os.path.join( + si.VCInstallDir, host_dir % self.pi.current_dir(x64=True) + ) + ] + + else: + tools += [os.path.join(si.VCInstallDir, 'Bin')] + + return tools + + @property + def OSLibraries(self): + """ + Microsoft Windows SDK Libraries. + + Return + ------ + list of str + paths + """ + if self.vs_ver <= 10.0: + arch_subdir = self.pi.target_dir(hidex86=True, x64=True) + return [os.path.join(self.si.WindowsSdkDir, f'Lib{arch_subdir}')] + + else: + arch_subdir = self.pi.target_dir(x64=True) + lib = os.path.join(self.si.WindowsSdkDir, 'lib') + libver = self._sdk_subdir + return [os.path.join(lib, f'{libver}um{arch_subdir}')] + + @property + def OSIncludes(self): + """ + Microsoft Windows SDK Include. + + Return + ------ + list of str + paths + """ + include = os.path.join(self.si.WindowsSdkDir, 'include') + + if self.vs_ver <= 10.0: + return [include, os.path.join(include, 'gl')] + + else: + if self.vs_ver >= 14.0: + sdkver = self._sdk_subdir + else: + sdkver = '' + return [ + os.path.join(include, f'{sdkver}shared'), + os.path.join(include, f'{sdkver}um'), + os.path.join(include, f'{sdkver}winrt'), + ] + + @property + def OSLibpath(self): + """ + Microsoft Windows SDK Libraries Paths. + + Return + ------ + list of str + paths + """ + ref = os.path.join(self.si.WindowsSdkDir, 'References') + libpath = [] + + if self.vs_ver <= 9.0: + libpath += self.OSLibraries + + if self.vs_ver >= 11.0: + libpath += [os.path.join(ref, r'CommonConfiguration\Neutral')] + + if self.vs_ver >= 14.0: + libpath += [ + ref, + os.path.join(self.si.WindowsSdkDir, 'UnionMetadata'), + os.path.join(ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'), + os.path.join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'), + os.path.join( + ref, 'Windows.Networking.Connectivity.WwanContract', '1.0.0.0' + ), + os.path.join( + self.si.WindowsSdkDir, + 'ExtensionSDKs', + 'Microsoft.VCLibs', + f'{self.vs_ver:0.1f}', + 'References', + 'CommonConfiguration', + 'neutral', + ), + ] + return libpath + + @property + def SdkTools(self): + """ + Microsoft Windows SDK Tools. + + Return + ------ + list of str + paths + """ + return list(self._sdk_tools()) + + def _sdk_tools(self): + """ + Microsoft Windows SDK Tools paths generator. + + Return + ------ + generator of str + paths + """ + if self.vs_ver < 15.0: + bin_dir = 'Bin' if self.vs_ver <= 11.0 else r'Bin\x86' + yield os.path.join(self.si.WindowsSdkDir, bin_dir) + + if not self.pi.current_is_x86(): + arch_subdir = self.pi.current_dir(x64=True) + path = f'Bin{arch_subdir}' + yield os.path.join(self.si.WindowsSdkDir, path) + + if self.vs_ver in (10.0, 11.0): + if self.pi.target_is_x86(): + arch_subdir = '' + else: + arch_subdir = self.pi.current_dir(hidex86=True, x64=True) + path = rf'Bin\NETFX 4.0 Tools{arch_subdir}' + yield os.path.join(self.si.WindowsSdkDir, path) + + elif self.vs_ver >= 15.0: + path = os.path.join(self.si.WindowsSdkDir, 'Bin') + arch_subdir = self.pi.current_dir(x64=True) + sdkver = self.si.WindowsSdkLastVersion + yield os.path.join(path, f'{sdkver}{arch_subdir}') + + if self.si.WindowsSDKExecutablePath: + yield self.si.WindowsSDKExecutablePath + + @property + def _sdk_subdir(self): + """ + Microsoft Windows SDK version subdir. + + Return + ------ + str + subdir + """ + ucrtver = self.si.WindowsSdkLastVersion + return (f'{ucrtver}\\') if ucrtver else '' + + @property + def SdkSetup(self): + """ + Microsoft Windows SDK Setup. + + Return + ------ + list of str + paths + """ + if self.vs_ver > 9.0: + return [] + + return [os.path.join(self.si.WindowsSdkDir, 'Setup')] + + @property + def FxTools(self): + """ + Microsoft .NET Framework Tools. + + Return + ------ + list of str + paths + """ + pi = self.pi + si = self.si + + if self.vs_ver <= 10.0: + include32 = True + include64 = not pi.target_is_x86() and not pi.current_is_x86() + else: + include32 = pi.target_is_x86() or pi.current_is_x86() + include64 = pi.current_cpu == 'amd64' or pi.target_cpu == 'amd64' + + tools = [] + if include32: + tools += [ + os.path.join(si.FrameworkDir32, ver) for ver in si.FrameworkVersion32 + ] + if include64: + tools += [ + os.path.join(si.FrameworkDir64, ver) for ver in si.FrameworkVersion64 + ] + return tools + + @property + def NetFxSDKLibraries(self): + """ + Microsoft .Net Framework SDK Libraries. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: + return [] + + arch_subdir = self.pi.target_dir(x64=True) + return [os.path.join(self.si.NetFxSdkDir, rf'lib\um{arch_subdir}')] + + @property + def NetFxSDKIncludes(self): + """ + Microsoft .Net Framework SDK Includes. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: + return [] + + return [os.path.join(self.si.NetFxSdkDir, r'include\um')] + + @property + def VsTDb(self): + """ + Microsoft Visual Studio Team System Database. + + Return + ------ + list of str + paths + """ + return [os.path.join(self.si.VSInstallDir, r'VSTSDB\Deploy')] + + @property + def MSBuild(self): + """ + Microsoft Build Engine. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 12.0: + return [] + elif self.vs_ver < 15.0: + base_path = self.si.ProgramFilesx86 + arch_subdir = self.pi.current_dir(hidex86=True) + else: + base_path = self.si.VSInstallDir + arch_subdir = '' + + path = rf'MSBuild\{self.vs_ver:0.1f}\bin{arch_subdir}' + build = [os.path.join(base_path, path)] + + if self.vs_ver >= 15.0: + # Add Roslyn C# & Visual Basic Compiler + build += [os.path.join(base_path, path, 'Roslyn')] + + return build + + @property + def HTMLHelpWorkshop(self): + """ + Microsoft HTML Help Workshop. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 11.0: + return [] + + return [os.path.join(self.si.ProgramFilesx86, 'HTML Help Workshop')] + + @property + def UCRTLibraries(self): + """ + Microsoft Universal C Runtime SDK Libraries. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 14.0: + return [] + + arch_subdir = self.pi.target_dir(x64=True) + lib = os.path.join(self.si.UniversalCRTSdkDir, 'lib') + ucrtver = self._ucrt_subdir + return [os.path.join(lib, f'{ucrtver}ucrt{arch_subdir}')] + + @property + def UCRTIncludes(self): + """ + Microsoft Universal C Runtime SDK Include. + + Return + ------ + list of str + paths + """ + if self.vs_ver < 14.0: + return [] + + include = os.path.join(self.si.UniversalCRTSdkDir, 'include') + return [os.path.join(include, f'{self._ucrt_subdir}ucrt')] + + @property + def _ucrt_subdir(self): + """ + Microsoft Universal C Runtime SDK version subdir. + + Return + ------ + str + subdir + """ + ucrtver = self.si.UniversalCRTSdkLastVersion + return (f'{ucrtver}\\') if ucrtver else '' + + @property + def FSharp(self): + """ + Microsoft Visual F#. + + Return + ------ + list of str + paths + """ + if 11.0 > self.vs_ver > 12.0: + return [] + + return [self.si.FSharpInstallDir] + + @property + def VCRuntimeRedist(self) -> str | None: + """ + Microsoft Visual C++ runtime redistributable dll. + + Returns the first suitable path found or None. + """ + vcruntime = f'vcruntime{self.vc_ver}0.dll' + arch_subdir = self.pi.target_dir(x64=True).strip('\\') + + # Installation prefixes candidates + prefixes = [] + tools_path = self.si.VCInstallDir + redist_path = os.path.dirname(tools_path.replace(r'\Tools', r'\Redist')) + if os.path.isdir(redist_path): + # Redist version may not be exactly the same as tools + redist_path = os.path.join(redist_path, os.listdir(redist_path)[-1]) + prefixes += [redist_path, os.path.join(redist_path, 'onecore')] + + prefixes += [os.path.join(tools_path, 'redist')] # VS14 legacy path + + # CRT directory + crt_dirs = ( + f'Microsoft.VC{self.vc_ver * 10}.CRT', + # Sometime store in directory with VS version instead of VC + f'Microsoft.VC{int(self.vs_ver) * 10}.CRT', + ) + + # vcruntime path + candidate_paths = ( + os.path.join(prefix, arch_subdir, crt_dir, vcruntime) + for (prefix, crt_dir) in itertools.product(prefixes, crt_dirs) + ) + return next(filter(os.path.isfile, candidate_paths), None) # type: ignore[arg-type] #python/mypy#12682 + + def return_env(self, exists: bool = True) -> _EnvironmentDict: + """ + Return environment dict. + + Parameters + ---------- + exists: bool + It True, only return existing paths. + + Return + ------ + dict + environment + """ + env = _EnvironmentDict( + include=self._build_paths( + 'include', + [ + self.VCIncludes, + self.OSIncludes, + self.UCRTIncludes, + self.NetFxSDKIncludes, + ], + exists, + ), + lib=self._build_paths( + 'lib', + [ + self.VCLibraries, + self.OSLibraries, + self.FxTools, + self.UCRTLibraries, + self.NetFxSDKLibraries, + ], + exists, + ), + libpath=self._build_paths( + 'libpath', + [self.VCLibraries, self.FxTools, self.VCStoreRefs, self.OSLibpath], + exists, + ), + path=self._build_paths( + 'path', + [ + self.VCTools, + self.VSTools, + self.VsTDb, + self.SdkTools, + self.SdkSetup, + self.FxTools, + self.MSBuild, + self.HTMLHelpWorkshop, + self.FSharp, + ], + exists, + ), + ) + if self.vs_ver >= 14 and self.VCRuntimeRedist: + env['py_vcruntime_redist'] = self.VCRuntimeRedist + return env + + def _build_paths(self, name, spec_path_lists, exists): + """ + Given an environment variable name and specified paths, + return a pathsep-separated string of paths containing + unique, extant, directories from those paths and from + the environment variable. Raise an error if no paths + are resolved. + + Parameters + ---------- + name: str + Environment variable name + spec_path_lists: list of str + Paths + exists: bool + It True, only return existing paths. + + Return + ------ + str + Pathsep-separated paths + """ + # flatten spec_path_lists + spec_paths = itertools.chain.from_iterable(spec_path_lists) + env_paths = environ.get(name, '').split(os.pathsep) + paths = itertools.chain(spec_paths, env_paths) + extant_paths = list(filter(os.path.isdir, paths)) if exists else paths + if not extant_paths: + msg = f"{name.upper()} environment variable is empty" + raise distutils.errors.DistutilsPlatformError(msg) + unique_paths = unique_everseen(extant_paths) + return os.pathsep.join(unique_paths) diff --git a/lib/python3.10/site-packages/setuptools/namespaces.py b/lib/python3.10/site-packages/setuptools/namespaces.py new file mode 100644 index 0000000000000000000000000000000000000000..85ea2ebd654c480b8c19d1715b3772c4bcfd812e --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/namespaces.py @@ -0,0 +1,106 @@ +import itertools +import os + +from .compat import py312 + +from distutils import log + +flatten = itertools.chain.from_iterable + + +class Installer: + nspkg_ext = '-nspkg.pth' + + def install_namespaces(self) -> None: + nsp = self._get_all_ns_packages() + if not nsp: + return + filename = self._get_nspkg_file() + self.outputs.append(filename) + log.info("Installing %s", filename) + lines = map(self._gen_nspkg_line, nsp) + + if self.dry_run: + # always generate the lines, even in dry run + list(lines) + return + + with open(filename, 'wt', encoding=py312.PTH_ENCODING) as f: + # Python<3.13 requires encoding="locale" instead of "utf-8" + # See: python/cpython#77102 + f.writelines(lines) + + def uninstall_namespaces(self) -> None: + filename = self._get_nspkg_file() + if not os.path.exists(filename): + return + log.info("Removing %s", filename) + os.remove(filename) + + def _get_nspkg_file(self): + filename, _ = os.path.splitext(self._get_target()) + return filename + self.nspkg_ext + + def _get_target(self): + return self.target + + _nspkg_tmpl = ( + "import sys, types, os", + "p = os.path.join(%(root)s, *%(pth)r)", + "importlib = __import__('importlib.util')", + "__import__('importlib.machinery')", + ( + "m = " + "sys.modules.setdefault(%(pkg)r, " + "importlib.util.module_from_spec(" + "importlib.machinery.PathFinder.find_spec(%(pkg)r, " + "[os.path.dirname(p)])))" + ), + ("m = m or sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))"), + "mp = (m or []) and m.__dict__.setdefault('__path__',[])", + "(p not in mp) and mp.append(p)", + ) + "lines for the namespace installer" + + _nspkg_tmpl_multi = ('m and setattr(sys.modules[%(parent)r], %(child)r, m)',) + "additional line(s) when a parent package is indicated" + + def _get_root(self): + return "sys._getframe(1).f_locals['sitedir']" + + def _gen_nspkg_line(self, pkg): + pth = tuple(pkg.split('.')) + root = self._get_root() + tmpl_lines = self._nspkg_tmpl + parent, sep, child = pkg.rpartition('.') + if parent: + tmpl_lines += self._nspkg_tmpl_multi + return ';'.join(tmpl_lines) % locals() + '\n' + + def _get_all_ns_packages(self): + """Return sorted list of all package namespaces""" + pkgs = self.distribution.namespace_packages or [] + return sorted(set(flatten(map(self._pkg_names, pkgs)))) + + @staticmethod + def _pkg_names(pkg): + """ + Given a namespace package, yield the components of that + package. + + >>> names = Installer._pkg_names('a.b.c') + >>> set(names) == set(['a', 'a.b', 'a.b.c']) + True + """ + parts = pkg.split('.') + while parts: + yield '.'.join(parts) + parts.pop() + + +class DevelopInstaller(Installer): + def _get_root(self): + return repr(str(self.egg_path)) + + def _get_target(self): + return self.egg_link diff --git a/lib/python3.10/site-packages/setuptools/package_index.py b/lib/python3.10/site-packages/setuptools/package_index.py new file mode 100644 index 0000000000000000000000000000000000000000..112267755d29a3f4347aa87f9032dd0e875440a6 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/package_index.py @@ -0,0 +1,1183 @@ +"""PyPI and direct package downloading.""" + +from __future__ import annotations + +import base64 +import configparser +import hashlib +import html +import http.client +import io +import itertools +import os +import re +import shutil +import socket +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from fnmatch import translate +from functools import wraps +from typing import NamedTuple + +from more_itertools import unique_everseen + +import setuptools +from pkg_resources import ( + BINARY_DIST, + CHECKOUT_DIST, + DEVELOP_DIST, + EGG_DIST, + SOURCE_DIST, + Distribution, + Environment, + Requirement, + find_distributions, + normalize_path, + parse_version, + safe_name, + safe_version, + to_filename, +) +from setuptools.wheel import Wheel + +from .unicode_utils import _cfg_read_utf8_with_fallback, _read_utf8_with_fallback + +from distutils import log +from distutils.errors import DistutilsError + +EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$') +HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I) +PYPI_MD5 = re.compile( + r'([^<]+)\n\s+\(md5\)' +) +URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match +EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split() + +__all__ = [ + 'PackageIndex', + 'distros_for_url', + 'parse_bdist_wininst', + 'interpret_distro_name', +] + +_SOCKET_TIMEOUT = 15 + +user_agent = f"setuptools/{setuptools.__version__} Python-urllib/{sys.version_info.major}.{sys.version_info.minor}" + + +def parse_requirement_arg(spec): + try: + return Requirement.parse(spec) + except ValueError as e: + raise DistutilsError( + f"Not a URL, existing file, or requirement spec: {spec!r}" + ) from e + + +def parse_bdist_wininst(name): + """Return (base,pyversion) or (None,None) for possible .exe name""" + + lower = name.lower() + base, py_ver, plat = None, None, None + + if lower.endswith('.exe'): + if lower.endswith('.win32.exe'): + base = name[:-10] + plat = 'win32' + elif lower.startswith('.win32-py', -16): + py_ver = name[-7:-4] + base = name[:-16] + plat = 'win32' + elif lower.endswith('.win-amd64.exe'): + base = name[:-14] + plat = 'win-amd64' + elif lower.startswith('.win-amd64-py', -20): + py_ver = name[-7:-4] + base = name[:-20] + plat = 'win-amd64' + return base, py_ver, plat + + +def egg_info_for_url(url): + parts = urllib.parse.urlparse(url) + _scheme, server, path, _parameters, _query, fragment = parts + base = urllib.parse.unquote(path.split('/')[-1]) + if server == 'sourceforge.net' and base == 'download': # XXX Yuck + base = urllib.parse.unquote(path.split('/')[-2]) + if '#' in base: + base, fragment = base.split('#', 1) + return base, fragment + + +def distros_for_url(url, metadata=None): + """Yield egg or source distribution objects that might be found at a URL""" + base, fragment = egg_info_for_url(url) + yield from distros_for_location(url, base, metadata) + if fragment: + match = EGG_FRAGMENT.match(fragment) + if match: + yield from interpret_distro_name( + url, match.group(1), metadata, precedence=CHECKOUT_DIST + ) + + +def distros_for_location(location, basename, metadata=None): + """Yield egg or source distribution objects based on basename""" + if basename.endswith('.egg.zip'): + basename = basename[:-4] # strip the .zip + if basename.endswith('.egg') and '-' in basename: + # only one, unambiguous interpretation + return [Distribution.from_location(location, basename, metadata)] + if basename.endswith('.whl') and '-' in basename: + wheel = Wheel(basename) + if not wheel.is_compatible(): + return [] + return [ + Distribution( + location=location, + project_name=wheel.project_name, + version=wheel.version, + # Increase priority over eggs. + precedence=EGG_DIST + 1, + ) + ] + if basename.endswith('.exe'): + win_base, py_ver, platform = parse_bdist_wininst(basename) + if win_base is not None: + return interpret_distro_name( + location, win_base, metadata, py_ver, BINARY_DIST, platform + ) + # Try source distro extensions (.zip, .tgz, etc.) + # + for ext in EXTENSIONS: + if basename.endswith(ext): + basename = basename[: -len(ext)] + return interpret_distro_name(location, basename, metadata) + return [] # no extension matched + + +def distros_for_filename(filename, metadata=None): + """Yield possible egg or source distribution objects based on a filename""" + return distros_for_location( + normalize_path(filename), os.path.basename(filename), metadata + ) + + +def interpret_distro_name( + location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None +): + """Generate the interpretation of a source distro name + + Note: if `location` is a filesystem filename, you should call + ``pkg_resources.normalize_path()`` on it before passing it to this + routine! + """ + + parts = basename.split('-') + if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]): + # it is a bdist_dumb, not an sdist -- bail out + return + + # find the pivot (p) that splits the name from the version. + # infer the version as the first item that has a digit. + for p in range(len(parts)): + if parts[p][:1].isdigit(): + break + else: + p = len(parts) + + yield Distribution( + location, + metadata, + '-'.join(parts[:p]), + '-'.join(parts[p:]), + py_version=py_version, + precedence=precedence, + platform=platform, + ) + + +def unique_values(func): + """ + Wrap a function returning an iterable such that the resulting iterable + only ever yields unique items. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + return unique_everseen(func(*args, **kwargs)) + + return wrapper + + +REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I) +""" +Regex for an HTML tag with 'rel="val"' attributes. +""" + + +@unique_values +def find_external_links(url, page): + """Find rel="homepage" and rel="download" links in `page`, yielding URLs""" + + for match in REL.finditer(page): + tag, rel = match.groups() + rels = set(map(str.strip, rel.lower().split(','))) + if 'homepage' in rels or 'download' in rels: + for match in HREF.finditer(tag): + yield urllib.parse.urljoin(url, htmldecode(match.group(1))) + + for tag in ("
Home Page", "Download URL"): + pos = page.find(tag) + if pos != -1: + match = HREF.search(page, pos) + if match: + yield urllib.parse.urljoin(url, htmldecode(match.group(1))) + + +class ContentChecker: + """ + A null content checker that defines the interface for checking content + """ + + def feed(self, block): + """ + Feed a block of data to the hash. + """ + return + + def is_valid(self): + """ + Check the hash. Return False if validation fails. + """ + return True + + def report(self, reporter, template): + """ + Call reporter with information about the checker (hash name) + substituted into the template. + """ + return + + +class HashChecker(ContentChecker): + pattern = re.compile( + r'(?Psha1|sha224|sha384|sha256|sha512|md5)=' + r'(?P[a-f0-9]+)' + ) + + def __init__(self, hash_name, expected) -> None: + self.hash_name = hash_name + self.hash = hashlib.new(hash_name) + self.expected = expected + + @classmethod + def from_url(cls, url): + "Construct a (possibly null) ContentChecker from a URL" + fragment = urllib.parse.urlparse(url)[-1] + if not fragment: + return ContentChecker() + match = cls.pattern.search(fragment) + if not match: + return ContentChecker() + return cls(**match.groupdict()) + + def feed(self, block): + self.hash.update(block) + + def is_valid(self): + return self.hash.hexdigest() == self.expected + + def report(self, reporter, template): + msg = template % self.hash_name + return reporter(msg) + + +class PackageIndex(Environment): + """A distribution index that scans web pages for download URLs""" + + def __init__( + self, + index_url: str = "https://pypi.org/simple/", + hosts=('*',), + ca_bundle=None, + verify_ssl: bool = True, + *args, + **kw, + ) -> None: + super().__init__(*args, **kw) + self.index_url = index_url + "/"[: not index_url.endswith('/')] + self.scanned_urls: dict = {} + self.fetched_urls: dict = {} + self.package_pages: dict = {} + self.allows = re.compile('|'.join(map(translate, hosts))).match + self.to_scan: list = [] + self.opener = urllib.request.urlopen + + def add(self, dist): + # ignore invalid versions + try: + parse_version(dist.version) + except Exception: + return None + return super().add(dist) + + # FIXME: 'PackageIndex.process_url' is too complex (14) + def process_url(self, url, retrieve: bool = False) -> None: # noqa: C901 + """Evaluate a URL as a possible download, and maybe retrieve it""" + if os.getenv("CONDA_BUILD"): + raise RuntimeError("Setuptools downloading is disabled in conda build. " + "Be sure to add all dependencies in the meta.yaml url=%s" % url) + + if url in self.scanned_urls and not retrieve: + return + self.scanned_urls[url] = True + if not URL_SCHEME(url): + self.process_filename(url) + return + else: + dists = list(distros_for_url(url)) + if dists: + if not self.url_ok(url): + return + self.debug("Found link: %s", url) + + if dists or not retrieve or url in self.fetched_urls: + list(map(self.add, dists)) + return # don't need the actual page + + if not self.url_ok(url): + self.fetched_urls[url] = True + return + + self.info("Reading %s", url) + self.fetched_urls[url] = True # prevent multiple fetch attempts + tmpl = "Download error on %s: %%s -- Some packages may not be found!" + f = self.open_url(url, tmpl % url) + if f is None: + return + if isinstance(f, urllib.error.HTTPError) and f.code == 401: + self.info(f"Authentication error: {f.msg}") + self.fetched_urls[f.url] = True + if 'html' not in f.headers.get('content-type', '').lower(): + f.close() # not html, we can't process it + return + + base = f.url # handle redirects + page = f.read() + if not isinstance(page, str): + # In Python 3 and got bytes but want str. + if isinstance(f, urllib.error.HTTPError): + # Errors have no charset, assume latin1: + charset = 'latin-1' + else: + charset = f.headers.get_param('charset') or 'latin-1' + page = page.decode(charset, "ignore") + f.close() + for match in HREF.finditer(page): + link = urllib.parse.urljoin(base, htmldecode(match.group(1))) + self.process_url(link) + if url.startswith(self.index_url) and getattr(f, 'code', None) != 404: + page = self.process_index(url, page) + + def process_filename(self, fn, nested: bool = False) -> None: + # process filenames or directories + if not os.path.exists(fn): + self.warn("Not found: %s", fn) + return + + if os.path.isdir(fn) and not nested: + path = os.path.realpath(fn) + for item in os.listdir(path): + self.process_filename(os.path.join(path, item), True) + + dists = distros_for_filename(fn) + if dists: + self.debug("Found: %s", fn) + list(map(self.add, dists)) + + def url_ok(self, url, fatal: bool = False) -> bool: + s = URL_SCHEME(url) + is_file = s and s.group(1).lower() == 'file' + if is_file or self.allows(urllib.parse.urlparse(url)[1]): + return True + msg = ( + "\nNote: Bypassing %s (disallowed host; see " + "https://setuptools.pypa.io/en/latest/deprecated/" + "easy_install.html#restricting-downloads-with-allow-hosts for details).\n" + ) + if fatal: + raise DistutilsError(msg % url) + else: + self.warn(msg, url) + return False + + def scan_egg_links(self, search_path) -> None: + dirs = filter(os.path.isdir, search_path) + egg_links = ( + (path, entry) + for path in dirs + for entry in os.listdir(path) + if entry.endswith('.egg-link') + ) + list(itertools.starmap(self.scan_egg_link, egg_links)) + + def scan_egg_link(self, path, entry) -> None: + content = _read_utf8_with_fallback(os.path.join(path, entry)) + # filter non-empty lines + lines = list(filter(None, map(str.strip, content.splitlines()))) + + if len(lines) != 2: + # format is not recognized; punt + return + + egg_path, _setup_path = lines + + for dist in find_distributions(os.path.join(path, egg_path)): + dist.location = os.path.join(path, *lines) + dist.precedence = SOURCE_DIST + self.add(dist) + + def _scan(self, link): + # Process a URL to see if it's for a package page + NO_MATCH_SENTINEL = None, None + if not link.startswith(self.index_url): + return NO_MATCH_SENTINEL + + parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/'))) + if len(parts) != 2 or '#' in parts[1]: + return NO_MATCH_SENTINEL + + # it's a package page, sanitize and index it + pkg = safe_name(parts[0]) + ver = safe_version(parts[1]) + self.package_pages.setdefault(pkg.lower(), {})[link] = True + return to_filename(pkg), to_filename(ver) + + def process_index(self, url, page): + """Process the contents of a PyPI page""" + + # process an index page into the package-page index + for match in HREF.finditer(page): + try: + self._scan(urllib.parse.urljoin(url, htmldecode(match.group(1)))) + except ValueError: + pass + + pkg, ver = self._scan(url) # ensure this page is in the page index + if not pkg: + return "" # no sense double-scanning non-package pages + + # process individual package page + for new_url in find_external_links(url, page): + # Process the found URL + base, frag = egg_info_for_url(new_url) + if base.endswith('.py') and not frag: + if ver: + new_url += f'#egg={pkg}-{ver}' + else: + self.need_version_info(url) + self.scan_url(new_url) + + return PYPI_MD5.sub( + lambda m: '{}'.format(*m.group(1, 3, 2)), page + ) + + def need_version_info(self, url) -> None: + self.scan_all( + "Page at %s links to .py file(s) without version info; an index " + "scan is required.", + url, + ) + + def scan_all(self, msg=None, *args) -> None: + if self.index_url not in self.fetched_urls: + if msg: + self.warn(msg, *args) + self.info("Scanning index of all packages (this may take a while)") + self.scan_url(self.index_url) + + def find_packages(self, requirement) -> None: + self.scan_url(self.index_url + requirement.unsafe_name + '/') + + if not self.package_pages.get(requirement.key): + # Fall back to safe version of the name + self.scan_url(self.index_url + requirement.project_name + '/') + + if not self.package_pages.get(requirement.key): + # We couldn't find the target package, so search the index page too + self.not_found_in_index(requirement) + + for url in list(self.package_pages.get(requirement.key, ())): + # scan each page that might be related to the desired package + self.scan_url(url) + + def obtain(self, requirement, installer=None): + self.prescan() + self.find_packages(requirement) + for dist in self[requirement.key]: + if dist in requirement: + return dist + self.debug("%s does not match %s", requirement, dist) + return super().obtain(requirement, installer) + + def check_hash(self, checker, filename, tfp) -> None: + """ + checker is a ContentChecker + """ + checker.report(self.debug, f"Validating %s checksum for {filename}") + if not checker.is_valid(): + tfp.close() + os.unlink(filename) + raise DistutilsError( + f"{checker.hash.name} validation failed for {os.path.basename(filename)}; " + "possible download problem?" + ) + + def add_find_links(self, urls) -> None: + """Add `urls` to the list that will be prescanned for searches""" + for url in urls: + if ( + self.to_scan is None # if we have already "gone online" + or not URL_SCHEME(url) # or it's a local file/directory + or url.startswith('file:') + or list(distros_for_url(url)) # or a direct package link + ): + # then go ahead and process it now + self.scan_url(url) + else: + # otherwise, defer retrieval till later + self.to_scan.append(url) + + def prescan(self): + """Scan urls scheduled for prescanning (e.g. --find-links)""" + if self.to_scan: + list(map(self.scan_url, self.to_scan)) + self.to_scan = None # from now on, go ahead and process immediately + + def not_found_in_index(self, requirement) -> None: + if self[requirement.key]: # we've seen at least one distro + meth, msg = self.info, "Couldn't retrieve index page for %r" + else: # no distros seen for this name, might be misspelled + meth, msg = self.warn, "Couldn't find index page for %r (maybe misspelled?)" + meth(msg, requirement.unsafe_name) + self.scan_all() + + def download(self, spec, tmpdir): + """Locate and/or download `spec` to `tmpdir`, returning a local path + + `spec` may be a ``Requirement`` object, or a string containing a URL, + an existing local filename, or a project/version requirement spec + (i.e. the string form of a ``Requirement`` object). If it is the URL + of a .py file with an unambiguous ``#egg=name-version`` tag (i.e., one + that escapes ``-`` as ``_`` throughout), a trivial ``setup.py`` is + automatically created alongside the downloaded file. + + If `spec` is a ``Requirement`` object or a string containing a + project/version requirement spec, this method returns the location of + a matching distribution (possibly after downloading it to `tmpdir`). + If `spec` is a locally existing file or directory name, it is simply + returned unchanged. If `spec` is a URL, it is downloaded to a subpath + of `tmpdir`, and the local filename is returned. Various errors may be + raised if a problem occurs during downloading. + """ + if not isinstance(spec, Requirement): + scheme = URL_SCHEME(spec) + if scheme: + # It's a url, download it to tmpdir + found = self._download_url(spec, tmpdir) + base, fragment = egg_info_for_url(spec) + if base.endswith('.py'): + found = self.gen_setup(found, fragment, tmpdir) + return found + elif os.path.exists(spec): + # Existing file or directory, just return it + return spec + else: + spec = parse_requirement_arg(spec) + return getattr(self.fetch_distribution(spec, tmpdir), 'location', None) + + def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME + self, + requirement, + tmpdir, + force_scan: bool = False, + source: bool = False, + develop_ok: bool = False, + local_index=None, + ) -> Distribution | None: + """Obtain a distribution suitable for fulfilling `requirement` + + `requirement` must be a ``pkg_resources.Requirement`` instance. + If necessary, or if the `force_scan` flag is set, the requirement is + searched for in the (online) package index as well as the locally + installed packages. If a distribution matching `requirement` is found, + the returned distribution's ``location`` is the value you would have + gotten from calling the ``download()`` method with the matching + distribution's URL or filename. If no matching distribution is found, + ``None`` is returned. + + If the `source` flag is set, only source distributions and source + checkout links will be considered. Unless the `develop_ok` flag is + set, development and system eggs (i.e., those using the ``.egg-info`` + format) will be ignored. + """ + # process a Requirement + self.info("Searching for %s", requirement) + skipped = set() + dist = None + + def find(req, env: Environment | None = None): + if env is None: + env = self + # Find a matching distribution; may be called more than once + + for dist in env[req.key]: + if dist.precedence == DEVELOP_DIST and not develop_ok: + if dist not in skipped: + self.warn( + "Skipping development or system egg: %s", + dist, + ) + skipped.add(dist) + continue + + test = dist in req and (dist.precedence <= SOURCE_DIST or not source) + if test: + loc = self.download(dist.location, tmpdir) + dist.download_location = loc + if os.path.exists(dist.download_location): + return dist + + return None + + if force_scan: + self.prescan() + self.find_packages(requirement) + dist = find(requirement) + + if not dist and local_index is not None: + dist = find(requirement, local_index) + + if dist is None: + if self.to_scan is not None: + self.prescan() + dist = find(requirement) + + if dist is None and not force_scan: + self.find_packages(requirement) + dist = find(requirement) + + if dist is None: + self.warn( + "No local packages or working download links found for %s%s", + (source and "a source distribution of " or ""), + requirement, + ) + return None + else: + self.info("Best match: %s", dist) + return dist.clone(location=dist.download_location) + + def fetch( + self, requirement, tmpdir, force_scan: bool = False, source: bool = False + ) -> str | None: + """Obtain a file suitable for fulfilling `requirement` + + DEPRECATED; use the ``fetch_distribution()`` method now instead. For + backward compatibility, this routine is identical but returns the + ``location`` of the downloaded distribution instead of a distribution + object. + """ + dist = self.fetch_distribution(requirement, tmpdir, force_scan, source) + if dist is not None: + return dist.location + return None + + def gen_setup(self, filename, fragment, tmpdir): + match = EGG_FRAGMENT.match(fragment) + dists = ( + match + and [ + d + for d in interpret_distro_name(filename, match.group(1), None) + if d.version + ] + or [] + ) + + if len(dists) == 1: # unambiguous ``#egg`` fragment + basename = os.path.basename(filename) + + # Make sure the file has been downloaded to the temp dir. + if os.path.dirname(filename) != tmpdir: + dst = os.path.join(tmpdir, basename) + if not (os.path.exists(dst) and os.path.samefile(filename, dst)): + shutil.copy2(filename, dst) + filename = dst + + with open(os.path.join(tmpdir, 'setup.py'), 'w', encoding="utf-8") as file: + file.write( + "from setuptools import setup\n" + f"setup(name={dists[0].project_name!r}, version={dists[0].version!r}, py_modules=[{os.path.splitext(basename)[0]!r}])\n" + ) + return filename + + elif match: + raise DistutilsError( + f"Can't unambiguously interpret project/version identifier {fragment!r}; " + "any dashes in the name or version should be escaped using " + f"underscores. {dists!r}" + ) + else: + raise DistutilsError( + "Can't process plain .py files without an '#egg=name-version'" + " suffix to enable automatic setup script generation." + ) + + dl_blocksize = 8192 + + def _download_to(self, url, filename): + self.info("Downloading %s", url) + # Download the file + fp = None + try: + checker = HashChecker.from_url(url) + fp = self.open_url(url) + if isinstance(fp, urllib.error.HTTPError): + raise DistutilsError(f"Can't download {url}: {fp.code} {fp.msg}") + headers = fp.info() + blocknum = 0 + bs = self.dl_blocksize + size = -1 + if "content-length" in headers: + # Some servers return multiple Content-Length headers :( + sizes = headers.get_all('Content-Length') + size = max(map(int, sizes)) + self.reporthook(url, filename, blocknum, bs, size) + with open(filename, 'wb') as tfp: + while True: + block = fp.read(bs) + if block: + checker.feed(block) + tfp.write(block) + blocknum += 1 + self.reporthook(url, filename, blocknum, bs, size) + else: + break + self.check_hash(checker, filename, tfp) + return headers + finally: + if fp: + fp.close() + + def reporthook(self, url, filename, blocknum, blksize, size) -> None: + pass # no-op + + # FIXME: + def open_url(self, url, warning=None): # noqa: C901 # is too complex (12) + if url.startswith('file:'): + return local_open(url) + try: + return open_with_auth(url, self.opener) + except (ValueError, http.client.InvalidURL) as v: + msg = ' '.join([str(arg) for arg in v.args]) + if warning: + self.warn(warning, msg) + else: + raise DistutilsError(f'{url} {msg}') from v + except urllib.error.HTTPError as v: + return v + except urllib.error.URLError as v: + if warning: + self.warn(warning, v.reason) + else: + raise DistutilsError(f"Download error for {url}: {v.reason}") from v + except http.client.BadStatusLine as v: + if warning: + self.warn(warning, v.line) + else: + raise DistutilsError( + f'{url} returned a bad status line. The server might be ' + f'down, {v.line}' + ) from v + except (http.client.HTTPException, OSError) as v: + if warning: + self.warn(warning, v) + else: + raise DistutilsError(f"Download error for {url}: {v}") from v + + @staticmethod + def _sanitize(name): + r""" + Replace unsafe path directives with underscores. + + >>> san = PackageIndex._sanitize + >>> san('/home/user/.ssh/authorized_keys') + '_home_user_.ssh_authorized_keys' + >>> san('..\\foo\\bing') + '__foo_bing' + >>> san('D:bar') + 'D_bar' + >>> san('C:\\bar') + 'C__bar' + >>> san('foo..bar') + 'foo..bar' + >>> san('D:../foo') + 'D___foo' + """ + pattern = '|'.join(( + # drive letters + r':', + # path separators + r'[/\\]', + # parent dirs + r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))', + )) + return re.sub(pattern, r'_', name) + + @classmethod + def _resolve_download_filename(cls, url, tmpdir): + """ + >>> import pathlib + >>> du = PackageIndex._resolve_download_filename + >>> root = getfixture('tmp_path') + >>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz' + >>> str(pathlib.Path(du(url, root)).relative_to(root)) + 'setuptools-78.1.0.tar.gz' + """ + name, _fragment = egg_info_for_url(url) + name = cls._sanitize( + name + or + # default if URL has no path contents + '__downloaded__' + ) + + # strip any extra .zip before download + name = re.sub(r'\.egg\.zip$', '.egg', name) + + return os.path.join(tmpdir, name) + + def _download_url(self, url, tmpdir): + """ + Determine the download filename. + """ + filename = self._resolve_download_filename(url, tmpdir) + return self._download_vcs(url, filename) or self._download_other(url, filename) + + @staticmethod + def _resolve_vcs(url): + """ + >>> rvcs = PackageIndex._resolve_vcs + >>> rvcs('git+http://foo/bar') + 'git' + >>> rvcs('hg+https://foo/bar') + 'hg' + >>> rvcs('git:myhost') + 'git' + >>> rvcs('hg:myhost') + >>> rvcs('http://foo/bar') + """ + scheme = urllib.parse.urlsplit(url).scheme + pre, sep, _post = scheme.partition('+') + # svn and git have their own protocol; hg does not + allowed = set(['svn', 'git'] + ['hg'] * bool(sep)) + return next(iter({pre} & allowed), None) + + def _download_vcs(self, url, spec_filename): + vcs = self._resolve_vcs(url) + if not vcs: + return None + if vcs == 'svn': + raise DistutilsError( + f"Invalid config, SVN download is not supported: {url}" + ) + + filename, _, _ = spec_filename.partition('#') + url, rev = self._vcs_split_rev_from_url(url) + + self.info(f"Doing {vcs} clone from {url} to {filename}") + subprocess.check_call([vcs, 'clone', '--quiet', url, filename]) + + co_commands = dict( + git=[vcs, '-C', filename, 'checkout', '--quiet', rev], + hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'], + ) + if rev is not None: + self.info(f"Checking out {rev}") + subprocess.check_call(co_commands[vcs]) + + return filename + + def _download_other(self, url, filename): + scheme = urllib.parse.urlsplit(url).scheme + if scheme == 'file': # pragma: no cover + return urllib.request.url2pathname(urllib.parse.urlparse(url).path) + # raise error if not allowed + self.url_ok(url, True) + return self._attempt_download(url, filename) + + def scan_url(self, url) -> None: + self.process_url(url, True) + + def _attempt_download(self, url, filename): + headers = self._download_to(url, filename) + if 'html' in headers.get('content-type', '').lower(): + return self._invalid_download_html(url, headers, filename) + else: + return filename + + def _invalid_download_html(self, url, headers, filename): + os.unlink(filename) + raise DistutilsError(f"Unexpected HTML page found at {url}") + + @staticmethod + def _vcs_split_rev_from_url(url): + """ + Given a possible VCS URL, return a clean URL and resolved revision if any. + + >>> vsrfu = PackageIndex._vcs_split_rev_from_url + >>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools') + ('https://github.com/pypa/setuptools', 'v69.0.0') + >>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools') + ('https://github.com/pypa/setuptools', None) + >>> vsrfu('http://foo/bar') + ('http://foo/bar', None) + """ + parts = urllib.parse.urlsplit(url) + + clean_scheme = parts.scheme.split('+', 1)[-1] + + # Some fragment identification fails + no_fragment_path, _, _ = parts.path.partition('#') + + pre, sep, post = no_fragment_path.rpartition('@') + clean_path, rev = (pre, post) if sep else (post, None) + + resolved = parts._replace( + scheme=clean_scheme, + path=clean_path, + # discard the fragment + fragment='', + ).geturl() + + return resolved, rev + + def debug(self, msg, *args) -> None: + log.debug(msg, *args) + + def info(self, msg, *args) -> None: + log.info(msg, *args) + + def warn(self, msg, *args) -> None: + log.warn(msg, *args) + + +# This pattern matches a character entity reference (a decimal numeric +# references, a hexadecimal numeric reference, or a named reference). +entity_sub = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub + + +def decode_entity(match): + what = match.group(0) + return html.unescape(what) + + +def htmldecode(text): + """ + Decode HTML entities in the given text. + + >>> htmldecode( + ... 'https://../package_name-0.1.2.tar.gz' + ... '?tokena=A&tokenb=B">package_name-0.1.2.tar.gz') + 'https://../package_name-0.1.2.tar.gz?tokena=A&tokenb=B">package_name-0.1.2.tar.gz' + """ + return entity_sub(decode_entity, text) + + +def socket_timeout(timeout=15): + def _socket_timeout(func): + def _socket_timeout(*args, **kwargs): + old_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(timeout) + try: + return func(*args, **kwargs) + finally: + socket.setdefaulttimeout(old_timeout) + + return _socket_timeout + + return _socket_timeout + + +def _encode_auth(auth): + """ + Encode auth from a URL suitable for an HTTP header. + >>> str(_encode_auth('username%3Apassword')) + 'dXNlcm5hbWU6cGFzc3dvcmQ=' + + Long auth strings should not cause a newline to be inserted. + >>> long_auth = 'username:' + 'password'*10 + >>> chr(10) in str(_encode_auth(long_auth)) + False + """ + auth_s = urllib.parse.unquote(auth) + # convert to bytes + auth_bytes = auth_s.encode() + encoded_bytes = base64.b64encode(auth_bytes) + # convert back to a string + encoded = encoded_bytes.decode() + # strip the trailing carriage return + return encoded.replace('\n', '') + + +class Credential(NamedTuple): + """ + A username/password pair. + + Displayed separated by `:`. + >>> str(Credential('username', 'password')) + 'username:password' + """ + + username: str + password: str + + def __str__(self) -> str: + return f'{self.username}:{self.password}' + + +class PyPIConfig(configparser.RawConfigParser): + def __init__(self): + """ + Load from ~/.pypirc + """ + defaults = dict.fromkeys(['username', 'password', 'repository'], '') + super().__init__(defaults) + + rc = os.path.join(os.path.expanduser('~'), '.pypirc') + if os.path.exists(rc): + _cfg_read_utf8_with_fallback(self, rc) + + @property + def creds_by_repository(self): + sections_with_repositories = [ + section + for section in self.sections() + if self.get(section, 'repository').strip() + ] + + return dict(map(self._get_repo_cred, sections_with_repositories)) + + def _get_repo_cred(self, section): + repo = self.get(section, 'repository').strip() + return repo, Credential( + self.get(section, 'username').strip(), + self.get(section, 'password').strip(), + ) + + def find_credential(self, url): + """ + If the URL indicated appears to be a repository defined in this + config, return the credential for that repository. + """ + for repository, cred in self.creds_by_repository.items(): + if url.startswith(repository): + return cred + return None + + +def open_with_auth(url, opener=urllib.request.urlopen): + """Open a urllib2 request, handling HTTP authentication""" + + parsed = urllib.parse.urlparse(url) + scheme, netloc, path, params, query, frag = parsed + + # Double scheme does not raise on macOS as revealed by a + # failing test. We would expect "nonnumeric port". Refs #20. + if netloc.endswith(':'): + raise http.client.InvalidURL("nonnumeric port: ''") + + if scheme in ('http', 'https'): + auth, address = _splituser(netloc) + else: + auth, address = (None, None) + + if not auth: + cred = PyPIConfig().find_credential(url) + if cred: + auth = str(cred) + info = cred.username, url + log.info('Authenticating as %s for %s (from .pypirc)', *info) + + if auth: + auth = "Basic " + _encode_auth(auth) + parts = scheme, address, path, params, query, frag + new_url = urllib.parse.urlunparse(parts) + request = urllib.request.Request(new_url) + request.add_header("Authorization", auth) + else: + request = urllib.request.Request(url) + + request.add_header('User-Agent', user_agent) + fp = opener(request) + + if auth: + # Put authentication info back into request URL if same host, + # so that links found on the page will work + s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url) + if s2 == scheme and h2 == address: + parts = s2, netloc, path2, param2, query2, frag2 + fp.url = urllib.parse.urlunparse(parts) + + return fp + + +# copy of urllib.parse._splituser from Python 3.8 +# See https://github.com/python/cpython/issues/80072. +def _splituser(host): + """splituser('user[:passwd]@host[:port]') + --> 'user[:passwd]', 'host[:port]'.""" + user, delim, host = host.rpartition('@') + return (user if delim else None), host + + +# adding a timeout to avoid freezing package_index +open_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth) + + +def fix_sf_url(url): + return url # backward compatibility + + +def local_open(url): + """Read a local path, with special support for directories""" + _scheme, _server, path, _param, _query, _frag = urllib.parse.urlparse(url) + filename = urllib.request.url2pathname(path) + if os.path.isfile(filename): + return urllib.request.urlopen(url) + elif path.endswith('/') and os.path.isdir(filename): + files = [] + for f in os.listdir(filename): + filepath = os.path.join(filename, f) + if f == 'index.html': + body = _read_utf8_with_fallback(filepath) + break + elif os.path.isdir(filepath): + f += '/' + files.append(f'{f}') + else: + tmpl = "{url}{files}" + body = tmpl.format(url=url, files='\n'.join(files)) + status, message = 200, "OK" + else: + status, message, body = 404, "Path not found", "Not found" + + headers = {'content-type': 'text/html'} + body_stream = io.StringIO(body) + return urllib.error.HTTPError(url, status, message, headers, body_stream) diff --git a/lib/python3.10/site-packages/setuptools/sandbox.py b/lib/python3.10/site-packages/setuptools/sandbox.py new file mode 100644 index 0000000000000000000000000000000000000000..2d84242d667c7df6a20aa56eabce6ac34ddc4a7e --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/sandbox.py @@ -0,0 +1,536 @@ +from __future__ import annotations + +import builtins +import contextlib +import functools +import itertools +import operator +import os +import pickle +import re +import sys +import tempfile +import textwrap +from types import TracebackType +from typing import TYPE_CHECKING, Any, ClassVar + +import pkg_resources +from pkg_resources import working_set + +from distutils.errors import DistutilsError + +if TYPE_CHECKING: + import os as _os +elif sys.platform.startswith('java'): + import org.python.modules.posix.PosixModule as _os # pyright: ignore[reportMissingImports] +else: + _os = sys.modules[os.name] +_open = open + + +if TYPE_CHECKING: + from typing_extensions import Self + +__all__ = [ + "AbstractSandbox", + "DirectorySandbox", + "SandboxViolation", + "run_setup", +] + + +def _execfile(filename, globals, locals=None): + """ + Python 3 implementation of execfile. + """ + mode = 'rb' + with open(filename, mode) as stream: + script = stream.read() + if locals is None: + locals = globals + code = compile(script, filename, 'exec') + exec(code, globals, locals) + + +@contextlib.contextmanager +def save_argv(repl=None): + saved = sys.argv[:] + if repl is not None: + sys.argv[:] = repl + try: + yield saved + finally: + sys.argv[:] = saved + + +@contextlib.contextmanager +def save_path(): + saved = sys.path[:] + try: + yield saved + finally: + sys.path[:] = saved + + +@contextlib.contextmanager +def override_temp(replacement): + """ + Monkey-patch tempfile.tempdir with replacement, ensuring it exists + """ + os.makedirs(replacement, exist_ok=True) + + saved = tempfile.tempdir + + tempfile.tempdir = replacement + + try: + yield + finally: + tempfile.tempdir = saved + + +@contextlib.contextmanager +def pushd(target): + saved = os.getcwd() + os.chdir(target) + try: + yield saved + finally: + os.chdir(saved) + + +class UnpickleableException(Exception): + """ + An exception representing another Exception that could not be pickled. + """ + + @staticmethod + def dump(type, exc): + """ + Always return a dumped (pickled) type and exc. If exc can't be pickled, + wrap it in UnpickleableException first. + """ + try: + return pickle.dumps(type), pickle.dumps(exc) + except Exception: + # get UnpickleableException inside the sandbox + from setuptools.sandbox import UnpickleableException as cls + + return cls.dump(cls, cls(repr(exc))) + + +class ExceptionSaver: + """ + A Context Manager that will save an exception, serialize, and restore it + later. + """ + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> bool: + if not exc: + return False + + # dump the exception + self._saved = UnpickleableException.dump(type, exc) + self._tb = tb + + # suppress the exception + return True + + def resume(self): + "restore and re-raise any exception" + + if '_saved' not in vars(self): + return + + _type, exc = map(pickle.loads, self._saved) + raise exc.with_traceback(self._tb) + + +@contextlib.contextmanager +def save_modules(): + """ + Context in which imported modules are saved. + + Translates exceptions internal to the context into the equivalent exception + outside the context. + """ + saved = sys.modules.copy() + with ExceptionSaver() as saved_exc: + yield saved + + sys.modules.update(saved) + # remove any modules imported since + del_modules = ( + mod_name + for mod_name in sys.modules + if mod_name not in saved + # exclude any encodings modules. See #285 + and not mod_name.startswith('encodings.') + ) + _clear_modules(del_modules) + + saved_exc.resume() + + +def _clear_modules(module_names): + for mod_name in list(module_names): + del sys.modules[mod_name] + + +@contextlib.contextmanager +def save_pkg_resources_state(): + saved = pkg_resources.__getstate__() + try: + yield saved + finally: + pkg_resources.__setstate__(saved) + + +@contextlib.contextmanager +def setup_context(setup_dir): + temp_dir = os.path.join(setup_dir, 'temp') + with save_pkg_resources_state(): + with save_modules(): + with save_path(): + hide_setuptools() + with save_argv(): + with override_temp(temp_dir): + with pushd(setup_dir): + # ensure setuptools commands are available + __import__('setuptools') + yield + + +_MODULES_TO_HIDE = { + 'setuptools', + 'distutils', + 'pkg_resources', + 'Cython', + '_distutils_hack', +} + + +def _needs_hiding(mod_name): + """ + >>> _needs_hiding('setuptools') + True + >>> _needs_hiding('pkg_resources') + True + >>> _needs_hiding('setuptools_plugin') + False + >>> _needs_hiding('setuptools.__init__') + True + >>> _needs_hiding('distutils') + True + >>> _needs_hiding('os') + False + >>> _needs_hiding('Cython') + True + """ + base_module = mod_name.split('.', 1)[0] + return base_module in _MODULES_TO_HIDE + + +def hide_setuptools(): + """ + Remove references to setuptools' modules from sys.modules to allow the + invocation to import the most appropriate setuptools. This technique is + necessary to avoid issues such as #315 where setuptools upgrading itself + would fail to find a function declared in the metadata. + """ + _distutils_hack = sys.modules.get('_distutils_hack', None) + if _distutils_hack is not None: + _distutils_hack._remove_shim() + + modules = filter(_needs_hiding, sys.modules) + _clear_modules(modules) + + +def run_setup(setup_script, args): + """Run a distutils setup script, sandboxed in its directory""" + setup_dir = os.path.abspath(os.path.dirname(setup_script)) + with setup_context(setup_dir): + try: + sys.argv[:] = [setup_script] + list(args) + sys.path.insert(0, setup_dir) + # reset to include setup dir, w/clean callback list + working_set.__init__() + working_set.callbacks.append(lambda dist: dist.activate()) + + with DirectorySandbox(setup_dir): + ns = dict(__file__=setup_script, __name__='__main__') + _execfile(setup_script, ns) + except SystemExit as v: + if v.args and v.args[0]: + raise + # Normal exit, just return + + +class AbstractSandbox: + """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" + + _active = False + + def __init__(self) -> None: + self._attrs = [ + name + for name in dir(_os) + if not name.startswith('_') and hasattr(self, name) + ] + + def _copy(self, source): + for name in self._attrs: + setattr(os, name, getattr(source, name)) + + def __enter__(self) -> None: + self._copy(self) + builtins.open = self._open + self._active = True + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ): + self._active = False + builtins.open = _open + self._copy(_os) + + def run(self, func): + """Run 'func' under os sandboxing""" + with self: + return func() + + def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 + original = getattr(_os, name) + + def wrap(self, src, dst, *args, **kw): + if self._active: + src, dst = self._remap_pair(name, src, dst, *args, **kw) + return original(src, dst, *args, **kw) + + return wrap + + for __name in ["rename", "link", "symlink"]: + if hasattr(_os, __name): + locals()[__name] = _mk_dual_path_wrapper(__name) + + def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 + original = original or getattr(_os, name) + + def wrap(self, path, *args, **kw): + if self._active: + path = self._remap_input(name, path, *args, **kw) + return original(path, *args, **kw) + + return wrap + + _open = _mk_single_path_wrapper('open', _open) + for __name in [ + "stat", + "listdir", + "chdir", + "open", + "chmod", + "chown", + "mkdir", + "remove", + "unlink", + "rmdir", + "utime", + "lchown", + "chroot", + "lstat", + "startfile", + "mkfifo", + "mknod", + "pathconf", + "access", + ]: + if hasattr(_os, __name): + locals()[__name] = _mk_single_path_wrapper(__name) + + def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 + original = getattr(_os, name) + + def wrap(self, path, *args, **kw): + if self._active: + path = self._remap_input(name, path, *args, **kw) + return self._remap_output(name, original(path, *args, **kw)) + return original(path, *args, **kw) + + return wrap + + for __name in ['readlink', 'tempnam']: + if hasattr(_os, __name): + locals()[__name] = _mk_single_with_return(__name) + + def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 + original = getattr(_os, name) + + def wrap(self, *args, **kw): + retval = original(*args, **kw) + if self._active: + return self._remap_output(name, retval) + return retval + + return wrap + + for __name in ['getcwd', 'tmpnam']: + if hasattr(_os, __name): + locals()[__name] = _mk_query(__name) + + def _validate_path(self, path): + """Called to remap or validate any path, whether input or output""" + return path + + def _remap_input(self, operation, path, *args, **kw): + """Called for path inputs""" + return self._validate_path(path) + + def _remap_output(self, operation, path): + """Called for path outputs""" + return self._validate_path(path) + + def _remap_pair(self, operation, src, dst, *args, **kw): + """Called for path pairs like rename, link, and symlink operations""" + return ( + self._remap_input(operation + '-from', src, *args, **kw), + self._remap_input(operation + '-to', dst, *args, **kw), + ) + + if TYPE_CHECKING: + # This is a catch-all for all the dynamically created attributes. + # This isn't public API anyway + def __getattribute__(self, name: str) -> Any: ... + + +if hasattr(os, 'devnull'): + _EXCEPTIONS = [os.devnull] +else: + _EXCEPTIONS = [] + + +class DirectorySandbox(AbstractSandbox): + """Restrict operations to a single subdirectory - pseudo-chroot""" + + write_ops: ClassVar[dict[str, None]] = dict.fromkeys([ + "open", + "chmod", + "chown", + "mkdir", + "remove", + "unlink", + "rmdir", + "utime", + "lchown", + "chroot", + "mkfifo", + "mknod", + "tempnam", + ]) + + _exception_patterns: list[str | re.Pattern] = [] + "exempt writing to paths that match the pattern" + + def __init__(self, sandbox, exceptions=_EXCEPTIONS) -> None: + self._sandbox = os.path.normcase(os.path.realpath(sandbox)) + self._prefix = os.path.join(self._sandbox, '') + self._exceptions = [ + os.path.normcase(os.path.realpath(path)) for path in exceptions + ] + AbstractSandbox.__init__(self) + + def _violation(self, operation, *args, **kw): + from setuptools.sandbox import SandboxViolation + + raise SandboxViolation(operation, args, kw) + + def _open(self, path, mode='r', *args, **kw): + if mode not in ('r', 'rt', 'rb', 'rU', 'U') and not self._ok(path): + self._violation("open", path, mode, *args, **kw) + return _open(path, mode, *args, **kw) + + def tmpnam(self) -> None: + self._violation("tmpnam") + + def _ok(self, path): + active = self._active + try: + self._active = False + realpath = os.path.normcase(os.path.realpath(path)) + return ( + self._exempted(realpath) + or realpath == self._sandbox + or realpath.startswith(self._prefix) + ) + finally: + self._active = active + + def _exempted(self, filepath): + start_matches = ( + filepath.startswith(exception) for exception in self._exceptions + ) + pattern_matches = ( + re.match(pattern, filepath) for pattern in self._exception_patterns + ) + candidates = itertools.chain(start_matches, pattern_matches) + return any(candidates) + + def _remap_input(self, operation, path, *args, **kw): + """Called for path inputs""" + if operation in self.write_ops and not self._ok(path): + self._violation(operation, os.path.realpath(path), *args, **kw) + return path + + def _remap_pair(self, operation, src, dst, *args, **kw): + """Called for path pairs like rename, link, and symlink operations""" + if not self._ok(src) or not self._ok(dst): + self._violation(operation, src, dst, *args, **kw) + return (src, dst) + + def open(self, file, flags, mode: int = 0o777, *args, **kw) -> int: + """Called for low-level os.open()""" + if flags & WRITE_FLAGS and not self._ok(file): + self._violation("os.open", file, flags, mode, *args, **kw) + return _os.open(file, flags, mode, *args, **kw) + + +WRITE_FLAGS = functools.reduce( + operator.or_, + [ + getattr(_os, a, 0) + for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split() + ], +) + + +class SandboxViolation(DistutilsError): + """A setup script attempted to modify the filesystem outside the sandbox""" + + tmpl = textwrap.dedent( + """ + SandboxViolation: {cmd}{args!r} {kwargs} + + The package setup script has attempted to modify files on your system + that are not within the EasyInstall build area, and has been aborted. + + This package cannot be safely installed by EasyInstall, and may not + support alternate installation locations even if you run its setup + script by hand. Please inform the package's author and the EasyInstall + maintainers to find out if a fix or workaround is available. + """ + ).lstrip() + + def __str__(self) -> str: + cmd, args, kwargs = self.args + return self.tmpl.format(**locals()) diff --git a/lib/python3.10/site-packages/setuptools/script (dev).tmpl b/lib/python3.10/site-packages/setuptools/script (dev).tmpl new file mode 100644 index 0000000000000000000000000000000000000000..39a24b04888e79df51e2237577b303a2f901be63 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/script (dev).tmpl @@ -0,0 +1,6 @@ +# EASY-INSTALL-DEV-SCRIPT: %(spec)r,%(script_name)r +__requires__ = %(spec)r +__import__('pkg_resources').require(%(spec)r) +__file__ = %(dev_path)r +with open(__file__) as f: + exec(compile(f.read(), __file__, 'exec')) diff --git a/lib/python3.10/site-packages/setuptools/script.tmpl b/lib/python3.10/site-packages/setuptools/script.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..ff5efbcab3b58063dd84787181c26a95fb663d94 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/script.tmpl @@ -0,0 +1,3 @@ +# EASY-INSTALL-SCRIPT: %(spec)r,%(script_name)r +__requires__ = %(spec)r +__import__('pkg_resources').run_script(%(spec)r, %(script_name)r) diff --git a/lib/python3.10/site-packages/setuptools/unicode_utils.py b/lib/python3.10/site-packages/setuptools/unicode_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f502f5b089619eafd28e2c7a61967e34e16920e5 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/unicode_utils.py @@ -0,0 +1,102 @@ +import sys +import unicodedata +from configparser import RawConfigParser + +from .compat import py39 +from .warnings import SetuptoolsDeprecationWarning + + +# HFS Plus uses decomposed UTF-8 +def decompose(path): + if isinstance(path, str): + return unicodedata.normalize('NFD', path) + try: + path = path.decode('utf-8') + path = unicodedata.normalize('NFD', path) + path = path.encode('utf-8') + except UnicodeError: + pass # Not UTF-8 + return path + + +def filesys_decode(path): + """ + Ensure that the given path is decoded, + ``None`` when no expected encoding works + """ + + if isinstance(path, str): + return path + + fs_enc = sys.getfilesystemencoding() or 'utf-8' + candidates = fs_enc, 'utf-8' + + for enc in candidates: + try: + return path.decode(enc) + except UnicodeDecodeError: + continue + + return None + + +def try_encode(string, enc): + "turn unicode encoding into a functional routine" + try: + return string.encode(enc) + except UnicodeEncodeError: + return None + + +def _read_utf8_with_fallback(file: str, fallback_encoding=py39.LOCALE_ENCODING) -> str: + """ + First try to read the file with UTF-8, if there is an error fallback to a + different encoding ("locale" by default). Returns the content of the file. + Also useful when reading files that might have been produced by an older version of + setuptools. + """ + try: + with open(file, "r", encoding="utf-8") as f: + return f.read() + except UnicodeDecodeError: # pragma: no cover + _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding) + with open(file, "r", encoding=fallback_encoding) as f: + return f.read() + + +def _cfg_read_utf8_with_fallback( + cfg: RawConfigParser, file: str, fallback_encoding=py39.LOCALE_ENCODING +) -> None: + """Same idea as :func:`_read_utf8_with_fallback`, but for the + :meth:`RawConfigParser.read` method. + + This method may call ``cfg.clear()``. + """ + try: + cfg.read(file, encoding="utf-8") + except UnicodeDecodeError: # pragma: no cover + _Utf8EncodingNeeded.emit(file=file, fallback_encoding=fallback_encoding) + cfg.clear() + cfg.read(file, encoding=fallback_encoding) + + +class _Utf8EncodingNeeded(SetuptoolsDeprecationWarning): + _SUMMARY = """ + `encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`. + """ + + _DETAILS = """ + Fallback behavior for UTF-8 is considered **deprecated** and future versions of + `setuptools` may not implement it. + + Please encode {file!r} with "utf-8" to ensure future builds will succeed. + + If this file was produced by `setuptools` itself, cleaning up the cached files + and re-building/re-installing the package with a newer version of `setuptools` + (e.g. by updating `build-system.requires` in its `pyproject.toml`) + might solve the problem. + """ + # TODO: Add a deadline? + # Will we be able to remove this? + # The question comes to mind mainly because of sdists that have been produced + # by old versions of setuptools and published to PyPI... diff --git a/lib/python3.10/site-packages/setuptools/version.py b/lib/python3.10/site-packages/setuptools/version.py new file mode 100644 index 0000000000000000000000000000000000000000..ec253c414474677d3a5977511cfe901bfb786740 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/version.py @@ -0,0 +1,6 @@ +from ._importlib import metadata + +try: + __version__ = metadata.version('setuptools') or '0.dev0+unknown' +except Exception: + __version__ = '0.dev0+unknown' diff --git a/lib/python3.10/site-packages/setuptools/warnings.py b/lib/python3.10/site-packages/setuptools/warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..96467787c237846bfbacf2d44eb833be0a88b633 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/warnings.py @@ -0,0 +1,110 @@ +"""Provide basic warnings used by setuptools modules. + +Using custom classes (other than ``UserWarning``) allow users to set +``PYTHONWARNINGS`` filters to run tests and prepare for upcoming changes in +setuptools. +""" + +from __future__ import annotations + +import os +import warnings +from datetime import date +from inspect import cleandoc +from textwrap import indent +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing_extensions import TypeAlias + +_DueDate: TypeAlias = tuple[int, int, int] # time tuple +_INDENT = 8 * " " +_TEMPLATE = f"""{80 * '*'}\n{{details}}\n{80 * '*'}""" + + +class SetuptoolsWarning(UserWarning): + """Base class in ``setuptools`` warning hierarchy.""" + + @classmethod + def emit( + cls, + summary: str | None = None, + details: str | None = None, + due_date: _DueDate | None = None, + see_docs: str | None = None, + see_url: str | None = None, + stacklevel: int = 2, + **kwargs, + ) -> None: + """Private: reserved for ``setuptools`` internal use only""" + # Default values: + summary_ = summary or getattr(cls, "_SUMMARY", None) or "" + details_ = details or getattr(cls, "_DETAILS", None) or "" + due_date = due_date or getattr(cls, "_DUE_DATE", None) + docs_ref = see_docs or getattr(cls, "_SEE_DOCS", None) + docs_url = docs_ref and f"https://setuptools.pypa.io/en/latest/{docs_ref}" + see_url = see_url or getattr(cls, "_SEE_URL", None) + due = date(*due_date) if due_date else None + + text = cls._format(summary_, details_, due, see_url or docs_url, kwargs) + if due and due < date.today() and _should_enforce(): + raise cls(text) + warnings.warn(text, cls, stacklevel=stacklevel + 1) + + @classmethod + def _format( + cls, + summary: str, + details: str, + due_date: date | None = None, + see_url: str | None = None, + format_args: dict | None = None, + ) -> str: + """Private: reserved for ``setuptools`` internal use only""" + today = date.today() + summary = cleandoc(summary).format_map(format_args or {}) + possible_parts = [ + cleandoc(details).format_map(format_args or {}), + ( + f"\nBy {due_date:%Y-%b-%d}, you need to update your project and remove " + "deprecated calls\nor your builds will no longer be supported." + if due_date and due_date > today + else None + ), + ( + "\nThis deprecation is overdue, please update your project and remove " + "deprecated\ncalls to avoid build errors in the future." + if due_date and due_date < today + else None + ), + (f"\nSee {see_url} for details." if see_url else None), + ] + parts = [x for x in possible_parts if x] + if parts: + body = indent(_TEMPLATE.format(details="\n".join(parts)), _INDENT) + return "\n".join([summary, "!!\n", body, "\n!!"]) + return summary + + +class InformationOnly(SetuptoolsWarning): + """Currently there is no clear way of displaying messages to the users + that use the setuptools backend directly via ``pip``. + The only thing that might work is a warning, although it is not the + most appropriate tool for the job... + + See pypa/packaging-problems#558. + """ + + +class SetuptoolsDeprecationWarning(SetuptoolsWarning): + """ + Base class for warning deprecations in ``setuptools`` + + This class is not derived from ``DeprecationWarning``, and as such is + visible by default. + """ + + +def _should_enforce(): + enforce = os.getenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false").lower() + return enforce in ("true", "on", "ok", "1") diff --git a/lib/python3.10/site-packages/setuptools/wheel.py b/lib/python3.10/site-packages/setuptools/wheel.py new file mode 100644 index 0000000000000000000000000000000000000000..c7ca43b5cfb2aff8d6983bbba4b7e6fdc9d01f83 --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/wheel.py @@ -0,0 +1,236 @@ +"""Wheels support.""" + +import contextlib +import email +import functools +import itertools +import os +import posixpath +import re +import zipfile + +from packaging.tags import sys_tags +from packaging.utils import canonicalize_name +from packaging.version import Version as parse_version + +import setuptools +from setuptools.archive_util import _unpack_zipfile_obj +from setuptools.command.egg_info import _egg_basename, write_requirements + +from .unicode_utils import _read_utf8_with_fallback + +from distutils.util import get_platform + +WHEEL_NAME = re.compile( + r"""^(?P.+?)-(?P\d.*?) + ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) + )\.whl$""", + re.VERBOSE, +).match + +NAMESPACE_PACKAGE_INIT = "__import__('pkg_resources').declare_namespace(__name__)\n" + + +@functools.cache +def _get_supported_tags(): + # We calculate the supported tags only once, otherwise calling + # this method on thousands of wheels takes seconds instead of + # milliseconds. + return {(t.interpreter, t.abi, t.platform) for t in sys_tags()} + + +def unpack(src_dir, dst_dir) -> None: + """Move everything under `src_dir` to `dst_dir`, and delete the former.""" + for dirpath, dirnames, filenames in os.walk(src_dir): + subdir = os.path.relpath(dirpath, src_dir) + for f in filenames: + src = os.path.join(dirpath, f) + dst = os.path.join(dst_dir, subdir, f) + os.renames(src, dst) + for n, d in reversed(list(enumerate(dirnames))): + src = os.path.join(dirpath, d) + dst = os.path.join(dst_dir, subdir, d) + if not os.path.exists(dst): + # Directory does not exist in destination, + # rename it and prune it from os.walk list. + os.renames(src, dst) + del dirnames[n] + # Cleanup. + for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True): + assert not filenames + os.rmdir(dirpath) + + +@contextlib.contextmanager +def disable_info_traces(): + """ + Temporarily disable info traces. + """ + from distutils import log + + saved = log.set_threshold(log.WARN) + try: + yield + finally: + log.set_threshold(saved) + + +class Wheel: + def __init__(self, filename) -> None: + match = WHEEL_NAME(os.path.basename(filename)) + if match is None: + raise ValueError(f'invalid wheel name: {filename!r}') + self.filename = filename + for k, v in match.groupdict().items(): + setattr(self, k, v) + + def tags(self): + """List tags (py_version, abi, platform) supported by this wheel.""" + return itertools.product( + self.py_version.split('.'), + self.abi.split('.'), + self.platform.split('.'), + ) + + def is_compatible(self): + """Is the wheel compatible with the current platform?""" + return next((True for t in self.tags() if t in _get_supported_tags()), False) + + def egg_name(self): + return ( + _egg_basename( + self.project_name, + self.version, + platform=(None if self.platform == 'any' else get_platform()), + ) + + ".egg" + ) + + def get_dist_info(self, zf): + # find the correct name of the .dist-info dir in the wheel file + for member in zf.namelist(): + dirname = posixpath.dirname(member) + if dirname.endswith('.dist-info') and canonicalize_name(dirname).startswith( + canonicalize_name(self.project_name) + ): + return dirname + raise ValueError("unsupported wheel format. .dist-info not found") + + def install_as_egg(self, destination_eggdir) -> None: + """Install wheel as an egg directory.""" + with zipfile.ZipFile(self.filename) as zf: + self._install_as_egg(destination_eggdir, zf) + + def _install_as_egg(self, destination_eggdir, zf): + dist_basename = f'{self.project_name}-{self.version}' + dist_info = self.get_dist_info(zf) + dist_data = f'{dist_basename}.data' + egg_info = os.path.join(destination_eggdir, 'EGG-INFO') + + self._convert_metadata(zf, destination_eggdir, dist_info, egg_info) + self._move_data_entries(destination_eggdir, dist_data) + self._fix_namespace_packages(egg_info, destination_eggdir) + + @staticmethod + def _convert_metadata(zf, destination_eggdir, dist_info, egg_info): + import pkg_resources + + def get_metadata(name): + with zf.open(posixpath.join(dist_info, name)) as fp: + value = fp.read().decode('utf-8') + return email.parser.Parser().parsestr(value) + + wheel_metadata = get_metadata('WHEEL') + # Check wheel format version is supported. + wheel_version = parse_version(wheel_metadata.get('Wheel-Version')) + wheel_v1 = parse_version('1.0') <= wheel_version < parse_version('2.0dev0') + if not wheel_v1: + raise ValueError(f'unsupported wheel format version: {wheel_version}') + # Extract to target directory. + _unpack_zipfile_obj(zf, destination_eggdir) + # Convert metadata. + dist_info = os.path.join(destination_eggdir, dist_info) + dist = pkg_resources.Distribution.from_location( + destination_eggdir, + dist_info, + metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info), + ) + + # Note: Evaluate and strip markers now, + # as it's difficult to convert back from the syntax: + # foobar; "linux" in sys_platform and extra == 'test' + def raw_req(req): + req.marker = None + return str(req) + + install_requires = list(map(raw_req, dist.requires())) + extras_require = { + extra: [ + req + for req in map(raw_req, dist.requires((extra,))) + if req not in install_requires + ] + for extra in dist.extras + } + os.rename(dist_info, egg_info) + os.rename( + os.path.join(egg_info, 'METADATA'), + os.path.join(egg_info, 'PKG-INFO'), + ) + setup_dist = setuptools.Distribution( + attrs=dict( + install_requires=install_requires, + extras_require=extras_require, + ), + ) + with disable_info_traces(): + write_requirements( + setup_dist.get_command_obj('egg_info'), + None, + os.path.join(egg_info, 'requires.txt'), + ) + + @staticmethod + def _move_data_entries(destination_eggdir, dist_data): + """Move data entries to their correct location.""" + dist_data = os.path.join(destination_eggdir, dist_data) + dist_data_scripts = os.path.join(dist_data, 'scripts') + if os.path.exists(dist_data_scripts): + egg_info_scripts = os.path.join(destination_eggdir, 'EGG-INFO', 'scripts') + os.mkdir(egg_info_scripts) + for entry in os.listdir(dist_data_scripts): + # Remove bytecode, as it's not properly handled + # during easy_install scripts install phase. + if entry.endswith('.pyc'): + os.unlink(os.path.join(dist_data_scripts, entry)) + else: + os.rename( + os.path.join(dist_data_scripts, entry), + os.path.join(egg_info_scripts, entry), + ) + os.rmdir(dist_data_scripts) + for subdir in filter( + os.path.exists, + ( + os.path.join(dist_data, d) + for d in ('data', 'headers', 'purelib', 'platlib') + ), + ): + unpack(subdir, destination_eggdir) + if os.path.exists(dist_data): + os.rmdir(dist_data) + + @staticmethod + def _fix_namespace_packages(egg_info, destination_eggdir): + namespace_packages = os.path.join(egg_info, 'namespace_packages.txt') + if os.path.exists(namespace_packages): + namespace_packages = _read_utf8_with_fallback(namespace_packages).split() + + for mod in namespace_packages: + mod_dir = os.path.join(destination_eggdir, *mod.split('.')) + mod_init = os.path.join(mod_dir, '__init__.py') + if not os.path.exists(mod_dir): + os.mkdir(mod_dir) + if not os.path.exists(mod_init): + with open(mod_init, 'w', encoding="utf-8") as fp: + fp.write(NAMESPACE_PACKAGE_INIT) diff --git a/lib/python3.10/site-packages/setuptools/windows_support.py b/lib/python3.10/site-packages/setuptools/windows_support.py new file mode 100644 index 0000000000000000000000000000000000000000..7a2b53a291409c66851961a559eb4d69be0f4acc --- /dev/null +++ b/lib/python3.10/site-packages/setuptools/windows_support.py @@ -0,0 +1,30 @@ +import platform + + +def windows_only(func): + if platform.system() != 'Windows': + return lambda *args, **kwargs: None + return func + + +@windows_only +def hide_file(path: str) -> None: + """ + Set the hidden attribute on a file or directory. + + From https://stackoverflow.com/questions/19622133/ + + `path` must be text. + """ + import ctypes + import ctypes.wintypes + + SetFileAttributes = ctypes.windll.kernel32.SetFileAttributesW + SetFileAttributes.argtypes = ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD + SetFileAttributes.restype = ctypes.wintypes.BOOL + + FILE_ATTRIBUTE_HIDDEN = 0x02 + + ret = SetFileAttributes(path, FILE_ATTRIBUTE_HIDDEN) + if not ret: + raise ctypes.WinError() diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/INSTALLER b/lib/python3.10/site-packages/six-1.17.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..f79e4cb9aaf0b2d9e8ba78861e2071317b2384b3 --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/INSTALLER @@ -0,0 +1 @@ +conda \ No newline at end of file diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/LICENSE b/lib/python3.10/site-packages/six-1.17.0.dist-info/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..1cc22a5aa7679ebaa10934212f356823931bdc3e --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/LICENSE @@ -0,0 +1,18 @@ +Copyright (c) 2010-2024 Benjamin Peterson + +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/lib/python3.10/site-packages/six-1.17.0.dist-info/METADATA b/lib/python3.10/site-packages/six-1.17.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..b8a720abd20c223030d24712d6ee5b3474161687 --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/METADATA @@ -0,0 +1,51 @@ +Metadata-Version: 2.2 +Name: six +Version: 1.17.0 +Summary: Python 2 and 3 compatibility utilities +Home-page: https://github.com/benjaminp/six +Author: Benjamin Peterson +Author-email: benjamin@python.org +License: MIT +Classifier: Development Status :: 5 - Production/Stable +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: requires-python +Dynamic: summary + +.. image:: https://img.shields.io/pypi/v/six.svg + :target: https://pypi.org/project/six/ + :alt: six on PyPI + +.. image:: https://readthedocs.org/projects/six/badge/?version=latest + :target: https://six.readthedocs.io/ + :alt: six's documentation on Read the Docs + +.. image:: https://img.shields.io/badge/license-MIT-green.svg + :target: https://github.com/benjaminp/six/blob/master/LICENSE + :alt: MIT License badge + +Six is a Python 2 and 3 compatibility library. It provides utility functions +for smoothing over the differences between the Python versions with the goal of +writing Python code that is compatible on both Python versions. See the +documentation for more information on what is provided. + +Six supports Python 2.7 and 3.3+. It is contained in only one Python +file, so it can be easily copied into your project. (The copyright and license +notice must be retained.) + +Online documentation is at https://six.readthedocs.io/. + +Bugs can be reported to https://github.com/benjaminp/six. The code can also +be found there. diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/RECORD b/lib/python3.10/site-packages/six-1.17.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..5220f80df7ac554fdbdfb873e89f50c5c7d6ac64 --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/RECORD @@ -0,0 +1,10 @@ +__pycache__/six.cpython-310.pyc,, +six-1.17.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +six-1.17.0.dist-info/LICENSE,sha256=Q3W6IOK5xsTnytKUCmKP2Q6VzD1Q7pKq51VxXYuh-9A,1066 +six-1.17.0.dist-info/METADATA,sha256=vwFbZNcS2I3oE4FlfLPlu4Fk1Er553AEgqoa22BSoxA,1815 +six-1.17.0.dist-info/RECORD,, +six-1.17.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +six-1.17.0.dist-info/WHEEL,sha256=9Hm2OB-j1QcCUq9Jguht7ayGIIZBRTdOXD1qg9cCgPM,109 +six-1.17.0.dist-info/direct_url.json,sha256=wYj7gfZFL4_SnRwMpZ4lmHwetIzoJZQK4J9R7BHs6oI,63 +six-1.17.0.dist-info/top_level.txt,sha256=_iVH_iYEtEXnD8nYGQYpYFUvkUW9sEO1GYbkeKSAais,4 +six.py,sha256=xRyR9wPT1LNpbJI8tf7CE-BeddkhU5O--sfy-mo5BN8,34703 diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/REQUESTED b/lib/python3.10/site-packages/six-1.17.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/WHEEL b/lib/python3.10/site-packages/six-1.17.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..eaea6f3b57e8165ab7d2673900a9939348ee5e42 --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/direct_url.json b/lib/python3.10/site-packages/six-1.17.0.dist-info/direct_url.json new file mode 100644 index 0000000000000000000000000000000000000000..ff8f4c0de595b0ecd1df9b0d6f84aa1115a2b15d --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {}, "url": "file:///croot/six_1744271502820/work"} \ No newline at end of file diff --git a/lib/python3.10/site-packages/six-1.17.0.dist-info/top_level.txt b/lib/python3.10/site-packages/six-1.17.0.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..ffe2fce498955b628014618b28c6bcf152466a4a --- /dev/null +++ b/lib/python3.10/site-packages/six-1.17.0.dist-info/top_level.txt @@ -0,0 +1 @@ +six diff --git a/lib/python3.10/site-packages/skimage/__init__.py b/lib/python3.10/site-packages/skimage/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..55baf7454d2f1fc2d52719bc29e49516a79ea152 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/__init__.py @@ -0,0 +1,148 @@ +"""Image Processing for Python + +scikit-image (a.k.a. ``skimage``) is a collection of algorithms for image +processing and computer vision. + +Attributes +---------- +__version__ : str + The scikit-image version string. + +Subpackages +----------- +color + Color space conversion. +data + Example images and datasets. +draw + Drawing primitives, such as lines, circles, text, etc. +exposure + Image intensity adjustment, e.g., histogram equalization, etc. +feature + Feature detection and extraction, e.g., texture analysis, corners, etc. +filters + Sharpening, edge finding, rank filters, thresholding, etc. +future + Functionality with an experimental API. +graph + Graph-based operations, e.g., shortest paths. +io + Reading and saving of images and videos. +measure + Measurement of image properties, e.g., region properties, contours. +metrics + Metrics corresponding to images, e.g., distance metrics, similarity, etc. +morphology + Morphological algorithms, e.g., closing, opening, skeletonization. +registration + Image registration algorithms, e.g., optical flow or phase cross correlation. +restoration + Restoration algorithms, e.g., deconvolution algorithms, denoising, etc. +segmentation + Algorithms to partition images into meaningful regions or boundaries. +transform + Geometric and other transformations, e.g., rotations, Radon transform. +util + Generic utilities. +""" + +__version__ = '0.25.2' + +import lazy_loader as _lazy + +__getattr__, *_ = _lazy.attach_stub(__name__, __file__) + + +# Don't use the `__all__` and `__dir__` returned by `attach_stubs` since that +# one would expose utility functions we don't want to advertise in our +# top-level module anymore. +__all__ = [ + "__version__", + "color", + "data", + "draw", + "exposure", + "feature", + "filters", + "future", + "graph", + "io", + "measure", + "metrics", + "morphology", + "registration", + "restoration", + "segmentation", + "transform", + "util", +] + + +def __dir__(): + return __all__.copy() + + +# Logic for checking for improper install and importing while in the source +# tree when package has not been installed inplace. +# Code adapted from scikit-learn's __check_build module. +_INPLACE_MSG = """ +It appears that you are importing a local scikit-image source tree. For +this, you need to have an inplace install. Maybe you are in the source +directory and you need to try from another location.""" + +_STANDARD_MSG = """ +Your install of scikit-image appears to be broken. +Try re-installing the package following the instructions at: +https://scikit-image.org/docs/stable/user_guide/install.html""" + + +def _raise_build_error(e): + # Raise a comprehensible error + import os.path as osp + + local_dir = osp.split(__file__)[0] + msg = _STANDARD_MSG + if local_dir == "skimage": + # Picking up the local install: this will work only if the + # install is an 'inplace build' + msg = _INPLACE_MSG + raise ImportError( + f"{e}\nIt seems that scikit-image has not been built correctly.\n{msg}" + ) + + +def _try_append_commit_info(version): + """Append last commit date and hash to `version`, if available.""" + import subprocess + from pathlib import Path + + try: + output = subprocess.check_output( + ['git', 'log', '-1', '--format="%h %aI"'], + cwd=Path(__file__).parent, + text=True, + ) + if output: + git_hash, git_date = ( + output.strip().replace('"', '').split('T')[0].replace('-', '').split() + ) + version = '+'.join( + [tag for tag in version.split('+') if not tag.startswith('git')] + ) + version += f'+git{git_date}.{git_hash}' + + except (FileNotFoundError, subprocess.CalledProcessError): + pass + except OSError: + pass # If skimage is built with emscripten which does not support processes + + return version + + +if 'dev' in __version__: + __version__ = _try_append_commit_info(__version__) + + +from skimage._shared.tester import PytestTester as _PytestTester + +test = _PytestTester(__name__) diff --git a/lib/python3.10/site-packages/skimage/__init__.pyi b/lib/python3.10/site-packages/skimage/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..853ff7aaa7c7f2c2a31bd7c38fcf40bb0936f9aa --- /dev/null +++ b/lib/python3.10/site-packages/skimage/__init__.pyi @@ -0,0 +1,55 @@ +_submodules = [ + 'color', + 'data', + 'draw', + 'exposure', + 'feature', + 'filters', + 'future', + 'graph', + 'io', + 'measure', + 'metrics', + 'morphology', + 'registration', + 'restoration', + 'segmentation', + 'transform', + 'util', +] + +__all__ = _submodules + ['__version__'] # noqa: F822 + +from . import ( + color, + data, + draw, + exposure, + feature, + filters, + future, + graph, + io, + measure, + metrics, + morphology, + registration, + restoration, + segmentation, + transform, + util, +) + +# Legacy imports, not advertised in __all__ +from .util.dtype import ( + dtype_limits, + img_as_float32, + img_as_float64, + img_as_float, + img_as_int, + img_as_uint, + img_as_ubyte, + img_as_bool, +) +from .util.lookfor import lookfor +from .data import data_dir diff --git a/lib/python3.10/site-packages/skimage/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fe67c651adaacf2bf3394d765fd29c8d6fc95db Binary files /dev/null and b/lib/python3.10/site-packages/skimage/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/__pycache__/conftest.cpython-310.pyc b/lib/python3.10/site-packages/skimage/__pycache__/conftest.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee3f62c44752a8daa5492cb11eec0ed3ee294c66 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/__pycache__/conftest.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__init__.py b/lib/python3.10/site-packages/skimage/_shared/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b0a5c7a93837dc5ad6ec53584e622ae61b624dc Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/_dependency_checks.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_dependency_checks.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..65112dbec87936dccc568bdc0a6b1222ff01ca41 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_dependency_checks.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/_geometry.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_geometry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45a1e54fd18ddf69cf726ac86bc2fec7a5b8ac9f Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_geometry.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/_tempfile.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_tempfile.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5474c1f6a191d77a8aa22be84b406d4c77bb4706 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_tempfile.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/_warnings.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_warnings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1dfeabfcccd08e7f1e44e62fc2b59bec520f5de6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/_warnings.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/compat.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/compat.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35804622e20aa3b26716400800afa3d27400aa2d Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/compat.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/coord.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/coord.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffb93b45e275ce93f9f7c63c843d67a4fc326eea Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/coord.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/dtype.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/dtype.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0fd009046a4483a84c3f48c5d6b9e3e0cfe9bda Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/dtype.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/filters.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/filters.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54a4b4b80f121cff20a831e85d40e8325224f050 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/filters.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/tester.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/tester.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ac06d736f70189bb0e8affefcc9d3c907aa5307 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/tester.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/testing.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/testing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33bd50512c72512226776e8dcc99d138271827b6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/testing.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/utils.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12c1b3ced3f1bd1c0b0945d14fc06cc2033d69d9 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/utils.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/__pycache__/version_requirements.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/__pycache__/version_requirements.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b74c61ed4903077d9a1bbcfe136ea9604c90677b Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/__pycache__/version_requirements.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/_dependency_checks.py b/lib/python3.10/site-packages/skimage/_shared/_dependency_checks.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2cb660e5dddbff22b6ee859c3f214c4d484e82 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/_dependency_checks.py @@ -0,0 +1,7 @@ +from .version_requirements import is_installed +import sys +import platform + +has_mpl = is_installed("matplotlib", ">=3.3") + +is_wasm = (sys.platform == "emscripten") or (platform.machine() in ["wasm32", "wasm64"]) diff --git a/lib/python3.10/site-packages/skimage/_shared/_geometry.py b/lib/python3.10/site-packages/skimage/_shared/_geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..2c486e9194aef9e1ce1e6bc43fcd90ea10dd6db6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/_geometry.py @@ -0,0 +1,54 @@ +__all__ = ['polygon_clip', 'polygon_area'] + +import numpy as np + +from .version_requirements import require + + +@require("matplotlib", ">=3.3") +def polygon_clip(rp, cp, r0, c0, r1, c1): + """Clip a polygon to the given bounding box. + + Parameters + ---------- + rp, cp : (K,) ndarray of double + Row and column coordinates of the polygon. + (r0, c0), (r1, c1) : double + Top-left and bottom-right coordinates of the bounding box. + + Returns + ------- + r_clipped, c_clipped : (L,) ndarray of double + Coordinates of clipped polygon. + + Notes + ----- + This makes use of Sutherland-Hodgman clipping as implemented in + AGG 2.4 and exposed in Matplotlib. + + """ + from matplotlib import path, transforms + + poly = path.Path(np.vstack((rp, cp)).T, closed=True) + clip_rect = transforms.Bbox([[r0, c0], [r1, c1]]) + poly_clipped = poly.clip_to_bbox(clip_rect).to_polygons()[0] + + return poly_clipped[:, 0], poly_clipped[:, 1] + + +def polygon_area(pr, pc): + """Compute the area of a polygon. + + Parameters + ---------- + pr, pc : (K,) array of float + Polygon row and column coordinates. + + Returns + ------- + a : float + Area of the polygon. + """ + pr = np.asarray(pr) + pc = np.asarray(pc) + return 0.5 * np.abs(np.sum((pc[:-1] * pr[1:]) - (pc[1:] * pr[:-1]))) diff --git a/lib/python3.10/site-packages/skimage/_shared/_tempfile.py b/lib/python3.10/site-packages/skimage/_shared/_tempfile.py new file mode 100644 index 0000000000000000000000000000000000000000..7fb1585f097cb95bf07ee4be12eb787878559fba --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/_tempfile.py @@ -0,0 +1,28 @@ +from tempfile import NamedTemporaryFile +from contextlib import contextmanager +import os + + +@contextmanager +def temporary_file(suffix=''): + """Yield a writeable temporary filename that is deleted on context exit. + + Parameters + ---------- + suffix : string, optional + The suffix for the file. + + Examples + -------- + >>> import numpy as np + >>> from skimage import io + >>> with temporary_file('.tif') as tempfile: + ... im = np.arange(25, dtype=np.uint8).reshape((5, 5)) + ... io.imsave(tempfile, im) + ... assert np.all(io.imread(tempfile) == im) + """ + with NamedTemporaryFile(suffix=suffix, delete=False) as tempfile_stream: + tempfile = tempfile_stream.name + + yield tempfile + os.remove(tempfile) diff --git a/lib/python3.10/site-packages/skimage/_shared/_warnings.py b/lib/python3.10/site-packages/skimage/_shared/_warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..cf2f3c3cfaa618948fb2ffb0b96dbf037d6f5ce7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/_warnings.py @@ -0,0 +1,149 @@ +from contextlib import contextmanager +import sys +import warnings +import re +import functools +import os + +__all__ = ['all_warnings', 'expected_warnings', 'warn'] + + +# A version of `warnings.warn` with a default stacklevel of 2. +# functool is used so as not to increase the call stack accidentally +warn = functools.partial(warnings.warn, stacklevel=2) + + +@contextmanager +def all_warnings(): + """ + Context for use in testing to ensure that all warnings are raised. + + Examples + -------- + >>> import warnings + >>> def foo(): + ... warnings.warn(RuntimeWarning("bar"), stacklevel=2) + + We raise the warning once, while the warning filter is set to "once". + Hereafter, the warning is invisible, even with custom filters: + + >>> with warnings.catch_warnings(): + ... warnings.simplefilter('once') + ... foo() # doctest: +SKIP + + We can now run ``foo()`` without a warning being raised: + + >>> from numpy.testing import assert_warns + >>> foo() # doctest: +SKIP + + To catch the warning, we call in the help of ``all_warnings``: + + >>> with all_warnings(): + ... assert_warns(RuntimeWarning, foo) + """ + # _warnings.py is on the critical import path. + # Since this is a testing only function, we lazy import inspect. + import inspect + + # Whenever a warning is triggered, Python adds a __warningregistry__ + # member to the *calling* module. The exercise here is to find + # and eradicate all those breadcrumbs that were left lying around. + # + # We proceed by first searching all parent calling frames and explicitly + # clearing their warning registries (necessary for the doctests above to + # pass). Then, we search for all submodules of skimage and clear theirs + # as well (necessary for the skimage test suite to pass). + + frame = inspect.currentframe() + if frame: + for f in inspect.getouterframes(frame): + f[0].f_locals['__warningregistry__'] = {} + del frame + + for mod_name, mod in list(sys.modules.items()): + try: + mod.__warningregistry__.clear() + except AttributeError: + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + yield w + + +@contextmanager +def expected_warnings(matching): + r"""Context for use in testing to catch known warnings matching regexes + + Parameters + ---------- + matching : None or a list of strings or compiled regexes + Regexes for the desired warning to catch + If matching is None, this behaves as a no-op. + + Examples + -------- + >>> import numpy as np + >>> rng = np.random.default_rng() + >>> image = rng.integers(0, 2**16, size=(100, 100), dtype=np.uint16) + >>> # rank filters are slow when bit-depth exceeds 10 bits + >>> from skimage import filters + >>> with expected_warnings(['Bad rank filter performance']): + ... median_filtered = filters.rank.median(image) + + Notes + ----- + Uses `all_warnings` to ensure all warnings are raised. + Upon exiting, it checks the recorded warnings for the desired matching + pattern(s). + Raises a ValueError if any match was not found or an unexpected + warning was raised. + Allows for three types of behaviors: `and`, `or`, and `optional` matches. + This is done to accommodate different build environments or loop conditions + that may produce different warnings. The behaviors can be combined. + If you pass multiple patterns, you get an orderless `and`, where all of the + warnings must be raised. + If you use the `|` operator in a pattern, you can catch one of several + warnings. + Finally, you can use `|\A\Z` in a pattern to signify it as optional. + + """ + if isinstance(matching, str): + raise ValueError( + '``matching`` should be a list of strings and not ' 'a string itself.' + ) + + # Special case for disabling the context manager + if matching is None: + yield None + return + + strict_warnings = os.environ.get('SKIMAGE_TEST_STRICT_WARNINGS', '1') + if strict_warnings.lower() == 'true': + strict_warnings = True + elif strict_warnings.lower() == 'false': + strict_warnings = False + else: + strict_warnings = bool(int(strict_warnings)) + + with all_warnings() as w: + # enter context + yield w + # exited user context, check the recorded warnings + # Allow users to provide None + while None in matching: + matching.remove(None) + remaining = [m for m in matching if r'\A\Z' not in m.split('|')] + for warn in w: + found = False + for match in matching: + if re.search(match, str(warn.message)) is not None: + found = True + if match in remaining: + remaining.remove(match) + if strict_warnings and not found: + raise ValueError(f'Unexpected warning: {str(warn.message)}') + if strict_warnings and (len(remaining) > 0): + newline = "\n" + msg = f"No warning raised matching:{newline}{newline.join(remaining)}" + raise ValueError(msg) diff --git a/lib/python3.10/site-packages/skimage/_shared/compat.py b/lib/python3.10/site-packages/skimage/_shared/compat.py new file mode 100644 index 0000000000000000000000000000000000000000..23a0ded0a7ab4f7449a9d3a18b4e11b741d7fa99 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/compat.py @@ -0,0 +1,30 @@ +"""Compatibility helpers for dependencies.""" + +from packaging.version import parse + +import numpy as np +import scipy as sp + + +__all__ = [ + "NP_COPY_IF_NEEDED", + "SCIPY_CG_TOL_PARAM_NAME", +] + + +NUMPY_LT_2_0_0 = parse(np.__version__) < parse('2.0.0.dev0') + +# With NumPy 2.0.0, `copy=False` now raises a ValueError if the copy cannot be +# made. The previous behavior to only copy if needed is provided with `copy=None`. +# During the transition period, use this symbol instead. +# Remove once NumPy 2.0.0 is the minimal required version. +# https://numpy.org/devdocs/release/2.0.0-notes.html#new-copy-keyword-meaning-for-array-and-asarray-constructors +# https://github.com/numpy/numpy/pull/25168 +NP_COPY_IF_NEEDED = False if NUMPY_LT_2_0_0 else None + + +SCIPY_LT_1_12 = parse(sp.__version__) < parse('1.12') + +# Starting in SciPy v1.12, 'scipy.sparse.linalg.cg' keyword argument `tol` is +# deprecated in favor of `rtol`. +SCIPY_CG_TOL_PARAM_NAME = "tol" if SCIPY_LT_1_12 else "rtol" diff --git a/lib/python3.10/site-packages/skimage/_shared/coord.py b/lib/python3.10/site-packages/skimage/_shared/coord.py new file mode 100644 index 0000000000000000000000000000000000000000..e8ffbc20c1d7832e7a806d72cc58a7bb1606425f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/coord.py @@ -0,0 +1,124 @@ +import numpy as np +from scipy.spatial import cKDTree, distance + + +def _ensure_spacing(coord, spacing, p_norm, max_out): + """Returns a subset of coord where a minimum spacing is guaranteed. + + Parameters + ---------- + coord : ndarray + The coordinates of the considered points. + spacing : float + the maximum allowed spacing between the points. + p_norm : float + Which Minkowski p-norm to use. Should be in the range [1, inf]. + A finite large p may cause a ValueError if overflow can occur. + ``inf`` corresponds to the Chebyshev distance and 2 to the + Euclidean distance. + max_out: int + If not None, at most the first ``max_out`` candidates are + returned. + + Returns + ------- + output : ndarray + A subset of coord where a minimum spacing is guaranteed. + + """ + + # Use KDtree to find the peaks that are too close to each other + tree = cKDTree(coord) + + indices = tree.query_ball_point(coord, r=spacing, p=p_norm) + rejected_peaks_indices = set() + naccepted = 0 + for idx, candidates in enumerate(indices): + if idx not in rejected_peaks_indices: + # keep current point and the points at exactly spacing from it + candidates.remove(idx) + dist = distance.cdist( + [coord[idx]], coord[candidates], "minkowski", p=p_norm + ).reshape(-1) + candidates = [c for c, d in zip(candidates, dist) if d < spacing] + + # candidates.remove(keep) + rejected_peaks_indices.update(candidates) + naccepted += 1 + if max_out is not None and naccepted >= max_out: + break + + # Remove the peaks that are too close to each other + output = np.delete(coord, tuple(rejected_peaks_indices), axis=0) + if max_out is not None: + output = output[:max_out] + + return output + + +def ensure_spacing( + coords, + spacing=1, + p_norm=np.inf, + min_split_size=50, + max_out=None, + *, + max_split_size=2000, +): + """Returns a subset of coord where a minimum spacing is guaranteed. + + Parameters + ---------- + coords : array_like + The coordinates of the considered points. + spacing : float + the maximum allowed spacing between the points. + p_norm : float + Which Minkowski p-norm to use. Should be in the range [1, inf]. + A finite large p may cause a ValueError if overflow can occur. + ``inf`` corresponds to the Chebyshev distance and 2 to the + Euclidean distance. + min_split_size : int + Minimum split size used to process ``coords`` by batch to save + memory. If None, the memory saving strategy is not applied. + max_out : int + If not None, only the first ``max_out`` candidates are returned. + max_split_size : int + Maximum split size used to process ``coords`` by batch to save + memory. This number was decided by profiling with a large number + of points. Too small a number results in too much looping in + Python instead of C, slowing down the process, while too large + a number results in large memory allocations, slowdowns, and, + potentially, in the process being killed -- see gh-6010. See + benchmark results `here + `_. + + Returns + ------- + output : array_like + A subset of coord where a minimum spacing is guaranteed. + + """ + output = coords + if len(coords): + coords = np.atleast_2d(coords) + if min_split_size is None: + batch_list = [coords] + else: + coord_count = len(coords) + split_idx = [min_split_size] + split_size = min_split_size + while coord_count - split_idx[-1] > max_split_size: + split_size *= 2 + split_idx.append(split_idx[-1] + min(split_size, max_split_size)) + batch_list = np.array_split(coords, split_idx) + + output = np.zeros((0, coords.shape[1]), dtype=coords.dtype) + for batch in batch_list: + output = _ensure_spacing( + np.vstack([output, batch]), spacing, p_norm, max_out + ) + if max_out is not None and len(output) >= max_out: + break + + return output diff --git a/lib/python3.10/site-packages/skimage/_shared/dtype.py b/lib/python3.10/site-packages/skimage/_shared/dtype.py new file mode 100644 index 0000000000000000000000000000000000000000..6aed88c21b12bf527d66eadd68f34bbe503f243f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/dtype.py @@ -0,0 +1,73 @@ +import numpy as np + +# Define classes of supported dtypes and Python scalar types +# Variables ending in `_dtypes` only contain numpy.dtypes of the respective +# class; variables ending in `_types` additionally include Python scalar types. +signed_integer_dtypes = {np.int8, np.int16, np.int32, np.int64} +signed_integer_types = signed_integer_dtypes | {int} + +unsigned_integer_dtypes = {np.uint8, np.uint16, np.uint32, np.uint64} + +integer_dtypes = signed_integer_dtypes | unsigned_integer_dtypes +integer_types = signed_integer_types | unsigned_integer_dtypes + +floating_dtypes = {np.float16, np.float32, np.float64} +floating_types = floating_dtypes | {float} + +complex_dtypes = {np.complex64, np.complex128} +complex_types = complex_dtypes | {complex} + +inexact_dtypes = floating_dtypes | complex_dtypes +inexact_types = floating_types | complex_types + +bool_types = {np.dtype(bool), bool} + +numeric_dtypes = integer_dtypes | inexact_dtypes | {np.bool_} +numeric_types = integer_types | inexact_types | bool_types + + +def numeric_dtype_min_max(dtype): + """Return minimum and maximum representable value for a given dtype. + + A convenient wrapper around `numpy.finfo` and `numpy.iinfo` that + additionally supports numpy.bool as well. + + Parameters + ---------- + dtype : numpy.dtype + The dtype. Tries to convert Python "types" such as int or float, to + the corresponding NumPy dtype. + + Returns + ------- + min, max : number + Minimum and maximum of the given `dtype`. These scalars are themselves + of the given `dtype`. + + Examples + -------- + >>> import numpy as np + >>> numeric_dtype_min_max(np.uint8) + (0, 255) + >>> numeric_dtype_min_max(bool) + (False, True) + >>> numeric_dtype_min_max(np.float64) + (-1.7976931348623157e+308, 1.7976931348623157e+308) + >>> numeric_dtype_min_max(int) + (-9223372036854775808, 9223372036854775807) + """ + dtype = np.dtype(dtype) + if np.issubdtype(dtype, np.integer): + info = np.iinfo(dtype) + min_ = dtype.type(info.min) + max_ = dtype.type(info.max) + elif np.issubdtype(dtype, np.inexact): + info = np.finfo(dtype) + min_ = info.min + max_ = info.max + elif np.issubdtype(dtype, np.dtype(bool)): + min_ = dtype.type(False) + max_ = dtype.type(True) + else: + raise ValueError(f"unsupported dtype {dtype!r}") + return min_, max_ diff --git a/lib/python3.10/site-packages/skimage/_shared/fast_exp.cpython-310-x86_64-linux-gnu.so b/lib/python3.10/site-packages/skimage/_shared/fast_exp.cpython-310-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..7ef4b264b199598257b94762c5e77dc0896a3668 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/fast_exp.cpython-310-x86_64-linux-gnu.so differ diff --git a/lib/python3.10/site-packages/skimage/_shared/fast_exp.h b/lib/python3.10/site-packages/skimage/_shared/fast_exp.h new file mode 100644 index 0000000000000000000000000000000000000000..fae57fde5f13d797163cccf906ad6130269e0353 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/fast_exp.h @@ -0,0 +1,47 @@ +/* A fast approximation of the exponential function. + * Reference [1]: https://schraudolph.org/pubs/Schraudolph99.pdf + * Reference [2]: https://doi.org/10.1162/089976600300015033 + * Additional improvements by Leonid Bloch. */ + +#include + +/* use just EXP_A = 1512775 for integer version, to avoid FP calculations */ +#define EXP_A (1512775.3951951856938) /* 2^20/ln2 */ +/* For min. RMS error */ +#define EXP_BC 1072632447 /* 1023*2^20 - 60801 */ +/* For min. max. relative error */ +/* #define EXP_BC 1072647449 */ /* 1023*2^20 - 45799 */ +/* For min. mean relative error */ +/* #define EXP_BC 1072625005 */ /* 1023*2^20 - 68243 */ + +__inline double _fast_exp (double y) +{ + union + { + double d; + struct { int32_t i, j; } n; + char t[8]; + } _eco; + + _eco.n.i = 1; + + switch(_eco.t[0]) { + case 1: + /* Little endian */ + _eco.n.j = (int32_t)(EXP_A*(y)) + EXP_BC; + _eco.n.i = 0; + break; + case 0: + /* Big endian */ + _eco.n.i = (int32_t)(EXP_A*(y)) + EXP_BC; + _eco.n.j = 0; + break; + } + + return _eco.d; +} + +__inline float _fast_expf (float y) +{ + return (float)_fast_exp((double)y); +} diff --git a/lib/python3.10/site-packages/skimage/_shared/filters.py b/lib/python3.10/site-packages/skimage/_shared/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..88a6cb7af1f468dd6334f3b71c22c6c6b75d96f5 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/filters.py @@ -0,0 +1,136 @@ +"""Filters used across multiple skimage submodules. + +These are defined here to avoid circular imports. + +The unit tests remain under skimage/filters/tests/ +""" + +from collections.abc import Iterable + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import ( + _supported_float_type, + convert_to_float, +) + + +def gaussian( + image, + sigma=1.0, + *, + mode='nearest', + cval=0, + preserve_range=False, + truncate=4.0, + channel_axis=None, + out=None, +): + """Multi-dimensional Gaussian filter. + + Parameters + ---------- + image : ndarray + Input image (grayscale or color) to filter. + sigma : scalar or sequence of scalars, optional + Standard deviation for Gaussian kernel. The standard + deviations of the Gaussian filter are given for each axis as a + sequence, or as a single number, in which case it is equal for + all axes. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The ``mode`` parameter determines how the array borders are + handled, where ``cval`` is the value when mode is equal to + 'constant'. Default is 'nearest'. + cval : scalar, optional + Value to fill past edges of input if ``mode`` is 'constant'. Default + is 0.0 + preserve_range : bool, optional + If True, keep the original range of values. Otherwise, the input + ``image`` is converted according to the conventions of ``img_as_float`` + (Normalized first to values [-1.0 ; 1.0] or [0 ; 1.0] depending on + dtype of input) + + For more information, see: + https://scikit-image.org/docs/dev/user_guide/data_types.html + truncate : float, optional + Truncate the filter at this many standard deviations. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + `channel_axis` was added in 0.19. + out : ndarray, optional + If given, the filtered image will be stored in this array. + + .. versionadded:: 0.23 + `out` was added in 0.23. + + Returns + ------- + filtered_image : ndarray + the filtered array + + Notes + ----- + This function is a wrapper around :func:`scipy.ndimage.gaussian_filter`. + + Integer arrays are converted to float. + + `out` should be of floating-point data type since `gaussian` converts the + input `image` to float. If `out` is not provided, another array + will be allocated and returned as the result. + + The multi-dimensional filter is implemented as a sequence of + one-dimensional convolution filters. The intermediate arrays are + stored in the same data type as the output. Therefore, for output + types with a limited precision, the results may be imprecise + because intermediate results may be stored with insufficient + precision. + + Examples + -------- + >>> import skimage as ski + >>> a = np.zeros((3, 3)) + >>> a[1, 1] = 1 + >>> a + array([[0., 0., 0.], + [0., 1., 0.], + [0., 0., 0.]]) + >>> ski.filters.gaussian(a, sigma=0.4) # mild smoothing + array([[0.00163116, 0.03712502, 0.00163116], + [0.03712502, 0.84496158, 0.03712502], + [0.00163116, 0.03712502, 0.00163116]]) + >>> ski.filters.gaussian(a, sigma=1) # more smoothing + array([[0.05855018, 0.09653293, 0.05855018], + [0.09653293, 0.15915589, 0.09653293], + [0.05855018, 0.09653293, 0.05855018]]) + >>> # Several modes are possible for handling boundaries + >>> ski.filters.gaussian(a, sigma=1, mode='reflect') + array([[0.08767308, 0.12075024, 0.08767308], + [0.12075024, 0.16630671, 0.12075024], + [0.08767308, 0.12075024, 0.08767308]]) + >>> # For RGB images, each is filtered separately + >>> image = ski.data.astronaut() + >>> filtered_img = ski.filters.gaussian(image, sigma=1, channel_axis=-1) + + """ + if np.any(np.asarray(sigma) < 0.0): + raise ValueError("Sigma values less than zero are not valid") + if channel_axis is not None: + # do not filter across channels + if not isinstance(sigma, Iterable): + sigma = [sigma] * (image.ndim - 1) + if len(sigma) == image.ndim - 1: + sigma = list(sigma) + sigma.insert(channel_axis % image.ndim, 0) + image = convert_to_float(image, preserve_range) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + if (out is not None) and (not np.issubdtype(out.dtype, np.floating)): + raise ValueError(f"dtype of `out` must be float; got {out.dtype!r}.") + return ndi.gaussian_filter( + image, sigma, output=out, mode=mode, cval=cval, truncate=truncate + ) diff --git a/lib/python3.10/site-packages/skimage/_shared/interpolation.cpython-310-x86_64-linux-gnu.so b/lib/python3.10/site-packages/skimage/_shared/interpolation.cpython-310-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..440267bb8f42651429db0f32e8bfbc7df5032149 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/interpolation.cpython-310-x86_64-linux-gnu.so differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tester.py b/lib/python3.10/site-packages/skimage/_shared/tester.py new file mode 100644 index 0000000000000000000000000000000000000000..f1247e4111d85d2717fe0b05ab180fe327e65fc0 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tester.py @@ -0,0 +1,131 @@ +import os +import sys + + +def _show_skimage_info(): + import skimage + + print(f"skimage version {skimage.__version__}") + + +class PytestTester: + """ + Pytest test runner. + + This class is made available in ``skimage._shared.testing``, and a test + function is typically added to a package's __init__.py like so:: + + from skimage._shared.testing import PytestTester + test = PytestTester(__name__) + del PytestTester + + Calling this test function finds and runs all tests associated with the + module and all its sub-modules. + + Attributes + ---------- + module_name : str + Full path to the package to test. + + Parameters + ---------- + module_name : module name + The name of the module to test. + + """ + + def __init__(self, module_name): + self.module_name = module_name + + def __call__( + self, + label='fast', + verbose=1, + extra_argv=None, + doctests=False, + coverage=False, + durations=-1, + tests=None, + ): + """ + Run tests for module using pytest. + + Parameters + ---------- + label : {'fast', 'full'}, optional + Identifies the tests to run. When set to 'fast', tests decorated + with `pytest.mark.slow` are skipped, when 'full', the slow marker + is ignored. + verbose : int, optional + Verbosity value for test outputs, in the range 1-3. Default is 1. + extra_argv : list, optional + List with any extra arguments to pass to pytests. + doctests : bool, optional + .. note:: Not supported + coverage : bool, optional + If True, report coverage of scikit-image code. Default is False. + Requires installation of (pip) pytest-cov. + durations : int, optional + If < 0, do nothing, If 0, report time of all tests, if > 0, + report the time of the slowest `timer` tests. Default is -1. + tests : test or list of tests + Tests to be executed with pytest '--pyargs' + + Returns + ------- + result : bool + Return True on success, false otherwise. + """ + import pytest + + module = sys.modules[self.module_name] + module_path = os.path.abspath(module.__path__[0]) + + # setup the pytest arguments + pytest_args = ["-l"] + + # offset verbosity. The "-q" cancels a "-v". + pytest_args += ["-q"] + + # Filter out annoying import messages. Want these in both develop and + # release mode. + pytest_args += [ + "-W ignore:Not importing directory", + "-W ignore:numpy.dtype size changed", + "-W ignore:numpy.ufunc size changed", + ] + + if doctests: + raise ValueError("Doctests not supported") + + if extra_argv: + pytest_args += list(extra_argv) + + if verbose > 1: + pytest_args += ["-" + "v" * (verbose - 1)] + + if coverage: + pytest_args += ["--cov=" + module_path] + + if label == "fast": + pytest_args += ["-m", "not slow"] + elif label != "full": + pytest_args += ["-m", label] + + if durations >= 0: + pytest_args += [f"--durations={durations}"] + + if tests is None: + tests = [self.module_name] + + pytest_args += ["--pyargs"] + list(tests) + + # run tests. + _show_skimage_info() + + try: + code = pytest.main(pytest_args) + except SystemExit as exc: + code = exc.code + + return code == 0 diff --git a/lib/python3.10/site-packages/skimage/_shared/testing.py b/lib/python3.10/site-packages/skimage/_shared/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..da038563846e948b1e2fb647380ad6b9500b8aa5 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/testing.py @@ -0,0 +1,311 @@ +""" +Testing utilities. +""" + +import os +import platform +import re +import struct +import sys +import functools +import inspect +from tempfile import NamedTemporaryFile + +import numpy as np +from numpy import testing +from numpy.testing import ( + TestCase, + assert_, + assert_warns, + assert_no_warnings, + assert_equal, + assert_almost_equal, + assert_array_equal, + assert_allclose, + assert_array_almost_equal, + assert_array_almost_equal_nulp, + assert_array_less, +) + +from .. import data, io +from ..data._fetchers import _fetch +from ..util import img_as_uint, img_as_float, img_as_int, img_as_ubyte +from ._warnings import expected_warnings +from ._dependency_checks import is_wasm + +import pytest + + +skipif = pytest.mark.skipif +xfail = pytest.mark.xfail +parametrize = pytest.mark.parametrize +raises = pytest.raises +fixture = pytest.fixture + +SKIP_RE = re.compile(r"(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$") + +# true if python is running in 32bit mode +# Calculate the size of a void * pointer in bits +# https://docs.python.org/3/library/struct.html +arch32 = struct.calcsize("P") * 8 == 32 + + +def assert_less(a, b, msg=None): + message = f"{a!r} is not lower than {b!r}" + if msg is not None: + message += ": " + msg + assert a < b, message + + +def assert_greater(a, b, msg=None): + message = f"{a!r} is not greater than {b!r}" + if msg is not None: + message += ": " + msg + assert a > b, message + + +def doctest_skip_parser(func): + """Decorator replaces custom skip test markup in doctests + + Say a function has a docstring:: + + >>> something, HAVE_AMODULE, HAVE_BMODULE = 0, False, False + >>> something # skip if not HAVE_AMODULE + 0 + >>> something # skip if HAVE_BMODULE + 0 + + This decorator will evaluate the expression after ``skip if``. If this + evaluates to True, then the comment is replaced by ``# doctest: +SKIP``. If + False, then the comment is just removed. The expression is evaluated in the + ``globals`` scope of `func`. + + For example, if the module global ``HAVE_AMODULE`` is False, and module + global ``HAVE_BMODULE`` is False, the returned function will have docstring:: + + >>> something # doctest: +SKIP + >>> something + else # doctest: +SKIP + >>> something # doctest: +SKIP + + """ + lines = func.__doc__.split('\n') + new_lines = [] + for line in lines: + match = SKIP_RE.match(line) + if match is None: + new_lines.append(line) + continue + code, space, expr = match.groups() + + try: + # Works as a function decorator + if eval(expr, func.__globals__): + code = code + space + "# doctest: +SKIP" + except AttributeError: + # Works as a class decorator + if eval(expr, func.__init__.__globals__): + code = code + space + "# doctest: +SKIP" + + new_lines.append(code) + func.__doc__ = "\n".join(new_lines) + return func + + +def roundtrip(image, plugin, suffix): + """Save and read an image using a specified plugin""" + if '.' not in suffix: + suffix = '.' + suffix + with NamedTemporaryFile(suffix=suffix, delete=False) as temp_file: + fname = temp_file.name + io.imsave(fname, image, plugin=plugin) + new = io.imread(fname, plugin=plugin) + try: + os.remove(fname) + except Exception: + pass + return new + + +def color_check(plugin, fmt='png'): + """Check roundtrip behavior for color images. + + All major input types should be handled as ubytes and read + back correctly. + """ + img = img_as_ubyte(data.chelsea()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2, r2.astype(bool)) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + testing.assert_allclose(r3, img) + + img4 = img_as_int(img) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_ubyte(img4)) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img) + + +def mono_check(plugin, fmt='png'): + """Check the roundtrip behavior for images that support most types. + + All major input types should be handled. + """ + + img = img_as_ubyte(data.moon()) + r1 = roundtrip(img, plugin, fmt) + testing.assert_allclose(img, r1) + + img2 = img > 128 + r2 = roundtrip(img2, plugin, fmt) + testing.assert_allclose(img2, r2.astype(bool)) + + img3 = img_as_float(img) + r3 = roundtrip(img3, plugin, fmt) + if r3.dtype.kind == 'f': + testing.assert_allclose(img3, r3) + else: + testing.assert_allclose(r3, img_as_uint(img)) + + img4 = img_as_int(img) + if fmt.lower() in (('tif', 'tiff')): + img4 -= 100 + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img4) + else: + r4 = roundtrip(img4, plugin, fmt) + testing.assert_allclose(r4, img_as_uint(img4)) + + img5 = img_as_uint(img) + r5 = roundtrip(img5, plugin, fmt) + testing.assert_allclose(r5, img5) + + +def fetch(data_filename): + """Attempt to fetch data, but if unavailable, skip the tests.""" + try: + return _fetch(data_filename) + except (ConnectionError, ModuleNotFoundError): + pytest.skip(f'Unable to download {data_filename}', allow_module_level=True) + + +# Ref: about the lack of threading support in WASM, please see +# https://github.com/pyodide/pyodide/issues/237 +def run_in_parallel(num_threads=2, warnings_matching=None): + """Decorator to run the same function multiple times in parallel. + + This decorator is useful to ensure that separate threads execute + concurrently and correctly while releasing the GIL. + + It is currently skipped when running on WASM-based platforms, as + the threading module is not supported. + + Parameters + ---------- + num_threads : int, optional + The number of times the function is run in parallel. + + warnings_matching: list or None + This parameter is passed on to `expected_warnings` so as not to have + race conditions with the warnings filters. A single + `expected_warnings` context manager is used for all threads. + If None, then no warnings are checked. + + """ + + assert num_threads > 0 + + def wrapper(func): + if is_wasm: + # Threading isn't supported on WASM, return early + return func + + import threading + + @functools.wraps(func) + def inner(*args, **kwargs): + with expected_warnings(warnings_matching): + threads = [] + for i in range(num_threads - 1): + thread = threading.Thread(target=func, args=args, kwargs=kwargs) + threads.append(thread) + for thread in threads: + thread.start() + + func(*args, **kwargs) + + for thread in threads: + thread.join() + + return inner + + return wrapper + + +def assert_stacklevel(warnings, *, offset=-1): + """Assert correct stacklevel of captured warnings. + + When scikit-image raises warnings, the stacklevel should ideally be set + so that the origin of the warnings will point to the public function + that was called by the user and not necessarily the very place where the + warnings were emitted (which may be inside some internal function). + This utility function helps with checking that + the stacklevel was set correctly on warnings captured by `pytest.warns`. + + Parameters + ---------- + warnings : collections.abc.Iterable[warning.WarningMessage] + Warnings that were captured by `pytest.warns`. + offset : int, optional + Offset from the line this function is called to the line were the + warning is supposed to originate from. For multiline calls, the + first line is relevant. Defaults to -1 which corresponds to the line + right above the one where this function is called. + + Raises + ------ + AssertionError + If a warning in `warnings` does not match the expected line number or + file name. + + Examples + -------- + >>> def test_something(): + ... with pytest.warns(UserWarning, match="some message") as record: + ... something_raising_a_warning() + ... assert_stacklevel(record) + ... + >>> def test_another_thing(): + ... with pytest.warns(UserWarning, match="some message") as record: + ... iam_raising_many_warnings( + ... "A long argument that forces the call to wrap." + ... ) + ... assert_stacklevel(record, offset=-3) + """ + __tracebackhide__ = True # Hide traceback for py.test + + frame = inspect.stack()[1].frame # 0 is current frame, 1 is outer frame + line_number = frame.f_lineno + offset + filename = frame.f_code.co_filename + expected = f"{filename}:{line_number}" + for warning in warnings: + actual = f"{warning.filename}:{warning.lineno}" + msg = ( + "Warning with wrong stacklevel:\n" + f" Expected: {expected}\n" + f" Actual: {actual}\n" + f" {warning.category.__name__}: {warning.message}" + ) + assert actual == expected, msg diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__init__.py b/lib/python3.10/site-packages/skimage/_shared/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae5252f00a3a7ff464034969ca5e58e08a361ea1 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_coord.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_coord.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d144fbe54b9e30902aeb91f0d019e9180701880 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_coord.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_dtype.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_dtype.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc473ea608b15f96f2ed312ce327608f81971c46 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_dtype.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_fast_exp.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_fast_exp.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f151ae8f194fa6f8f473e1634859007686fd6888 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_fast_exp.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_geometry.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_geometry.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b46f156727abf7309beb5cfd8020753cde3f34e Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_geometry.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_interpolation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_interpolation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7ecee64d4ee9eb1b5fa62b6adb30a8326e121da Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_interpolation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_safe_as_int.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_safe_as_int.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d45fadac73aab2b3dc747c692a99a74a7ddf4ab7 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_safe_as_int.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_testing.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_testing.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc7f9406c1fdd941355c75e64a253204eb506e38 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_testing.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_utils.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f59e2ce9d756c1882a28393fe441e7bf05d51cd Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_utils.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_version_requirements.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_version_requirements.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..889a14d27d04e679779a450dac8d87d717593da4 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_version_requirements.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_warnings.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_warnings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..266b47a2a48d5d15c85ec93e4f7f2e8fdac5791d Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_shared/tests/__pycache__/test_warnings.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_coord.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_coord.py new file mode 100644 index 0000000000000000000000000000000000000000..0aad2685ac034a1f1fe29a47411973b997913d88 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_coord.py @@ -0,0 +1,91 @@ +import time + +import numpy as np +import pytest +from scipy.spatial.distance import pdist, minkowski + +from skimage._shared.coord import ensure_spacing + + +@pytest.mark.parametrize("p", [1, 2, np.inf]) +@pytest.mark.parametrize("size", [30, 50, None]) +def test_ensure_spacing_trivial(p, size): + # --- Empty input + assert ensure_spacing([], p_norm=p) == [] + + # --- A unique point + coord = np.random.randn(1, 2) + assert np.array_equal(coord, ensure_spacing(coord, p_norm=p, min_split_size=size)) + + # --- Verified spacing + coord = np.random.randn(100, 2) + + # --- 0 spacing + assert np.array_equal( + coord, ensure_spacing(coord, spacing=0, p_norm=p, min_split_size=size) + ) + + # Spacing is chosen to be half the minimum distance + spacing = pdist(coord, metric=minkowski, p=p).min() * 0.5 + + out = ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size) + + assert np.array_equal(coord, out) + + +@pytest.mark.parametrize("ndim", [1, 2, 3, 4, 5]) +@pytest.mark.parametrize("size", [2, 10, None]) +def test_ensure_spacing_nD(ndim, size): + coord = np.ones((5, ndim)) + + expected = np.ones((1, ndim)) + + assert np.array_equal(ensure_spacing(coord, min_split_size=size), expected) + + +@pytest.mark.parametrize("p", [1, 2, np.inf]) +@pytest.mark.parametrize("size", [50, 100, None]) +def test_ensure_spacing_batch_processing(p, size): + coord = np.random.randn(100, 2) + + # --- Consider the average distance btween the point as spacing + spacing = np.median(pdist(coord, metric=minkowski, p=p)) + + expected = ensure_spacing(coord, spacing=spacing, p_norm=p) + + assert np.array_equal( + ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size), expected + ) + + +def test_max_batch_size(): + """Small batches are slow, large batches -> large allocations -> also slow. + + https://github.com/scikit-image/scikit-image/pull/6035#discussion_r751518691 + """ + coords = np.random.randint(low=0, high=1848, size=(40000, 2)) + tstart = time.time() + ensure_spacing(coords, spacing=100, min_split_size=50, max_split_size=2000) + dur1 = time.time() - tstart + + tstart = time.time() + ensure_spacing(coords, spacing=100, min_split_size=50, max_split_size=20000) + dur2 = time.time() - tstart + + # Originally checked dur1 < dur2 to assert that the default batch size was + # faster than a much larger batch size. However, on rare occasion a CI test + # case would fail with dur1 ~5% larger than dur2. To be more robust to + # variable load or differences across architectures, we relax this here. + assert dur1 < 1.33 * dur2 + + +@pytest.mark.parametrize("p", [1, 2, np.inf]) +@pytest.mark.parametrize("size", [30, 50, None]) +def test_ensure_spacing_p_norm(p, size): + coord = np.random.randn(100, 2) + + # --- Consider the average distance btween the point as spacing + spacing = np.median(pdist(coord, metric=minkowski, p=p)) + out = ensure_spacing(coord, spacing=spacing, p_norm=p, min_split_size=size) + + assert pdist(out, metric=minkowski, p=p).min() > spacing diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_dtype.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_dtype.py new file mode 100644 index 0000000000000000000000000000000000000000..c4afdce96e54be71335dec5c1ce5fc7cc7cafcda --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_dtype.py @@ -0,0 +1,14 @@ +import numpy as np +import pytest + + +from ..dtype import numeric_dtype_min_max, numeric_types + + +class Test_numeric_dtype_min_max: + @pytest.mark.parametrize("dtype", numeric_types) + def test_all_numeric_types(self, dtype): + min_, max_ = numeric_dtype_min_max(dtype) + assert np.isscalar(min_) + assert np.isscalar(max_) + assert min_ < max_ diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_fast_exp.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_fast_exp.py new file mode 100644 index 0000000000000000000000000000000000000000..9d26885f8b06d913bfbb86b932fe0840d17c2c1a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_fast_exp.py @@ -0,0 +1,20 @@ +from ..fast_exp import fast_exp +import numpy as np + + +def test_fast_exp(): + X = np.linspace(-5, 0, 5000, endpoint=True) + + # Ground truth + Y = np.exp(X) + + # Approximation at double precision + _y_f64 = np.array([fast_exp['float64_t'](x) for x in X]) + + # Approximation at single precision + _y_f32 = np.array( + [fast_exp['float32_t'](x) for x in X.astype('float32')], dtype='float32' + ) + + for _y in [_y_f64, _y_f32]: + assert np.abs(Y - _y).mean() < 3e-3 diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_geometry.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_geometry.py new file mode 100644 index 0000000000000000000000000000000000000000..153dfedbc808a39fa2de747f275235d4708eaa25 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_geometry.py @@ -0,0 +1,81 @@ +import pytest +from skimage._shared._geometry import polygon_clip, polygon_area + +import numpy as np +from numpy.testing import assert_equal, assert_almost_equal + +pytest.importorskip("matplotlib") + + +hand = np.array( + [ + [1.64516129, 1.16145833], + [1.64516129, 1.59375], + [1.35080645, 1.921875], + [1.375, 2.18229167], + [1.68548387, 1.9375], + [1.60887097, 2.55208333], + [1.68548387, 2.69791667], + [1.76209677, 2.56770833], + [1.83064516, 1.97395833], + [1.89516129, 2.75], + [1.9516129, 2.84895833], + [2.01209677, 2.76041667], + [1.99193548, 1.99479167], + [2.11290323, 2.63020833], + [2.2016129, 2.734375], + [2.25403226, 2.60416667], + [2.14919355, 1.953125], + [2.30645161, 2.36979167], + [2.39112903, 2.36979167], + [2.41532258, 2.1875], + [2.1733871, 1.703125], + [2.07782258, 1.16666667], + ] +) + + +def test_polygon_area(): + x = [0, 0, 1, 1] + y = [0, 1, 1, 0] + + assert_almost_equal(polygon_area(y, x), 1) + + x = [0, 0, 1] + y = [0, 1, 1] + + assert_almost_equal(polygon_area(y, x), 0.5) + + x = [0, 0, 0.5, 1, 1, 0.5] + y = [0, 1, 0.5, 1, 0, 0.5] + + assert_almost_equal(polygon_area(y, x), 0.5) + + +def test_poly_clip(): + x = [0, 1, 2, 1] + y = [0, -1, 0, 1] + + yc, xc = polygon_clip(y, x, 0, 0, 1, 1) + assert_equal(polygon_area(yc, xc), 0.5) + + x = [-1, 1.5, 1.5, -1] + y = [0.5, 0.5, 1.5, 1.5] + yc, xc = polygon_clip(y, x, 0, 0, 1, 1) + assert_equal(polygon_area(yc, xc), 0.5) + + +def test_hand_clip(): + (r0, c0, r1, c1) = (1.0, 1.5, 2.1, 2.5) + clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1) + assert_equal(clip_r.size, 19) + assert_equal(clip_r[0], clip_r[-1]) + assert_equal(clip_c[0], clip_c[-1]) + + (r0, c0, r1, c1) = (1.0, 1.5, 1.7, 2.5) + clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1) + assert_equal(clip_r.size, 6) + + (r0, c0, r1, c1) = (1.0, 1.5, 1.5, 2.5) + clip_r, clip_c = polygon_clip(hand[:, 1], hand[:, 0], r0, c0, r1, c1) + assert_equal(clip_r.size, 5) diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_interpolation.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_interpolation.py new file mode 100644 index 0000000000000000000000000000000000000000..1b795b9909ad485000402d09b88d0302f8a95fea --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_interpolation.py @@ -0,0 +1,28 @@ +from skimage._shared.interpolation import coord_map_py +from skimage._shared.testing import assert_array_equal + + +def test_coord_map(): + symmetric = [coord_map_py(4, n, 'S') for n in range(-6, 6)] + expected_symmetric = [2, 3, 3, 2, 1, 0, 0, 1, 2, 3, 3, 2] + assert_array_equal(symmetric, expected_symmetric) + + wrap = [coord_map_py(4, n, 'W') for n in range(-6, 6)] + expected_wrap = [2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1] + assert_array_equal(wrap, expected_wrap) + + edge = [coord_map_py(4, n, 'E') for n in range(-6, 6)] + expected_edge = [0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3] + assert_array_equal(edge, expected_edge) + + reflect = [coord_map_py(4, n, 'R') for n in range(-6, 6)] + expected_reflect = [0, 1, 2, 3, 2, 1, 0, 1, 2, 3, 2, 1] + assert_array_equal(reflect, expected_reflect) + + reflect = [coord_map_py(1, n, 'R') for n in range(-6, 6)] + expected_reflect = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + assert_array_equal(reflect, expected_reflect) + + other = [coord_map_py(4, n, 'undefined') for n in range(-6, 6)] + expected_other = list(range(-6, 6)) + assert_array_equal(other, expected_other) diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_safe_as_int.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_safe_as_int.py new file mode 100644 index 0000000000000000000000000000000000000000..0d5cb003a1b1ae81a54989a915bab1bbff97b0b2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_safe_as_int.py @@ -0,0 +1,41 @@ +import numpy as np +from skimage._shared.utils import safe_as_int +from skimage._shared import testing + + +def test_int_cast_not_possible(): + with testing.raises(ValueError): + safe_as_int(7.1) + with testing.raises(ValueError): + safe_as_int([7.1, 0.9]) + with testing.raises(ValueError): + safe_as_int(np.r_[7.1, 0.9]) + with testing.raises(ValueError): + safe_as_int((7.1, 0.9)) + with testing.raises(ValueError): + safe_as_int(((3, 4, 1), (2, 7.6, 289))) + with testing.raises(ValueError): + safe_as_int(7.1, 0.09) + with testing.raises(ValueError): + safe_as_int([7.1, 0.9], 0.09) + with testing.raises(ValueError): + safe_as_int(np.r_[7.1, 0.9], 0.09) + with testing.raises(ValueError): + safe_as_int((7.1, 0.9), 0.09) + with testing.raises(ValueError): + safe_as_int(((3, 4, 1), (2, 7.6, 289)), 0.25) + + +def test_int_cast_possible(): + testing.assert_equal(safe_as_int(7.1, atol=0.11), 7) + testing.assert_equal(safe_as_int(-7.1, atol=0.11), -7) + testing.assert_equal(safe_as_int(41.9, atol=0.11), 42) + testing.assert_array_equal( + safe_as_int([2, 42, 5789234.0, 87, 4]), np.r_[2, 42, 5789234, 87, 4] + ) + testing.assert_array_equal( + safe_as_int( + np.r_[[[3, 4, 1.000000001], [7, 2, -8.999999999], [6, 9, -4234918347.0]]] + ), + np.r_[[[3, 4, 1], [7, 2, -9], [6, 9, -4234918347]]], + ) diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_testing.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_testing.py new file mode 100644 index 0000000000000000000000000000000000000000..17969348b19f7590dfcf01a9bec8b01f6f843973 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_testing.py @@ -0,0 +1,154 @@ +"""Testing decorators module""" + +import inspect +import re +import warnings + +import pytest +from numpy.testing import assert_equal +from skimage._shared.testing import ( + doctest_skip_parser, + run_in_parallel, + assert_stacklevel, +) +from skimage._shared import testing +from skimage._shared._dependency_checks import is_wasm + +from skimage._shared._warnings import expected_warnings +from warnings import warn + + +def test_skipper(): + def f(): + pass + + class c: + def __init__(self): + self.me = "I think, therefore..." + + docstring = """ Header + + >>> something # skip if not HAVE_AMODULE + >>> something + else + >>> a = 1 # skip if not HAVE_BMODULE + >>> something2 # skip if HAVE_AMODULE + """ + f.__doc__ = docstring + c.__doc__ = docstring + + global HAVE_AMODULE, HAVE_BMODULE + HAVE_AMODULE = False + HAVE_BMODULE = True + + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + assert f is f2 + assert c is c2 + + expected = """ Header + + >>> something # doctest: +SKIP + >>> something + else + >>> a = 1 + >>> something2 + """ + assert_equal(f2.__doc__, expected) + assert_equal(c2.__doc__, expected) + + HAVE_AMODULE = True + HAVE_BMODULE = False + f.__doc__ = docstring + c.__doc__ = docstring + f2 = doctest_skip_parser(f) + c2 = doctest_skip_parser(c) + + assert f is f2 + expected = """ Header + + >>> something + >>> something + else + >>> a = 1 # doctest: +SKIP + >>> something2 # doctest: +SKIP + """ + assert_equal(f2.__doc__, expected) + assert_equal(c2.__doc__, expected) + + del HAVE_AMODULE + f.__doc__ = docstring + c.__doc__ = docstring + with testing.raises(NameError): + doctest_skip_parser(f) + with testing.raises(NameError): + doctest_skip_parser(c) + + +@pytest.mark.skipif(is_wasm, reason="Cannot start threads in WASM") +def test_run_in_parallel(): + state = [] + + @run_in_parallel() + def change_state1(): + state.append(None) + + change_state1() + assert len(state) == 2 + + @run_in_parallel(num_threads=1) + def change_state2(): + state.append(None) + + change_state2() + assert len(state) == 3 + + @run_in_parallel(num_threads=3) + def change_state3(): + state.append(None) + + change_state3() + assert len(state) == 6 + + +@pytest.mark.skipif(is_wasm, reason="Cannot run parallel code in WASM") +def test_parallel_warning(): + @run_in_parallel() + def change_state_warns_fails(): + warn("Test warning for test parallel", stacklevel=2) + + with expected_warnings(['Test warning for test parallel']): + change_state_warns_fails() + + @run_in_parallel(warnings_matching=['Test warning for test parallel']) + def change_state_warns_passes(): + warn("Test warning for test parallel", stacklevel=2) + + change_state_warns_passes() + + +def test_expected_warnings_noop(): + # This will ensure the line beolow it behaves like a no-op + with expected_warnings(['Expected warnings test']): + # This should behave as a no-op + with expected_warnings(None): + warn('Expected warnings test') + + +class Test_assert_stacklevel: + def raise_warning(self, *args, **kwargs): + warnings.warn(*args, **kwargs) + + def test_correct_stacklevel(self): + # Should pass if stacklevel is set correctly + with pytest.warns(UserWarning, match="passes") as record: + self.raise_warning("passes", UserWarning, stacklevel=2) + assert_stacklevel(record) + + @pytest.mark.parametrize("level", [1, 3]) + def test_wrong_stacklevel(self, level): + # AssertionError should be raised for wrong stacklevel + with pytest.warns(UserWarning, match="wrong") as record: + self.raise_warning("wrong", UserWarning, stacklevel=level) + # Check that message contains expected line on right side + line_number = inspect.currentframe().f_lineno - 2 + regex = ".*" + re.escape(f"Expected: {__file__}:{line_number}") + with pytest.raises(AssertionError, match=regex): + assert_stacklevel(record, offset=-5) diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_utils.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a1e21e3b9cda808db42e655ee85777d68fe167fb --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_utils.py @@ -0,0 +1,516 @@ +import sys +import warnings + +import numpy as np +import pytest + +from skimage._shared import testing +from skimage._shared.utils import ( + _supported_float_type, + _validate_interpolation_order, + change_default_value, + channel_as_last_axis, + check_nD, + deprecate_func, + deprecate_parameter, + DEPRECATED, +) + +complex_dtypes = [np.complex64, np.complex128] +if hasattr(np, 'complex256'): + complex_dtypes += [np.complex256] + +have_numpydoc = False +try: + import numpydoc # noqa: F401 + + have_numpydoc = True +except ImportError: + pass + + +def test_change_default_value(): + @change_default_value('arg1', new_value=-1, changed_version='0.12') + def foo(arg0, arg1=0, arg2=1): + """Expected docstring""" + return arg0, arg1, arg2 + + @change_default_value( + 'arg1', + new_value=-1, + changed_version='0.12', + warning_msg="Custom warning message", + ) + def bar(arg0, arg1=0, arg2=1): + """Expected docstring""" + return arg0, arg1, arg2 + + # Assert warning messages + with pytest.warns(FutureWarning) as record: + assert foo(0) == (0, 0, 1) + assert bar(0) == (0, 0, 1) + + expected_msg = ( + "The new recommended value for arg1 is -1. Until " + "version 0.12, the default arg1 value is 0. From " + "version 0.12, the arg1 default value will be -1. " + "To avoid this warning, please explicitly set arg1 value." + ) + + assert str(record[0].message) == expected_msg + assert str(record[1].message) == "Custom warning message" + + # Assert that nothing happens if arg1 is set + with warnings.catch_warnings(record=True) as recorded: + # No kwargs + assert foo(0, 2) == (0, 2, 1) + assert foo(0, arg1=0) == (0, 0, 1) + + # Function name and doc is preserved + assert foo.__name__ == 'foo' + if sys.flags.optimize < 2: + # if PYTHONOPTIMIZE is set to 2, docstrings are stripped + assert foo.__doc__ == 'Expected docstring' + # Assert no warnings were raised + assert len(recorded) == 0 + + +def test_check_nD(): + z = np.random.random(200**2).reshape((200, 200)) + x = z[10:30, 30:10] + with testing.raises(ValueError): + check_nD(x, 2) + + +@pytest.mark.parametrize( + 'dtype', [bool, int, np.uint8, np.uint16, float, np.float32, np.float64] +) +@pytest.mark.parametrize('order', [None, -1, 0, 1, 2, 3, 4, 5, 6]) +def test_validate_interpolation_order(dtype, order): + if order is None: + # Default order + assert _validate_interpolation_order(dtype, None) == 0 if dtype == bool else 1 + elif order < 0 or order > 5: + # Order not in valid range + with testing.raises(ValueError): + _validate_interpolation_order(dtype, order) + elif dtype == bool and order != 0: + # Deprecated order for bool array + with pytest.raises(ValueError): + _validate_interpolation_order(bool, order) + else: + # Valid use case + assert _validate_interpolation_order(dtype, order) == order + + +@pytest.mark.parametrize( + 'dtype', + [ + bool, + np.float16, + np.float32, + np.float64, + np.uint8, + np.uint16, + np.uint32, + np.uint64, + np.int8, + np.int16, + np.int32, + np.int64, + ], +) +def test_supported_float_dtype_real(dtype): + float_dtype = _supported_float_type(dtype) + if dtype in [np.float16, np.float32]: + assert float_dtype == np.float32 + else: + assert float_dtype == np.float64 + + +@pytest.mark.parametrize('dtype', complex_dtypes) +@pytest.mark.parametrize('allow_complex', [False, True]) +def test_supported_float_dtype_complex(dtype, allow_complex): + if allow_complex: + float_dtype = _supported_float_type(dtype, allow_complex=allow_complex) + if dtype == np.complex64: + assert float_dtype == np.complex64 + else: + assert float_dtype == np.complex128 + else: + with testing.raises(ValueError): + _supported_float_type(dtype, allow_complex=allow_complex) + + +@pytest.mark.parametrize('dtype', ['f', 'float32', np.float32, np.dtype(np.float32)]) +def test_supported_float_dtype_input_kinds(dtype): + assert _supported_float_type(dtype) == np.float32 + + +@pytest.mark.parametrize( + 'dtypes, expected', + [ + ((np.float16, np.float64), np.float64), + ((np.float32, np.uint16, np.int8), np.float64), + ((np.float32, np.float16), np.float32), + ], +) +def test_supported_float_dtype_sequence(dtypes, expected): + float_dtype = _supported_float_type(dtypes) + assert float_dtype == expected + + +@channel_as_last_axis(multichannel_output=False) +def _decorated_channel_axis_size(x, *, channel_axis=None): + if channel_axis is None: + return None + assert channel_axis == -1 + return x.shape[-1] + + +@testing.parametrize('channel_axis', [None, 0, 1, 2, -1, -2, -3]) +def test_decorated_channel_axis_shape(channel_axis): + # Verify that channel_as_last_axis modifies the channel_axis as expected + + # need unique size per axis here + x = np.zeros((2, 3, 4)) + + size = _decorated_channel_axis_size(x, channel_axis=channel_axis) + if channel_axis is None: + assert size is None + else: + assert size == x.shape[channel_axis] + + +@deprecate_func( + deprecated_version="x", removed_version="y", hint="You are on your own." +) +def _deprecated_func(): + """Dummy function used in `test_deprecate_func`. + + The decorated function must be outside the test function, otherwise it + seems that the warning does not point at the calling location. + """ + + +def test_deprecate_func(): + with pytest.warns(FutureWarning) as record: + _deprecated_func() + testing.assert_stacklevel(record) + + assert len(record) == 1 + assert record[0].message.args[0] == ( + "`_deprecated_func` is deprecated since version x and will be removed in " + "version y. You are on your own." + ) + + +@deprecate_parameter("old1", start_version="0.10", stop_version="0.12") +@deprecate_parameter("old0", start_version="0.10", stop_version="0.12") +def _func_deprecated_params(arg0, old0=DEPRECATED, old1=DEPRECATED, arg1=None): + """Expected docstring. + + Parameters + ---------- + arg0 : int + First unchanged parameter. + arg1 : int, optional + Second unchanged parameter. + """ + return arg0, old0, old1, arg1 + + +@deprecate_parameter("old1", new_name="new0", start_version="0.10", stop_version="0.12") +@deprecate_parameter("old0", new_name="new1", start_version="0.10", stop_version="0.12") +def _func_replace_params( + arg0, old0=DEPRECATED, old1=DEPRECATED, new0=None, new1=None, arg1=None +): + """Expected docstring. + + Parameters + ---------- + arg0 : int + First unchanged parameter. + new0 : int, optional + First new parameter. + + .. versionadded:: 0.10 + new1 : int, optional + Second new parameter. + + .. versionadded:: 0.10 + arg1 : int, optional + Second unchanged parameter. + """ + return arg0, old0, old1, new0, new1, arg1 + + +class Test_deprecate_parameter: + @pytest.mark.skipif(not have_numpydoc, reason="requires numpydoc") + def test_docstring_removed_param(self): + # function name and doc are preserved + assert _func_deprecated_params.__name__ == "_func_deprecated_params" + if sys.flags.optimize < 2: + # if PYTHONOPTIMIZE is set to 2, docstrings are stripped + assert ( + _func_deprecated_params.__doc__ + == """Expected docstring. + + + Parameters + ---------- + arg0 : int + First unchanged parameter. + arg1 : int, optional + Second unchanged parameter. + + Other Parameters + ---------------- + old0 : DEPRECATED + `old0` is deprecated. + + .. deprecated:: 0.10 + old1 : DEPRECATED + `old1` is deprecated. + + .. deprecated:: 0.10 +""" + ) + + @pytest.mark.skipif(not have_numpydoc, reason="requires numpydoc") + def test_docstring_replaced_param(self): + assert _func_replace_params.__name__ == "_func_replace_params" + if sys.flags.optimize < 2: + # if PYTHONOPTIMIZE is set to 2, docstrings are stripped + assert ( + _func_replace_params.__doc__ + == """Expected docstring. + + + Parameters + ---------- + arg0 : int + First unchanged parameter. + new0 : int, optional + First new parameter. + + .. versionadded:: 0.10 + new1 : int, optional + Second new parameter. + + .. versionadded:: 0.10 + arg1 : int, optional + Second unchanged parameter. + + Other Parameters + ---------------- + old0 : DEPRECATED + Deprecated in favor of `new1`. + + .. deprecated:: 0.10 + old1 : DEPRECATED + Deprecated in favor of `new0`. + + .. deprecated:: 0.10 +""" + ) + + def test_warning_removed_param(self): + match = ( + r".*`old[01]` is deprecated since version 0\.10 and will be removed " + r"in 0\.12.* see the documentation of .*_func_deprecated_params`." + ) + with pytest.warns(FutureWarning, match=match): + assert _func_deprecated_params(1, 2) == (1, 2, DEPRECATED, None) + with pytest.warns(FutureWarning, match=match): + assert _func_deprecated_params(1, 2, 3) == (1, 2, 3, None) + with pytest.warns(FutureWarning, match=match): + assert _func_deprecated_params(1, old0=2) == ( + 1, + 2, + DEPRECATED, + None, + ) + with pytest.warns(FutureWarning, match=match): + assert _func_deprecated_params(1, old1=2) == ( + 1, + DEPRECATED, + 2, + None, + ) + + with warnings.catch_warnings(record=True) as record: + assert _func_deprecated_params(1, arg1=3) == (1, DEPRECATED, DEPRECATED, 3) + assert len(record) == 0 + + def test_warning_replaced_param(self): + match = ( + r".*`old[0,1]` is deprecated since version 0\.10 and will be removed " + r"in 0\.12.* see the documentation of .*_func_replace_params`." + ) + + with pytest.warns(FutureWarning, match=match): + assert _func_replace_params(1, 2) == ( + 1, + DEPRECATED, + DEPRECATED, + None, + 2, + None, + ) + + with pytest.warns(FutureWarning, match=match) as records: + assert _func_replace_params(1, 2, 3) == ( + 1, + DEPRECATED, + DEPRECATED, + 3, + 2, + None, + ) + assert len(records) == 2 + assert "`old1` is deprecated" in records[0].message.args[0] + assert "`old0` is deprecated" in records[1].message.args[0] + + with pytest.warns(FutureWarning, match=match): + assert _func_replace_params(1, old0=2) == ( + 1, + DEPRECATED, + DEPRECATED, + None, + 2, + None, + ) + + with pytest.warns(FutureWarning, match=match): + assert _func_replace_params(1, old1=3) == ( + 1, + DEPRECATED, + DEPRECATED, + 3, + None, + None, + ) + + # Otherwise, no warnings are emitted! + with warnings.catch_warnings(record=True) as record: + assert _func_replace_params(1, new0=2, new1=3) == ( + 1, + DEPRECATED, + DEPRECATED, + 2, + 3, + None, + ) + assert len(record) == 0 + + def test_missing_DEPRECATED(self): + decorate = deprecate_parameter( + "old", start_version="0.10", stop_version="0.12", stacklevel=2 + ) + + def foo(arg0, old=None): + return arg0, old + + with pytest.raises(RuntimeError, match="Expected .* "): + decorate(foo) + + def bar(arg0, old=DEPRECATED): + return arg0 + + assert decorate(bar)(1) == 1 + + def test_new_keyword_only(self): + @deprecate_parameter( + "old", + new_name="new", + start_version="0.19", + stop_version="0.21", + ) + def foo(arg0, old=DEPRECATED, *, new=1, arg3=None): + """Expected docstring""" + return arg0, new, arg3 + + # Assert that nothing happens when the function is called with the + # new API + with warnings.catch_warnings(record=True) as recorded: + # No kwargs + assert foo(0) == (0, 1, None) + # Kwargs without deprecated argument + assert foo(0, new=1, arg3=2) == (0, 1, 2) + assert foo(0, new=2) == (0, 2, None) + assert foo(0, arg3=2) == (0, 1, 2) + assert len(recorded) == 0 + + def test_conflicting_old_and_new(self): + match = r".*`old[0,1]` is deprecated" + with pytest.warns(FutureWarning, match=match): + with pytest.raises(ValueError, match=".* avoid conflicting values"): + _func_replace_params(1, old0=2, new1=2) + + with pytest.warns(FutureWarning, match=match): + with pytest.raises(ValueError, match=".* avoid conflicting values"): + _func_replace_params(1, old1=2, new0=2) + + with pytest.warns(FutureWarning, match=match): + with pytest.raises(ValueError, match=".* avoid conflicting values"): + _func_replace_params(1, old0=1, old1=1, new0=1, new1=1) + + def test_wrong_call_signature(self): + """Check that normal errors for faulty calls are unchanged.""" + with pytest.raises( + TypeError, match=r".* required positional argument\: 'arg0'" + ): + _func_replace_params() + + with pytest.warns(FutureWarning, match=r".*`old[0,1]` is deprecated"): + with pytest.raises( + TypeError, match=".* multiple values for argument 'old0'" + ): + _func_deprecated_params(1, 2, old0=2) + + def test_wrong_param_name(self): + with pytest.raises(ValueError, match="'old' is not in list"): + + @deprecate_parameter("old", start_version="0.10", stop_version="0.12") + def foo(arg0): + pass + + with pytest.raises(ValueError, match="'new' is not in list"): + + @deprecate_parameter( + "old", new_name="new", start_version="0.10", stop_version="0.12" + ) + def bar(arg0, old, arg1): + pass + + def test_warning_location(self): + with pytest.warns(FutureWarning) as records: + _func_deprecated_params(1, old0=2, old1=2) + testing.assert_stacklevel(records) + assert len(records) == 2 + + def test_stacklevel(self): + @deprecate_parameter( + "old", + start_version="0.19", + stop_version="0.21", + ) + def foo(arg0, old=DEPRECATED): + pass + + with pytest.raises(RuntimeError, match="Set stacklevel manually"): + foo(0, 1) + + @deprecate_parameter( + "old", + start_version="0.19", + stop_version="0.21", + stacklevel=2, + ) + def bar(arg0, old=DEPRECATED): + pass + + with pytest.warns(FutureWarning, match="`old` is deprecated") as records: + bar(0, 1) + testing.assert_stacklevel(records) diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_version_requirements.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_version_requirements.py new file mode 100644 index 0000000000000000000000000000000000000000..be07eea96524f05c8ddda5343d7614f0faf3bcce --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_version_requirements.py @@ -0,0 +1,40 @@ +"""Tests for the version requirement functions.""" + +import numpy as np +from numpy.testing import assert_equal +from skimage._shared import version_requirements as version_req +from skimage._shared import testing + + +def test_get_module_version(): + assert version_req.get_module_version('numpy') + assert version_req.get_module_version('scipy') + with testing.raises(ImportError): + version_req.get_module_version('fakenumpy') + + +def test_is_installed(): + assert version_req.is_installed('python', '>=2.7') + assert not version_req.is_installed('numpy', '<1.0') + + +def test_require(): + # A function that only runs on Python >2.7 and numpy > 1.5 (should pass) + @version_req.require('python', '>2.7') + @version_req.require('numpy', '>1.5') + def foo(): + return 1 + + assert_equal(foo(), 1) + + # function that requires scipy < 0.1 (should fail) + @version_req.require('scipy', '<0.1') + def bar(): + return 0 + + with testing.raises(ImportError): + bar() + + +def test_get_module(): + assert version_req.get_module("numpy") is np diff --git a/lib/python3.10/site-packages/skimage/_shared/tests/test_warnings.py b/lib/python3.10/site-packages/skimage/_shared/tests/test_warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..e8751daa4f6d2b39e7514eeb5138aceabe40fc35 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/tests/test_warnings.py @@ -0,0 +1,37 @@ +import os +from skimage._shared._warnings import expected_warnings +import pytest + + +@pytest.fixture(scope='function') +def setup(): + # Remove any environment variable if it exists + old_strictness = os.environ.pop('SKIMAGE_TEST_STRICT_WARNINGS', None) + yield + # Add the user's desired strictness + if old_strictness is not None: + os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = old_strictness + + +def test_strict_warnigns_default(setup): + # By default we should fail on missing expected warnings + with pytest.raises(ValueError): + with expected_warnings(['some warnings']): + pass + + +@pytest.mark.parametrize('strictness', ['1', 'true', 'True', 'TRUE']) +def test_strict_warning_true(setup, strictness): + os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = strictness + with pytest.raises(ValueError): + with expected_warnings(['some warnings']): + pass + + +@pytest.mark.parametrize('strictness', ['0', 'false', 'False', 'FALSE']) +def test_strict_warning_false(setup, strictness): + # If the user doesn't wish to be strict about warnings + # the following shouldn't raise any error + os.environ['SKIMAGE_TEST_STRICT_WARNINGS'] = strictness + with expected_warnings(['some warnings']): + pass diff --git a/lib/python3.10/site-packages/skimage/_shared/utils.py b/lib/python3.10/site-packages/skimage/_shared/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a793509fb39e9bc7fe85e7010d829dc16ab48617 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/utils.py @@ -0,0 +1,891 @@ +import functools +import inspect +import sys +import warnings + +import numpy as np + +from ._warnings import all_warnings, warn + +__all__ = [ + 'deprecate_func', + 'get_bound_method_class', + 'all_warnings', + 'safe_as_int', + 'check_shape_equality', + 'check_nD', + 'warn', + 'reshape_nd', + 'identity', + 'slice_at_axis', + "deprecate_parameter", + "DEPRECATED", +] + + +def _count_wrappers(func): + """Count the number of wrappers around `func`.""" + unwrapped = func + count = 0 + while hasattr(unwrapped, "__wrapped__"): + unwrapped = unwrapped.__wrapped__ + count += 1 + return count + + +def _warning_stacklevel(func): + """Find stacklevel for a warning raised from a wrapper around `func`. + + Try to determine the number of + + Parameters + ---------- + func : Callable + + + Returns + ------- + stacklevel : int + The stacklevel. Minimum of 2. + """ + # Count number of wrappers around `func` + wrapped_count = _count_wrappers(func) + + # Count number of total wrappers around global version of `func` + module = sys.modules.get(func.__module__) + try: + for name in func.__qualname__.split("."): + global_func = getattr(module, name) + except AttributeError as e: + raise RuntimeError( + f"Could not access `{func.__qualname__}` in {module!r}, " + f" may be a closure. Set stacklevel manually. ", + ) from e + else: + global_wrapped_count = _count_wrappers(global_func) + + stacklevel = global_wrapped_count - wrapped_count + 1 + return max(stacklevel, 2) + + +def _get_stack_length(func): + """Return function call stack length.""" + _func = func.__globals__.get(func.__name__, func) + length = _count_wrappers(_func) + return length + + +class _DecoratorBaseClass: + """Used to manage decorators' warnings stacklevel. + + The `_stack_length` class variable is used to store the number of + times a function is wrapped by a decorator. + + Let `stack_length` be the total number of times a decorated + function is wrapped, and `stack_rank` be the rank of the decorator + in the decorators stack. The stacklevel of a warning is then + `stacklevel = 1 + stack_length - stack_rank`. + """ + + _stack_length = {} + + def get_stack_length(self, func): + length = self._stack_length.get(func.__name__, _get_stack_length(func)) + return length + + +class change_default_value(_DecoratorBaseClass): + """Decorator for changing the default value of an argument. + + Parameters + ---------- + arg_name: str + The name of the argument to be updated. + new_value: any + The argument new value. + changed_version : str + The package version in which the change will be introduced. + warning_msg: str + Optional warning message. If None, a generic warning message + is used. + + """ + + def __init__(self, arg_name, *, new_value, changed_version, warning_msg=None): + self.arg_name = arg_name + self.new_value = new_value + self.warning_msg = warning_msg + self.changed_version = changed_version + + def __call__(self, func): + parameters = inspect.signature(func).parameters + arg_idx = list(parameters.keys()).index(self.arg_name) + old_value = parameters[self.arg_name].default + + stack_rank = _count_wrappers(func) + + if self.warning_msg is None: + self.warning_msg = ( + f'The new recommended value for {self.arg_name} is ' + f'{self.new_value}. Until version {self.changed_version}, ' + f'the default {self.arg_name} value is {old_value}. ' + f'From version {self.changed_version}, the {self.arg_name} ' + f'default value will be {self.new_value}. To avoid ' + f'this warning, please explicitly set {self.arg_name} value.' + ) + + @functools.wraps(func) + def fixed_func(*args, **kwargs): + stacklevel = 1 + self.get_stack_length(func) - stack_rank + if len(args) < arg_idx + 1 and self.arg_name not in kwargs.keys(): + # warn that arg_name default value changed: + warnings.warn(self.warning_msg, FutureWarning, stacklevel=stacklevel) + return func(*args, **kwargs) + + return fixed_func + + +class PatchClassRepr(type): + """Control class representations in rendered signatures.""" + + def __repr__(cls): + return f"<{cls.__name__}>" + + +class DEPRECATED(metaclass=PatchClassRepr): + """Signal value to help with deprecating parameters that use None. + + This is a proxy object, used to signal that a parameter has not been set. + This is useful if ``None`` is already used for a different purpose or just + to highlight a deprecated parameter in the signature. + """ + + +class deprecate_parameter: + """Deprecate a parameter of a function. + + Parameters + ---------- + deprecated_name : str + The name of the deprecated parameter. + start_version : str + The package version in which the warning was introduced. + stop_version : str + The package version in which the warning will be replaced by + an error / the deprecation is completed. + template : str, optional + If given, this message template is used instead of the default one. + new_name : str, optional + If given, the default message will recommend the new parameter name and an + error will be raised if the user uses both old and new names for the + same parameter. + modify_docstring : bool, optional + If the wrapped function has a docstring, add the deprecated parameters + to the "Other Parameters" section. + stacklevel : int, optional + This decorator attempts to detect the appropriate stacklevel for the + deprecation warning automatically. If this fails, e.g., due to + decorating a closure, you can set the stacklevel manually. The + outermost decorator should have stacklevel 2, the next inner one + stacklevel 3, etc. + + Notes + ----- + Assign `DEPRECATED` as the new default value for the deprecated parameter. + This marks the status of the parameter also in the signature and rendered + HTML docs. + + This decorator can be stacked to deprecate more than one parameter. + + Examples + -------- + >>> from skimage._shared.utils import deprecate_parameter, DEPRECATED + >>> @deprecate_parameter( + ... "b", new_name="c", start_version="0.1", stop_version="0.3" + ... ) + ... def foo(a, b=DEPRECATED, *, c=None): + ... return a, c + + Calling ``foo(1, b=2)`` will warn with:: + + FutureWarning: Parameter `b` is deprecated since version 0.1 and will + be removed in 0.3 (or later). To avoid this warning, please use the + parameter `c` instead. For more details, see the documentation of + `foo`. + """ + + DEPRECATED = DEPRECATED # Make signal value accessible for convenience + + remove_parameter_template = ( + "Parameter `{deprecated_name}` is deprecated since version " + "{deprecated_version} and will be removed in {changed_version} (or " + "later). To avoid this warning, please do not use the parameter " + "`{deprecated_name}`. For more details, see the documentation of " + "`{func_name}`." + ) + + replace_parameter_template = ( + "Parameter `{deprecated_name}` is deprecated since version " + "{deprecated_version} and will be removed in {changed_version} (or " + "later). To avoid this warning, please use the parameter `{new_name}` " + "instead. For more details, see the documentation of `{func_name}`." + ) + + def __init__( + self, + deprecated_name, + *, + start_version, + stop_version, + template=None, + new_name=None, + modify_docstring=True, + stacklevel=None, + ): + self.deprecated_name = deprecated_name + self.new_name = new_name + self.template = template + self.start_version = start_version + self.stop_version = stop_version + self.modify_docstring = modify_docstring + self.stacklevel = stacklevel + + def __call__(self, func): + parameters = inspect.signature(func).parameters + deprecated_idx = list(parameters.keys()).index(self.deprecated_name) + if self.new_name: + new_idx = list(parameters.keys()).index(self.new_name) + else: + new_idx = False + + if parameters[self.deprecated_name].default is not DEPRECATED: + raise RuntimeError( + f"Expected `{self.deprecated_name}` to have the value {DEPRECATED!r} " + f"to indicate its status in the rendered signature." + ) + + if self.template is not None: + template = self.template + elif self.new_name is not None: + template = self.replace_parameter_template + else: + template = self.remove_parameter_template + warning_message = template.format( + deprecated_name=self.deprecated_name, + deprecated_version=self.start_version, + changed_version=self.stop_version, + func_name=func.__qualname__, + new_name=self.new_name, + ) + + @functools.wraps(func) + def fixed_func(*args, **kwargs): + deprecated_value = DEPRECATED + new_value = DEPRECATED + + # Extract value of deprecated parameter + if len(args) > deprecated_idx: + deprecated_value = args[deprecated_idx] + # Overwrite old with DEPRECATED if replacement exists + if self.new_name is not None: + args = ( + args[:deprecated_idx] + + (DEPRECATED,) + + args[deprecated_idx + 1 :] + ) + if self.deprecated_name in kwargs.keys(): + deprecated_value = kwargs[self.deprecated_name] + # Overwrite old with DEPRECATED if replacement exists + if self.new_name is not None: + kwargs[self.deprecated_name] = DEPRECATED + + # Extract value of new parameter (if present) + if new_idx is not False and len(args) > new_idx: + new_value = args[new_idx] + if self.new_name and self.new_name in kwargs.keys(): + new_value = kwargs[self.new_name] + + if deprecated_value is not DEPRECATED: + stacklevel = ( + self.stacklevel + if self.stacklevel is not None + else _warning_stacklevel(func) + ) + warnings.warn( + warning_message, category=FutureWarning, stacklevel=stacklevel + ) + + if new_value is not DEPRECATED: + raise ValueError( + f"Both deprecated parameter `{self.deprecated_name}` " + f"and new parameter `{self.new_name}` are used. Use " + f"only the latter to avoid conflicting values." + ) + elif self.new_name is not None: + # Assign old value to new one + kwargs[self.new_name] = deprecated_value + + return func(*args, **kwargs) + + if self.modify_docstring and func.__doc__ is not None: + newdoc = _docstring_add_deprecated( + func, {self.deprecated_name: self.new_name}, self.start_version + ) + fixed_func.__doc__ = newdoc + + return fixed_func + + +def _docstring_add_deprecated(func, kwarg_mapping, deprecated_version): + """Add deprecated kwarg(s) to the "Other Params" section of a docstring. + + Parameters + ---------- + func : function + The function whose docstring we wish to update. + kwarg_mapping : dict + A dict containing {old_arg: new_arg} key/value pairs, see + `deprecate_parameter`. + deprecated_version : str + A major.minor version string specifying when old_arg was + deprecated. + + Returns + ------- + new_doc : str + The updated docstring. Returns the original docstring if numpydoc is + not available. + """ + if func.__doc__ is None: + return None + try: + from numpydoc.docscrape import FunctionDoc, Parameter + except ImportError: + # Return an unmodified docstring if numpydoc is not available. + return func.__doc__ + + Doc = FunctionDoc(func) + for old_arg, new_arg in kwarg_mapping.items(): + desc = [] + if new_arg is None: + desc.append(f'`{old_arg}` is deprecated.') + else: + desc.append(f'Deprecated in favor of `{new_arg}`.') + + desc += ['', f'.. deprecated:: {deprecated_version}'] + Doc['Other Parameters'].append( + Parameter(name=old_arg, type='DEPRECATED', desc=desc) + ) + new_docstring = str(Doc) + + # new_docstring will have a header starting with: + # + # .. function:: func.__name__ + # + # and some additional blank lines. We strip these off below. + split = new_docstring.split('\n') + no_header = split[1:] + while not no_header[0].strip(): + no_header.pop(0) + + # Store the initial description before any of the Parameters fields. + # Usually this is a single line, but the while loop covers any case + # where it is not. + descr = no_header.pop(0) + while no_header[0].strip(): + descr += '\n ' + no_header.pop(0) + descr += '\n\n' + # '\n ' rather than '\n' here to restore the original indentation. + final_docstring = descr + '\n '.join(no_header) + # strip any extra spaces from ends of lines + final_docstring = '\n'.join([line.rstrip() for line in final_docstring.split('\n')]) + return final_docstring + + +class channel_as_last_axis: + """Decorator for automatically making channels axis last for all arrays. + + This decorator reorders axes for compatibility with functions that only + support channels along the last axis. After the function call is complete + the channels axis is restored back to its original position. + + Parameters + ---------- + channel_arg_positions : tuple of int, optional + Positional arguments at the positions specified in this tuple are + assumed to be multichannel arrays. The default is to assume only the + first argument to the function is a multichannel array. + channel_kwarg_names : tuple of str, optional + A tuple containing the names of any keyword arguments corresponding to + multichannel arrays. + multichannel_output : bool, optional + A boolean that should be True if the output of the function is not a + multichannel array and False otherwise. This decorator does not + currently support the general case of functions with multiple outputs + where some or all are multichannel. + + """ + + def __init__( + self, + channel_arg_positions=(0,), + channel_kwarg_names=(), + multichannel_output=True, + ): + self.arg_positions = set(channel_arg_positions) + self.kwarg_names = set(channel_kwarg_names) + self.multichannel_output = multichannel_output + + def __call__(self, func): + @functools.wraps(func) + def fixed_func(*args, **kwargs): + channel_axis = kwargs.get('channel_axis', None) + + if channel_axis is None: + return func(*args, **kwargs) + + # TODO: convert scalars to a tuple in anticipation of eventually + # supporting a tuple of channel axes. Right now, only an + # integer or a single-element tuple is supported, though. + if np.isscalar(channel_axis): + channel_axis = (channel_axis,) + if len(channel_axis) > 1: + raise ValueError("only a single channel axis is currently supported") + + if channel_axis == (-1,) or channel_axis == -1: + return func(*args, **kwargs) + + if self.arg_positions: + new_args = [] + for pos, arg in enumerate(args): + if pos in self.arg_positions: + new_args.append(np.moveaxis(arg, channel_axis[0], -1)) + else: + new_args.append(arg) + new_args = tuple(new_args) + else: + new_args = args + + for name in self.kwarg_names: + kwargs[name] = np.moveaxis(kwargs[name], channel_axis[0], -1) + + # now that we have moved the channels axis to the last position, + # change the channel_axis argument to -1 + kwargs["channel_axis"] = -1 + + # Call the function with the fixed arguments + out = func(*new_args, **kwargs) + if self.multichannel_output: + out = np.moveaxis(out, -1, channel_axis[0]) + return out + + return fixed_func + + +class deprecate_func(_DecoratorBaseClass): + """Decorate a deprecated function and warn when it is called. + + Adapted from . + + Parameters + ---------- + deprecated_version : str + The package version when the deprecation was introduced. + removed_version : str + The package version in which the deprecated function will be removed. + hint : str, optional + A hint on how to address this deprecation, + e.g., "Use `skimage.submodule.alternative_func` instead." + + Examples + -------- + >>> @deprecate_func( + ... deprecated_version="1.0.0", + ... removed_version="1.2.0", + ... hint="Use `bar` instead." + ... ) + ... def foo(): + ... pass + + Calling ``foo`` will warn with:: + + FutureWarning: `foo` is deprecated since version 1.0.0 + and will be removed in version 1.2.0. Use `bar` instead. + """ + + def __init__(self, *, deprecated_version, removed_version=None, hint=None): + self.deprecated_version = deprecated_version + self.removed_version = removed_version + self.hint = hint + + def __call__(self, func): + message = ( + f"`{func.__name__}` is deprecated since version " + f"{self.deprecated_version}" + ) + if self.removed_version: + message += f" and will be removed in version {self.removed_version}." + if self.hint: + # Prepend space and make sure it closes with "." + message += f" {self.hint.rstrip('.')}." + + stack_rank = _count_wrappers(func) + + @functools.wraps(func) + def wrapped(*args, **kwargs): + stacklevel = 1 + self.get_stack_length(func) - stack_rank + warnings.warn(message, category=FutureWarning, stacklevel=stacklevel) + return func(*args, **kwargs) + + # modify docstring to display deprecation warning + doc = f'**Deprecated:** {message}' + if wrapped.__doc__ is None: + wrapped.__doc__ = doc + else: + wrapped.__doc__ = doc + '\n\n ' + wrapped.__doc__ + + return wrapped + + +def get_bound_method_class(m): + """Return the class for a bound method.""" + return m.im_class if sys.version < '3' else m.__self__.__class__ + + +def safe_as_int(val, atol=1e-3): + """ + Attempt to safely cast values to integer format. + + Parameters + ---------- + val : scalar or iterable of scalars + Number or container of numbers which are intended to be interpreted as + integers, e.g., for indexing purposes, but which may not carry integer + type. + atol : float + Absolute tolerance away from nearest integer to consider values in + ``val`` functionally integers. + + Returns + ------- + val_int : NumPy scalar or ndarray of dtype `np.int64` + Returns the input value(s) coerced to dtype `np.int64` assuming all + were within ``atol`` of the nearest integer. + + Notes + ----- + This operation calculates ``val`` modulo 1, which returns the mantissa of + all values. Then all mantissas greater than 0.5 are subtracted from one. + Finally, the absolute tolerance from zero is calculated. If it is less + than ``atol`` for all value(s) in ``val``, they are rounded and returned + in an integer array. Or, if ``val`` was a scalar, a NumPy scalar type is + returned. + + If any value(s) are outside the specified tolerance, an informative error + is raised. + + Examples + -------- + >>> safe_as_int(7.0) + 7 + + >>> safe_as_int([9, 4, 2.9999999999]) + array([9, 4, 3]) + + >>> safe_as_int(53.1) + Traceback (most recent call last): + ... + ValueError: Integer argument required but received 53.1, check inputs. + + >>> safe_as_int(53.01, atol=0.01) + 53 + + """ + mod = np.asarray(val) % 1 # Extract mantissa + + # Check for and subtract any mod values > 0.5 from 1 + if mod.ndim == 0: # Scalar input, cannot be indexed + if mod > 0.5: + mod = 1 - mod + else: # Iterable input, now ndarray + mod[mod > 0.5] = 1 - mod[mod > 0.5] # Test on each side of nearest int + + try: + np.testing.assert_allclose(mod, 0, atol=atol) + except AssertionError: + raise ValueError( + f'Integer argument required but received ' f'{val}, check inputs.' + ) + + return np.round(val).astype(np.int64) + + +def check_shape_equality(*images): + """Check that all images have the same shape""" + image0 = images[0] + if not all(image0.shape == image.shape for image in images[1:]): + raise ValueError('Input images must have the same dimensions.') + return + + +def slice_at_axis(sl, axis): + """ + Construct tuple of slices to slice an array in the given dimension. + + Parameters + ---------- + sl : slice + The slice for the given dimension. + axis : int + The axis to which `sl` is applied. All other dimensions are left + "unsliced". + + Returns + ------- + sl : tuple of slices + A tuple with slices matching `shape` in length. + + Examples + -------- + >>> slice_at_axis(slice(None, 3, -1), 1) + (slice(None, None, None), slice(None, 3, -1), Ellipsis) + """ + return (slice(None),) * axis + (sl,) + (...,) + + +def reshape_nd(arr, ndim, dim): + """Reshape a 1D array to have n dimensions, all singletons but one. + + Parameters + ---------- + arr : array, shape (N,) + Input array + ndim : int + Number of desired dimensions of reshaped array. + dim : int + Which dimension/axis will not be singleton-sized. + + Returns + ------- + arr_reshaped : array, shape ([1, ...], N, [1,...]) + View of `arr` reshaped to the desired shape. + + Examples + -------- + >>> rng = np.random.default_rng() + >>> arr = rng.random(7) + >>> reshape_nd(arr, 2, 0).shape + (7, 1) + >>> reshape_nd(arr, 3, 1).shape + (1, 7, 1) + >>> reshape_nd(arr, 4, -1).shape + (1, 1, 1, 7) + """ + if arr.ndim != 1: + raise ValueError("arr must be a 1D array") + new_shape = [1] * ndim + new_shape[dim] = -1 + return np.reshape(arr, new_shape) + + +def check_nD(array, ndim, arg_name='image'): + """ + Verify an array meets the desired ndims and array isn't empty. + + Parameters + ---------- + array : array-like + Input array to be validated + ndim : int or iterable of ints + Allowable ndim or ndims for the array. + arg_name : str, optional + The name of the array in the original function. + + """ + array = np.asanyarray(array) + msg_incorrect_dim = "The parameter `%s` must be a %s-dimensional array" + msg_empty_array = "The parameter `%s` cannot be an empty array" + if isinstance(ndim, int): + ndim = [ndim] + if array.size == 0: + raise ValueError(msg_empty_array % (arg_name)) + if array.ndim not in ndim: + raise ValueError( + msg_incorrect_dim % (arg_name, '-or-'.join([str(n) for n in ndim])) + ) + + +def convert_to_float(image, preserve_range): + """Convert input image to float image with the appropriate range. + + Parameters + ---------- + image : ndarray + Input image. + preserve_range : bool + Determines if the range of the image should be kept or transformed + using img_as_float. Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + + Notes + ----- + * Input images with `float32` data type are not upcast. + + Returns + ------- + image : ndarray + Transformed version of the input. + + """ + if image.dtype == np.float16: + return image.astype(np.float32) + if preserve_range: + # Convert image to double only if it is not single or double + # precision float + if image.dtype.char not in 'df': + image = image.astype(float) + else: + from ..util.dtype import img_as_float + + image = img_as_float(image) + return image + + +def _validate_interpolation_order(image_dtype, order): + """Validate and return spline interpolation's order. + + Parameters + ---------- + image_dtype : dtype + Image dtype. + order : int, optional + The order of the spline interpolation. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + + Returns + ------- + order : int + if input order is None, returns 0 if image_dtype is bool and 1 + otherwise. Otherwise, image_dtype is checked and input order + is validated accordingly (order > 0 is not supported for bool + image dtype) + + """ + + if order is None: + return 0 if image_dtype == bool else 1 + + if order < 0 or order > 5: + raise ValueError("Spline interpolation order has to be in the " "range 0-5.") + + if image_dtype == bool and order != 0: + raise ValueError( + "Input image dtype is bool. Interpolation is not defined " + "with bool data type. Please set order to 0 or explicitly " + "cast input image to another data type." + ) + + return order + + +def _to_np_mode(mode): + """Convert padding modes from `ndi.correlate` to `np.pad`.""" + mode_translation_dict = dict(nearest='edge', reflect='symmetric', mirror='reflect') + if mode in mode_translation_dict: + mode = mode_translation_dict[mode] + return mode + + +def _to_ndimage_mode(mode): + """Convert from `numpy.pad` mode name to the corresponding ndimage mode.""" + mode_translation_dict = dict( + constant='constant', + edge='nearest', + symmetric='reflect', + reflect='mirror', + wrap='wrap', + ) + if mode not in mode_translation_dict: + raise ValueError( + f"Unknown mode: '{mode}', or cannot translate mode. The " + f"mode should be one of 'constant', 'edge', 'symmetric', " + f"'reflect', or 'wrap'. See the documentation of numpy.pad for " + f"more info." + ) + return _fix_ndimage_mode(mode_translation_dict[mode]) + + +def _fix_ndimage_mode(mode): + # SciPy 1.6.0 introduced grid variants of constant and wrap which + # have less surprising behavior for images. Use these when available + grid_modes = {'constant': 'grid-constant', 'wrap': 'grid-wrap'} + return grid_modes.get(mode, mode) + + +new_float_type = { + # preserved types + np.float32().dtype.char: np.float32, + np.float64().dtype.char: np.float64, + np.complex64().dtype.char: np.complex64, + np.complex128().dtype.char: np.complex128, + # altered types + np.float16().dtype.char: np.float32, + 'g': np.float64, # np.float128 ; doesn't exist on windows + 'G': np.complex128, # np.complex256 ; doesn't exist on windows +} + + +def _supported_float_type(input_dtype, allow_complex=False): + """Return an appropriate floating-point dtype for a given dtype. + + float32, float64, complex64, complex128 are preserved. + float16 is promoted to float32. + complex256 is demoted to complex128. + Other types are cast to float64. + + Parameters + ---------- + input_dtype : np.dtype or tuple of np.dtype + The input dtype. If a tuple of multiple dtypes is provided, each + dtype is first converted to a supported floating point type and the + final dtype is then determined by applying `np.result_type` on the + sequence of supported floating point types. + allow_complex : bool, optional + If False, raise a ValueError on complex-valued inputs. + + Returns + ------- + float_type : dtype + Floating-point dtype for the image. + """ + if isinstance(input_dtype, tuple): + return np.result_type(*(_supported_float_type(d) for d in input_dtype)) + input_dtype = np.dtype(input_dtype) + if not allow_complex and input_dtype.kind == 'c': + raise ValueError("complex valued input is not supported") + return new_float_type.get(input_dtype.char, np.float64) + + +def identity(image, *args, **kwargs): + """Returns the first argument unmodified.""" + return image + + +def as_binary_ndarray(array, *, variable_name): + """Return `array` as a numpy.ndarray of dtype bool. + + Raises + ------ + ValueError: + An error including the given `variable_name` if `array` can not be + safely cast to a boolean array. + """ + array = np.asarray(array) + if array.dtype != bool: + if np.any((array != 1) & (array != 0)): + raise ValueError( + f"{variable_name} array is not of dtype boolean or " + f"contains values other than 0 and 1 so cannot be " + f"safely cast to boolean array." + ) + return np.asarray(array, dtype=bool) diff --git a/lib/python3.10/site-packages/skimage/_shared/version_requirements.py b/lib/python3.10/site-packages/skimage/_shared/version_requirements.py new file mode 100644 index 0000000000000000000000000000000000000000..f55ebb3369c68a3bd9cb60910e451d6239812291 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_shared/version_requirements.py @@ -0,0 +1,138 @@ +import sys + +from packaging import version as _version + + +def _check_version(actver, version, cmp_op): + """ + Check version string of an active module against a required version. + + If dev/prerelease tags result in TypeError for string-number comparison, + it is assumed that the dependency is satisfied. + Users on dev branches are responsible for keeping their own packages up to + date. + """ + try: + if cmp_op == '>': + return _version.parse(actver) > _version.parse(version) + elif cmp_op == '>=': + return _version.parse(actver) >= _version.parse(version) + elif cmp_op == '=': + return _version.parse(actver) == _version.parse(version) + elif cmp_op == '<': + return _version.parse(actver) < _version.parse(version) + else: + return False + except TypeError: + return True + + +def get_module_version(module_name): + """Return module version or None if version can't be retrieved.""" + mod = __import__(module_name, fromlist=[module_name.rpartition('.')[-1]]) + return getattr(mod, '__version__', getattr(mod, 'VERSION', None)) + + +def is_installed(name, version=None): + """Test if *name* is installed. + + Parameters + ---------- + name : str + Name of module or "python" + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + out : bool + True if `name` is installed matching the optional version. + """ + if name.lower() == 'python': + actver = sys.version[:6] + else: + try: + actver = get_module_version(name) + except ImportError: + return False + if version is None: + return True + else: + # since version_requirements is in the critical import path, + # we lazy import re + import re + + match = re.search('[0-9]', version) + assert match is not None, "Invalid version number" + symb = version[: match.start()] + if not symb: + symb = '=' + assert symb in ('>=', '>', '=', '<'), f"Invalid version condition '{symb}'" + version = version[match.start() :] + return _check_version(actver, version, symb) + + +def require(name, version=None): + """Return decorator that forces a requirement for a function or class. + + Parameters + ---------- + name : str + Name of module or "python". + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + func : function + A decorator that raises an ImportError if a function is run + in the absence of the input dependency. + """ + # since version_requirements is in the critical import path, we lazy import + # functools + import functools + + def decorator(obj): + @functools.wraps(obj) + def func_wrapped(*args, **kwargs): + if is_installed(name, version): + return obj(*args, **kwargs) + else: + msg = f'"{obj}" in "{obj.__module__}" requires "{name}' + if version is not None: + msg += f" {version}" + raise ImportError(msg + '"') + + return func_wrapped + + return decorator + + +def get_module(module_name, version=None): + """Return a module object of name *module_name* if installed. + + Parameters + ---------- + module_name : str + Name of module. + version : str, optional + Version string to test against. + If version is not None, checking version + (must have an attribute named '__version__' or 'VERSION') + Version may start with =, >=, > or < to specify the exact requirement + + Returns + ------- + mod : module or None + Module if *module_name* is installed matching the optional version + or None otherwise. + """ + if not is_installed(module_name, version): + return None + return __import__(module_name, fromlist=[module_name.rpartition('.')[-1]]) diff --git a/lib/python3.10/site-packages/skimage/_vendored/__init__.py b/lib/python3.10/site-packages/skimage/_vendored/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/_vendored/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_vendored/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74183b507beb87cda4f551b852accc2c4f936ea7 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_vendored/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_vendored/__pycache__/numpy_lookfor.cpython-310.pyc b/lib/python3.10/site-packages/skimage/_vendored/__pycache__/numpy_lookfor.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a84247eba23aec158cc8b5d3363cff2946c94a4 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/_vendored/__pycache__/numpy_lookfor.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/_vendored/numpy_lookfor.py b/lib/python3.10/site-packages/skimage/_vendored/numpy_lookfor.py new file mode 100644 index 0000000000000000000000000000000000000000..6deff3b92977bcae1869bbcc81c9424a6cc50b58 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/_vendored/numpy_lookfor.py @@ -0,0 +1,295 @@ +# Vendored subset of numpy/lib/utils.py in 1.26.3 +# https://github.com/numpy/numpy/blob/b4bf93b936802618ebb49ee43e382b576b29a0a6/numpy/lib/utils.py +# +# Can be removed after deprecation of `skimage.lookfor` is completed. + +import sys +import os +import re + +from numpy import ufunc + + +# Cache for lookfor: {id(module): {name: (docstring, kind, index), ...}...} +# where kind: "func", "class", "module", "object" +# and index: index in breadth-first namespace traversal +_lookfor_caches = {} + + +# regexp whose match indicates that the string may contain a function +# signature +_function_signature_re = re.compile(r"[a-z0-9_]+\(.*[,=].*\)", re.I) + + +def _getmembers(item): + import inspect + + try: + members = inspect.getmembers(item) + except Exception: + members = [(x, getattr(item, x)) for x in dir(item) if hasattr(item, x)] + return members + + +def _lookfor_generate_cache(module, import_modules, regenerate): + """ + Generate docstring cache for given module. + + Parameters + ---------- + module : str, None, module + Module for which to generate docstring cache + import_modules : bool + Whether to import sub-modules in packages. + regenerate : bool + Re-generate the docstring cache + + Returns + ------- + cache : dict {obj_full_name: (docstring, kind, index), ...} + Docstring cache for the module, either cached one (regenerate=False) + or newly generated. + + """ + # Local import to speed up numpy's import time. + import inspect + + from io import StringIO + + if module is None: + module = "numpy" + + if isinstance(module, str): + try: + __import__(module) + except ImportError: + return {} + module = sys.modules[module] + elif isinstance(module, list) or isinstance(module, tuple): + cache = {} + for mod in module: + cache.update(_lookfor_generate_cache(mod, import_modules, regenerate)) + return cache + + if id(module) in _lookfor_caches and not regenerate: + return _lookfor_caches[id(module)] + + # walk items and collect docstrings + cache = {} + _lookfor_caches[id(module)] = cache + seen = {} + index = 0 + stack = [(module.__name__, module)] + while stack: + name, item = stack.pop(0) + if id(item) in seen: + continue + seen[id(item)] = True + + index += 1 + kind = "object" + + if inspect.ismodule(item): + kind = "module" + try: + _all = item.__all__ + except AttributeError: + _all = None + + # import sub-packages + if import_modules and hasattr(item, '__path__'): + for pth in item.__path__: + for mod_path in os.listdir(pth): + this_py = os.path.join(pth, mod_path) + init_py = os.path.join(pth, mod_path, '__init__.py') + if os.path.isfile(this_py) and mod_path.endswith('.py'): + to_import = mod_path[:-3] + elif os.path.isfile(init_py): + to_import = mod_path + else: + continue + if to_import == '__init__': + continue + + try: + old_stdout = sys.stdout + old_stderr = sys.stderr + try: + sys.stdout = StringIO() + sys.stderr = StringIO() + __import__(f"{name}.{to_import}") + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + except KeyboardInterrupt: + # Assume keyboard interrupt came from a user + raise + except BaseException: + # Ignore also SystemExit and pytests.importorskip + # `Skipped` (these are BaseExceptions; gh-22345) + continue + + for n, v in _getmembers(item): + try: + item_name = getattr( + v, + '__name__', + f"{name}.{n}", + ) + mod_name = getattr(v, '__module__', None) + except NameError: + # ref. SWIG's global cvars + # NameError: Unknown C global variable + item_name = f"{name}.{n}" + mod_name = None + if '.' not in item_name and mod_name: + item_name = f"{mod_name}.{item_name}" + + if not item_name.startswith(name + '.'): + # don't crawl "foreign" objects + if isinstance(v, ufunc): + # ... unless they are ufuncs + pass + else: + continue + elif not (inspect.ismodule(v) or _all is None or n in _all): + continue + stack.append((f"{name}.{n}", v)) + elif inspect.isclass(item): + kind = "class" + for n, v in _getmembers(item): + stack.append((f"{name}.{n}", v)) + elif hasattr(item, "__call__"): + kind = "func" + + try: + doc = inspect.getdoc(item) + except NameError: + # ref SWIG's NameError: Unknown C global variable + doc = None + if doc is not None: + cache[name] = (doc, kind, index) + + return cache + + +def lookfor(what, module=None, import_modules=True, regenerate=False, output=None): + """ + Do a keyword search on docstrings. + + A list of objects that matched the search is displayed, + sorted by relevance. All given keywords need to be found in the + docstring for it to be returned as a result, but the order does + not matter. + + Parameters + ---------- + what : str + String containing words to look for. + module : str or list, optional + Name of module(s) whose docstrings to go through. + import_modules : bool, optional + Whether to import sub-modules in packages. Default is True. + regenerate : bool, optional + Whether to re-generate the docstring cache. Default is False. + output : file-like, optional + File-like object to write the output to. If omitted, use a pager. + + See Also + -------- + source, info + + Notes + ----- + Relevance is determined only roughly, by checking if the keywords occur + in the function name, at the start of a docstring, etc. + + Examples + -------- + >>> np.lookfor('binary representation') # doctest: +SKIP + Search results for 'binary representation' + ------------------------------------------ + numpy.binary_repr + Return the binary representation of the input number as a string. + numpy.core.setup_common.long_double_representation + Given a binary dump as given by GNU od -b, look for long double + numpy.base_repr + Return a string representation of a number in the given base system. + ... + + """ + import pydoc + + # Cache + cache = _lookfor_generate_cache(module, import_modules, regenerate) + + # Search + # XXX: maybe using a real stemming search engine would be better? + found = [] + whats = str(what).lower().split() + if not whats: + return + + for name, (docstring, kind, index) in cache.items(): + if kind in ('module', 'object'): + # don't show modules or objects + continue + doc = docstring.lower() + if all(w in doc for w in whats): + found.append(name) + + # Relevance sort + # XXX: this is full Harrison-Stetson heuristics now, + # XXX: it probably could be improved + + kind_relevance = {'func': 1000, 'class': 1000, 'module': -1000, 'object': -1000} + + def relevance(name, docstr, kind, index): + r = 0 + # do the keywords occur within the start of the docstring? + first_doc = "\n".join(docstr.lower().strip().split("\n")[:3]) + r += sum([200 for w in whats if w in first_doc]) + # do the keywords occur in the function name? + r += sum([30 for w in whats if w in name]) + # is the full name long? + r += -len(name) * 5 + # is the object of bad type? + r += kind_relevance.get(kind, -1000) + # is the object deep in namespace hierarchy? + r += -name.count('.') * 10 + r += max(-index / 100, -100) + return r + + def relevance_value(a): + return relevance(a, *cache[a]) + + found.sort(key=relevance_value) + + # Pretty-print + s = f"Search results for '{' '.join(whats)}'" + help_text = [s, "-" * len(s)] + for name in found[::-1]: + doc, kind, ix = cache[name] + + doclines = [line.strip() for line in doc.strip().split("\n") if line.strip()] + + # find a suitable short description + try: + first_doc = doclines[0].strip() + if _function_signature_re.search(first_doc): + first_doc = doclines[1].strip() + except IndexError: + first_doc = "" + help_text.append(f"{name}\n {first_doc}") + + if not found: + help_text.append("Nothing found.") + + # Output + if output is not None: + output.write("\n".join(help_text)) + elif len(help_text) > 10: + pager = pydoc.getpager() + pager("\n".join(help_text)) + else: + print("\n".join(help_text)) diff --git a/lib/python3.10/site-packages/skimage/color/__init__.py b/lib/python3.10/site-packages/skimage/color/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..00bf7a4652f420ff7f59fafa7cbe4a65b2c1cbd4 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/__init__.py @@ -0,0 +1,5 @@ +"""Color space conversion.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/color/__init__.pyi b/lib/python3.10/site-packages/skimage/color/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..9f330f3dc49975fea588a9c4f7883c837d3d655c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/__init__.pyi @@ -0,0 +1,135 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'convert_colorspace', + 'xyz_tristimulus_values', + 'rgba2rgb', + 'rgb2hsv', + 'hsv2rgb', + 'rgb2xyz', + 'xyz2rgb', + 'rgb2rgbcie', + 'rgbcie2rgb', + 'rgb2gray', + 'gray2rgb', + 'gray2rgba', + 'xyz2lab', + 'lab2xyz', + 'lab2rgb', + 'rgb2lab', + 'rgb2hed', + 'hed2rgb', + 'lab2lch', + 'lch2lab', + 'rgb2yuv', + 'yuv2rgb', + 'rgb2yiq', + 'yiq2rgb', + 'rgb2ypbpr', + 'ypbpr2rgb', + 'rgb2ycbcr', + 'ycbcr2rgb', + 'rgb2ydbdr', + 'ydbdr2rgb', + 'separate_stains', + 'combine_stains', + 'rgb_from_hed', + 'hed_from_rgb', + 'rgb_from_hdx', + 'hdx_from_rgb', + 'rgb_from_fgx', + 'fgx_from_rgb', + 'rgb_from_bex', + 'bex_from_rgb', + 'rgb_from_rbd', + 'rbd_from_rgb', + 'rgb_from_gdx', + 'gdx_from_rgb', + 'rgb_from_hax', + 'hax_from_rgb', + 'rgb_from_bro', + 'bro_from_rgb', + 'rgb_from_bpx', + 'bpx_from_rgb', + 'rgb_from_ahx', + 'ahx_from_rgb', + 'rgb_from_hpx', + 'hpx_from_rgb', + 'color_dict', + 'label2rgb', + 'deltaE_cie76', + 'deltaE_ciede94', + 'deltaE_ciede2000', + 'deltaE_cmc', +] + +from .colorconv import ( + convert_colorspace, + xyz_tristimulus_values, + rgba2rgb, + rgb2hsv, + hsv2rgb, + rgb2xyz, + xyz2rgb, + rgb2rgbcie, + rgbcie2rgb, + rgb2gray, + gray2rgb, + gray2rgba, + xyz2lab, + lab2xyz, + lab2rgb, + rgb2lab, + xyz2luv, + luv2xyz, + luv2rgb, + rgb2luv, + rgb2hed, + hed2rgb, + lab2lch, + lch2lab, + rgb2yuv, + yuv2rgb, + rgb2yiq, + yiq2rgb, + rgb2ypbpr, + ypbpr2rgb, + rgb2ycbcr, + ycbcr2rgb, + rgb2ydbdr, + ydbdr2rgb, + separate_stains, + combine_stains, + rgb_from_hed, + hed_from_rgb, + rgb_from_hdx, + hdx_from_rgb, + rgb_from_fgx, + fgx_from_rgb, + rgb_from_bex, + bex_from_rgb, + rgb_from_rbd, + rbd_from_rgb, + rgb_from_gdx, + gdx_from_rgb, + rgb_from_hax, + hax_from_rgb, + rgb_from_bro, + bro_from_rgb, + rgb_from_bpx, + bpx_from_rgb, + rgb_from_ahx, + ahx_from_rgb, + rgb_from_hpx, + hpx_from_rgb, +) + +from .colorlabel import color_dict, label2rgb +from .delta_e import ( + deltaE_cie76, + deltaE_ciede94, + deltaE_ciede2000, + deltaE_cmc, +) diff --git a/lib/python3.10/site-packages/skimage/color/adapt_rgb.py b/lib/python3.10/site-packages/skimage/color/adapt_rgb.py new file mode 100644 index 0000000000000000000000000000000000000000..5d7a553b3f605c91182547f13dbfd9cbdcaa5cb2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/adapt_rgb.py @@ -0,0 +1,81 @@ +import functools + +import numpy as np + +from .. import color +from ..util.dtype import _convert + + +__all__ = ['adapt_rgb', 'hsv_value', 'each_channel'] + + +def is_rgb_like(image, channel_axis=-1): + """Return True if the image *looks* like it's RGB. + + This function should not be public because it is only intended to be used + for functions that don't accept volumes as input, since checking an image's + shape is fragile. + """ + return (image.ndim == 3) and (image.shape[channel_axis] in (3, 4)) + + +def adapt_rgb(apply_to_rgb): + """Return decorator that adapts to RGB images to a gray-scale filter. + + This function is only intended to be used for functions that don't accept + volumes as input, since checking an image's shape is fragile. + + Parameters + ---------- + apply_to_rgb : function + Function that returns a filtered image from an image-filter and RGB + image. This will only be called if the image is RGB-like. + """ + + def decorator(image_filter): + @functools.wraps(image_filter) + def image_filter_adapted(image, *args, **kwargs): + if is_rgb_like(image): + return apply_to_rgb(image_filter, image, *args, **kwargs) + else: + return image_filter(image, *args, **kwargs) + + return image_filter_adapted + + return decorator + + +def hsv_value(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on HSV-value of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. Note that RGBA images are treated as RGB. + """ + # Slice the first three channels so that we remove any alpha channels. + hsv = color.rgb2hsv(image[:, :, :3]) + value = hsv[:, :, 2].copy() + value = image_filter(value, *args, **kwargs) + hsv[:, :, 2] = _convert(value, hsv.dtype) + return color.hsv2rgb(hsv) + + +def each_channel(image_filter, image, *args, **kwargs): + """Return color image by applying `image_filter` on channels of `image`. + + Note that this function is intended for use with `adapt_rgb`. + + Parameters + ---------- + image_filter : function + Function that filters a gray-scale image. + image : array + Input image. + """ + c_new = [image_filter(c, *args, **kwargs) for c in np.moveaxis(image, -1, 0)] + return np.stack(c_new, axis=-1) diff --git a/lib/python3.10/site-packages/skimage/color/colorconv.py b/lib/python3.10/site-packages/skimage/color/colorconv.py new file mode 100644 index 0000000000000000000000000000000000000000..a28b305bdbc4a26315fff8230e9067949b161f6d --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/colorconv.py @@ -0,0 +1,2314 @@ +"""Functions for converting between color spaces. + +The "central" color space in this module is RGB, more specifically the linear +sRGB color space using D65 as a white-point [1]_. This represents a +standard monitor (w/o gamma correction). For a good FAQ on color spaces see +[2]_. + +The API consists of functions to convert to and from RGB as defined above, as +well as a generic function to convert to and from any supported color space +(which is done through RGB in most cases). + + +Supported color spaces +---------------------- +* RGB : Red Green Blue. + Here the sRGB standard [1]_. +* HSV : Hue, Saturation, Value. + Uniquely defined when related to sRGB [3]_. +* RGB CIE : Red Green Blue. + The original RGB CIE standard from 1931 [4]_. Primary colors are 700 nm + (red), 546.1 nm (blue) and 435.8 nm (green). +* XYZ CIE : XYZ + Derived from the RGB CIE color space. Chosen such that + ``x == y == z == 1/3`` at the whitepoint, and all color matching + functions are greater than zero everywhere. +* LAB CIE : Lightness, a, b + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform +* LUV CIE : Lightness, u, v + Colorspace derived from XYZ CIE that is intended to be more + perceptually uniform +* LCH CIE : Lightness, Chroma, Hue + Defined in terms of LAB CIE. C and H are the polar representation of + a and b. The polar angle C is defined to be on ``(0, 2*pi)`` + +:author: Nicolas Pinto (rgb2hsv) +:author: Ralf Gommers (hsv2rgb) +:author: Travis Oliphant (XYZ and RGB CIE functions) +:author: Matt Terry (lab2lch) +:author: Alex Izvorski (yuv2rgb, rgb2yuv and related) + +:license: modified BSD + +References +---------- +.. [1] Official specification of sRGB, IEC 61966-2-1:1999. +.. [2] http://www.poynton.com/ColorFAQ.html +.. [3] https://en.wikipedia.org/wiki/HSL_and_HSV +.. [4] https://en.wikipedia.org/wiki/CIE_1931_color_space +""" + +from warnings import warn + +import numpy as np +from scipy import linalg + + +from .._shared.utils import ( + _supported_float_type, + channel_as_last_axis, + identity, + reshape_nd, + slice_at_axis, +) +from ..util import dtype, dtype_limits + +# TODO: when minimum numpy dependency is 1.25 use: +# np..exceptions.AxisError instead of AxisError +# and remove this try-except +try: + from numpy import AxisError +except ImportError: + from numpy.exceptions import AxisError + + +def convert_colorspace(arr, fromspace, tospace, *, channel_axis=-1): + """Convert an image array to a new color space. + + Valid color spaces are: + 'RGB', 'HSV', 'RGB CIE', 'XYZ', 'YUV', 'YIQ', 'YPbPr', 'YCbCr', 'YDbDr' + + Parameters + ---------- + arr : (..., C=3, ...) array_like + The image to convert. By default, the final dimension denotes + channels. + fromspace : str + The color space to convert from. Can be specified in lower case. + tospace : str + The color space to convert to. Can be specified in lower case. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The converted image. Same dimensions as input. + + Raises + ------ + ValueError + If fromspace is not a valid color space + ValueError + If tospace is not a valid color space + + Notes + ----- + Conversion is performed through the "central" RGB color space, + i.e. conversion from XYZ to HSV is implemented as ``XYZ -> RGB -> HSV`` + instead of directly. + + Examples + -------- + >>> from skimage import data + >>> img = data.astronaut() + >>> img_hsv = convert_colorspace(img, 'RGB', 'HSV') + """ + fromdict = { + 'rgb': identity, + 'hsv': hsv2rgb, + 'rgb cie': rgbcie2rgb, + 'xyz': xyz2rgb, + 'yuv': yuv2rgb, + 'yiq': yiq2rgb, + 'ypbpr': ypbpr2rgb, + 'ycbcr': ycbcr2rgb, + 'ydbdr': ydbdr2rgb, + } + todict = { + 'rgb': identity, + 'hsv': rgb2hsv, + 'rgb cie': rgb2rgbcie, + 'xyz': rgb2xyz, + 'yuv': rgb2yuv, + 'yiq': rgb2yiq, + 'ypbpr': rgb2ypbpr, + 'ycbcr': rgb2ycbcr, + 'ydbdr': rgb2ydbdr, + } + + fromspace = fromspace.lower() + tospace = tospace.lower() + if fromspace not in fromdict: + msg = f'`fromspace` has to be one of {fromdict.keys()}' + raise ValueError(msg) + if tospace not in todict: + msg = f'`tospace` has to be one of {todict.keys()}' + raise ValueError(msg) + + return todict[tospace]( + fromdict[fromspace](arr, channel_axis=channel_axis), channel_axis=channel_axis + ) + + +def _prepare_colorarray(arr, force_copy=False, *, channel_axis=-1): + """Check the shape of the array and convert it to + floating point representation. + """ + arr = np.asanyarray(arr) + + if arr.shape[channel_axis] != 3: + msg = ( + f'the input array must have size 3 along `channel_axis`, ' + f'got {arr.shape}' + ) + raise ValueError(msg) + + float_dtype = _supported_float_type(arr.dtype) + if float_dtype == np.float32: + _func = dtype.img_as_float32 + else: + _func = dtype.img_as_float64 + return _func(arr, force_copy=force_copy) + + +def _validate_channel_axis(channel_axis, ndim): + if not isinstance(channel_axis, int): + raise TypeError("channel_axis must be an integer") + if channel_axis < -ndim or channel_axis >= ndim: + raise AxisError("channel_axis exceeds array dimensions") + + +def rgba2rgb(rgba, background=(1, 1, 1), *, channel_axis=-1): + """RGBA to RGB conversion using alpha blending [1]_. + + Parameters + ---------- + rgba : (..., C=4, ...) array_like + The image in RGBA format. By default, the final dimension denotes + channels. + background : array_like + The color of the background to blend the image with (3 floats + between 0 to 1 - the RGB value of the background). + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgba` is not at least 2D with shape (..., 4, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending + + Examples + -------- + >>> from skimage import color + >>> from skimage import data + >>> img_rgba = data.logo() + >>> img_rgb = color.rgba2rgb(img_rgba) + """ + arr = np.asanyarray(rgba) + _validate_channel_axis(channel_axis, arr.ndim) + channel_axis = channel_axis % arr.ndim + + if arr.shape[channel_axis] != 4: + msg = ( + f'the input array must have size 4 along `channel_axis`, ' + f'got {arr.shape}' + ) + raise ValueError(msg) + + float_dtype = _supported_float_type(arr.dtype) + if float_dtype == np.float32: + arr = dtype.img_as_float32(arr) + else: + arr = dtype.img_as_float64(arr) + + background = np.ravel(background).astype(arr.dtype) + if len(background) != 3: + raise ValueError( + 'background must be an array-like containing 3 RGB ' + f'values. Got {len(background)} items' + ) + if np.any(background < 0) or np.any(background > 1): + raise ValueError('background RGB values must be floats between ' '0 and 1.') + # reshape background for broadcasting along non-channel axes + background = reshape_nd(background, arr.ndim, channel_axis) + + alpha = arr[slice_at_axis(slice(3, 4), axis=channel_axis)] + channels = arr[slice_at_axis(slice(3), axis=channel_axis)] + out = np.clip((1 - alpha) * background + alpha * channels, a_min=0, a_max=1) + return out + + +@channel_as_last_axis() +def rgb2hsv(rgb, *, channel_axis=-1): + """RGB to HSV color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in HSV format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Conversion between RGB and HSV color spaces results in some loss of + precision, due to integer arithmetic and rounding [1]_. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/HSL_and_HSV + + Examples + -------- + >>> from skimage import color + >>> from skimage import data + >>> img = data.astronaut() + >>> img_hsv = color.rgb2hsv(img) + """ + input_is_one_pixel = rgb.ndim == 1 + if input_is_one_pixel: + rgb = rgb[np.newaxis, ...] + + arr = _prepare_colorarray(rgb, channel_axis=-1) + out = np.empty_like(arr) + + # -- V channel + out_v = arr.max(-1) + + # -- S channel + delta = np.ptp(arr, axis=-1) + # Ignore warning for zero divided by zero + old_settings = np.seterr(invalid='ignore') + out_s = delta / out_v + out_s[delta == 0.0] = 0.0 + + # -- H channel + # red is max + idx = arr[..., 0] == out_v + out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx] + + # green is max + idx = arr[..., 1] == out_v + out[idx, 0] = 2.0 + (arr[idx, 2] - arr[idx, 0]) / delta[idx] + + # blue is max + idx = arr[..., 2] == out_v + out[idx, 0] = 4.0 + (arr[idx, 0] - arr[idx, 1]) / delta[idx] + out_h = (out[..., 0] / 6.0) % 1.0 + out_h[delta == 0.0] = 0.0 + + np.seterr(**old_settings) + + # -- output + out[..., 0] = out_h + out[..., 1] = out_s + out[..., 2] = out_v + + # # remove NaN + out[np.isnan(out)] = 0 + + if input_is_one_pixel: + out = np.squeeze(out, axis=0) + + return out + + +@channel_as_last_axis() +def hsv2rgb(hsv, *, channel_axis=-1): + """HSV to RGB color space conversion. + + Parameters + ---------- + hsv : (..., C=3, ...) array_like + The image in HSV format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `hsv` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Conversion between RGB and HSV color spaces results in some loss of + precision, due to integer arithmetic and rounding [1]_. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/HSL_and_HSV + + Examples + -------- + >>> from skimage import data + >>> img = data.astronaut() + >>> img_hsv = rgb2hsv(img) + >>> img_rgb = hsv2rgb(img_hsv) + """ + arr = _prepare_colorarray(hsv, channel_axis=-1) + + hi = np.floor(arr[..., 0] * 6) + f = arr[..., 0] * 6 - hi + p = arr[..., 2] * (1 - arr[..., 1]) + q = arr[..., 2] * (1 - f * arr[..., 1]) + t = arr[..., 2] * (1 - (1 - f) * arr[..., 1]) + v = arr[..., 2] + + hi = np.stack([hi, hi, hi], axis=-1).astype(np.uint8) % 6 + out = np.choose( + hi, + np.stack( + [ + np.stack((v, t, p), axis=-1), + np.stack((q, v, p), axis=-1), + np.stack((p, v, t), axis=-1), + np.stack((p, q, v), axis=-1), + np.stack((t, p, v), axis=-1), + np.stack((v, p, q), axis=-1), + ] + ), + ) + + return out + + +# --------------------------------------------------------------- +# Primaries for the coordinate systems +# --------------------------------------------------------------- +cie_primaries = np.array([700, 546.1, 435.8]) +sb_primaries = np.array([1.0 / 155, 1.0 / 190, 1.0 / 225]) * 1e5 + +# --------------------------------------------------------------- +# Matrices that define conversion between different color spaces +# --------------------------------------------------------------- + +# From sRGB specification +xyz_from_rgb = np.array( + [ + [0.412453, 0.357580, 0.180423], + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227], + ] +) + +rgb_from_xyz = linalg.inv(xyz_from_rgb) + +# From https://en.wikipedia.org/wiki/CIE_1931_color_space +# Note: Travis's code did not have the divide by 0.17697 +xyz_from_rgbcie = ( + np.array([[0.49, 0.31, 0.20], [0.17697, 0.81240, 0.01063], [0.00, 0.01, 0.99]]) + / 0.17697 +) + +rgbcie_from_xyz = linalg.inv(xyz_from_rgbcie) + +# construct matrices to and from rgb: +rgbcie_from_rgb = rgbcie_from_xyz @ xyz_from_rgb +rgb_from_rgbcie = rgb_from_xyz @ xyz_from_rgbcie + + +gray_from_rgb = np.array([[0.2125, 0.7154, 0.0721], [0, 0, 0], [0, 0, 0]]) + +yuv_from_rgb = np.array( + [ + [0.299, 0.587, 0.114], + [-0.14714119, -0.28886916, 0.43601035], + [0.61497538, -0.51496512, -0.10001026], + ] +) + +rgb_from_yuv = linalg.inv(yuv_from_rgb) + +yiq_from_rgb = np.array( + [ + [0.299, 0.587, 0.114], + [0.59590059, -0.27455667, -0.32134392], + [0.21153661, -0.52273617, 0.31119955], + ] +) + +rgb_from_yiq = linalg.inv(yiq_from_rgb) + +ypbpr_from_rgb = np.array( + [[0.299, 0.587, 0.114], [-0.168736, -0.331264, 0.5], [0.5, -0.418688, -0.081312]] +) + +rgb_from_ypbpr = linalg.inv(ypbpr_from_rgb) + +ycbcr_from_rgb = np.array( + [[65.481, 128.553, 24.966], [-37.797, -74.203, 112.0], [112.0, -93.786, -18.214]] +) + +rgb_from_ycbcr = linalg.inv(ycbcr_from_rgb) + +ydbdr_from_rgb = np.array( + [[0.299, 0.587, 0.114], [-0.45, -0.883, 1.333], [-1.333, 1.116, 0.217]] +) + +rgb_from_ydbdr = linalg.inv(ydbdr_from_rgb) + + +# CIE LAB constants for Observer=2A, Illuminant=D65 +# NOTE: this is actually the XYZ values for the illuminant above. +lab_ref_white = np.array([0.95047, 1.0, 1.08883]) + +# CIE XYZ tristimulus values of the illuminants, scaled to [0, 1]. For each illuminant I +# we have: +# +# illuminant[I]['2'] corresponds to the CIE XYZ tristimulus values for the 2 degree +# field of view. +# +# illuminant[I]['10'] corresponds to the CIE XYZ tristimulus values for the 10 degree +# field of view. +# +# illuminant[I]['R'] corresponds to the CIE XYZ tristimulus values for R illuminants +# in grDevices::convertColor +# +# The CIE XYZ tristimulus values are calculated from [1], using the formula: +# +# X = x * ( Y / y ) +# Y = Y +# Z = ( 1 - x - y ) * ( Y / y ) +# +# where Y = 1. The only exception is the illuminant "D65" with aperture angle +# 2, whose coordinates are copied from 'lab_ref_white' for +# backward-compatibility reasons. +# +# References +# ---------- +# .. [1] https://en.wikipedia.org/wiki/Standard_illuminant + +_illuminants = { + "A": { + '2': (1.098466069456375, 1, 0.3558228003436005), + '10': (1.111420406956693, 1, 0.3519978321919493), + 'R': (1.098466069456375, 1, 0.3558228003436005), + }, + "B": { + '2': (0.9909274480248003, 1, 0.8531327322886154), + '10': (0.9917777147717607, 1, 0.8434930535866175), + 'R': (0.9909274480248003, 1, 0.8531327322886154), + }, + "C": { + '2': (0.980705971659919, 1, 1.1822494939271255), + '10': (0.9728569189782166, 1, 1.1614480488951577), + 'R': (0.980705971659919, 1, 1.1822494939271255), + }, + "D50": { + '2': (0.9642119944211994, 1, 0.8251882845188288), + '10': (0.9672062750333777, 1, 0.8142801513128616), + 'R': (0.9639501491621826, 1, 0.8241280285499208), + }, + "D55": { + '2': (0.956797052643698, 1, 0.9214805860173273), + '10': (0.9579665682254781, 1, 0.9092525159847462), + 'R': (0.9565317453467969, 1, 0.9202554587037198), + }, + "D65": { + '2': (0.95047, 1.0, 1.08883), # This was: `lab_ref_white` + '10': (0.94809667673716, 1, 1.0730513595166162), + 'R': (0.9532057125493769, 1, 1.0853843816469158), + }, + "D75": { + '2': (0.9497220898840717, 1, 1.226393520724154), + '10': (0.9441713925645873, 1, 1.2064272211720228), + 'R': (0.9497220898840717, 1, 1.226393520724154), + }, + "E": {'2': (1.0, 1.0, 1.0), '10': (1.0, 1.0, 1.0), 'R': (1.0, 1.0, 1.0)}, +} + + +def xyz_tristimulus_values(*, illuminant, observer, dtype=float): + """Get the CIE XYZ tristimulus values. + + Given an illuminant and observer, this function returns the CIE XYZ tristimulus + values [2]_ scaled such that :math:`Y = 1`. + + Parameters + ---------- + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"} + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"} + One of: 2-degree observer, 10-degree observer, or 'R' observer as in + R function ``grDevices::convertColor`` [3]_. + dtype: dtype, optional + Output data type. + + Returns + ------- + values : array + Array with 3 elements :math:`X, Y, Z` containing the CIE XYZ tristimulus values + of the given illuminant. + + Raises + ------ + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Standard_illuminant#White_points_of_standard_illuminants + .. [2] https://en.wikipedia.org/wiki/CIE_1931_color_space#Meaning_of_X,_Y_and_Z + .. [3] https://www.rdocumentation.org/packages/grDevices/versions/3.6.2/topics/convertColor + + Notes + ----- + The CIE XYZ tristimulus values are calculated from :math:`x, y` [1]_, using the + formula + + .. math:: X = x / y + + .. math:: Y = 1 + + .. math:: Z = (1 - x - y) / y + + The only exception is the illuminant "D65" with aperture angle 2° for + backward-compatibility reasons. + + Examples + -------- + Get the CIE XYZ tristimulus values for a "D65" illuminant for a 10 degree field of + view + + >>> xyz_tristimulus_values(illuminant="D65", observer="10") + array([0.94809668, 1. , 1.07305136]) + """ + illuminant = illuminant.upper() + observer = observer.upper() + try: + return np.asarray(_illuminants[illuminant][observer], dtype=dtype) + except KeyError: + raise ValueError( + f'Unknown illuminant/observer combination ' + f'(`{illuminant}`, `{observer}`)' + ) + + +# Haematoxylin-Eosin-DAB colorspace +# From original Ruifrok's paper: A. C. Ruifrok and D. A. Johnston, +# "Quantification of histochemical staining by color deconvolution," +# Analytical and quantitative cytology and histology / the International +# Academy of Cytology [and] American Society of Cytology, vol. 23, no. 4, +# pp. 291-9, Aug. 2001. +rgb_from_hed = np.array([[0.65, 0.70, 0.29], [0.07, 0.99, 0.11], [0.27, 0.57, 0.78]]) +hed_from_rgb = linalg.inv(rgb_from_hed) + +# Following matrices are adapted form the Java code written by G.Landini. +# The original code is available at: +# https://web.archive.org/web/20160624145052/http://www.mecourse.com/landinig/software/cdeconv/cdeconv.html + +# Hematoxylin + DAB +rgb_from_hdx = np.array([[0.650, 0.704, 0.286], [0.268, 0.570, 0.776], [0.0, 0.0, 0.0]]) +rgb_from_hdx[2, :] = np.cross(rgb_from_hdx[0, :], rgb_from_hdx[1, :]) +hdx_from_rgb = linalg.inv(rgb_from_hdx) + +# Feulgen + Light Green +rgb_from_fgx = np.array( + [ + [0.46420921, 0.83008335, 0.30827187], + [0.94705542, 0.25373821, 0.19650764], + [0.0, 0.0, 0.0], + ] +) +rgb_from_fgx[2, :] = np.cross(rgb_from_fgx[0, :], rgb_from_fgx[1, :]) +fgx_from_rgb = linalg.inv(rgb_from_fgx) + +# Giemsa: Methyl Blue + Eosin +rgb_from_bex = np.array( + [ + [0.834750233, 0.513556283, 0.196330403], + [0.092789, 0.954111, 0.283111], + [0.0, 0.0, 0.0], + ] +) +rgb_from_bex[2, :] = np.cross(rgb_from_bex[0, :], rgb_from_bex[1, :]) +bex_from_rgb = linalg.inv(rgb_from_bex) + +# FastRed + FastBlue + DAB +rgb_from_rbd = np.array( + [ + [0.21393921, 0.85112669, 0.47794022], + [0.74890292, 0.60624161, 0.26731082], + [0.268, 0.570, 0.776], + ] +) +rbd_from_rgb = linalg.inv(rgb_from_rbd) + +# Methyl Green + DAB +rgb_from_gdx = np.array( + [[0.98003, 0.144316, 0.133146], [0.268, 0.570, 0.776], [0.0, 0.0, 0.0]] +) +rgb_from_gdx[2, :] = np.cross(rgb_from_gdx[0, :], rgb_from_gdx[1, :]) +gdx_from_rgb = linalg.inv(rgb_from_gdx) + +# Hematoxylin + AEC +rgb_from_hax = np.array( + [[0.650, 0.704, 0.286], [0.2743, 0.6796, 0.6803], [0.0, 0.0, 0.0]] +) +rgb_from_hax[2, :] = np.cross(rgb_from_hax[0, :], rgb_from_hax[1, :]) +hax_from_rgb = linalg.inv(rgb_from_hax) + +# Blue matrix Anilline Blue + Red matrix Azocarmine + Orange matrix Orange-G +rgb_from_bro = np.array( + [ + [0.853033, 0.508733, 0.112656], + [0.09289875, 0.8662008, 0.49098468], + [0.10732849, 0.36765403, 0.9237484], + ] +) +bro_from_rgb = linalg.inv(rgb_from_bro) + +# Methyl Blue + Ponceau Fuchsin +rgb_from_bpx = np.array( + [ + [0.7995107, 0.5913521, 0.10528667], + [0.09997159, 0.73738605, 0.6680326], + [0.0, 0.0, 0.0], + ] +) +rgb_from_bpx[2, :] = np.cross(rgb_from_bpx[0, :], rgb_from_bpx[1, :]) +bpx_from_rgb = linalg.inv(rgb_from_bpx) + +# Alcian Blue + Hematoxylin +rgb_from_ahx = np.array( + [[0.874622, 0.457711, 0.158256], [0.552556, 0.7544, 0.353744], [0.0, 0.0, 0.0]] +) +rgb_from_ahx[2, :] = np.cross(rgb_from_ahx[0, :], rgb_from_ahx[1, :]) +ahx_from_rgb = linalg.inv(rgb_from_ahx) + +# Hematoxylin + PAS +rgb_from_hpx = np.array( + [[0.644211, 0.716556, 0.266844], [0.175411, 0.972178, 0.154589], [0.0, 0.0, 0.0]] +) +rgb_from_hpx[2, :] = np.cross(rgb_from_hpx[0, :], rgb_from_hpx[1, :]) +hpx_from_rgb = linalg.inv(rgb_from_hpx) + +# ------------------------------------------------------------- +# The conversion functions that make use of the matrices above +# ------------------------------------------------------------- + + +def _convert(matrix, arr): + """Do the color space conversion. + + Parameters + ---------- + matrix : array_like + The 3x3 matrix to use. + arr : (..., C=3, ...) array_like + The input array. By default, the final dimension denotes + channels. + + Returns + ------- + out : (..., C=3, ...) ndarray + The converted array. Same dimensions as input. + """ + arr = _prepare_colorarray(arr) + + return arr @ matrix.T.astype(arr.dtype) + + +@channel_as_last_axis() +def xyz2rgb(xyz, *, channel_axis=-1): + """XYZ to RGB color space conversion. + + Parameters + ---------- + xyz : (..., C=3, ...) array_like + The image in XYZ format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `xyz` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + The CIE XYZ color space is derived from the CIE RGB color space. Note + however that this function converts to sRGB. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2rgb + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_rgb = xyz2rgb(img_xyz) + """ + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _convert(rgb_from_xyz, xyz) + mask = arr > 0.0031308 + arr[mask] = 1.055 * np.power(arr[mask], 1 / 2.4) - 0.055 + arr[~mask] *= 12.92 + np.clip(arr, 0, 1, out=arr) + return arr + + +@channel_as_last_axis() +def rgb2xyz(rgb, *, channel_axis=-1): + """RGB to XYZ color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in XYZ format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + The CIE XYZ color space is derived from the CIE RGB color space. Note + however that this function converts from sRGB. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> from skimage import data + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + """ + # Follow the algorithm from http://www.easyrgb.com/index.php + # except we don't multiply/divide by 100 in the conversion + arr = _prepare_colorarray(rgb, channel_axis=-1).copy() + mask = arr > 0.04045 + arr[mask] = np.power((arr[mask] + 0.055) / 1.055, 2.4) + arr[~mask] /= 12.92 + return arr @ xyz_from_rgb.T.astype(arr.dtype) + + +@channel_as_last_axis() +def rgb2rgbcie(rgb, *, channel_axis=-1): + """RGB to RGB CIE color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB CIE format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2rgbcie + >>> img = data.astronaut() + >>> img_rgbcie = rgb2rgbcie(img) + """ + return _convert(rgbcie_from_rgb, rgb) + + +@channel_as_last_axis() +def rgbcie2rgb(rgbcie, *, channel_axis=-1): + """RGB CIE to RGB color space conversion. + + Parameters + ---------- + rgbcie : (..., C=3, ...) array_like + The image in RGB CIE format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgbcie` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/CIE_1931_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2rgbcie, rgbcie2rgb + >>> img = data.astronaut() + >>> img_rgbcie = rgb2rgbcie(img) + >>> img_rgb = rgbcie2rgb(img_rgbcie) + """ + return _convert(rgb_from_rgbcie, rgbcie) + + +@channel_as_last_axis(multichannel_output=False) +def rgb2gray(rgb, *, channel_axis=-1): + """Compute luminance of an RGB image. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + + Returns + ------- + out : ndarray + The luminance image - an array which is the same size as the input + array, but with the channel dimension removed. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + The weights used in this conversion are calibrated for contemporary + CRT phosphors:: + + Y = 0.2125 R + 0.7154 G + 0.0721 B + + If there is an alpha channel present, it is ignored. + + References + ---------- + .. [1] http://poynton.ca/PDFs/ColorFAQ.pdf + + Examples + -------- + >>> from skimage.color import rgb2gray + >>> from skimage import data + >>> img = data.astronaut() + >>> img_gray = rgb2gray(img) + """ + rgb = _prepare_colorarray(rgb) + coeffs = np.array([0.2125, 0.7154, 0.0721], dtype=rgb.dtype) + return rgb @ coeffs + + +def gray2rgba(image, alpha=None, *, channel_axis=-1): + """Create a RGBA representation of a gray-level image. + + Parameters + ---------- + image : array_like + Input image. + alpha : array_like, optional + Alpha channel of the output image. It may be a scalar or an + array that can be broadcast to ``image``. If not specified it is + set to the maximum limit corresponding to the ``image`` dtype. + channel_axis : int, optional + This parameter indicates which axis of the output array will correspond + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + rgba : ndarray + RGBA image. A new dimension of length 4 is added to input + image shape. + """ + arr = np.asarray(image) + if alpha is None: + _, alpha = dtype_limits(arr, clip_negative=False) + with np.errstate(over="ignore", under="ignore"): + alpha_arr = np.asarray(alpha).astype(arr.dtype) + if not np.array_equal(alpha_arr, alpha): + warn( + f'alpha cannot be safely cast to image dtype {arr.dtype.name}', stacklevel=2 + ) + try: + alpha_arr = np.broadcast_to(alpha_arr, arr.shape) + except ValueError as e: + raise ValueError("alpha.shape must match image.shape") from e + rgba = np.stack((arr,) * 3 + (alpha_arr,), axis=channel_axis) + return rgba + + +def gray2rgb(image, *, channel_axis=-1): + """Create an RGB representation of a gray-level image. + + Parameters + ---------- + image : array_like + Input image. + channel_axis : int, optional + This parameter indicates which axis of the output array will correspond + to channels. + + Returns + ------- + rgb : (..., C=3, ...) ndarray + RGB image. A new dimension of length 3 is added to input image. + + Notes + ----- + If the input is a 1-dimensional image of shape ``(M,)``, the output + will be shape ``(M, C=3)``. + """ + return np.stack(3 * (image,), axis=channel_axis) + + +@channel_as_last_axis() +def xyz2lab(xyz, illuminant="D65", observer="2", *, channel_axis=-1): + """XYZ to CIE-LAB color space conversion. + + Parameters + ---------- + xyz : (..., C=3, ...) array_like + The image in XYZ format. By default, the final dimension denotes + channels. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + One of: 2-degree observer, 10-degree observer, or 'R' observer as in + R function grDevices::convertColor. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in CIE-LAB format. Same dimensions as input. + + Raises + ------ + ValueError + If `xyz` is not at least 2-D with shape (..., C=3, ...). + ValueError + If either the illuminant or the observer angle is unsupported or + unknown. + + Notes + ----- + By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values + x_ref=95.047, y_ref=100., z_ref=108.883. See function + :func:`~.xyz_tristimulus_values` for a list of supported illuminants. + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELAB_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2lab + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_lab = xyz2lab(img_xyz) + """ + arr = _prepare_colorarray(xyz, channel_axis=-1) + + xyz_ref_white = xyz_tristimulus_values( + illuminant=illuminant, observer=observer, dtype=arr.dtype + ) + + # scale by CIE XYZ tristimulus values of the reference white point + arr = arr / xyz_ref_white + + # Nonlinear distortion and linear transformation + mask = arr > 0.008856 + arr[mask] = np.cbrt(arr[mask]) + arr[~mask] = 7.787 * arr[~mask] + 16.0 / 116.0 + + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] + + # Vector scaling + L = (116.0 * y) - 16.0 + a = 500.0 * (x - y) + b = 200.0 * (y - z) + + return np.concatenate([x[..., np.newaxis] for x in [L, a, b]], axis=-1) + + +@channel_as_last_axis() +def lab2xyz(lab, illuminant="D65", observer="2", *, channel_axis=-1): + """Convert image in CIE-LAB to XYZ color space. + + Parameters + ---------- + lab : (..., C=3, ...) array_like + The input image in CIE-LAB color space. + Unless `channel_axis` is set, the final dimension denotes the CIE-LAB + channels. + The L* values range from 0 to 100; + the a* and b* values range from -128 to 127. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + The aperture angle of the observer. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in XYZ color space, of same shape as input. + + Raises + ------ + ValueError + If `lab` is not at least 2-D with shape (..., C=3, ...). + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + UserWarning + If any of the pixels are invalid (Z < 0). + + Notes + ----- + The CIE XYZ tristimulus values are x_ref = 95.047, y_ref = 100., and + z_ref = 108.883. See function :func:`~.xyz_tristimulus_values` for a list of + supported illuminants. + + See Also + -------- + xyz2lab + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELAB_color_space + """ + xyz, n_invalid = _lab2xyz(lab, illuminant, observer) + if n_invalid != 0: + warn( + "Conversion from CIE-LAB to XYZ color space resulted in " + f"{n_invalid} negative Z values that have been clipped to zero", + stacklevel=3, + ) + return xyz + + +def _lab2xyz(lab, illuminant, observer): + """Convert CIE-LAB to XYZ color space. + + Internal function for :func:`~.lab2xyz` and others. In addition to the + converted image, return the number of invalid pixels in the Z channel for + correct warning propagation. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in XYZ format. Same dimensions as input. + n_invalid : int + Number of invalid pixels in the Z channel after conversion. + """ + arr = _prepare_colorarray(lab, channel_axis=-1).copy() + + L, a, b = arr[..., 0], arr[..., 1], arr[..., 2] + y = (L + 16.0) / 116.0 + x = (a / 500.0) + y + z = y - (b / 200.0) + + invalid = np.atleast_1d(z < 0).nonzero() + n_invalid = invalid[0].size + if n_invalid != 0: + # Warning should be emitted by caller + if z.ndim > 0: + z[invalid] = 0 + else: + z = 0 + + out = np.stack([x, y, z], axis=-1) + + mask = out > 0.2068966 + out[mask] = np.power(out[mask], 3.0) + out[~mask] = (out[~mask] - 16.0 / 116.0) / 7.787 + + # rescale to the reference white (illuminant) + xyz_ref_white = xyz_tristimulus_values(illuminant=illuminant, observer=observer) + out *= xyz_ref_white + return out, n_invalid + + +@channel_as_last_axis() +def rgb2lab(rgb, illuminant="D65", observer="2", *, channel_axis=-1): + """Conversion from the sRGB color space (IEC 61966-2-1:1999) + to the CIE Lab colorspace under the given illuminant and observer. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + The aperture angle of the observer. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in Lab format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + RGB is a device-dependent color space so, if you use this function, be + sure that the image you are analyzing has been mapped to the sRGB color + space. + + This function uses rgb2xyz and xyz2lab. + By default Observer="2", Illuminant="D65". CIE XYZ tristimulus values + x_ref=95.047, y_ref=100., z_ref=108.883. See function + :func:`~.xyz_tristimulus_values` for a list of supported illuminants. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Standard_illuminant + """ + return xyz2lab(rgb2xyz(rgb), illuminant, observer) + + +@channel_as_last_axis() +def lab2rgb(lab, illuminant="D65", observer="2", *, channel_axis=-1): + """Convert image in CIE-LAB to sRGB color space. + + Parameters + ---------- + lab : (..., C=3, ...) array_like + The input image in CIE-LAB color space. + Unless `channel_axis` is set, the final dimension denotes the CIE-LAB + channels. + The L* values range from 0 to 100; + the a* and b* values range from -128 to 127. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + The aperture angle of the observer. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in sRGB color space, of same shape as input. + + Raises + ------ + ValueError + If `lab` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + This function uses :func:`~.lab2xyz` and :func:`~.xyz2rgb`. + The CIE XYZ tristimulus values are x_ref = 95.047, y_ref = 100., and + z_ref = 108.883. See function :func:`~.xyz_tristimulus_values` for a list of + supported illuminants. + + See Also + -------- + rgb2lab + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Standard_illuminant + .. [2] https://en.wikipedia.org/wiki/CIELAB_color_space + """ + xyz, n_invalid = _lab2xyz(lab, illuminant, observer) + if n_invalid != 0: + warn( + "Conversion from CIE-LAB, via XYZ to sRGB color space resulted in " + f"{n_invalid} negative Z values that have been clipped to zero", + stacklevel=3, + ) + return xyz2rgb(xyz) + + +@channel_as_last_axis() +def xyz2luv(xyz, illuminant="D65", observer="2", *, channel_axis=-1): + """XYZ to CIE-Luv color space conversion. + + Parameters + ---------- + xyz : (..., C=3, ...) array_like + The image in XYZ format. By default, the final dimension denotes + channels. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + The aperture angle of the observer. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in CIE-Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `xyz` is not at least 2-D with shape (..., C=3, ...). + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + + Notes + ----- + By default XYZ conversion weights use observer=2A. Reference whitepoint + for D65 Illuminant, with XYZ tristimulus values of ``(95.047, 100., + 108.883)``. See function :func:`~.xyz_tristimulus_values` for a list of supported + illuminants. + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELUV + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2xyz, xyz2luv + >>> img = data.astronaut() + >>> img_xyz = rgb2xyz(img) + >>> img_luv = xyz2luv(img_xyz) + """ + input_is_one_pixel = xyz.ndim == 1 + if input_is_one_pixel: + xyz = xyz[np.newaxis, ...] + + arr = _prepare_colorarray(xyz, channel_axis=-1) + + # extract channels + x, y, z = arr[..., 0], arr[..., 1], arr[..., 2] + + eps = np.finfo(arr.dtype).eps + + # compute y_r and L + xyz_ref_white = xyz_tristimulus_values( + illuminant=illuminant, observer=observer, dtype=arr.dtype + ) + L = y / xyz_ref_white[1] + mask = L > 0.008856 + L[mask] = 116.0 * np.cbrt(L[mask]) - 16.0 + L[~mask] = 903.3 * L[~mask] + + uv_weights = np.array([1, 15, 3], dtype=arr.dtype) + u0 = 4 * xyz_ref_white[0] / (uv_weights @ xyz_ref_white) + v0 = 9 * xyz_ref_white[1] / (uv_weights @ xyz_ref_white) + + # u' and v' helper functions + def fu(X, Y, Z): + return (4.0 * X) / (X + 15.0 * Y + 3.0 * Z + eps) + + def fv(X, Y, Z): + return (9.0 * Y) / (X + 15.0 * Y + 3.0 * Z + eps) + + # compute u and v using helper functions + u = 13.0 * L * (fu(x, y, z) - u0) + v = 13.0 * L * (fv(x, y, z) - v0) + + out = np.stack([L, u, v], axis=-1) + + if input_is_one_pixel: + out = np.squeeze(out, axis=0) + + return out + + +@channel_as_last_axis() +def luv2xyz(luv, illuminant="D65", observer="2", *, channel_axis=-1): + """CIE-Luv to XYZ color space conversion. + + Parameters + ---------- + luv : (..., C=3, ...) array_like + The image in CIE-Luv format. By default, the final dimension denotes + channels. + illuminant : {"A", "B", "C", "D50", "D55", "D65", "D75", "E"}, optional + The name of the illuminant (the function is NOT case sensitive). + observer : {"2", "10", "R"}, optional + The aperture angle of the observer. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in XYZ format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not at least 2-D with shape (..., C=3, ...). + ValueError + If either the illuminant or the observer angle are not supported or + unknown. + + Notes + ----- + XYZ conversion weights use observer=2A. Reference whitepoint for D65 + Illuminant, with XYZ tristimulus values of ``(95.047, 100., 108.883)``. See + function :func:`~.xyz_tristimulus_values` for a list of supported illuminants. + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELUV + """ + arr = _prepare_colorarray(luv, channel_axis=-1).copy() + + L, u, v = arr[..., 0], arr[..., 1], arr[..., 2] + + eps = np.finfo(arr.dtype).eps + + # compute y + y = L.copy() + mask = y > 7.999625 + y[mask] = np.power((y[mask] + 16.0) / 116.0, 3.0) + y[~mask] = y[~mask] / 903.3 + xyz_ref_white = xyz_tristimulus_values( + illuminant=illuminant, observer=observer, dtype=arr.dtype + ) + y *= xyz_ref_white[1] + + # reference white x,z + uv_weights = np.array([1, 15, 3], dtype=arr.dtype) + u0 = 4 * xyz_ref_white[0] / (uv_weights @ xyz_ref_white) + v0 = 9 * xyz_ref_white[1] / (uv_weights @ xyz_ref_white) + + # compute intermediate values + a = u0 + u / (13.0 * L + eps) + b = v0 + v / (13.0 * L + eps) + c = 3 * y * (5 * b - 3) + + # compute x and z + z = ((a - 4) * c - 15 * a * b * y) / (12 * b) + x = -(c / b + 3.0 * z) + + return np.concatenate([q[..., np.newaxis] for q in [x, y, z]], axis=-1) + + +@channel_as_last_axis() +def rgb2luv(rgb, *, channel_axis=-1): + """RGB to CIE-Luv color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in CIE Luv format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + This function uses rgb2xyz and xyz2luv. + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELUV + """ + return xyz2luv(rgb2xyz(rgb)) + + +@channel_as_last_axis() +def luv2rgb(luv, *, channel_axis=-1): + """Luv to RGB color space conversion. + + Parameters + ---------- + luv : (..., C=3, ...) array_like + The image in CIE Luv format. By default, the final dimension denotes + channels. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `luv` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + This function uses luv2xyz and xyz2rgb. + """ + return xyz2rgb(luv2xyz(luv)) + + +@channel_as_last_axis() +def rgb2hed(rgb, *, channel_axis=-1): + """RGB to Haematoxylin-Eosin-DAB (HED) color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in HED format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] A. C. Ruifrok and D. A. Johnston, "Quantification of histochemical + staining by color deconvolution.," Analytical and quantitative + cytology and histology / the International Academy of Cytology [and] + American Society of Cytology, vol. 23, no. 4, pp. 291-9, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2hed + >>> ihc = data.immunohistochemistry() + >>> ihc_hed = rgb2hed(ihc) + """ + return separate_stains(rgb, hed_from_rgb) + + +@channel_as_last_axis() +def hed2rgb(hed, *, channel_axis=-1): + """Haematoxylin-Eosin-DAB (HED) to RGB color space conversion. + + Parameters + ---------- + hed : (..., C=3, ...) array_like + The image in the HED color space. By default, the final dimension + denotes channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB. Same dimensions as input. + + Raises + ------ + ValueError + If `hed` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] A. C. Ruifrok and D. A. Johnston, "Quantification of histochemical + staining by color deconvolution.," Analytical and quantitative + cytology and histology / the International Academy of Cytology [and] + American Society of Cytology, vol. 23, no. 4, pp. 291-9, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2hed, hed2rgb + >>> ihc = data.immunohistochemistry() + >>> ihc_hed = rgb2hed(ihc) + >>> ihc_rgb = hed2rgb(ihc_hed) + """ + return combine_stains(hed, rgb_from_hed) + + +@channel_as_last_axis() +def separate_stains(rgb, conv_matrix, *, channel_axis=-1): + """RGB to stain color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + conv_matrix: ndarray + The stain separation matrix as described by G. Landini [1]_. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in stain color space. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Stain separation matrices available in the ``color`` module and their + respective colorspace: + + * ``hed_from_rgb``: Hematoxylin + Eosin + DAB + * ``hdx_from_rgb``: Hematoxylin + DAB + * ``fgx_from_rgb``: Feulgen + Light Green + * ``bex_from_rgb``: Giemsa stain : Methyl Blue + Eosin + * ``rbd_from_rgb``: FastRed + FastBlue + DAB + * ``gdx_from_rgb``: Methyl Green + DAB + * ``hax_from_rgb``: Hematoxylin + AEC + * ``bro_from_rgb``: Blue matrix Anilline Blue + Red matrix Azocarmine\ + + Orange matrix Orange-G + * ``bpx_from_rgb``: Methyl Blue + Ponceau Fuchsin + * ``ahx_from_rgb``: Alcian Blue + Hematoxylin + * ``hpx_from_rgb``: Hematoxylin + PAS + + This implementation borrows some ideas from DIPlib [2]_, e.g. the + compensation using a small value to avoid log artifacts when + calculating the Beer-Lambert law. + + References + ---------- + .. [1] https://web.archive.org/web/20160624145052/http://www.mecourse.com/landinig/software/cdeconv/cdeconv.html + .. [2] https://github.com/DIPlib/diplib/ + .. [3] A. C. Ruifrok and D. A. Johnston, “Quantification of histochemical + staining by color deconvolution,” Anal. Quant. Cytol. Histol., vol. + 23, no. 4, pp. 291–299, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import separate_stains, hdx_from_rgb + >>> ihc = data.immunohistochemistry() + >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) + """ + rgb = _prepare_colorarray(rgb, force_copy=True, channel_axis=-1) + np.maximum(rgb, 1e-6, out=rgb) # avoiding log artifacts + log_adjust = np.log(1e-6) # used to compensate the sum above + + stains = (np.log(rgb) / log_adjust) @ conv_matrix + + np.maximum(stains, 0, out=stains) + + return stains + + +@channel_as_last_axis() +def combine_stains(stains, conv_matrix, *, channel_axis=-1): + """Stain to RGB color space conversion. + + Parameters + ---------- + stains : (..., C=3, ...) array_like + The image in stain color space. By default, the final dimension denotes + channels. + conv_matrix: ndarray + The stain separation matrix as described by G. Landini [1]_. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `stains` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Stain combination matrices available in the ``color`` module and their + respective colorspace: + + * ``rgb_from_hed``: Hematoxylin + Eosin + DAB + * ``rgb_from_hdx``: Hematoxylin + DAB + * ``rgb_from_fgx``: Feulgen + Light Green + * ``rgb_from_bex``: Giemsa stain : Methyl Blue + Eosin + * ``rgb_from_rbd``: FastRed + FastBlue + DAB + * ``rgb_from_gdx``: Methyl Green + DAB + * ``rgb_from_hax``: Hematoxylin + AEC + * ``rgb_from_bro``: Blue matrix Anilline Blue + Red matrix Azocarmine\ + + Orange matrix Orange-G + * ``rgb_from_bpx``: Methyl Blue + Ponceau Fuchsin + * ``rgb_from_ahx``: Alcian Blue + Hematoxylin + * ``rgb_from_hpx``: Hematoxylin + PAS + + References + ---------- + .. [1] https://web.archive.org/web/20160624145052/http://www.mecourse.com/landinig/software/cdeconv/cdeconv.html + .. [2] A. C. Ruifrok and D. A. Johnston, “Quantification of histochemical + staining by color deconvolution,” Anal. Quant. Cytol. Histol., vol. + 23, no. 4, pp. 291–299, Aug. 2001. + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import (separate_stains, combine_stains, + ... hdx_from_rgb, rgb_from_hdx) + >>> ihc = data.immunohistochemistry() + >>> ihc_hdx = separate_stains(ihc, hdx_from_rgb) + >>> ihc_rgb = combine_stains(ihc_hdx, rgb_from_hdx) + """ + stains = _prepare_colorarray(stains, channel_axis=-1) + + # log_adjust here is used to compensate the sum within separate_stains(). + log_adjust = -np.log(1e-6) + log_rgb = -(stains * log_adjust) @ conv_matrix + rgb = np.exp(log_rgb) + + return np.clip(rgb, a_min=0, a_max=1) + + +@channel_as_last_axis() +def lab2lch(lab, *, channel_axis=-1): + """Convert image in CIE-LAB to CIE-LCh color space. + + CIE-LCh is the cylindrical representation of the CIE-LAB (Cartesian) color + space. + + Parameters + ---------- + lab : (..., C=3, ...) array_like + The input image in CIE-LAB color space. + Unless `channel_axis` is set, the final dimension denotes the CIE-LAB + channels. + The L* values range from 0 to 100; + the a* and b* values range from -128 to 127. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in CIE-LCh color space, of same shape as input. + + Raises + ------ + ValueError + If `lab` does not have at least 3 channels (i.e., L*, a*, and b*). + + Notes + ----- + The h channel (i.e., hue) is expressed as an angle in range ``(0, 2*pi)``. + + See Also + -------- + lch2lab + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/CIELAB_color_space + .. [3] https://en.wikipedia.org/wiki/HCL_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lab2lch + >>> img = data.astronaut() + >>> img_lab = rgb2lab(img) + >>> img_lch = lab2lch(img_lab) + """ + lch = _prepare_lab_array(lab) + + a, b = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = _cart2polar_2pi(a, b) + return lch + + +def _cart2polar_2pi(x, y): + """convert cartesian coordinates to polar (uses non-standard theta range!) + + NON-STANDARD RANGE! Maps to ``(0, 2*pi)`` rather than usual ``(-pi, +pi)`` + """ + r, t = np.hypot(x, y), np.arctan2(y, x) + t += np.where(t < 0.0, 2 * np.pi, 0) + return r, t + + +@channel_as_last_axis() +def lch2lab(lch, *, channel_axis=-1): + """Convert image in CIE-LCh to CIE-LAB color space. + + CIE-LCh is the cylindrical representation of the CIE-LAB (Cartesian) color + space. + + Parameters + ---------- + lch : (..., C=3, ...) array_like + The input image in CIE-LCh color space. + Unless `channel_axis` is set, the final dimension denotes the CIE-LAB + channels. + The L* values range from 0 to 100; + the C values range from 0 to 100; + the h values range from 0 to ``2*pi``. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in CIE-LAB format, of same shape as input. + + Raises + ------ + ValueError + If `lch` does not have at least 3 channels (i.e., L*, C, and h). + + Notes + ----- + The h channel (i.e., hue) is expressed as an angle in range ``(0, 2*pi)``. + + See Also + -------- + lab2lch + + References + ---------- + .. [1] http://www.easyrgb.com/en/math.php + .. [2] https://en.wikipedia.org/wiki/HCL_color_space + .. [3] https://en.wikipedia.org/wiki/CIELAB_color_space + + Examples + -------- + >>> from skimage import data + >>> from skimage.color import rgb2lab, lch2lab, lab2lch + >>> img = data.astronaut() + >>> img_lab = rgb2lab(img) + >>> img_lch = lab2lch(img_lab) + >>> img_lab2 = lch2lab(img_lch) + """ + lch = _prepare_lab_array(lch) + + c, h = lch[..., 1], lch[..., 2] + lch[..., 1], lch[..., 2] = c * np.cos(h), c * np.sin(h) + return lch + + +def _prepare_lab_array(arr, force_copy=True): + """Ensure input for lab2lch and lch2lab is well-formed. + + Input array must be in floating point and have at least 3 elements in the + last dimension. Returns a new array by default. + """ + arr = np.asarray(arr) + shape = arr.shape + if shape[-1] < 3: + raise ValueError('Input image has less than 3 channels.') + float_dtype = _supported_float_type(arr.dtype) + if float_dtype == np.float32: + _func = dtype.img_as_float32 + else: + _func = dtype.img_as_float64 + return _func(arr, force_copy=force_copy) + + +@channel_as_last_axis() +def rgb2yuv(rgb, *, channel_axis=-1): + """RGB to YUV color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in YUV format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Y is between 0 and 1. Use YCbCr instead of YUV for the color space + commonly used by video codecs, where Y ranges from 16 to 235. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YUV + """ + return _convert(yuv_from_rgb, rgb) + + +@channel_as_last_axis() +def rgb2yiq(rgb, *, channel_axis=-1): + """RGB to YIQ color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in YIQ format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + """ + return _convert(yiq_from_rgb, rgb) + + +@channel_as_last_axis() +def rgb2ypbpr(rgb, *, channel_axis=-1): + """RGB to YPbPr color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in YPbPr format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YPbPr + """ + return _convert(ypbpr_from_rgb, rgb) + + +@channel_as_last_axis() +def rgb2ycbcr(rgb, *, channel_axis=-1): + """RGB to YCbCr color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in YCbCr format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Y is between 16 and 235. This is the color space commonly used by video + codecs; it is sometimes incorrectly called "YUV". + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YCbCr + """ + arr = _convert(ycbcr_from_rgb, rgb) + arr[..., 0] += 16 + arr[..., 1] += 128 + arr[..., 2] += 128 + return arr + + +@channel_as_last_axis() +def rgb2ydbdr(rgb, *, channel_axis=-1): + """RGB to YDbDr color space conversion. + + Parameters + ---------- + rgb : (..., C=3, ...) array_like + The image in RGB format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in YDbDr format. Same dimensions as input. + + Raises + ------ + ValueError + If `rgb` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + This is the color space commonly used by video codecs. It is also the + reversible color transform in JPEG2000. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YDbDr + """ + arr = _convert(ydbdr_from_rgb, rgb) + return arr + + +@channel_as_last_axis() +def yuv2rgb(yuv, *, channel_axis=-1): + """YUV to RGB color space conversion. + + Parameters + ---------- + yuv : (..., C=3, ...) array_like + The image in YUV format. By default, the final dimension denotes + channels. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `yuv` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YUV + """ + return _convert(rgb_from_yuv, yuv) + + +@channel_as_last_axis() +def yiq2rgb(yiq, *, channel_axis=-1): + """YIQ to RGB color space conversion. + + Parameters + ---------- + yiq : (..., C=3, ...) array_like + The image in YIQ format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `yiq` is not at least 2-D with shape (..., C=3, ...). + """ + return _convert(rgb_from_yiq, yiq) + + +@channel_as_last_axis() +def ypbpr2rgb(ypbpr, *, channel_axis=-1): + """YPbPr to RGB color space conversion. + + Parameters + ---------- + ypbpr : (..., C=3, ...) array_like + The image in YPbPr format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `ypbpr` is not at least 2-D with shape (..., C=3, ...). + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YPbPr + """ + return _convert(rgb_from_ypbpr, ypbpr) + + +@channel_as_last_axis() +def ycbcr2rgb(ycbcr, *, channel_axis=-1): + """YCbCr to RGB color space conversion. + + Parameters + ---------- + ycbcr : (..., C=3, ...) array_like + The image in YCbCr format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `ycbcr` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + Y is between 16 and 235. This is the color space commonly used by video + codecs; it is sometimes incorrectly called "YUV". + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YCbCr + """ + arr = ycbcr.copy() + arr[..., 0] -= 16 + arr[..., 1] -= 128 + arr[..., 2] -= 128 + return _convert(rgb_from_ycbcr, arr) + + +@channel_as_last_axis() +def ydbdr2rgb(ydbdr, *, channel_axis=-1): + """YDbDr to RGB color space conversion. + + Parameters + ---------- + ydbdr : (..., C=3, ...) array_like + The image in YDbDr format. By default, the final dimension denotes + channels. + channel_axis : int, optional + This parameter indicates which axis of the array corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (..., C=3, ...) ndarray + The image in RGB format. Same dimensions as input. + + Raises + ------ + ValueError + If `ydbdr` is not at least 2-D with shape (..., C=3, ...). + + Notes + ----- + This is the color space commonly used by video codecs, also called the + reversible color transform in JPEG2000. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/YDbDr + """ + return _convert(rgb_from_ydbdr, ydbdr) diff --git a/lib/python3.10/site-packages/skimage/color/colorlabel.py b/lib/python3.10/site-packages/skimage/color/colorlabel.py new file mode 100644 index 0000000000000000000000000000000000000000..2cd887a974247e1abb230c2785c6d8718cd18832 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/colorlabel.py @@ -0,0 +1,299 @@ +import itertools + +import numpy as np + +from .._shared.utils import _supported_float_type, warn +from ..util import img_as_float +from . import rgb_colors +from .colorconv import gray2rgb, rgb2hsv, hsv2rgb + + +__all__ = ['color_dict', 'label2rgb', 'DEFAULT_COLORS'] + + +DEFAULT_COLORS = ( + 'red', + 'blue', + 'yellow', + 'magenta', + 'green', + 'indigo', + 'darkorange', + 'cyan', + 'pink', + 'yellowgreen', +) + + +color_dict = {k: v for k, v in rgb_colors.__dict__.items() if isinstance(v, tuple)} + + +def _rgb_vector(color): + """Return RGB color as (1, 3) array. + + This RGB array gets multiplied by masked regions of an RGB image, which are + partially flattened by masking (i.e. dimensions 2D + RGB -> 1D + RGB). + + Parameters + ---------- + color : str or array + Color name in ``skimage.color.color_dict`` or RGB float values between [0, 1]. + """ + if isinstance(color, str): + color = color_dict[color] + # Slice to handle RGBA colors. + return np.array(color[:3]) + + +def _match_label_with_color(label, colors, bg_label, bg_color): + """Return `unique_labels` and `color_cycle` for label array and color list. + + Colors are cycled for normal labels, but the background color should only + be used for the background. + """ + # Temporarily set background color; it will be removed later. + if bg_color is None: + bg_color = (0, 0, 0) + bg_color = _rgb_vector(bg_color) + + # map labels to their ranks among all labels from small to large + unique_labels, mapped_labels = np.unique(label, return_inverse=True) + # unique_inverse is no longer flat in NumPy 2.0 + mapped_labels = mapped_labels.reshape(-1) + + # get rank of bg_label + bg_label_rank_list = mapped_labels[label.flat == bg_label] + + # The rank of each label is the index of the color it is matched to in + # color cycle. bg_label should always be mapped to the first color, so + # its rank must be 0. Other labels should be ranked from small to large + # from 1. + if len(bg_label_rank_list) > 0: + bg_label_rank = bg_label_rank_list[0] + mapped_labels[mapped_labels < bg_label_rank] += 1 + mapped_labels[label.flat == bg_label] = 0 + else: + mapped_labels += 1 + + # Modify labels and color cycle so background color is used only once. + color_cycle = itertools.cycle(colors) + color_cycle = itertools.chain([bg_color], color_cycle) + + return mapped_labels, color_cycle + + +def label2rgb( + label, + image=None, + colors=None, + alpha=0.3, + bg_label=0, + bg_color=(0, 0, 0), + image_alpha=1, + kind='overlay', + *, + saturation=0, + channel_axis=-1, +): + """Return an RGB image where color-coded labels are painted over the image. + + Parameters + ---------- + label : ndarray + Integer array of labels with the same shape as `image`. + image : ndarray, optional + Image used as underlay for labels. It should have the same shape as + `labels`, optionally with an additional RGB (channels) axis. If `image` + is an RGB image, it is converted to grayscale before coloring. + colors : list, optional + List of colors. If the number of labels exceeds the number of colors, + then the colors are cycled. + alpha : float [0, 1], optional + Opacity of colorized labels. Ignored if image is `None`. + bg_label : int, optional + Label that's treated as the background. If `bg_label` is specified, + `bg_color` is `None`, and `kind` is `overlay`, + background is not painted by any colors. + bg_color : str or array, optional + Background color. Must be a name in ``skimage.color.color_dict`` or RGB float + values between [0, 1]. + image_alpha : float [0, 1], optional + Opacity of the image. + kind : string, one of {'overlay', 'avg'} + The kind of color image desired. 'overlay' cycles over defined colors + and overlays the colored labels over the original image. 'avg' replaces + each labeled segment with its average color, for a stained-class or + pastel painting appearance. + saturation : float [0, 1], optional + Parameter to control the saturation applied to the original image + between fully saturated (original RGB, `saturation=1`) and fully + unsaturated (grayscale, `saturation=0`). Only applies when + `kind='overlay'`. + channel_axis : int, optional + This parameter indicates which axis of the output array will correspond + to channels. If `image` is provided, this must also match the axis of + `image` that corresponds to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + result : ndarray of float, same shape as `image` + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. + """ + if image is not None: + image = np.moveaxis(image, source=channel_axis, destination=-1) + if kind == 'overlay': + rgb = _label2rgb_overlay( + label, image, colors, alpha, bg_label, bg_color, image_alpha, saturation + ) + elif kind == 'avg': + rgb = _label2rgb_avg(label, image, bg_label, bg_color) + else: + raise ValueError("`kind` must be either 'overlay' or 'avg'.") + return np.moveaxis(rgb, source=-1, destination=channel_axis) + + +def _label2rgb_overlay( + label, + image=None, + colors=None, + alpha=0.3, + bg_label=-1, + bg_color=None, + image_alpha=1, + saturation=0, +): + """Return an RGB image where color-coded labels are painted over the image. + + Parameters + ---------- + label : ndarray + Integer array of labels with the same shape as `image`. + image : ndarray, optional + Image used as underlay for labels. It should have the same shape as + `labels`, optionally with an additional RGB (channels) axis. If `image` + is an RGB image, it is converted to grayscale before coloring. + colors : list, optional + List of colors. If the number of labels exceeds the number of colors, + then the colors are cycled. + alpha : float [0, 1], optional + Opacity of colorized labels. Ignored if image is `None`. + bg_label : int, optional + Label that's treated as the background. If `bg_label` is specified and + `bg_color` is `None`, background is not painted by any colors. + bg_color : str or array, optional + Background color. Must be a name in ``skimage.color.color_dict`` or RGB float + values between [0, 1]. + image_alpha : float [0, 1], optional + Opacity of the image. + saturation : float [0, 1], optional + Parameter to control the saturation applied to the original image + between fully saturated (original RGB, `saturation=1`) and fully + unsaturated (grayscale, `saturation=0`). + + Returns + ------- + result : ndarray of float, same shape as `image` + The result of blending a cycling colormap (`colors`) for each distinct + value in `label` with the image, at a certain alpha value. + """ + if not 0 <= saturation <= 1: + warn(f'saturation must be in range [0, 1], got {saturation}') + + if colors is None: + colors = DEFAULT_COLORS + colors = [_rgb_vector(c) for c in colors] + + if image is None: + image = np.zeros(label.shape + (3,), dtype=np.float64) + # Opacity doesn't make sense if no image exists. + alpha = 1 + else: + if image.shape[: label.ndim] != label.shape or image.ndim > label.ndim + 1: + raise ValueError("`image` and `label` must be the same shape") + + if image.ndim == label.ndim + 1 and image.shape[-1] != 3: + raise ValueError("`image` must be RGB (image.shape[-1] must be 3).") + + if image.min() < 0: + warn("Negative intensities in `image` are not supported") + + float_dtype = _supported_float_type(image.dtype) + image = img_as_float(image).astype(float_dtype, copy=False) + if image.ndim > label.ndim: + hsv = rgb2hsv(image) + hsv[..., 1] *= saturation + image = hsv2rgb(hsv) + elif image.ndim == label.ndim: + image = gray2rgb(image) + image = image * image_alpha + (1 - image_alpha) + + # Ensure that all labels are non-negative so we can index into + # `label_to_color` correctly. + offset = min(label.min(), bg_label) + if offset != 0: + label = label - offset # Make sure you don't modify the input array. + bg_label -= offset + + new_type = np.min_scalar_type(int(label.max())) + if new_type == bool: + new_type = np.uint8 + label = label.astype(new_type) + + mapped_labels_flat, color_cycle = _match_label_with_color( + label, colors, bg_label, bg_color + ) + + if len(mapped_labels_flat) == 0: + return image + + dense_labels = range(np.max(mapped_labels_flat) + 1) + + label_to_color = np.stack([c for i, c in zip(dense_labels, color_cycle)]) + + mapped_labels = label + mapped_labels.flat = mapped_labels_flat + result = label_to_color[mapped_labels] * alpha + image * (1 - alpha) + + # Remove background label if its color was not specified. + remove_background = 0 in mapped_labels_flat and bg_color is None + if remove_background: + result[label == bg_label] = image[label == bg_label] + + return result + + +def _label2rgb_avg(label_field, image, bg_label=0, bg_color=(0, 0, 0)): + """Visualise each segment in `label_field` with its mean color in `image`. + + Parameters + ---------- + label_field : ndarray of int + A segmentation of an image. + image : array, shape ``label_field.shape + (3,)`` + A color image of the same spatial shape as `label_field`. + bg_label : int, optional + A value in `label_field` to be treated as background. + bg_color : 3-tuple of int, optional + The color for the background label + + Returns + ------- + out : ndarray, same shape and type as `image` + The output visualization. + """ + out = np.zeros(label_field.shape + (3,), dtype=image.dtype) + labels = np.unique(label_field) + bg = labels == bg_label + if bg.any(): + labels = labels[labels != bg_label] + mask = (label_field == bg_label).nonzero() + out[mask] = bg_color + for label in labels: + mask = (label_field == label).nonzero() + color = image[mask].mean(axis=0) + out[mask] = color + return out diff --git a/lib/python3.10/site-packages/skimage/color/delta_e.py b/lib/python3.10/site-packages/skimage/color/delta_e.py new file mode 100644 index 0000000000000000000000000000000000000000..ee75ba52cf00061a8fb97b256c21bab9ae37f399 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/delta_e.py @@ -0,0 +1,393 @@ +""" +Functions for calculating the "distance" between colors. + +Implicit in these definitions of "distance" is the notion of "Just Noticeable +Distance" (JND). This represents the distance between colors where a human can +perceive different colors. Humans are more sensitive to certain colors than +others, which different deltaE metrics correct for with varying degrees of +sophistication. + +The literature often mentions 1 as the minimum distance for visual +differentiation, but more recent studies (Mahy 1994) peg JND at 2.3 + +The delta-E notation comes from the German word for "Sensation" (Empfindung). + +Reference +--------- +https://en.wikipedia.org/wiki/Color_difference + +""" + +import numpy as np + +from .._shared.utils import _supported_float_type +from .colorconv import lab2lch, _cart2polar_2pi + + +def _float_inputs(lab1, lab2, allow_float32=True): + lab1 = np.asarray(lab1) + lab2 = np.asarray(lab2) + if allow_float32: + float_dtype = _supported_float_type((lab1.dtype, lab2.dtype)) + else: + float_dtype = np.float64 + lab1 = lab1.astype(float_dtype, copy=False) + lab2 = lab2.astype(float_dtype, copy=False) + return lab1, lab2 + + +def deltaE_cie76(lab1, lab2, channel_axis=-1): + """Euclidean distance between two points in Lab color space + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + channel_axis : int, optional + This parameter indicates which axis of the arrays corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Color_difference + .. [2] A. R. Robertson, "The CIE 1976 color-difference formulae," + Color Res. Appl. 2, 7-11 (1977). + """ + lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True) + L1, a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[:3] + L2, a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[:3] + return np.sqrt((L2 - L1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2) + + +def deltaE_ciede94( + lab1, lab2, kH=1, kC=1, kL=1, k1=0.045, k2=0.015, *, channel_axis=-1 +): + """Color difference according to CIEDE 94 standard + + Accommodates perceptual non-uniformities through the use of application + specific scale factors (`kH`, `kC`, `kL`, `k1`, and `k2`). + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kH : float, optional + Hue scale + kC : float, optional + Chroma scale + kL : float, optional + Lightness scale + k1 : float, optional + first scale parameter + k2 : float, optional + second scale parameter + channel_axis : int, optional + This parameter indicates which axis of the arrays corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + dE : array_like + color difference between `lab1` and `lab2` + + Notes + ----- + deltaE_ciede94 is not symmetric with respect to lab1 and lab2. CIEDE94 + defines the scales for the lightness, hue, and chroma in terms of the first + color. Consequently, the first color should be regarded as the "reference" + color. + + `kL`, `k1`, `k2` depend on the application and default to the values + suggested for graphic arts + + ========== ============== ========== + Parameter Graphic Arts Textiles + ========== ============== ========== + `kL` 1.000 2.000 + `k1` 0.045 0.048 + `k2` 0.015 0.014 + ========== ============== ========== + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + """ + lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True) + lab1 = np.moveaxis(lab1, source=channel_axis, destination=0) + lab2 = np.moveaxis(lab2, source=channel_axis, destination=0) + + L1, C1 = lab2lch(lab1, channel_axis=0)[:2] + L2, C2 = lab2lch(lab2, channel_axis=0)[:2] + + dL = L1 - L2 + dC = C1 - C2 + dH2 = get_dH2(lab1, lab2, channel_axis=0) + + SL = 1 + SC = 1 + k1 * C1 + SH = 1 + k2 * C1 + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (kH * SH) ** 2 + return np.sqrt(np.maximum(dE2, 0)) + + +def deltaE_ciede2000(lab1, lab2, kL=1, kC=1, kH=1, *, channel_axis=-1): + """Color difference as given by the CIEDE 2000 standard. + + CIEDE 2000 is a major revision of CIDE94. The perceptual calibration is + largely based on experience with automotive paint on smooth surfaces. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + kL : float (range), optional + lightness scale factor, 1 for "acceptably close"; 2 for "imperceptible" + see deltaE_cmc + kC : float (range), optional + chroma scale factor, usually 1 + kH : float (range), optional + hue scale factor, usually 1 + channel_axis : int, optional + This parameter indicates which axis of the arrays corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + deltaE : array_like + The distance between `lab1` and `lab2` + + Notes + ----- + CIEDE 2000 assumes parametric weighting factors for the lightness, chroma, + and hue (`kL`, `kC`, `kH` respectively). These default to 1. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.ece.rochester.edu/~gsharma/ciede2000/ciede2000noteCRNA.pdf + :DOI:`10.1364/AO.33.008069` + .. [3] M. Melgosa, J. Quesada, and E. Hita, "Uniformity of some recent + color metrics tested with an accurate color-difference tolerance + dataset," Appl. Opt. 33, 8069-8077 (1994). + """ + lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True) + + channel_axis = channel_axis % lab1.ndim + unroll = False + if lab1.ndim == 1 and lab2.ndim == 1: + unroll = True + if lab1.ndim == 1: + lab1 = lab1[None, :] + if lab2.ndim == 1: + lab2 = lab2[None, :] + channel_axis += 1 + L1, a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[:3] + L2, a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[:3] + + # distort `a` based on average chroma + # then convert to lch coordinates from distorted `a` + # all subsequence calculations are in the new coordinates + # (often denoted "prime" in the literature) + Cbar = 0.5 * (np.hypot(a1, b1) + np.hypot(a2, b2)) + c7 = Cbar**7 + G = 0.5 * (1 - np.sqrt(c7 / (c7 + 25**7))) + scale = 1 + G + C1, h1 = _cart2polar_2pi(a1 * scale, b1) + C2, h2 = _cart2polar_2pi(a2 * scale, b2) + # recall that c, h are polar coordinates. c==r, h==theta + + # cide2000 has four terms to delta_e: + # 1) Luminance term + # 2) Hue term + # 3) Chroma term + # 4) hue Rotation term + + # lightness term + Lbar = 0.5 * (L1 + L2) + tmp = (Lbar - 50) ** 2 + SL = 1 + 0.015 * tmp / np.sqrt(20 + tmp) + L_term = (L2 - L1) / (kL * SL) + + # chroma term + Cbar = 0.5 * (C1 + C2) # new coordinates + SC = 1 + 0.045 * Cbar + C_term = (C2 - C1) / (kC * SC) + + # hue term + h_diff = h2 - h1 + h_sum = h1 + h2 + CC = C1 * C2 + + dH = h_diff.copy() + dH[h_diff > np.pi] -= 2 * np.pi + dH[h_diff < -np.pi] += 2 * np.pi + dH[CC == 0.0] = 0.0 # if r == 0, dtheta == 0 + dH_term = 2 * np.sqrt(CC) * np.sin(dH / 2) + + Hbar = h_sum.copy() + mask = np.logical_and(CC != 0.0, np.abs(h_diff) > np.pi) + Hbar[mask * (h_sum < 2 * np.pi)] += 2 * np.pi + Hbar[mask * (h_sum >= 2 * np.pi)] -= 2 * np.pi + Hbar[CC == 0.0] *= 2 + Hbar *= 0.5 + + T = ( + 1 + - 0.17 * np.cos(Hbar - np.deg2rad(30)) + + 0.24 * np.cos(2 * Hbar) + + 0.32 * np.cos(3 * Hbar + np.deg2rad(6)) + - 0.20 * np.cos(4 * Hbar - np.deg2rad(63)) + ) + SH = 1 + 0.015 * Cbar * T + + H_term = dH_term / (kH * SH) + + # hue rotation + c7 = Cbar**7 + Rc = 2 * np.sqrt(c7 / (c7 + 25**7)) + dtheta = np.deg2rad(30) * np.exp(-(((np.rad2deg(Hbar) - 275) / 25) ** 2)) + R_term = -np.sin(2 * dtheta) * Rc * C_term * H_term + + # put it all together + dE2 = L_term**2 + dE2 += C_term**2 + dE2 += H_term**2 + dE2 += R_term + ans = np.sqrt(np.maximum(dE2, 0)) + if unroll: + ans = ans[0] + return ans + + +def deltaE_cmc(lab1, lab2, kL=1, kC=1, *, channel_axis=-1): + """Color difference from the CMC l:c standard. + + This color difference was developed by the Colour Measurement Committee + (CMC) of the Society of Dyers and Colourists (United Kingdom). It is + intended for use in the textile industry. + + The scale factors `kL`, `kC` set the weight given to differences in + lightness and chroma relative to differences in hue. The usual values are + ``kL=2``, ``kC=1`` for "acceptability" and ``kL=1``, ``kC=1`` for + "imperceptibility". Colors with ``dE > 1`` are "different" for the given + scale factors. + + Parameters + ---------- + lab1 : array_like + reference color (Lab colorspace) + lab2 : array_like + comparison color (Lab colorspace) + channel_axis : int, optional + This parameter indicates which axis of the arrays corresponds to + channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + dE : array_like + distance between colors `lab1` and `lab2` + + Notes + ----- + deltaE_cmc the defines the scales for the lightness, hue, and chroma + in terms of the first color. Consequently + ``deltaE_cmc(lab1, lab2) != deltaE_cmc(lab2, lab1)`` + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Color_difference + .. [2] http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html + .. [3] F. J. J. Clarke, R. McDonald, and B. Rigg, "Modification to the + JPC79 colour-difference formula," J. Soc. Dyers Colour. 100, 128-132 + (1984). + """ + lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=True) + lab1 = np.moveaxis(lab1, source=channel_axis, destination=0) + lab2 = np.moveaxis(lab2, source=channel_axis, destination=0) + L1, C1, h1 = lab2lch(lab1, channel_axis=0)[:3] + L2, C2, h2 = lab2lch(lab2, channel_axis=0)[:3] + + dC = C1 - C2 + dL = L1 - L2 + dH2 = get_dH2(lab1, lab2, channel_axis=0) + + T = np.where( + np.logical_and(np.rad2deg(h1) >= 164, np.rad2deg(h1) <= 345), + 0.56 + 0.2 * np.abs(np.cos(h1 + np.deg2rad(168))), + 0.36 + 0.4 * np.abs(np.cos(h1 + np.deg2rad(35))), + ) + c1_4 = C1**4 + F = np.sqrt(c1_4 / (c1_4 + 1900)) + + SL = np.where(L1 < 16, 0.511, 0.040975 * L1 / (1.0 + 0.01765 * L1)) + SC = 0.638 + 0.0638 * C1 / (1.0 + 0.0131 * C1) + SH = SC * (F * T + 1 - F) + + dE2 = (dL / (kL * SL)) ** 2 + dE2 += (dC / (kC * SC)) ** 2 + dE2 += dH2 / (SH**2) + + return np.sqrt(np.maximum(dE2, 0)) + + +def get_dH2(lab1, lab2, *, channel_axis=-1): + """squared hue difference term occurring in deltaE_cmc and deltaE_ciede94 + + Despite its name, "dH" is not a simple difference of hue values. We avoid + working directly with the hue value, since differencing angles is + troublesome. The hue term is usually written as: + c1 = sqrt(a1**2 + b1**2) + c2 = sqrt(a2**2 + b2**2) + term = (a1-a2)**2 + (b1-b2)**2 - (c1-c2)**2 + dH = sqrt(term) + + However, this has poor roundoff properties when a or b is dominant. + Instead, ab is a vector with elements a and b. The same dH term can be + re-written as: + |ab1-ab2|**2 - (|ab1| - |ab2|)**2 + and then simplified to: + 2*|ab1|*|ab2| - 2*dot(ab1, ab2) + """ + # This function needs double precision internally for accuracy + input_is_float_32 = _supported_float_type((lab1.dtype, lab2.dtype)) == np.float32 + lab1, lab2 = _float_inputs(lab1, lab2, allow_float32=False) + + a1, b1 = np.moveaxis(lab1, source=channel_axis, destination=0)[1:3] + a2, b2 = np.moveaxis(lab2, source=channel_axis, destination=0)[1:3] + + # magnitude of (a, b) is the chroma + C1 = np.hypot(a1, b1) + C2 = np.hypot(a2, b2) + + term = (C1 * C2) - (a1 * a2 + b1 * b2) + out = 2 * term + if input_is_float_32: + out = out.astype(np.float32) + return out diff --git a/lib/python3.10/site-packages/skimage/color/rgb_colors.py b/lib/python3.10/site-packages/skimage/color/rgb_colors.py new file mode 100644 index 0000000000000000000000000000000000000000..23046105029b96ee8b418b9cc83815761ada31ce --- /dev/null +++ b/lib/python3.10/site-packages/skimage/color/rgb_colors.py @@ -0,0 +1,146 @@ +aliceblue = (0.941, 0.973, 1) +antiquewhite = (0.98, 0.922, 0.843) +aqua = (0, 1, 1) +aquamarine = (0.498, 1, 0.831) +azure = (0.941, 1, 1) +beige = (0.961, 0.961, 0.863) +bisque = (1, 0.894, 0.769) +black = (0, 0, 0) +blanchedalmond = (1, 0.922, 0.804) +blue = (0, 0, 1) +blueviolet = (0.541, 0.169, 0.886) +brown = (0.647, 0.165, 0.165) +burlywood = (0.871, 0.722, 0.529) +cadetblue = (0.373, 0.62, 0.627) +chartreuse = (0.498, 1, 0) +chocolate = (0.824, 0.412, 0.118) +coral = (1, 0.498, 0.314) +cornflowerblue = (0.392, 0.584, 0.929) +cornsilk = (1, 0.973, 0.863) +crimson = (0.863, 0.0784, 0.235) +cyan = (0, 1, 1) +darkblue = (0, 0, 0.545) +darkcyan = (0, 0.545, 0.545) +darkgoldenrod = (0.722, 0.525, 0.0431) +darkgray = (0.663, 0.663, 0.663) +darkgreen = (0, 0.392, 0) +darkgrey = (0.663, 0.663, 0.663) +darkkhaki = (0.741, 0.718, 0.42) +darkmagenta = (0.545, 0, 0.545) +darkolivegreen = (0.333, 0.42, 0.184) +darkorange = (1, 0.549, 0) +darkorchid = (0.6, 0.196, 0.8) +darkred = (0.545, 0, 0) +darksalmon = (0.914, 0.588, 0.478) +darkseagreen = (0.561, 0.737, 0.561) +darkslateblue = (0.282, 0.239, 0.545) +darkslategray = (0.184, 0.31, 0.31) +darkslategrey = (0.184, 0.31, 0.31) +darkturquoise = (0, 0.808, 0.82) +darkviolet = (0.58, 0, 0.827) +deeppink = (1, 0.0784, 0.576) +deepskyblue = (0, 0.749, 1) +dimgray = (0.412, 0.412, 0.412) +dimgrey = (0.412, 0.412, 0.412) +dodgerblue = (0.118, 0.565, 1) +firebrick = (0.698, 0.133, 0.133) +floralwhite = (1, 0.98, 0.941) +forestgreen = (0.133, 0.545, 0.133) +fuchsia = (1, 0, 1) +gainsboro = (0.863, 0.863, 0.863) +ghostwhite = (0.973, 0.973, 1) +gold = (1, 0.843, 0) +goldenrod = (0.855, 0.647, 0.125) +gray = (0.502, 0.502, 0.502) +green = (0, 0.502, 0) +greenyellow = (0.678, 1, 0.184) +grey = (0.502, 0.502, 0.502) +honeydew = (0.941, 1, 0.941) +hotpink = (1, 0.412, 0.706) +indianred = (0.804, 0.361, 0.361) +indigo = (0.294, 0, 0.51) +ivory = (1, 1, 0.941) +khaki = (0.941, 0.902, 0.549) +lavender = (0.902, 0.902, 0.98) +lavenderblush = (1, 0.941, 0.961) +lawngreen = (0.486, 0.988, 0) +lemonchiffon = (1, 0.98, 0.804) +lightblue = (0.678, 0.847, 0.902) +lightcoral = (0.941, 0.502, 0.502) +lightcyan = (0.878, 1, 1) +lightgoldenrodyellow = (0.98, 0.98, 0.824) +lightgray = (0.827, 0.827, 0.827) +lightgreen = (0.565, 0.933, 0.565) +lightgrey = (0.827, 0.827, 0.827) +lightpink = (1, 0.714, 0.757) +lightsalmon = (1, 0.627, 0.478) +lightseagreen = (0.125, 0.698, 0.667) +lightskyblue = (0.529, 0.808, 0.98) +lightslategray = (0.467, 0.533, 0.6) +lightslategrey = (0.467, 0.533, 0.6) +lightsteelblue = (0.69, 0.769, 0.871) +lightyellow = (1, 1, 0.878) +lime = (0, 1, 0) +limegreen = (0.196, 0.804, 0.196) +linen = (0.98, 0.941, 0.902) +magenta = (1, 0, 1) +maroon = (0.502, 0, 0) +mediumaquamarine = (0.4, 0.804, 0.667) +mediumblue = (0, 0, 0.804) +mediumorchid = (0.729, 0.333, 0.827) +mediumpurple = (0.576, 0.439, 0.859) +mediumseagreen = (0.235, 0.702, 0.443) +mediumslateblue = (0.482, 0.408, 0.933) +mediumspringgreen = (0, 0.98, 0.604) +mediumturquoise = (0.282, 0.82, 0.8) +mediumvioletred = (0.78, 0.0824, 0.522) +midnightblue = (0.098, 0.098, 0.439) +mintcream = (0.961, 1, 0.98) +mistyrose = (1, 0.894, 0.882) +moccasin = (1, 0.894, 0.71) +navajowhite = (1, 0.871, 0.678) +navy = (0, 0, 0.502) +oldlace = (0.992, 0.961, 0.902) +olive = (0.502, 0.502, 0) +olivedrab = (0.42, 0.557, 0.137) +orange = (1, 0.647, 0) +orangered = (1, 0.271, 0) +orchid = (0.855, 0.439, 0.839) +palegoldenrod = (0.933, 0.91, 0.667) +palegreen = (0.596, 0.984, 0.596) +palevioletred = (0.686, 0.933, 0.933) +papayawhip = (1, 0.937, 0.835) +peachpuff = (1, 0.855, 0.725) +peru = (0.804, 0.522, 0.247) +pink = (1, 0.753, 0.796) +plum = (0.867, 0.627, 0.867) +powderblue = (0.69, 0.878, 0.902) +purple = (0.502, 0, 0.502) +red = (1, 0, 0) +rosybrown = (0.737, 0.561, 0.561) +royalblue = (0.255, 0.412, 0.882) +saddlebrown = (0.545, 0.271, 0.0745) +salmon = (0.98, 0.502, 0.447) +sandybrown = (0.98, 0.643, 0.376) +seagreen = (0.18, 0.545, 0.341) +seashell = (1, 0.961, 0.933) +sienna = (0.627, 0.322, 0.176) +silver = (0.753, 0.753, 0.753) +skyblue = (0.529, 0.808, 0.922) +slateblue = (0.416, 0.353, 0.804) +slategray = (0.439, 0.502, 0.565) +slategrey = (0.439, 0.502, 0.565) +snow = (1, 0.98, 0.98) +springgreen = (0, 1, 0.498) +steelblue = (0.275, 0.51, 0.706) +tan = (0.824, 0.706, 0.549) +teal = (0, 0.502, 0.502) +thistle = (0.847, 0.749, 0.847) +tomato = (1, 0.388, 0.278) +turquoise = (0.251, 0.878, 0.816) +violet = (0.933, 0.51, 0.933) +wheat = (0.961, 0.871, 0.702) +white = (1, 1, 1) +whitesmoke = (0.961, 0.961, 0.961) +yellow = (1, 1, 0) +yellowgreen = (0.604, 0.804, 0.196) diff --git a/lib/python3.10/site-packages/skimage/conftest.py b/lib/python3.10/site-packages/skimage/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..4e6bbb8a2fa7803b52add4bab1fa03552e4774c2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/conftest.py @@ -0,0 +1,17 @@ +import pytest + +# List of files that pytest should ignore +collect_ignore = [ + "io/_plugins", +] + + +@pytest.fixture(autouse=True) +def handle_np2(): + # TODO: remove when we require numpy >= 2 + try: + import numpy as np + + np.set_printoptions(legacy="1.21") + except ImportError: + pass diff --git a/lib/python3.10/site-packages/skimage/data/README.txt b/lib/python3.10/site-packages/skimage/data/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..0b8c30aff89b1572bb836c13e1f2ec0c2ccd27c8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/README.txt @@ -0,0 +1,9 @@ +This directory contains sample data from scikit-image. + +By default, it only contains a small subset of the entire dataset. + +The full detaset can be downloaded by using the following commands from +a python console. + + >>> from skimage.data import download_all + >>> download_all() diff --git a/lib/python3.10/site-packages/skimage/data/__init__.py b/lib/python3.10/site-packages/skimage/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d89f7efee556205748bd35278518f591de6c2391 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/__init__.py @@ -0,0 +1,13 @@ +"""Example images and datasets. + +A curated set of general purpose and scientific images used in tests, examples, +and documentation. + +Newer datasets are no longer included as part of the package, but are +downloaded on demand. To make data available offline, use :func:`download_all`. + +""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/data/__init__.pyi b/lib/python3.10/site-packages/skimage/data/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..71dddcc7df4c8df11164fa7a9777aeea45d53236 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/__init__.pyi @@ -0,0 +1,88 @@ +__all__ = [ + 'astronaut', + 'binary_blobs', + 'brain', + 'brick', + 'camera', + 'cat', + 'cell', + 'cells3d', + 'checkerboard', + 'chelsea', + 'clock', + 'coffee', + 'coins', + 'colorwheel', + 'data_dir', + 'download_all', + 'eagle', + 'file_hash', + 'grass', + 'gravel', + 'horse', + 'hubble_deep_field', + 'human_mitosis', + 'immunohistochemistry', + 'kidney', + 'lbp_frontal_face_cascade_filename', + 'lfw_subset', + 'lily', + 'logo', + 'microaneurysms', + 'moon', + 'nickel_solidification', + 'page', + 'protein_transport', + 'retina', + 'rocket', + 'shepp_logan_phantom', + 'skin', + 'stereo_motorcycle', + 'text', + 'vortex', +] + +from ._binary_blobs import binary_blobs +from ._fetchers import ( + astronaut, + brain, + brick, + camera, + cat, + cell, + cells3d, + checkerboard, + chelsea, + clock, + coffee, + coins, + colorwheel, + data_dir, + download_all, + eagle, + file_hash, + grass, + gravel, + horse, + hubble_deep_field, + human_mitosis, + immunohistochemistry, + kidney, + lbp_frontal_face_cascade_filename, + lfw_subset, + lily, + logo, + microaneurysms, + moon, + nickel_solidification, + page, + palisades_of_vogt, + protein_transport, + retina, + rocket, + shepp_logan_phantom, + skin, + stereo_motorcycle, + text, + vortex, +) diff --git a/lib/python3.10/site-packages/skimage/data/_binary_blobs.py b/lib/python3.10/site-packages/skimage/data/_binary_blobs.py new file mode 100644 index 0000000000000000000000000000000000000000..dd00d2ddd239626568b6f23570f2c2f5c3b6fc0c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/_binary_blobs.py @@ -0,0 +1,61 @@ +import numpy as np + +from .._shared.filters import gaussian + + +def binary_blobs( + length=512, blob_size_fraction=0.1, n_dim=2, volume_fraction=0.5, rng=None +): + """ + Generate synthetic binary image with several rounded blob-like objects. + + Parameters + ---------- + length : int, optional + Linear size of output image. + blob_size_fraction : float, optional + Typical linear size of blob, as a fraction of ``length``, should be + smaller than 1. + n_dim : int, optional + Number of dimensions of output image. + volume_fraction : float, default 0.5 + Fraction of image pixels covered by the blobs (where the output is 1). + Should be in [0, 1]. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + Returns + ------- + blobs : ndarray of bools + Output binary image + + Examples + -------- + >>> from skimage import data + >>> data.binary_blobs(length=5, blob_size_fraction=0.2) # doctest: +SKIP + array([[ True, False, True, True, True], + [ True, True, True, False, True], + [False, True, False, True, True], + [ True, False, False, True, True], + [ True, False, False, False, True]]) + >>> blobs = data.binary_blobs(length=256, blob_size_fraction=0.1) + >>> # Finer structures + >>> blobs = data.binary_blobs(length=256, blob_size_fraction=0.05) + >>> # Blobs cover a smaller volume fraction of the image + >>> blobs = data.binary_blobs(length=256, volume_fraction=0.3) + + """ + + rs = np.random.default_rng(rng) + shape = tuple([length] * n_dim) + mask = np.zeros(shape) + n_pts = max(int(1.0 / blob_size_fraction) ** n_dim, 1) + points = (length * rs.random((n_dim, n_pts))).astype(int) + mask[tuple(indices for indices in points)] = 1 + mask = gaussian( + mask, sigma=0.25 * length * blob_size_fraction, preserve_range=False + ) + threshold = np.percentile(mask, 100 * (1 - volume_fraction)) + return np.logical_not(mask < threshold) diff --git a/lib/python3.10/site-packages/skimage/data/_fetchers.py b/lib/python3.10/site-packages/skimage/data/_fetchers.py new file mode 100644 index 0000000000000000000000000000000000000000..f5340954942d5e1501eb29eeb56018d4bcc057bd --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/_fetchers.py @@ -0,0 +1,1248 @@ +"""Standard test images. + +For more images, see + + - http://sipi.usc.edu/database/database.php + +""" + +import numpy as np +import shutil + +from ..util.dtype import img_as_bool +from ._registry import registry, registry_urls + +from .. import __version__ + +import os.path as osp +import os + +_LEGACY_DATA_DIR = osp.dirname(__file__) +_DISTRIBUTION_DIR = osp.dirname(_LEGACY_DATA_DIR) + +try: + from pooch import file_hash +except ModuleNotFoundError: + # Function taken from + # https://github.com/fatiando/pooch/blob/master/pooch/utils.py + def file_hash(fname, alg="sha256"): + """ + Calculate the hash of a given file. + Useful for checking if a file has changed or been corrupted. + Parameters + ---------- + fname : str + The name of the file. + alg : str + The type of the hashing algorithm + Returns + ------- + hash : str + The hash of the file. + Examples + -------- + >>> fname = "test-file-for-hash.txt" + >>> with open(fname, "w") as f: + ... __ = f.write("content of the file") + >>> print(file_hash(fname)) + 0fc74468e6a9a829f103d069aeb2bb4f8646bad58bf146bb0e3379b759ec4a00 + >>> import os + >>> os.remove(fname) + """ + import hashlib + + if alg not in hashlib.algorithms_available: + raise ValueError(f'Algorithm \'{alg}\' not available in hashlib') + # Calculate the hash in chunks to avoid overloading the memory + chunksize = 65536 + hasher = hashlib.new(alg) + with open(fname, "rb") as fin: + buff = fin.read(chunksize) + while buff: + hasher.update(buff) + buff = fin.read(chunksize) + return hasher.hexdigest() + + +def _has_hash(path, expected_hash): + """Check if the provided path has the expected hash.""" + if not osp.exists(path): + return False + return file_hash(path) == expected_hash + + +def _create_image_fetcher(): + try: + import pooch + + # older versions of Pooch don't have a __version__ attribute + if not hasattr(pooch, '__version__'): + retry = {} + else: + retry = {'retry_if_failed': 3} + except ImportError: + # Without pooch, fallback on the standard data directory + # which for now, includes a few limited data samples + return None, _LEGACY_DATA_DIR + + # Pooch expects a `+` to exist in development versions. + # Since scikit-image doesn't follow that convention, we have to manually + # remove `.dev` with a `+` if it exists. + # This helps pooch understand that it should look in master + # to find the required files + if '+git' in __version__: + skimage_version_for_pooch = __version__.replace('.dev0+git', '+git') + else: + skimage_version_for_pooch = __version__.replace('.dev', '+') + + if '+' in skimage_version_for_pooch: + url = "https://github.com/scikit-image/scikit-image/raw/" "{version}/skimage/" + else: + url = "https://github.com/scikit-image/scikit-image/raw/" "v{version}/skimage/" + + # Create a new friend to manage your sample data storage + image_fetcher = pooch.create( + # Pooch uses appdirs to select an appropriate directory for the cache + # on each platform. + # https://github.com/ActiveState/appdirs + # On linux this converges to + # '$HOME/.cache/scikit-image' + # With a version qualifier + path=pooch.os_cache("scikit-image"), + base_url=url, + version=skimage_version_for_pooch, + version_dev="main", + env="SKIMAGE_DATADIR", + registry=registry, + urls=registry_urls, + # Note: this should read `retry_if_failed=3,`, but we generate that + # dynamically at import time above, in case installed pooch is a less + # recent version + **retry, + ) + + data_dir = osp.join(str(image_fetcher.abspath), 'data') + return image_fetcher, data_dir + + +_image_fetcher, data_dir = _create_image_fetcher() + + +def _skip_pytest_case_requiring_pooch(data_filename): + """If a test case is calling pooch, skip it. + + This running the test suite in environments without internet + access, skipping only the tests that try to fetch external data. + """ + + # Check if pytest is currently running. + # Packagers might use pytest to run the tests suite, but may not + # want to run it online with pooch as a dependency. + # As such, we will avoid failing the test, and silently skipping it. + if 'PYTEST_CURRENT_TEST' in os.environ: + # https://docs.pytest.org/en/latest/example/simple.html#pytest-current-test-environment-variable + import pytest + + # Pytest skip raises an exception that allows the + # tests to be skipped + pytest.skip(f'Unable to download {data_filename}', allow_module_level=True) + + +def _ensure_cache_dir(*, target_dir): + """Prepare local cache directory if it doesn't exist already. + + Creates:: + + /path/to/target_dir/ + └─ data/ + └─ README.txt + """ + os.makedirs(osp.join(target_dir, "data"), exist_ok=True) + readme_src = osp.join(_DISTRIBUTION_DIR, "data/README.txt") + readme_dest = osp.join(target_dir, "data/README.txt") + if not osp.exists(readme_dest): + shutil.copy2(readme_src, readme_dest) + + +def _fetch(data_filename): + """Fetch a given data file from either the local cache or the repository. + + This function provides the path location of the data file given + its name in the scikit-image repository. If a data file is not included in the + distribution and pooch is available, it is downloaded and cached. + + Parameters + ---------- + data_filename : str + Name of the file in the scikit-image repository. e.g. + 'restoration/tess/camera_rl.npy'. + + Returns + ------- + file_path : str + Path of the local file. + + Raises + ------ + KeyError: + If the filename is not known to the scikit-image distribution. + + ModuleNotFoundError: + If the filename is known to the scikit-image distribution but pooch + is not installed. + + ConnectionError: + If scikit-image is unable to connect to the internet but the + dataset has not been downloaded yet. + """ + expected_hash = registry[data_filename] + if _image_fetcher is None: + cache_dir = osp.dirname(data_dir) + else: + cache_dir = str(_image_fetcher.abspath) + + # Case 1: the file is already cached in `data_cache_dir` + cached_file_path = osp.join(cache_dir, data_filename) + if _has_hash(cached_file_path, expected_hash): + # Nothing to be done, file is where it is expected to be + return cached_file_path + + # Case 2: file is present in `legacy_data_dir` + legacy_file_path = osp.join(_DISTRIBUTION_DIR, data_filename) + if _has_hash(legacy_file_path, expected_hash): + return legacy_file_path + + # Case 3: file is not present locally + if _image_fetcher is None: + _skip_pytest_case_requiring_pooch(data_filename) + raise ModuleNotFoundError( + "The requested file is part of the scikit-image distribution, " + "but requires the installation of an optional dependency, pooch. " + "To install pooch, use your preferred python package manager. " + "Follow installation instruction found at " + "https://scikit-image.org/docs/stable/user_guide/install.html" + ) + # Download the data with pooch which caches it automatically + _ensure_cache_dir(target_dir=cache_dir) + try: + cached_file_path = _image_fetcher.fetch(data_filename) + return cached_file_path + except ConnectionError as err: + _skip_pytest_case_requiring_pooch(data_filename) + # If we decide in the future to suppress the underlying 'requests' + # error, change this to `raise ... from None`. See PEP 3134. + raise ConnectionError( + 'Tried to download a scikit-image dataset, but no internet ' + 'connection is available. To avoid this message in the ' + 'future, try `skimage.data.download_all()` when you are ' + 'connected to the internet.' + ) from err + + +def download_all(directory=None): + """Download all datasets for use with scikit-image offline. + + Scikit-image datasets are no longer shipped with the library by default. + This allows us to use higher quality datasets, while keeping the + library download size small. + + This function requires the installation of an optional dependency, pooch, + to download the full dataset. Follow installation instruction found at + + https://scikit-image.org/docs/stable/user_guide/install.html + + Call this function to download all sample images making them available + offline on your machine. + + Parameters + ---------- + directory: path-like, optional + The directory where the dataset should be stored. + + Raises + ------ + ModuleNotFoundError: + If pooch is not install, this error will be raised. + + Notes + ----- + scikit-image will only search for images stored in the default directory. + Only specify the directory if you wish to download the images to your own + folder for a particular reason. You can access the location of the default + data directory by inspecting the variable ``skimage.data.data_dir``. + """ + + if _image_fetcher is None: + raise ModuleNotFoundError( + "To download all package data, scikit-image needs an optional " + "dependency, pooch." + "To install pooch, follow our installation instructions found at " + "https://scikit-image.org/docs/stable/user_guide/install.html" + ) + # Consider moving this kind of logic to Pooch + old_dir = _image_fetcher.path + try: + if directory is not None: + directory = osp.expanduser(directory) + _image_fetcher.path = directory + _ensure_cache_dir(target_dir=_image_fetcher.path) + + for data_filename in _image_fetcher.registry: + file_path = _fetch(data_filename) + + # Copy to `directory` or implicit cache if it is not already there + if not file_path.startswith(str(_image_fetcher.path)): + dest_path = osp.join(_image_fetcher.path, data_filename) + os.makedirs(osp.dirname(dest_path), exist_ok=True) + shutil.copy2(file_path, dest_path) + finally: + _image_fetcher.path = old_dir + + +def lbp_frontal_face_cascade_filename(): + """Return the path to the XML file containing the weak classifier cascade. + + These classifiers were trained using LBP features. The file is part + of the OpenCV repository [1]_. + + References + ---------- + .. [1] OpenCV lbpcascade trained files + https://github.com/opencv/opencv/tree/master/data/lbpcascades + """ + + return _fetch('data/lbpcascade_frontalface_opencv.xml') + + +def _load(f, as_gray=False): + """Load an image file located in the data directory. + + Parameters + ---------- + f : string + File name. + as_gray : bool, optional + Whether to convert the image to grayscale. + + Returns + ------- + img : ndarray + Image loaded from ``skimage.data_dir``. + """ + # importing io is quite slow since it scans all the backends + # we lazy import it here + from ..io import imread + + return imread(_fetch(f), as_gray=as_gray) + + +def camera(): + """Gray-level "camera" image. + + Can be used for segmentation and denoising examples. + + Returns + ------- + camera : (512, 512) uint8 ndarray + Camera image. + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Lav Varshney). + + .. versionchanged:: 0.18 + This image was replaced due to copyright restrictions. For more + information, please see [1]_. + + References + ---------- + .. [1] https://github.com/scikit-image/scikit-image/issues/3927 + """ + return _load("data/camera.png") + + +def eagle(): + """A golden eagle. + + Suitable for examples on segmentation, Hough transforms, and corner + detection. + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Dayane Machado). + + Returns + ------- + eagle : (2019, 1826) uint8 ndarray + Eagle image. + """ + return _load("data/eagle.png") + + +def astronaut(): + """Color image of the astronaut Eileen Collins. + + Photograph of Eileen Collins, an American astronaut. She was selected + as an astronaut in 1992 and first piloted the space shuttle STS-63 in + 1995. She retired in 2006 after spending a total of 38 days, 8 hours + and 10 minutes in outer space. + + This image was downloaded from the NASA Great Images database + `__. + + No known copyright restrictions, released into the public domain. + + Returns + ------- + astronaut : (512, 512, 3) uint8 ndarray + Astronaut image. + """ + + return _load("data/astronaut.png") + + +def brick(): + """Brick wall. + + Returns + ------- + brick : (512, 512) uint8 image + A small section of a brick wall. + + Notes + ----- + The original image was downloaded from + `CC0Textures `_ and licensed + under the Creative Commons CC0 License. + + A perspective transform was then applied to the image, prior to + rotating it by 90 degrees, cropping and scaling it to obtain the final + image. + """ + + """ + The following code was used to obtain the final image. + + >>> import sys; print(sys.version) + >>> import platform; print(platform.platform()) + >>> import skimage; print(f'scikit-image version: {skimage.__version__}') + >>> import numpy; print(f'numpy version: {numpy.__version__}') + >>> import imageio; print(f'imageio version {imageio.__version__}') + 3.7.3 | packaged by conda-forge | (default, Jul 1 2019, 21:52:21) + [GCC 7.3.0] + Linux-5.0.0-20-generic-x86_64-with-debian-buster-sid + scikit-image version: 0.16.dev0 + numpy version: 1.16.4 + imageio version 2.4.1 + + >>> import requests + >>> import zipfile + >>> url = 'https://cdn.struffelproductions.com/file/cc0textures/Bricks25/%5B2K%5DBricks25.zip' + >>> r = requests.get(url) + >>> with open('[2K]Bricks25.zip', 'bw') as f: + ... f.write(r.content) + >>> with zipfile.ZipFile('[2K]Bricks25.zip') as z: + ... z.extract('Bricks25_col.jpg') + + >>> from numpy.linalg import inv + >>> from skimage.transform import rescale, warp, rotate + >>> from skimage.color import rgb2gray + >>> from imageio import imread, imwrite + >>> from skimage import img_as_ubyte + >>> import numpy as np + + + >>> # Obtained playing around with GIMP 2.10 with their perspective tool + >>> H = inv(np.asarray([[ 0.54764, -0.00219, 0], + ... [-0.12822, 0.54688, 0], + ... [-0.00022, 0, 1]])) + + + >>> brick_orig = imread('Bricks25_col.jpg') + >>> brick = warp(brick_orig, H) + >>> brick = rescale(brick[:1024, :1024], (0.5, 0.5, 1)) + >>> brick = rotate(brick, -90) + >>> imwrite('brick.png', img_as_ubyte(rgb2gray(brick))) + """ + return _load("data/brick.png", as_gray=True) + + +def grass(): + """Grass. + + Returns + ------- + grass : (512, 512) uint8 image + Some grass. + + Notes + ----- + The original image was downloaded from + `DeviantArt `__ + and licensed under the Creative Commons CC0 License. + + The downloaded image was cropped to include a region of ``(512, 512)`` + pixels around the top left corner, converted to grayscale, then to uint8 + prior to saving the result in PNG format. + + """ + + """ + The following code was used to obtain the final image. + + >>> import sys; print(sys.version) + >>> import platform; print(platform.platform()) + >>> import skimage; print(f'scikit-image version: {skimage.__version__}') + >>> import numpy; print(f'numpy version: {numpy.__version__}') + >>> import imageio; print(f'imageio version {imageio.__version__}') + 3.7.3 | packaged by conda-forge | (default, Jul 1 2019, 21:52:21) + [GCC 7.3.0] + Linux-5.0.0-20-generic-x86_64-with-debian-buster-sid + scikit-image version: 0.16.dev0 + numpy version: 1.16.4 + imageio version 2.4.1 + + >>> import requests + >>> import zipfile + >>> url = 'https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/a407467e-4ff0-49f1-923f-c9e388e84612/d76wfef-2878b78d-5dce-43f9-be36-26ec9bc0df3b.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7InBhdGgiOiJcL2ZcL2E0MDc0NjdlLTRmZjAtNDlmMS05MjNmLWM5ZTM4OGU4NDYxMlwvZDc2d2ZlZi0yODc4Yjc4ZC01ZGNlLTQzZjktYmUzNi0yNmVjOWJjMGRmM2IuanBnIn1dXSwiYXVkIjpbInVybjpzZXJ2aWNlOmZpbGUuZG93bmxvYWQiXX0.98hIcOTCqXWQ67Ec5bM5eovKEn2p91mWB3uedH61ynI' + >>> r = requests.get(url) + >>> with open('grass_orig.jpg', 'bw') as f: + ... f.write(r.content) + >>> grass_orig = imageio.imread('grass_orig.jpg') + >>> grass = skimage.img_as_ubyte(skimage.color.rgb2gray(grass_orig[:512, :512])) + >>> imageio.imwrite('grass.png', grass) + """ + return _load("data/grass.png", as_gray=True) + + +def gravel(): + """Gravel + + Returns + ------- + gravel : (512, 512) uint8 image + Grayscale gravel sample. + + Notes + ----- + The original image was downloaded from + `CC0Textures `__ and + licensed under the Creative Commons CC0 License. + + The downloaded image was then rescaled to ``(1024, 1024)``, then the + top left ``(512, 512)`` pixel region was cropped prior to converting the + image to grayscale and uint8 data type. The result was saved using the + PNG format. + """ + + """ + The following code was used to obtain the final image. + + >>> import sys; print(sys.version) + >>> import platform; print(platform.platform()) + >>> import skimage; print(f'scikit-image version: {skimage.__version__}') + >>> import numpy; print(f'numpy version: {numpy.__version__}') + >>> import imageio; print(f'imageio version {imageio.__version__}') + 3.7.3 | packaged by conda-forge | (default, Jul 1 2019, 21:52:21) + [GCC 7.3.0] + Linux-5.0.0-20-generic-x86_64-with-debian-buster-sid + scikit-image version: 0.16.dev0 + numpy version: 1.16.4 + imageio version 2.4.1 + + >>> import requests + >>> import zipfile + + >>> url = 'https://cdn.struffelproductions.com/file/cc0textures/Gravel04/%5B2K%5DGravel04.zip' + >>> r = requests.get(url) + >>> with open('[2K]Gravel04.zip', 'bw') as f: + ... f.write(r.content) + + >>> with zipfile.ZipFile('[2K]Gravel04.zip') as z: + ... z.extract('Gravel04_col.jpg') + + >>> from skimage.transform import resize + >>> gravel_orig = imageio.imread('Gravel04_col.jpg') + >>> gravel = resize(gravel_orig, (1024, 1024)) + >>> gravel = skimage.img_as_ubyte(skimage.color.rgb2gray(gravel[:512, :512])) + >>> imageio.imwrite('gravel.png', gravel) + """ + return _load("data/gravel.png", as_gray=True) + + +def text(): + """Gray-level "text" image used for corner detection. + + Notes + ----- + This image was downloaded from Wikipedia + `__. + + No known copyright restrictions, released into the public domain. + + Returns + ------- + text : (172, 448) uint8 ndarray + Text image. + """ + + return _load("data/text.png") + + +def checkerboard(): + """Checkerboard image. + + Checkerboards are often used in image calibration, since the + corner-points are easy to locate. Because of the many parallel + edges, they also visualise distortions particularly well. + + Returns + ------- + checkerboard : (200, 200) uint8 ndarray + Checkerboard image. + """ + return _load("data/chessboard_GRAY.png") + + +def cells3d(): + """3D fluorescence microscopy image of cells. + + The returned data is a 3D multichannel array with dimensions provided in + ``(z, c, y, x)`` order. Each voxel has a size of ``(0.29 0.26 0.26)`` + micrometer. Channel 0 contains cell membranes, channel 1 contains nuclei. + + Returns + ------- + cells3d: (60, 2, 256, 256) uint16 ndarray + The volumetric images of cells taken with an optical microscope. + + Notes + ----- + The data for this was provided by the Allen Institute for Cell Science. + + It has been downsampled by a factor of 4 in the row and column dimensions + to reduce computational time. + + The microscope reports the following voxel spacing in microns: + + * Original voxel size is ``(0.290, 0.065, 0.065)``. + * Scaling factor is ``(1, 4, 4)`` in each dimension. + * After rescaling the voxel size is ``(0.29 0.26 0.26)``. + """ + + return _load("data/cells3d.tif") + + +def human_mitosis(): + """Image of human cells undergoing mitosis. + + Returns + ------- + human_mitosis: (512, 512) uint8 ndarray + Data of human cells undergoing mitosis taken during the preparation + of the manuscript in [1]_. + + Notes + ----- + Copyright David Root. Licensed under CC-0 [2]_. + + References + ---------- + .. [1] Moffat J, Grueneberg DA, Yang X, Kim SY, Kloepfer AM, Hinkle G, + Piqani B, Eisenhaure TM, Luo B, Grenier JK, Carpenter AE, Foo SY, + Stewart SA, Stockwell BR, Hacohen N, Hahn WC, Lander ES, + Sabatini DM, Root DE (2006) A lentiviral RNAi library for human and + mouse genes applied to an arrayed viral high-content screen. Cell, + 124(6):1283-98 / :DOI: `10.1016/j.cell.2006.01.040` PMID 16564017 + + .. [2] GitHub licensing discussion + https://github.com/CellProfiler/examples/issues/41 + + """ + return _load('data/mitosis.tif') + + +def cell(): + """Cell floating in saline. + + This is a quantitative phase image retrieved from a digital hologram using + the Python library ``qpformat``. The image shows a cell with high phase + value, above the background phase. + + Because of a banding pattern artifact in the background, this image is a + good test of thresholding algorithms. The pixel spacing is 0.107 µm. + + These data were part of a comparison between several refractive index + retrieval techniques for spherical objects as part of [1]_. + + This image is CC0, dedicated to the public domain. You may copy, modify, or + distribute it without asking permission. + + Returns + ------- + cell : (660, 550) uint8 array + Image of a cell. + + References + ---------- + .. [1] Paul Müller, Mirjam Schürmann, Salvatore Girardo, Gheorghe Cojoc, + and Jochen Guck. "Accurate evaluation of size and refractive index + for spherical objects in quantitative phase imaging." Optics Express + 26(8): 10729-10743 (2018). :DOI:`10.1364/OE.26.010729` + """ + return _load('data/cell.png') + + +def coins(): + """Greek coins from Pompeii. + + This image shows several coins outlined against a gray background. + It is especially useful in, e.g. segmentation tests, where + individual objects need to be identified against a background. + The background shares enough grey levels with the coins that a + simple segmentation is not sufficient. + + Notes + ----- + This image was downloaded from the + `Brooklyn Museum Collection + `__. + + No known copyright restrictions. + + Returns + ------- + coins : (303, 384) uint8 ndarray + Coins image. + """ + return _load("data/coins.png") + + +def kidney(): + """Mouse kidney tissue. + + This biological tissue on a pre-prepared slide was imaged with confocal + fluorescence microscopy (Nikon C1 inverted microscope). + Image shape is (16, 512, 512, 3). That is 512x512 pixels in X-Y, + 16 image slices in Z, and 3 color channels + (emission wavelengths 450nm, 515nm, and 605nm, respectively). + Real-space voxel size is 1.24 microns in X-Y, and 1.25 microns in Z. + Data type is unsigned 16-bit integers. + + Notes + ----- + This image was acquired by Genevieve Buckley at Monasoh Micro Imaging in + 2018. + License: CC0 + + Returns + ------- + kidney : (16, 512, 512, 3) uint16 ndarray + Kidney 3D multichannel image. + """ + return _load("data/kidney.tif") + + +def lily(): + """Lily of the valley plant stem. + + This plant stem on a pre-prepared slide was imaged with confocal + fluorescence microscopy (Nikon C1 inverted microscope). + Image shape is (922, 922, 4). That is 922x922 pixels in X-Y, + with 4 color channels. + Real-space voxel size is 1.24 microns in X-Y. + Data type is unsigned 16-bit integers. + + Notes + ----- + This image was acquired by Genevieve Buckley at Monasoh Micro Imaging in + 2018. + License: CC0 + + Returns + ------- + lily : (922, 922, 4) uint16 ndarray + Lily 2D multichannel image. + """ + return _load("data/lily.tif") + + +def logo(): + """Scikit-image logo, a RGBA image. + + Returns + ------- + logo : (500, 500, 4) uint8 ndarray + Logo image. + """ + return _load("data/logo.png") + + +def microaneurysms(): + """Gray-level "microaneurysms" image. + + Detail from an image of the retina (green channel). + The image is a crop of image 07_dr.JPG from the + High-Resolution Fundus (HRF) Image Database: + https://www5.cs.fau.de/research/data/fundus-images/ + + Notes + ----- + No copyright restrictions. CC0 given by owner (Andreas Maier). + + Returns + ------- + microaneurysms : (102, 102) uint8 ndarray + Retina image with lesions. + + References + ---------- + .. [1] Budai, A., Bock, R, Maier, A., Hornegger, J., + Michelson, G. (2013). Robust Vessel Segmentation in Fundus + Images. International Journal of Biomedical Imaging, vol. 2013, + 2013. + :DOI:`10.1155/2013/154860` + """ + return _load("data/microaneurysms.png") + + +def moon(): + """Surface of the moon. + + This low-contrast image of the surface of the moon is useful for + illustrating histogram equalization and contrast stretching. + + Returns + ------- + moon : (512, 512) uint8 ndarray + Moon image. + """ + return _load("data/moon.png") + + +def page(): + """Scanned page. + + This image of printed text is useful for demonstrations requiring uneven + background illumination. + + Returns + ------- + page : (191, 384) uint8 ndarray + Page image. + """ + return _load("data/page.png") + + +def horse(): + """Black and white silhouette of a horse. + + This image was downloaded from + `openclipart ` + + No copyright restrictions. CC0 given by owner (Andreas Preuss (marauder)). + + Returns + ------- + horse : (328, 400) bool ndarray + Horse image. + """ + return img_as_bool(_load("data/horse.png", as_gray=True)) + + +def clock(): + """Motion blurred clock. + + This photograph of a wall clock was taken while moving the camera in an + approximately horizontal direction. It may be used to illustrate + inverse filters and deconvolution. + + Released into the public domain by the photographer (Stefan van der Walt). + + Returns + ------- + clock : (300, 400) uint8 ndarray + Clock image. + """ + return _load("data/clock_motion.png") + + +def immunohistochemistry(): + """Immunohistochemical (IHC) staining with hematoxylin counterstaining. + + This picture shows colonic glands where the IHC expression of FHL2 protein + is revealed with DAB. Hematoxylin counterstaining is applied to enhance the + negative parts of the tissue. + + This image was acquired at the Center for Microscopy And Molecular Imaging + (CMMI). + + No known copyright restrictions. + + Returns + ------- + immunohistochemistry : (512, 512, 3) uint8 ndarray + Immunohistochemistry image. + """ + return _load("data/ihc.png") + + +def chelsea(): + """Chelsea the cat. + + An example with texture, prominent edges in horizontal and diagonal + directions, as well as features of differing scales. + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Stefan van der Walt). + + Returns + ------- + chelsea : (300, 451, 3) uint8 ndarray + Chelsea image. + """ + return _load("data/chelsea.png") + + +# Define an alias for chelsea that is more descriptive. +cat = chelsea + + +def coffee(): + """Coffee cup. + + This photograph is courtesy of Pikolo Espresso Bar. + It contains several elliptical shapes as well as varying texture (smooth + porcelain to coarse wood grain). + + Notes + ----- + No copyright restrictions. CC0 by the photographer (Rachel Michetti). + + Returns + ------- + coffee : (400, 600, 3) uint8 ndarray + Coffee image. + """ + return _load("data/coffee.png") + + +def hubble_deep_field(): + """Hubble eXtreme Deep Field. + + This photograph contains the Hubble Telescope's farthest ever view of + the universe. It can be useful as an example for multi-scale + detection. + + Notes + ----- + This image was downloaded from + `HubbleSite + `__. + + The image was captured by NASA and `may be freely used in the public domain + `_. + + Returns + ------- + hubble_deep_field : (872, 1000, 3) uint8 ndarray + Hubble deep field image. + """ + return _load("data/hubble_deep_field.jpg") + + +def retina(): + """Human retina. + + This image of a retina is useful for demonstrations requiring circular + images. + + Notes + ----- + This image was downloaded from + `wikimedia `. + This file is made available under the Creative Commons CC0 1.0 Universal + Public Domain Dedication. + + References + ---------- + .. [1] Häggström, Mikael (2014). "Medical gallery of Mikael Häggström 2014". + WikiJournal of Medicine 1 (2). :DOI:`10.15347/wjm/2014.008`. + ISSN 2002-4436. Public Domain + + Returns + ------- + retina : (1411, 1411, 3) uint8 ndarray + Retina image in RGB. + """ + return _load("data/retina.jpg") + + +def shepp_logan_phantom(): + """Shepp Logan Phantom. + + References + ---------- + .. [1] L. A. Shepp and B. F. Logan, "The Fourier reconstruction of a head + section," in IEEE Transactions on Nuclear Science, vol. 21, + no. 3, pp. 21-43, June 1974. :DOI:`10.1109/TNS.1974.6499235` + + Returns + ------- + phantom : (400, 400) float64 image + Image of the Shepp-Logan phantom in grayscale. + """ + return _load("data/phantom.png", as_gray=True) + + +def colorwheel(): + """Color Wheel. + + Returns + ------- + colorwheel : (370, 371, 3) uint8 image + A colorwheel. + """ + return _load("data/color.png") + + +def palisades_of_vogt(): + """Return image sequence of in-vivo tissue showing the palisades of Vogt. + + In the human eye, the palisades of Vogt are normal features of the corneal + limbus, which is the border between the cornea and the sclera (i.e., the + white of the eye). + In the image sequence, there are some dark spots due to the presence of + dust on the reference mirror. + + Returns + ------- + palisades_of_vogt: (60, 1440, 1440) uint16 ndarray + + Notes + ----- + See info under `in-vivo-cornea-spots.tif` at + https://gitlab.com/scikit-image/data/-/blob/master/README.md#data. + + """ + return _load('data/palisades_of_vogt.tif') + + +def rocket(): + """Launch photo of DSCOVR on Falcon 9 by SpaceX. + + This is the launch photo of Falcon 9 carrying DSCOVR lifted off from + SpaceX's Launch Complex 40 at Cape Canaveral Air Force Station, FL. + + Notes + ----- + This image was downloaded from + `SpaceX Photos + `__. + + The image was captured by SpaceX and `released in the public domain + `_. + + Returns + ------- + rocket : (427, 640, 3) uint8 ndarray + Rocket image. + """ + return _load("data/rocket.jpg") + + +def stereo_motorcycle(): + """Rectified stereo image pair with ground-truth disparities. + + The two images are rectified such that every pixel in the left image has + its corresponding pixel on the same scanline in the right image. That means + that both images are warped such that they have the same orientation but a + horizontal spatial offset (baseline). The ground-truth pixel offset in + column direction is specified by the included disparity map. + + The two images are part of the Middlebury 2014 stereo benchmark. The + dataset was created by Nera Nesic, Porter Westling, Xi Wang, York Kitajima, + Greg Krathwohl, and Daniel Scharstein at Middlebury College. A detailed + description of the acquisition process can be found in [1]_. + + The images included here are down-sampled versions of the default exposure + images in the benchmark. The images are down-sampled by a factor of 4 using + the function `skimage.transform.downscale_local_mean`. The calibration data + in the following and the included ground-truth disparity map are valid for + the down-sampled images:: + + Focal length: 994.978px + Principal point x: 311.193px + Principal point y: 254.877px + Principal point dx: 31.086px + Baseline: 193.001mm + + Returns + ------- + img_left : (500, 741, 3) uint8 ndarray + Left stereo image. + img_right : (500, 741, 3) uint8 ndarray + Right stereo image. + disp : (500, 741, 3) float ndarray + Ground-truth disparity map, where each value describes the offset in + column direction between corresponding pixels in the left and the right + stereo images. E.g. the corresponding pixel of + ``img_left[10, 10 + disp[10, 10]]`` is ``img_right[10, 10]``. + NaNs denote pixels in the left image that do not have ground-truth. + + Notes + ----- + The original resolution images, images with different exposure and + lighting, and ground-truth depth maps can be found at the Middlebury + website [2]_. + + References + ---------- + .. [1] D. Scharstein, H. Hirschmueller, Y. Kitajima, G. Krathwohl, N. + Nesic, X. Wang, and P. Westling. High-resolution stereo datasets + with subpixel-accurate ground truth. In German Conference on Pattern + Recognition (GCPR 2014), Muenster, Germany, September 2014. + .. [2] http://vision.middlebury.edu/stereo/data/scenes2014/ + + """ + filename = _fetch("data/motorcycle_disp.npz") + disp = np.load(filename)['arr_0'] + return (_load("data/motorcycle_left.png"), _load("data/motorcycle_right.png"), disp) + + +def lfw_subset(): + """Subset of data from the LFW dataset. + + This database is a subset of the LFW database containing: + + * 100 faces + * 100 non-faces + + The full dataset is available at [2]_. + + Returns + ------- + images : (200, 25, 25) uint8 ndarray + 100 first images are faces and subsequent 100 are non-faces. + + Notes + ----- + The faces were randomly selected from the LFW dataset and the non-faces + were extracted from the background of the same dataset. The cropped ROIs + have been resized to a 25 x 25 pixels. + + References + ---------- + .. [1] Huang, G., Mattar, M., Lee, H., & Learned-Miller, E. G. (2012). + Learning to align from scratch. In Advances in Neural Information + Processing Systems (pp. 764-772). + .. [2] http://vis-www.cs.umass.edu/lfw/ + + """ + return np.load(_fetch('data/lfw_subset.npy')) + + +def skin(): + """Microscopy image of dermis and epidermis (skin layers). + + Hematoxylin and eosin stained slide at 10x of normal epidermis and dermis + with a benign intradermal nevus. + + Notes + ----- + This image requires an Internet connection the first time it is called, + and to have the ``pooch`` package installed, in order to fetch the image + file from the scikit-image datasets repository. + + The source of this image is + https://en.wikipedia.org/wiki/File:Normal_Epidermis_and_Dermis_with_Intradermal_Nevus_10x.JPG + + The image was released in the public domain by its author Kilbad. + + Returns + ------- + skin : (960, 1280, 3) RGB image of uint8 + """ + return _load('data/skin.jpg') + + +def nickel_solidification(): + """Image sequence of synchrotron x-radiographs showing the rapid + solidification of a nickel alloy sample. + + Returns + ------- + nickel_solidification: (11, 384, 512) uint16 ndarray + + Notes + ----- + See info under `nickel_solidification.tif` at + https://gitlab.com/scikit-image/data/-/blob/master/README.md#data. + + """ + return _load('data/solidification.tif') + + +def protein_transport(): + """Microscopy image sequence with fluorescence tagging of proteins + re-localizing from the cytoplasmic area to the nuclear envelope. + + Returns + ------- + protein_transport: (15, 2, 180, 183) uint8 ndarray + + Notes + ----- + See info under `NPCsingleNucleus.tif` at + https://gitlab.com/scikit-image/data/-/blob/master/README.md#data. + + """ + return _load('data/protein_transport.tif') + + +def brain(): + """Subset of data from the University of North Carolina Volume Rendering + Test Data Set. + + The full dataset is available at [1]_. + + Returns + ------- + image : (10, 256, 256) uint16 ndarray + + Notes + ----- + The 3D volume consists of 10 layers from the larger volume. + + References + ---------- + .. [1] https://graphics.stanford.edu/data/voldata/ + + """ + return _load("data/brain.tiff") + + +def vortex(): + """Case B1 image pair from the first PIV challenge. + + Returns + ------- + image0, image1 : (512, 512) grayscale images + A pair of images featuring synthetic moving particles. + + Notes + ----- + This image was licensed as CC0 by its author, Prof. Koji Okamoto, with + thanks to Prof. Jun Sakakibara, who maintains the PIV Challenge site. + + References + ---------- + .. [1] Particle Image Velocimetry (PIV) Challenge site + http://pivchallenge.org + .. [2] 1st PIV challenge Case B: http://pivchallenge.org/pub/index.html#b + """ + return ( + _load('data/pivchallenge-B-B001_1.tif'), + _load('data/pivchallenge-B-B001_2.tif'), + ) diff --git a/lib/python3.10/site-packages/skimage/data/_registry.py b/lib/python3.10/site-packages/skimage/data/_registry.py new file mode 100644 index 0000000000000000000000000000000000000000..5e4423441e80daa6029b0c44b04dc391095b99d6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/_registry.py @@ -0,0 +1,187 @@ +# This minimal dataset was available as part of +# scikit-image 0.15 and will be retained until +# further notice. +# Testing data and additional datasets should only +# be made available by pooch +legacy_datasets = [ + 'astronaut.png', + 'brick.png', + 'camera.png', + 'chessboard_GRAY.png', + 'chessboard_RGB.png', + 'chelsea.png', + 'clock_motion.png', + 'coffee.png', + 'coins.png', + 'color.png', + 'cell.png', + 'grass.png', + 'gravel.png', + 'horse.png', + 'hubble_deep_field.jpg', + 'ihc.png', + 'lbpcascade_frontalface_opencv.xml', + 'lfw_subset.npy', + 'logo.png', + 'microaneurysms.png', + 'moon.png', + 'multipage.tif', + 'multipage_rgb.tif', + 'no_time_for_that_tiny.gif', + 'page.png', + 'text.png', + 'retina.jpg', + 'rocket.jpg', + 'phantom.png', + 'motorcycle_disp.npz', + 'motorcycle_left.png', + 'motorcycle_right.png', +] + +# Registry of datafiles that can be downloaded along with their SHA256 hashes +# To generate the SHA256 hash, use the command +# openssl sha256 filename +registry = { + "color/tests/data/lab_array_a_10.npy": "a3ef76f1530e374f9121020f1f220bc89767dc866f4bbd1b1f47e5b84891a38c", + "color/tests/data/lab_array_a_2.npy": "793d5981cbffceb14b5fb589f998a2b1acdb5ff9c14d364c8e9e8bd45a80b275", + "color/tests/data/lab_array_a_r.npy": "3d3613da109d0c87827525fc49b58111aefc12438fa6426654979f66807b9227", + "color/tests/data/lab_array_b_10.npy": "e8d648b28077c1bfcef55ec6dc8679819612b56a01647f8c0a78625bb06f99b6", + "color/tests/data/lab_array_b_2.npy": "da9c6aa99e4ab3af8ec3107bbf11647cc483a0760285dd5c9fb66988be393ca1", + "color/tests/data/lab_array_b_r.npy": "d9eee96f4d65a2fbba82039508aac8c18304752ee8e33233e2a013e65bb91464", + "color/tests/data/lab_array_c_10.npy": "88b4ff2a2d2c4f48e7bb265609221d4b9ef439a4e2d8a86989696bfdb47790e6", + "color/tests/data/lab_array_c_2.npy": "e1b8acfdc7284ab9cd339de66948134304073b6f734ecf9ad42f8297b83d3405", + "color/tests/data/lab_array_c_r.npy": "09ffba2ed69e467864fea883493cd2d2706da028433464e3e858a8086842867e", + "color/tests/data/lab_array_d50_10.npy": "42e2ff26cb10e2a98fcf1bc06c2483302ff4fabf971fe8d49b530f490b5d24c7", + "color/tests/data/lab_array_d50_2.npy": "4aa03b7018ff7276643d3c082123cf07304f9d8d898ae92a5756a86955de4faf", + "color/tests/data/lab_array_d50_r.npy": "57db02009f9a68dade33ce1ecffead0418d8ac8113b2a589fc02a20e6bf7e799", + "color/tests/data/lab_array_d55_10.npy": "ab4f21368b6d8351578ab093381c44b49ae87a6b7f25c11aa094b07f215eed7d", + "color/tests/data/lab_array_d55_2.npy": "0319723de4632a252bae828b7c96d038fb075a7df05beadfbad653da05efe372", + "color/tests/data/lab_array_d55_r.npy": "060ebc446f7b4da4df58a60f0006133dbca735da87ba61854f4a75d28db67a3a", + "color/tests/data/lab_array_d65_10.npy": "5cb9e9c384d2577aaf8b7d2d21ff5b505708b80605a2f59d10e89d22c3d308d2", + "color/tests/data/lab_array_d65_2.npy": "16e847160f7ba4f19806d8194ed44a6654c9367e5a2cb240aa6e7eece44a6649", + "color/tests/data/lab_array_d65_r.npy": "82d0dd7a46741f627b8868793e64cdc2f9944fe1e049b573f752a93760a1577c", + "color/tests/data/lab_array_d75_10.npy": "c2d3de5422c785c925926b0c6223aeaf50b9393619d1c30830190d433606cbe1", + "color/tests/data/lab_array_d75_2.npy": "c94d53da398d36e076471ff7e0dafcaffc64ce4ba33b4d04849c32d19c87494a", + "color/tests/data/lab_array_e_2.npy": "ac05f17a83961b020ceccbdd46bddc86943d43e678dabcc898caf4a1e4be6165", + "color/tests/data/luv_array_a_10.npy": "c8af67f9fd64a6e9c610ac0c12c5315a49ca229363f048e5d851409d4a3ae5b6", + "color/tests/data/luv_array_a_2.npy": "eaf05dc61f4a70ece367d5e751a14d42b7c397c7b1c2df4cfecec9ddf26e1c1a", + "color/tests/data/luv_array_a_r.npy": "2c0891add787ec757601f9c61ad14dd9621dd969af4e32753f2e64df437081b7", + "color/tests/data/luv_array_b_10.npy": "a5407736b8a43071139ca178d12cdf930f32f52a0644f0b13f89d8895c8b43db", + "color/tests/data/luv_array_b_2.npy": "8e74173d54dc549b6c0ebd1f1d70489d2905cad87744e41ed74384f21f22986d", + "color/tests/data/luv_array_b_r.npy": "0a74c41df369cbb5fc0a00c16d60dc6f946ebf144bc5e506545b0d160fa53dfa", + "color/tests/data/luv_array_c_10.npy": "3a5f975ffa57f69a1be9e02b153e8161f83040ce3002ea1b0a05b9fbdd0d8ec4", + "color/tests/data/luv_array_c_2.npy": "32506cd50ea2181997cb88d3511e275740e8151d6c693cd178f5eafd8b0c6e47", + "color/tests/data/luv_array_c_r.npy": "c0fbf98cc0e62ed426ab4d228986d6660089444a7bbfcc64cbb1c632644067bb", + "color/tests/data/luv_array_d50_10.npy": "fe223db556222ce3a59198bed3a3324c2c719b8083fb84dc5b00f214b4773b16", + "color/tests/data/luv_array_d50_2.npy": "48e8989048904bdf2c3c1ada265c1c29c5eff60f02f848a25cde622982c84901", + "color/tests/data/luv_array_d50_r.npy": "f93f0def9c93f872dd10ce4a91fdb3f06eea61ddb6e72387b7669909827d4f9c", + "color/tests/data/luv_array_d55_10.npy": "d88d53d2bad230c2331442187712ec52ffdee62bf0f60b200c33411bfed76c60", + "color/tests/data/luv_array_d55_2.npy": "c761b40475df591ae9c0475d54ef712d067190ca4652efc6308b69080a652061", + "color/tests/data/luv_array_d55_r.npy": "05fbd57e3602ee4d5202b9f18f9b5fc05b545891a9b4456d2a88aa798a5a774a", + "color/tests/data/luv_array_d65_10.npy": "41a5452ffac4d31dd579d9528e725432c60d77b5f505d801898d9401429c89bf", + "color/tests/data/luv_array_d65_2.npy": "962ce180132c6c11798cbc423b2b204d1d10187670f6eb5dec1058eaad301e0e", + "color/tests/data/luv_array_d65_r.npy": "78db8c19af26dd802ce98b039a33855f7c8d6a103a2721d094b1d9c619717449", + "color/tests/data/luv_array_d75_10.npy": "e1cc70d56eb6789633d4c2a4059b9533f616a7c8592c9bd342403e41d72f45e4", + "color/tests/data/luv_array_d75_2.npy": "07db3bd59bd89de8e5ff62dad786fe5f4b299133495ba9bea30495b375133a98", + "color/tests/data/luv_array_e_2.npy": "41b1037d81b267305ffe9e8e97e0affa9fa54b18e60413b01b8f11861cb32213", + "color/tests/ciede2000_test_data.txt": "2e005c6f76ddfb7bbcc8f68490f1f7b4b4a2a4b06b36a80c985677a2799c0e40", + "data/astronaut.png": "88431cd9653ccd539741b555fb0a46b61558b301d4110412b5bc28b5e3ea6cb5", + "data/brick.png": "7966caf324f6ba843118d98f7a07746d22f6a343430add0233eca5f6eaaa8fcf", + "data/cell.png": "8d23a7fb81f7cc877cd09f330357fc7f595651306e84e17252f6e0a1b3f61515", + "data/camera.png": "b0793d2adda0fa6ae899c03989482bff9a42d3d5690fc7e3648f2795d730c23a", + "data/chessboard_GRAY.png": "3e51870774515af4d07d820bd8827364c70839bf9b573c746e485095e893df90", + "data/chessboard_RGB.png": "1ac01eff2d4e50f4eda55a2ddecdc28a6576623a58d7a7ef84513c5cc19a0331", + "data/chelsea.png": "596aa1e7cb875eb79f437e310381d26b338a81c2da23439704a73c4651e8c4bb", + "data/clock_motion.png": "f029226b28b642e80113d86622e9b215ee067a0966feaf5e60604a1e05733955", + "data/coffee.png": "cc02f8ca188b167c775a7101b5d767d1e71792cf762c33d6fa15a4599b5a8de7", + "data/coins.png": "f8d773fc9cfa6f4d8e5942dc34d0a0788fcaed2a4fefbbed0aef5398d7ef4cba", + "data/color.png": "7d2df993de2b4fa2a78e04e5df8050f49a9c511aa75e59ab3bd56ac9c98aef7e", + "data/eagle.png": "928f1bbe7403b533265f56db3a6b07c835dfa8e2513f5c5075ca2f1960f6179e", + "data/horse.png": "c7fb60789fe394c485f842291ea3b21e50d140f39d6dcb5fb9917cc178225455", + "data/grass.png": "b6b6022426b38936c43a4ac09635cd78af074e90f42ffa8227ac8b7452d39f89", + "data/hubble_deep_field.jpg": "3a19c5dd8a927a9334bb1229a6d63711b1c0c767fb27e2286e7c84a3e2c2f5f4", + "data/ihc.png": "f8dd1aa387ddd1f49d8ad13b50921b237df8e9b262606d258770687b0ef93cef", + "data/logo.png": "f2c57fe8af089f08b5ba523d95573c26e62904ac5967f4c8851b27d033690168", + "data/lfw_subset.npy": "9560ec2f5edfac01973f63a8a99d00053fecd11e21877e18038fbe500f8e872c", + "data/microaneurysms.png": "a1e1be59aa447f8ce082f7fa809997ab369a2b137cb6c4202abc647c7ccf6456", + "data/moon.png": "78739619d11f7eb9c165bb5d2efd4772cee557812ec847532dbb1d92ef71f577", + "data/motorcycle_left.png": "db18e9c4157617403c3537a6ba355dfeafe9a7eabb6b9b94cb33f6525dd49179", + "data/motorcycle_right.png": "5fc913ae870e42a4b662314bc904d1786bcad8e2f0b9b67dba5a229406357797", + "data/motorcycle_disp.npz": "2e49c8cebff3fa20359a0cc6880c82e1c03bbb106da81a177218281bc2f113d7", + "data/mssim_matlab_output.npz": "cc11a14bfa040c75b02db32282439f2e2e3e96779196c171498afaa70528ed7a", + "data/page.png": "341a6f0a61557662b02734a9b6e56ec33a915b2c41886b97509dedf2a43b47a3", + "data/phantom.png": "552ff698167aa402cceb17981130607a228a0a0aa7c519299eaa4d5f301ba36c", + "data/retina.jpg": "38a07f36f27f095e818aea7b96d34202c05176d30253c66733f2e00379e9e0e6", + "data/rocket.jpg": "c2dd0de7c538df8d111e479619b129464d0269d0ae5fd18ca91d33a7fdfea95c", + "data/gravel.png": "c48615b451bf1e606fbd72c0aa9f8cc0f068ab7111ef7d93bb9b0f2586440c12", + "data/text.png": "bd84aa3a6e3c9887850d45d606c96b2e59433fbef50338570b63c319e668e6d1", + "data/chessboard_GRAY_U16.tif": "9fd3392c5b6cbc5f686d8ff83eb57ef91d038ee0852ac26817e5ac99df4c7f45", + "data/chessboard_GRAY_U16B.tif": "b0a9270751f0fc340c90b8b615b62b88187b9ab5995942717566735d523cddb2", + "data/chessboard_GRAY_U8.npy": "71f394694b721e8a33760a355b3666c9b7d7fc1188ff96b3cd23c2a1d73a38d8", + "data/lbpcascade_frontalface_opencv.xml": "03097789a3dcbb0e40d20b9ef82537dbc3b670b6a7f2268d735470f22e003a91", + "data/astronaut_GRAY_hog_L1.npy": "5d8ab22b166d1dd49c12caeff9d178ed28132efea3852b952e9d75f7f7f94954", + "data/astronaut_GRAY_hog_L2-Hys.npy": "c4dd6e50d1129aada358311cf8880ce8c775f31e0e550fc322c16e43a96d56fe", + "data/rank_filter_tests.npz": "efaf5699630f4a53255e91681dc72a965acd4a8aa1f84671c686fb93e7df046d", + "data/rank_filters_tests_3d.npz": "1741c2b978424e93558a07d345b2a0d9bfbb33c095c123da147fca066714ab16", + "data/multi.fits": "5c71a83436762a52b1925f2f0d83881af7765ed50aede155af2800e54bbd5040", + "data/simple.fits": "cd36087fdbb909b6ba506bbff6bcd4c5f4da3a41862608fbac5e8555ef53d40f", + "data/palette_color.png": "c4e817035fb9f7730fe95cff1da3866dea01728efc72b6e703d78f7ab9717bdd", + "data/palette_gray.png": "bace7f73783bf3ab3b7fdaf701707e4fa09f0dbd0ea72cf5b12ddc73d50b02a9", + "data/green_palette.png": "42d49d94be8f9bc76e50639d3701ed0484258721f6b0bd7f50bb1b9274a010f0", + "data/truncated.jpg": "4c226038acc78012d335efba29c6119a24444a886842182b7e18db378f4a557d", + "data/multipage.tif": "4da0ad0d3df4807a9847247d1b5e565b50d46481f643afb5c37c14802c78130f", + "data/multipage_rgb.tif": "1d23b844fd38dce0e2d06f30432817cdb85e52070d8f5460a2ba58aebf34a0de", + "data/no_time_for_that_tiny.gif": "20abe94ba9e45f18de416c5fbef8d1f57a499600be40f9a200fae246010eefce", + "data/foo3x5x4indexed.png": "48a64c25c6da000ffdb5fcc34ebafe9ba3b1c9b61d7984ea7ca6dc54f9312dfa", + "data/gray_morph_output.npz": "49a0dae607cd8d31e134b4bfcbf0d86b13751fdce9667d8bf1ade93d435191b1", + "data/disk-matlab-output.npz": "8a39d5c866f6216d6a9c9166312aa4bbf4d18fab3d0dcd963c024985bde5856b", + "data/diamond-matlab-output.npz": "02fca68907e2b252b501dfe977eef71ae39fadaaa3702ebdc855195422ae1cc2", + "data/bw_text.png": "308c2b09f8975a69b212e103b18520e8cbb7a4eccfce0f757836cd371f1b9094", + "data/bw_text_skeleton.npy": "9ff4fc23c6a01497d7987f14e3a97cbcc39cce54b2b3b7ee33b84c1b661d0ae1", + "data/_blobs_3d_fiji_skeleton.tif": "e3449ad9819425959952050c147278555e5ffe1c2c4a30df29f6a1f9023e10c3", + "data/checker_bilevel.png": "2e207e486545874a2a3e69ba653b28fdef923157be9017559540e65d1bcb8e28", + "restoration/tests/astronaut_rl.npy": "3f8373e2c6182a89366e51cef6624e3625deac75fdda1079cbdad2a33322152c", + "restoration/tests/camera_rl.npy": "fd4f59af84dd471fbbe79ee70c1b7e68a69864c461f0db5ac587e7975363f78f", + "restoration/tests/camera_unsup.npy": "3de10a0b97267352b18886b25d66a967f9e1d78ada61050577d78586cab82baa", + "restoration/tests/camera_unsup2.npy": "29cdc60605eb528c5f014baa8564d7d1ba0bd4b3170a66522058cbe5aed0960b", + "restoration/tests/camera_wiener.npy": "4505ea8b0d63d03250c6d756560d615751b76dd6ffc4a95972fa260c0c84633e", + "registration/tests/data/OriginalX-130Y130.png": "bf24a06d99ae131c97e582ef5e1cd0c648a8dad0caab31281f3564045492811f", + "registration/tests/data/OriginalX130Y130.png": "7fdd4c06d504fec35ee0703bd7ed2c08830b075a74c8506bae4a70d682f5a2db", + "registration/tests/data/OriginalX75Y75.png": "c5cd58893c93140df02896df80b13ecf432f5c86eeaaf8fb311aec52a65c7016", + "registration/tests/data/TransformedX-130Y130.png": "1cda90ed69c921eb7605b73b76d141cf4ea03fb8ce3336445ca08080e40d7375", + "registration/tests/data/TransformedX130Y130.png": "bb10c6ae3f91a313b0ac543efdb7ca69c4b95e55674c65a88472a6c4f4692a25", + "registration/tests/data/TransformedX75Y75.png": "a1e9ead5f8e4a0f604271e1f9c50e89baf53f068f1d19fab2876af4938e695ea", + "data/brain.tiff": "bcdbaf424fbad7b1fb0f855f608c68e5a838f35affc323ff04ea17f678eef5c6", + "data/cells3d.tif": "afc7c7d80d38bfde09788b4064ac1e64ec14e88454ab785ebdc8dbba5ca3b222", + "data/palisades_of_vogt.tif": "7f205b626407e194974cead67c4d3909344cf59c42d229a349da0198183e5bd0", + "data/kidney.tif": "80c0799bc58b08cf6eaa53ecd202305eb42fd7bc73746cb6c5064dbeae7e8476", + "data/lily.tif": "395c2f0194c25b9824a8cd79266920362a0816bc9e906dd392adce2d8309af03", + "data/mitosis.tif": "2751ba667c4067c5d30817cff004aa06f6f6287f1cdbb5b8c9c6a500308cb456", + "data/skin.jpg": "8759fe080509712163453f4b17106582b8513e73b0788d80160abf840e272075", + "data/pivchallenge-B-B001_1.tif": "e95e09abbcecba723df283ac7d361766328abd943701a2ec2f345d4a2014da2a", + "data/pivchallenge-B-B001_2.tif": "4ceb5407e4e333476a0f264c14b7a3f6c0e753fcdc99ee1c4b8196e5f823805e", + "data/protein_transport.tif": "a8e24e8d187f33e92ee28508d5615286c850ca75374af7e74e527d290e8b06ea", + "data/solidification.tif": "50ef9a52c621b7c0c506ad1fe1b8ee8a158a4d7c8e50ddfce1e273a422dca3f9", +} + +registry_urls = { + "data/brain.tiff": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/brain.tiff", + "data/cells3d.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/cells3d.tif", + "data/palisades_of_vogt.tif": "https://gitlab.com/scikit-image/data/-/raw/b2bc880f3bac23a583724befe8388dae368c52fe/in-vivo-cornea-spots.tif", + "data/eagle.png": "https://gitlab.com/scikit-image/data/-/raw/1e4f62ac31ba4553d176d4473a5967ad1b076d62/eagle.png", + "data/kidney.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/kidney-tissue-fluorescence.tif", + "data/lily.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/lily-of-the-valley-fluorescence.tif", + "data/mitosis.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/AS_09125_050116030001_D03f00d0.tif", + "data/rank_filters_tests_3d.npz": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/Tests_besides_Equalize_Otsu/add18_entropy/rank_filters_tests_3d.npz", + "data/skin.jpg": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/Normal_Epidermis_and_Dermis_with_Intradermal_Nevus_10x.JPG", + "data/pivchallenge-B-B001_1.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/pivchallenge/B/B001_1.tif", + "data/pivchallenge-B-B001_2.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/pivchallenge/B/B001_2.tif", + "data/protein_transport.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/NPCsingleNucleus.tif", + "data/solidification.tif": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/nickel_solidification.tif", + "restoration/tests/astronaut_rl.npy": "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/astronaut_rl.npy", + "data/gray_morph_output.npz": "https://gitlab.com/scikit-image/data/-/raw/806548e112bcf2b708a9a32275d335cb592480fd/Tests_besides_Equalize_Otsu/gray_morph_output.npz", +} + +legacy_registry = { + ('data/' + filename): registry['data/' + filename] for filename in legacy_datasets +} diff --git a/lib/python3.10/site-packages/skimage/data/cell.png b/lib/python3.10/site-packages/skimage/data/cell.png new file mode 100644 index 0000000000000000000000000000000000000000..0970658fa2be010a4654759373fc5a11757878b8 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/cell.png differ diff --git a/lib/python3.10/site-packages/skimage/data/chessboard_GRAY.png b/lib/python3.10/site-packages/skimage/data/chessboard_GRAY.png new file mode 100644 index 0000000000000000000000000000000000000000..c7490ca4357fbf09f9f83a28731d7d400c90c3b9 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/chessboard_GRAY.png differ diff --git a/lib/python3.10/site-packages/skimage/data/chessboard_RGB.png b/lib/python3.10/site-packages/skimage/data/chessboard_RGB.png new file mode 100644 index 0000000000000000000000000000000000000000..f35c0c6ff77c4456222288bd4456d2c9d3ba71c6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/chessboard_RGB.png differ diff --git a/lib/python3.10/site-packages/skimage/data/clock_motion.png b/lib/python3.10/site-packages/skimage/data/clock_motion.png new file mode 100644 index 0000000000000000000000000000000000000000..3a81bd748692b0c4e3a44199c2c9b872db0e3b73 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/clock_motion.png differ diff --git a/lib/python3.10/site-packages/skimage/data/coins.png b/lib/python3.10/site-packages/skimage/data/coins.png new file mode 100644 index 0000000000000000000000000000000000000000..f0bebd64c6d8695b90b7a89b236f97947a009f29 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/coins.png differ diff --git a/lib/python3.10/site-packages/skimage/data/color.png b/lib/python3.10/site-packages/skimage/data/color.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd3fe482b0ccad89b7fa851de6ea35a256a5c6d Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/color.png differ diff --git a/lib/python3.10/site-packages/skimage/data/horse.png b/lib/python3.10/site-packages/skimage/data/horse.png new file mode 100644 index 0000000000000000000000000000000000000000..59f48822efda15f3e71be42bc44cb993cf12bfca Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/horse.png differ diff --git a/lib/python3.10/site-packages/skimage/data/lbpcascade_frontalface_opencv.xml b/lib/python3.10/site-packages/skimage/data/lbpcascade_frontalface_opencv.xml new file mode 100644 index 0000000000000000000000000000000000000000..b6bd1de10acfc9fa305146a6d6061f323884035b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/data/lbpcascade_frontalface_opencv.xml @@ -0,0 +1,1505 @@ + + + + + BOOST + LBP + 24 + 24 + + GAB + 0.9950000047683716 + 0.5000000000000000 + 0.9500000000000000 + 1 + 100 + + 256 + 20 + + + <_> + 3 + -0.7520892024040222 + + + <_> + + 0 -1 46 -67130709 -21569 -1426120013 -1275125205 -21585 + -16385 587145899 -24005 + + -0.6543210148811340 0.8888888955116272 + + <_> + + 0 -1 13 -163512766 -769593758 -10027009 -262145 -514457854 + -193593353 -524289 -1 + + -0.7739216089248657 0.7278633713722229 + + <_> + + 0 -1 2 -363936790 -893203669 -1337948010 -136907894 + 1088782736 -134217726 -741544961 -1590337 + + -0.7068563103675842 0.6761534214019775 + + <_> + 4 + -0.4872078299522400 + + + <_> + + 0 -1 84 2147483647 1946124287 -536870913 2147450879 + 738132490 1061101567 243204619 2147446655 + + -0.8083735704421997 0.7685696482658386 + + <_> + + 0 -1 21 2147483647 263176079 1879048191 254749487 1879048191 + -134252545 -268435457 801111999 + + -0.7698410153388977 0.6592915654182434 + + <_> + + 0 -1 106 -98110272 1610939566 -285484400 -850010381 + -189334372 -1671954433 -571026695 -262145 + + -0.7506558895111084 0.5444605946540833 + + <_> + + 0 -1 48 -798690576 -131075 1095771153 -237144073 -65569 -1 + -216727745 -69206049 + + -0.7775990366935730 0.5465461611747742 + + <_> + 4 + -1.1592328548431396 + + + <_> + + 0 -1 47 -21585 -20549 -100818262 -738254174 -20561 -36865 + -151016790 -134238549 + + -0.5601882934570313 0.7743113040924072 + + <_> + + 0 -1 12 -286003217 183435247 -268994614 -421330945 + -402686081 1090387966 -286785545 -402653185 + + -0.6124526262283325 0.6978127956390381 + + <_> + + 0 -1 26 -50347012 970882927 -50463492 -1253377 -134218251 + -50364513 -33619992 -172490753 + + -0.6114496588706970 0.6537628173828125 + + <_> + + 0 -1 8 -273 -135266321 1877977738 -2088243418 -134217987 + 2146926575 -18910642 1095231247 + + -0.6854077577590942 0.5403239130973816 + + <_> + 5 + -0.7562355995178223 + + + <_> + + 0 -1 96 -1273 1870659519 -20971602 -67633153 -134250731 + 2004875127 -250 -150995969 + + -0.4051094949245453 0.7584033608436585 + + <_> + + 0 -1 33 -868162224 -76810262 -4262145 -257 1465211989 + -268959873 -2656269 -524289 + + -0.7388162612915039 0.5340843200683594 + + <_> + + 0 -1 57 -12817 -49 -541103378 -152950 -38993 -20481 -1153876 + -72478976 + + -0.6582943797111511 0.5339496731758118 + + <_> + + 0 -1 125 -269484161 -452984961 -319816180 -1594032130 -2111 + -990117891 -488975296 -520947741 + + -0.5981323719024658 0.5323504805564880 + + <_> + + 0 -1 53 557787431 670265215 -1342193665 -1075892225 + 1998528318 1056964607 -33570977 -1 + + -0.6498787999153137 0.4913350641727448 + + <_> + 5 + -0.8085358142852783 + + + <_> + + 0 -1 60 -536873708 880195381 -16842788 -20971521 -176687276 + -168427659 -16777260 -33554626 + + -0.5278195738792419 0.6946372389793396 + + <_> + + 0 -1 7 -1 -62981529 -1090591130 805330978 -8388827 -41945787 + -39577 -531118985 + + -0.5206505060195923 0.6329920291900635 + + <_> + + 0 -1 98 -725287348 1347747543 -852489 -16809993 1489881036 + -167903241 -1 -1 + + -0.7516061067581177 0.4232024252414703 + + <_> + + 0 -1 44 -32777 1006582562 -65 935312171 -8388609 -1078198273 + -1 733886267 + + -0.7639313936233521 0.4123568832874298 + + <_> + + 0 -1 24 -85474705 2138828511 -1036436754 817625855 + 1123369029 -58796809 -1013468481 -194513409 + + -0.5123769044876099 0.5791834592819214 + + <_> + 5 + -0.5549971461296082 + + + <_> + + 0 -1 42 -17409 -20481 -268457797 -134239493 -17473 -1 -21829 + -21846 + + -0.3763174116611481 0.7298233509063721 + + <_> + + 0 -1 6 -805310737 -2098262358 -269504725 682502698 + 2147483519 1740574719 -1090519233 -268472385 + + -0.5352765917778015 0.5659480094909668 + + <_> + + 0 -1 61 -67109678 -6145 -8 -87884584 -20481 -1073762305 + -50856216 -16849696 + + -0.5678374171257019 0.4961479902267456 + + <_> + + 0 -1 123 -138428633 1002418167 -1359008245 -1908670465 + -1346685918 910098423 -1359010520 -1346371657 + + -0.5706262588500977 0.4572288393974304 + + <_> + + 0 -1 9 -89138513 -4196353 1256531674 -1330665426 1216308261 + -36190633 33498198 -151796633 + + -0.5344601869583130 0.4672054052352905 + + <_> + 5 + -0.8776460289955139 + + + <_> + + 0 -1 105 1073769576 206601725 -34013449 -33554433 -789514004 + -101384321 -690225153 -264193 + + -0.7700348496437073 0.5943940877914429 + + <_> + + 0 -1 30 -1432340997 -823623681 -49153 -34291724 -269484035 + -1342767105 -1078198273 -1277955 + + -0.5043668746948242 0.6151274442672730 + + <_> + + 0 -1 35 -1067385040 -195758209 -436748425 -134217731 + -50855988 -129 -1 -1 + + -0.6808040738105774 0.4667325913906097 + + <_> + + 0 -1 119 832534325 -34111555 -26050561 -423659521 -268468364 + 2105014143 -2114244 -17367185 + + -0.4927591383457184 0.5401885509490967 + + <_> + + 0 -1 82 -1089439888 -1080524865 2143059967 -1114121 + -1140949004 -3 -2361356 -739516 + + -0.6445107460021973 0.4227822124958038 + + <_> + 6 + -1.1139287948608398 + + + <_> + + 0 -1 52 -1074071553 -1074003969 -1 -1280135430 -5324817 -1 + -335548482 582134442 + + -0.5307556986808777 0.6258179545402527 + + <_> + + 0 -1 99 -706937396 -705364068 -540016724 -570495027 + -570630659 -587857963 -33628164 -35848193 + + -0.5227634310722351 0.5049746036529541 + + <_> + + 0 -1 18 -2035630093 42119158 -268503053 -1671444 261017599 + 1325432815 1954394111 -805306449 + + -0.4983572661876679 0.5106441378593445 + + <_> + + 0 -1 111 -282529488 -1558073088 1426018736 -170526448 + -546832487 -5113037 -34243375 -570427929 + + -0.4990860521793366 0.5060507059097290 + + <_> + + 0 -1 92 1016332500 -606301707 915094269 -1080086049 + -1837027144 -1361600280 2147318747 1067975613 + + -0.5695009231567383 0.4460467398166657 + + <_> + + 0 -1 51 -656420166 -15413034 -141599534 -603435836 + 1505950458 -787556946 -79823438 -1326199134 + + -0.6590405106544495 0.3616424500942230 + + <_> + 7 + -0.8243625760078430 + + + <_> + + 0 -1 28 -901591776 -201916417 -262 -67371009 -143312112 + -524289 -41943178 -1 + + -0.4972776770591736 0.6027074456214905 + + <_> + + 0 -1 112 -4507851 -411340929 -268437513 -67502145 -17350859 + -32901 -71344315 -29377 + + -0.4383158981800079 0.5966237187385559 + + <_> + + 0 -1 69 -75894785 -117379438 -239063587 -12538500 1485072126 + 2076233213 2123118847 801906927 + + -0.6386105418205261 0.3977999985218048 + + <_> + + 0 -1 19 -823480413 786628589 -16876049 -1364262914 242165211 + 1315930109 -696268833 -455082829 + + -0.5512794256210327 0.4282079637050629 + + <_> + + 0 -1 73 -521411968 6746762 -1396236286 -2038436114 + -185612509 57669627 -143132877 -1041235973 + + -0.6418755054473877 0.3549866080284119 + + <_> + + 0 -1 126 -478153869 1076028979 -1645895615 1365298272 + -557859073 -339771473 1442574528 -1058802061 + + -0.4841901361942291 0.4668019413948059 + + <_> + + 0 -1 45 -246350404 -1650402048 -1610612745 -788400696 + 1467604861 -2787397 1476263935 -4481349 + + -0.5855734348297119 0.3879135847091675 + + <_> + 7 + -1.2237116098403931 + + + <_> + + 0 -1 114 -24819 1572863935 -16809993 -67108865 2146778388 + 1433927541 -268608444 -34865205 + + -0.2518476545810700 0.7088654041290283 + + <_> + + 0 -1 97 -1841359 -134271049 -32769 -5767369 -1116675 -2185 + -8231 -33603327 + + -0.4303432404994965 0.5283288359642029 + + <_> + + 0 -1 25 -1359507589 -1360593090 -1073778729 -269553812 + -809512977 1744707583 -41959433 -134758978 + + -0.4259553551673889 0.5440809130668640 + + <_> + + 0 -1 34 729753407 -134270989 -1140907329 -235200777 + 658456383 2147467263 -1140900929 -16385 + + -0.5605589151382446 0.4220733344554901 + + <_> + + 0 -1 134 -310380553 -420675595 -193005472 -353568129 + 1205338070 -990380036 887604324 -420544526 + + -0.5192656517028809 0.4399855434894562 + + <_> + + 0 -1 16 -1427119361 1978920959 -287119734 -487068946 + 114759245 -540578051 -707510259 -671660453 + + -0.5013077259063721 0.4570254683494568 + + <_> + + 0 -1 74 -738463762 -889949281 -328301948 -121832450 + -1142658284 -1863576559 2146417353 -263185 + + -0.4631414115428925 0.4790246188640595 + + <_> + 7 + -0.5544230937957764 + + + <_> + + 0 -1 113 -76228780 -65538 -1 -67174401 -148007 -33 -221796 + -272842924 + + -0.3949716091156006 0.6082032322883606 + + <_> + + 0 -1 110 369147696 -1625232112 2138570036 -1189900 790708019 + -1212613127 799948719 -4456483 + + -0.4855885505676270 0.4785369932651520 + + <_> + + 0 -1 37 784215839 -290015241 536832799 -402984963 + -1342414991 -838864897 -176769 -268456129 + + -0.4620285332202911 0.4989669024944305 + + <_> + + 0 -1 41 -486418688 -171915327 -340294900 -21938 -519766032 + -772751172 -73096060 -585322623 + + -0.6420643329620361 0.3624351918697357 + + <_> + + 0 -1 117 -33554953 -475332625 -1423463824 -2077230421 + -4849669 -2080505925 -219032928 -1071915349 + + -0.4820112884044647 0.4632140696048737 + + <_> + + 0 -1 65 -834130468 -134217476 -1349314083 -1073803559 + -619913764 -1449131844 -1386890321 -1979118423 + + -0.4465552568435669 0.5061788558959961 + + <_> + + 0 -1 56 -285249779 1912569855 -16530 -1731022870 -1161904146 + -1342177297 -268439634 -1464078708 + + -0.5190586447715759 0.4441480338573456 + + <_> + 7 + -0.7161560654640198 + + + <_> + + 0 -1 20 1246232575 1078001186 -10027057 60102 -277348353 + -43646987 -1210581153 1195769615 + + -0.4323809444904327 0.5663768053054810 + + <_> + + 0 -1 15 -778583572 -612921106 -578775890 -4036478 + -1946580497 -1164766570 -1986687009 -12103599 + + -0.4588732719421387 0.4547033011913300 + + <_> + + 0 -1 129 -1073759445 2013231743 -1363169553 -1082459201 + -1414286549 868185983 -1356133589 -1077936257 + + -0.5218553543090820 0.4111092388629913 + + <_> + + 0 -1 102 -84148365 -2093417722 -1204850272 564290299 + -67121221 -1342177350 -1309195902 -776734797 + + -0.4920000731945038 0.4326725304126740 + + <_> + + 0 -1 88 -25694458 67104495 -290216278 -168563037 2083877442 + 1702788383 -144191964 -234882162 + + -0.4494568109512329 0.4448510706424713 + + <_> + + 0 -1 59 -857980836 904682741 -1612267521 232279415 + 1550862252 -574825221 -357380888 -4579409 + + -0.5180826783180237 0.3888972699642181 + + <_> + + 0 -1 27 -98549440 -137838400 494928389 -246013630 939541351 + -1196072350 -620603549 2137216273 + + -0.6081240773200989 0.3333222270011902 + + <_> + 8 + -0.6743940711021423 + + + <_> + + 0 -1 29 -150995201 2071191945 -1302151626 536934335 + -1059008937 914128709 1147328110 -268369925 + + -0.1790193915367127 0.6605972051620483 + + <_> + + 0 -1 128 -134509479 1610575703 -1342177289 1861484541 + -1107833788 1577058173 -333558568 -136319041 + + -0.3681024610996246 0.5139749646186829 + + <_> + + 0 -1 70 -1 1060154476 -1090984524 -630918524 -539492875 + 779616255 -839568424 -321 + + -0.3217232525348663 0.6171553134918213 + + <_> + + 0 -1 4 -269562385 -285029906 -791084350 -17923776 235286671 + 1275504943 1344390399 -966276889 + + -0.4373284578323364 0.4358185231685638 + + <_> + + 0 -1 76 17825984 -747628419 595427229 1474759671 575672208 + -1684005538 872217086 -1155858277 + + -0.4404836893081665 0.4601220190525055 + + <_> + + 0 -1 124 -336593039 1873735591 -822231622 -355795238 + -470820869 -1997537409 -1057132384 -1015285005 + + -0.4294152259826660 0.4452161788940430 + + <_> + + 0 -1 54 -834212130 -593694721 -322142257 -364892500 + -951029539 -302125121 -1615106053 -79249765 + + -0.3973052501678467 0.4854526817798615 + + <_> + + 0 -1 95 1342144479 2147431935 -33554561 -47873 -855685912 -1 + 1988052447 536827383 + + -0.7054683566093445 0.2697997391223908 + + <_> + 9 + -1.2042298316955566 + + + <_> + + 0 -1 39 1431368960 -183437936 -537002499 -137497097 + 1560590321 -84611081 -2097193 -513 + + -0.5905947685241699 0.5101932883262634 + + <_> + + 0 -1 120 -1645259691 2105491231 2130706431 1458995007 + -8567536 -42483883 -33780003 -21004417 + + -0.4449204802513123 0.4490709304809570 + + <_> + + 0 -1 89 -612381022 -505806938 -362027516 -452985106 + 275854917 1920431639 -12600561 -134221825 + + -0.4693818688392639 0.4061094820499420 + + <_> + + 0 -1 14 -805573153 -161 -554172679 -530519488 -16779441 + 2000682871 -33604275 -150997129 + + -0.3600351214408875 0.5056326985359192 + + <_> + + 0 -1 67 6192 435166195 1467449341 2046691505 -1608493775 + -4755729 -1083162625 -71365637 + + -0.4459891915321350 0.4132415652275085 + + <_> + + 0 -1 86 -41689215 -3281034 1853357967 -420712635 -415924289 + -270209208 -1088293113 -825311232 + + -0.4466069042682648 0.4135067760944367 + + <_> + + 0 -1 80 -117391116 -42203396 2080374461 -188709 -542008165 + -356831940 -1091125345 -1073796897 + + -0.3394956290721893 0.5658645033836365 + + <_> + + 0 -1 75 -276830049 1378714472 -1342181951 757272098 + 1073740607 -282199241 -415761549 170896931 + + -0.5346512198448181 0.3584479391574860 + + <_> + + 0 -1 55 -796075825 -123166849 2113667055 -217530421 + -1107432194 -16385 -806359809 -391188771 + + -0.4379335641860962 0.4123645126819611 + + <_> + 10 + -0.8402050137519836 + + + <_> + + 0 -1 71 -890246622 15525883 -487690486 47116238 -1212319899 + -1291847681 -68159890 -469829921 + + -0.2670986354351044 0.6014143228530884 + + <_> + + 0 -1 31 -1361180685 -1898008841 -1090588811 -285410071 + -1074016265 -840443905 2147221487 -262145 + + -0.4149844348430634 0.4670888185501099 + + <_> + + 0 -1 40 1426190596 1899364271 2142731795 -142607505 + -508232452 -21563393 -41960001 -65 + + -0.4985891580581665 0.3719584941864014 + + <_> + + 0 -1 109 -201337965 10543906 -236498096 -746195597 + 1974565825 -15204415 921907633 -190058309 + + -0.4568729996681213 0.3965812027454376 + + <_> + + 0 -1 130 -595026732 -656401928 -268649235 -571490699 + -440600392 -133131 -358810952 -2004088646 + + -0.4770836830139160 0.3862601518630981 + + <_> + + 0 -1 66 941674740 -1107882114 1332789109 -67691015 + -1360463693 -1556612430 -609108546 733546933 + + -0.4877715110778809 0.3778986334800720 + + <_> + + 0 -1 49 -17114945 -240061474 1552871558 -82775604 -932393844 + -1308544889 -532635478 -99042357 + + -0.3721654713153839 0.4994400143623352 + + <_> + + 0 -1 133 -655906006 1405502603 -939205164 1884929228 + -498859222 559417357 -1928559445 -286264385 + + -0.3934195041656494 0.4769641458988190 + + <_> + + 0 -1 0 -335837777 1860677295 -90 -1946186226 931096183 + 251612987 2013265917 -671232197 + + -0.4323300719261169 0.4342164099216461 + + <_> + + 0 -1 103 37769424 -137772680 374692301 2002666345 -536176194 + -1644484728 807009019 1069089930 + + -0.4993278682231903 0.3665378093719482 + + <_> + 9 + -1.1974394321441650 + + + <_> + + 0 -1 43 -5505 2147462911 2143265466 -4511070 -16450 -257 + -201348440 -71333206 + + -0.3310225307941437 0.5624626278877258 + + <_> + + 0 -1 90 -136842268 -499330741 2015250980 -87107126 + -641665744 -788524639 -1147864792 -134892563 + + -0.5266560912132263 0.3704403042793274 + + <_> + + 0 -1 104 -146800880 -1780368555 2111170033 -140904684 + -16777551 -1946681885 -1646463595 -839131947 + + -0.4171888828277588 0.4540435671806335 + + <_> + + 0 -1 85 -832054034 -981663763 -301990281 -578814081 + -932319000 -1997406723 -33555201 -69206017 + + -0.4556705355644226 0.3704262077808380 + + <_> + + 0 -1 24 -118492417 -1209026825 1119023838 -1334313353 + 1112948738 -297319313 1378887291 -139469193 + + -0.4182529747486115 0.4267231225967407 + + <_> + + 0 -1 78 -1714382628 -2353704 -112094959 -549613092 + -1567058760 -1718550464 -342315012 -1074972227 + + -0.3625369668006897 0.4684656262397766 + + <_> + + 0 -1 5 -85219702 316836394 -33279 1904970288 2117267315 + -260901769 -621461759 -88607770 + + -0.4742925167083740 0.3689507246017456 + + <_> + + 0 -1 11 -294654041 -353603585 -1641159686 -50331921 + -2080899877 1145569279 -143132713 -152044037 + + -0.3666271567344666 0.4580127298831940 + + <_> + + 0 -1 32 1887453658 -638545712 -1877976819 -34320972 + -1071067983 -661345416 -583338277 1060190561 + + -0.4567637443542481 0.3894708156585693 + + <_> + 9 + -0.5733128190040588 + + + <_> + + 0 -1 122 -994063296 1088745462 -318837116 -319881377 + 1102566613 1165490103 -121679694 -134744129 + + -0.4055117964744568 0.5487945079803467 + + <_> + + 0 -1 68 -285233233 -538992907 1811935199 -369234005 -529 + -20593 -20505 -1561401854 + + -0.3787897229194641 0.4532003402709961 + + <_> + + 0 -1 58 -1335245632 1968917183 1940861695 536816369 + -1226071367 -570908176 457026619 1000020667 + + -0.4258328974246979 0.4202791750431061 + + <_> + + 0 -1 94 -1360318719 -1979797897 -50435249 -18646473 + -608879292 -805306691 -269304244 -17840167 + + -0.4561023116111755 0.4002747833728790 + + <_> + + 0 -1 87 2062765935 -16449 -1275080721 -16406 45764335 + -1090552065 -772846337 -570464322 + + -0.4314672648906708 0.4086346626281738 + + <_> + + 0 -1 127 -536896021 1080817663 -738234288 -965478709 + -2082767969 1290855887 1993822934 -990381609 + + -0.4174543321132660 0.4249868988990784 + + <_> + + 0 -1 3 -818943025 168730891 -293610428 -79249354 669224671 + 621166734 1086506807 1473768907 + + -0.4321364760398865 0.4090838730335236 + + <_> + + 0 -1 79 -68895696 -67107736 -1414315879 -841676168 + -619843344 -1180610531 -1081990469 1043203389 + + -0.5018386244773865 0.3702533841133118 + + <_> + + 0 -1 116 -54002134 -543485719 -2124882422 -1437445858 + -115617074 -1195787391 -1096024366 -2140472445 + + -0.5037505626678467 0.3564981222152710 + + <_> + 9 + -0.4892596900463104 + + + <_> + + 0 -1 132 -67113211 2003808111 1862135111 846461923 -2752 + 2002237273 -273154752 1937223539 + + -0.2448196411132813 0.5689709186553955 + + <_> + + 0 -1 62 1179423888 -78064940 -611839555 -539167899 + -1289358360 -1650810108 -892540499 -1432827684 + + -0.4633283913135529 0.3587929606437683 + + <_> + + 0 -1 23 -285212705 -78450761 -656212031 -264050110 -27787425 + -1334349961 -547662981 -135796924 + + -0.3731099069118500 0.4290455579757690 + + <_> + + 0 -1 77 341863476 403702016 -550588417 1600194541 + -1080690735 951127993 -1388580949 -1153717473 + + -0.3658909499645233 0.4556473195552826 + + <_> + + 0 -1 22 -586880702 -204831512 -100644596 -39319550 + -1191150794 705692513 457203315 -75806957 + + -0.5214384198188782 0.3221037387847900 + + <_> + + 0 -1 72 -416546870 545911370 -673716192 -775559454 + -264113598 139424 -183369982 -204474641 + + -0.4289036989212036 0.4004956185817719 + + <_> + + 0 -1 50 -1026505020 -589692154 -1740499937 -1563770497 + 1348491006 -60710713 -1109853489 -633909413 + + -0.4621542394161224 0.3832748532295227 + + <_> + + 0 -1 108 -1448872304 -477895040 -1778390608 -772418127 + -1789923416 -1612057181 -805306693 -1415842113 + + -0.3711548447608948 0.4612701535224915 + + <_> + + 0 -1 92 407905424 -582449988 52654751 -1294472 -285103725 + -74633006 1871559083 1057955850 + + -0.5180652141571045 0.3205870389938355 + + <_> + 10 + -0.5911940932273865 + + + <_> + + 0 -1 81 4112 -1259563825 -846671428 -100902460 1838164148 + -74153752 -90653988 -1074263896 + + -0.2592592537403107 0.5873016119003296 + + <_> + + 0 -1 1 -285216785 -823206977 -1085589 -1081346 1207959293 + 1157103471 2097133565 -2097169 + + -0.3801195919513702 0.4718827307224274 + + <_> + + 0 -1 121 -12465 -536875169 2147478367 2130706303 -37765492 + -866124467 -318782328 -1392509185 + + -0.3509117066860199 0.5094807147979736 + + <_> + + 0 -1 38 2147449663 -20741 -16794757 1945873146 -16710 -1 + -8406341 -67663041 + + -0.4068757295608521 0.4130136370658875 + + <_> + + 0 -1 17 -155191713 866117231 1651407483 548272812 -479201468 + -447742449 1354229504 -261884429 + + -0.4557141065597534 0.3539792001247406 + + <_> + + 0 -1 100 -225319378 -251682065 -492783986 -792341777 + -1287261695 1393643841 -11274182 -213909521 + + -0.4117803275585175 0.4118592441082001 + + <_> + + 0 -1 63 -382220122 -2002072729 -51404800 -371201558 + -923011069 -2135301457 -2066104743 -1042557441 + + -0.4008397758007050 0.4034757018089294 + + <_> + + 0 -1 101 -627353764 -48295149 1581203952 -436258614 + -105268268 -1435893445 -638126888 -1061107126 + + -0.5694189667701721 0.2964762747287750 + + <_> + + 0 -1 118 -8399181 1058107691 -621022752 -251003468 -12582915 + -574619739 -994397789 -1648362021 + + -0.3195341229438782 0.5294018983840942 + + <_> + + 0 -1 92 -348343812 -1078389516 1717960437 364735981 + -1783841602 -4883137 -457572354 -1076950384 + + -0.3365339040756226 0.5067458748817444 + + <_> + 10 + -0.7612916231155396 + + + <_> + + 0 -1 10 -1976661318 -287957604 -1659497122 -782068 43591089 + -453637880 1435470000 -1077438561 + + -0.4204545319080353 0.5165745615959168 + + <_> + + 0 -1 131 -67110925 14874979 -142633168 -1338923040 + 2046713291 -2067933195 1473503712 -789579837 + + -0.3762553930282593 0.4075302779674530 + + <_> + + 0 -1 83 -272814301 -1577073 -1118685 -305156120 -1052289 + -1073813756 -538971154 -355523038 + + -0.4253497421741486 0.3728055357933044 + + <_> + + 0 -1 135 -2233 -214486242 -538514758 573747007 -159390971 + 1994225489 -973738098 -203424005 + + -0.3601998090744019 0.4563256204128265 + + <_> + + 0 -1 115 -261031688 -1330369299 -641860609 1029570301 + -1306461192 -1196149518 -1529767778 683139823 + + -0.4034293889999390 0.4160816967487335 + + <_> + + 0 -1 64 -572993608 -34042628 -417865 -111109 -1433365268 + -19869715 -1920939864 -1279457063 + + -0.3620899617671967 0.4594142735004425 + + <_> + + 0 -1 36 -626275097 -615256993 1651946018 805366393 + 2016559730 -430780849 -799868165 -16580645 + + -0.3903816640377045 0.4381459355354309 + + <_> + + 0 -1 93 1354797300 -1090957603 1976418270 -1342502178 + -1851873892 -1194637077 -1153521668 -1108399474 + + -0.3591445386409760 0.4624078869819641 + + <_> + + 0 -1 91 68157712 1211368313 -304759523 1063017136 798797750 + -275513546 648167355 -1145357350 + + -0.4297670423984528 0.4023293554782867 + + <_> + + 0 -1 107 -546318240 -1628569602 -163577944 -537002306 + -545456389 -1325465645 -380446736 -1058473386 + + -0.5727006793022156 0.2995934784412384 + + <_> + + 0 0 3 5 + <_> + + 0 0 4 2 + <_> + + 0 0 6 3 + <_> + + 0 1 2 3 + <_> + + 0 1 3 3 + <_> + + 0 1 3 7 + <_> + + 0 4 3 3 + <_> + + 0 11 3 4 + <_> + + 0 12 8 4 + <_> + + 0 14 4 3 + <_> + + 1 0 5 3 + <_> + + 1 1 2 2 + <_> + + 1 3 3 1 + <_> + + 1 7 4 4 + <_> + + 1 12 2 2 + <_> + + 1 13 4 1 + <_> + + 1 14 4 3 + <_> + + 1 17 3 2 + <_> + + 2 0 2 3 + <_> + + 2 1 2 2 + <_> + + 2 2 4 6 + <_> + + 2 3 4 4 + <_> + + 2 7 2 1 + <_> + + 2 11 2 3 + <_> + + 2 17 3 2 + <_> + + 3 0 2 2 + <_> + + 3 1 7 3 + <_> + + 3 7 2 1 + <_> + + 3 7 2 4 + <_> + + 3 18 2 2 + <_> + + 4 0 2 3 + <_> + + 4 3 2 1 + <_> + + 4 6 2 1 + <_> + + 4 6 2 5 + <_> + + 4 7 5 2 + <_> + + 4 8 4 3 + <_> + + 4 18 2 2 + <_> + + 5 0 2 2 + <_> + + 5 3 4 4 + <_> + + 5 6 2 5 + <_> + + 5 9 2 2 + <_> + + 5 10 2 2 + <_> + + 6 3 4 4 + <_> + + 6 4 4 3 + <_> + + 6 5 2 3 + <_> + + 6 5 2 5 + <_> + + 6 5 4 3 + <_> + + 6 6 4 2 + <_> + + 6 6 4 4 + <_> + + 6 18 1 2 + <_> + + 6 21 2 1 + <_> + + 7 0 3 7 + <_> + + 7 4 2 3 + <_> + + 7 9 5 1 + <_> + + 7 21 2 1 + <_> + + 8 0 1 4 + <_> + + 8 5 2 2 + <_> + + 8 5 3 2 + <_> + + 8 17 3 1 + <_> + + 8 18 1 2 + <_> + + 9 0 5 3 + <_> + + 9 2 2 6 + <_> + + 9 5 1 1 + <_> + + 9 11 1 1 + <_> + + 9 16 1 1 + <_> + + 9 16 2 1 + <_> + + 9 17 1 1 + <_> + + 9 18 1 1 + <_> + + 10 5 1 2 + <_> + + 10 5 3 3 + <_> + + 10 7 1 5 + <_> + + 10 8 1 1 + <_> + + 10 9 1 1 + <_> + + 10 10 1 1 + <_> + + 10 10 1 2 + <_> + + 10 14 3 3 + <_> + + 10 15 1 1 + <_> + + 10 15 2 1 + <_> + + 10 16 1 1 + <_> + + 10 16 2 1 + <_> + + 10 17 1 1 + <_> + + 10 21 1 1 + <_> + + 11 3 2 2 + <_> + + 11 5 1 2 + <_> + + 11 5 3 3 + <_> + + 11 5 4 6 + <_> + + 11 6 1 1 + <_> + + 11 7 2 2 + <_> + + 11 8 1 2 + <_> + + 11 10 1 1 + <_> + + 11 10 1 2 + <_> + + 11 15 1 1 + <_> + + 11 17 1 1 + <_> + + 11 18 1 1 + <_> + + 12 0 2 2 + <_> + + 12 1 2 5 + <_> + + 12 2 4 1 + <_> + + 12 3 1 3 + <_> + + 12 7 3 4 + <_> + + 12 10 3 2 + <_> + + 12 11 1 1 + <_> + + 12 12 3 2 + <_> + + 12 14 4 3 + <_> + + 12 17 1 1 + <_> + + 12 21 2 1 + <_> + + 13 6 2 5 + <_> + + 13 7 3 5 + <_> + + 13 11 3 2 + <_> + + 13 17 2 2 + <_> + + 13 17 3 2 + <_> + + 13 18 1 2 + <_> + + 13 18 2 2 + <_> + + 14 0 2 2 + <_> + + 14 1 1 3 + <_> + + 14 2 3 2 + <_> + + 14 7 2 1 + <_> + + 14 13 2 1 + <_> + + 14 13 3 3 + <_> + + 14 17 2 2 + <_> + + 15 0 2 2 + <_> + + 15 0 2 3 + <_> + + 15 4 3 2 + <_> + + 15 4 3 6 + <_> + + 15 6 3 2 + <_> + + 15 11 3 4 + <_> + + 15 13 3 2 + <_> + + 15 17 2 2 + <_> + + 15 17 3 2 + <_> + + 16 1 2 3 + <_> + + 16 3 2 4 + <_> + + 16 6 1 1 + <_> + + 16 16 2 2 + <_> + + 17 1 2 2 + <_> + + 17 1 2 5 + <_> + + 17 12 2 2 + <_> + + 18 0 2 2 + diff --git a/lib/python3.10/site-packages/skimage/data/microaneurysms.png b/lib/python3.10/site-packages/skimage/data/microaneurysms.png new file mode 100644 index 0000000000000000000000000000000000000000..b1236923442a251427e6b5b416821fb085dfe365 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/microaneurysms.png differ diff --git a/lib/python3.10/site-packages/skimage/data/moon.png b/lib/python3.10/site-packages/skimage/data/moon.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6f077e78df2b0e759f14e68bae31c8ad62f06f Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/moon.png differ diff --git a/lib/python3.10/site-packages/skimage/data/multipage.tif b/lib/python3.10/site-packages/skimage/data/multipage.tif new file mode 100644 index 0000000000000000000000000000000000000000..77fa220a89fb74837c5c18f7b0698b8e50873a91 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/multipage.tif differ diff --git a/lib/python3.10/site-packages/skimage/data/multipage_rgb.tif b/lib/python3.10/site-packages/skimage/data/multipage_rgb.tif new file mode 100644 index 0000000000000000000000000000000000000000..6832c368f417fb4f96c27aada57e9290618a12ee Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/multipage_rgb.tif differ diff --git a/lib/python3.10/site-packages/skimage/data/no_time_for_that_tiny.gif b/lib/python3.10/site-packages/skimage/data/no_time_for_that_tiny.gif new file mode 100644 index 0000000000000000000000000000000000000000..f2d5c4a9efbde96ee4cf0472e04a9d1a8e968c81 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/no_time_for_that_tiny.gif differ diff --git a/lib/python3.10/site-packages/skimage/data/page.png b/lib/python3.10/site-packages/skimage/data/page.png new file mode 100644 index 0000000000000000000000000000000000000000..6c9554eae55c0cccd7e4709dd44fe4d78da8083a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/page.png differ diff --git a/lib/python3.10/site-packages/skimage/data/phantom.png b/lib/python3.10/site-packages/skimage/data/phantom.png new file mode 100644 index 0000000000000000000000000000000000000000..f9fd232986e73148b9c25bbf87bd27545401620e Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/phantom.png differ diff --git a/lib/python3.10/site-packages/skimage/data/text.png b/lib/python3.10/site-packages/skimage/data/text.png new file mode 100644 index 0000000000000000000000000000000000000000..9f6726afc3d4ba1e3cd7726db5ff10198d1a9d8c Binary files /dev/null and b/lib/python3.10/site-packages/skimage/data/text.png differ diff --git a/lib/python3.10/site-packages/skimage/draw/__init__.py b/lib/python3.10/site-packages/skimage/draw/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..01a8b726606eb04d9d359933ce976096215904a1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/__init__.py @@ -0,0 +1,5 @@ +"""Drawing primitives, such as lines, circles, text, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/draw/__init__.pyi b/lib/python3.10/site-packages/skimage/draw/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..52e425396c86fdd647c3fae7d69d7043453f8c94 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/__init__.pyi @@ -0,0 +1,45 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'line', + 'line_aa', + 'line_nd', + 'bezier_curve', + 'polygon', + 'polygon_perimeter', + 'ellipse', + 'ellipse_perimeter', + 'ellipsoid', + 'ellipsoid_stats', + 'circle_perimeter', + 'circle_perimeter_aa', + 'disk', + 'set_color', + 'random_shapes', + 'rectangle', + 'rectangle_perimeter', + 'polygon2mask', +] + +from .draw3d import ellipsoid, ellipsoid_stats +from ._draw import _bezier_segment +from ._random_shapes import random_shapes +from ._polygon2mask import polygon2mask +from .draw_nd import line_nd +from .draw import ( + ellipse, + set_color, + polygon_perimeter, + line, + line_aa, + polygon, + ellipse_perimeter, + circle_perimeter, + circle_perimeter_aa, + disk, + bezier_curve, + rectangle, + rectangle_perimeter, +) diff --git a/lib/python3.10/site-packages/skimage/draw/_polygon2mask.py b/lib/python3.10/site-packages/skimage/draw/_polygon2mask.py new file mode 100644 index 0000000000000000000000000000000000000000..59ea0891e04c7f88a3e267ce2e029d17d7ff7cbf --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/_polygon2mask.py @@ -0,0 +1,74 @@ +import numpy as np + +from . import draw + + +def polygon2mask(image_shape, polygon): + """Create a binary mask from a polygon. + + Parameters + ---------- + image_shape : tuple of size 2 + The shape of the mask. + polygon : (N, 2) array_like + The polygon coordinates of shape (N, 2) where N is + the number of points. The coordinates are (row, column). + + Returns + ------- + mask : 2-D ndarray of type 'bool' + The binary mask that corresponds to the input polygon. + + See Also + -------- + polygon: + Generate coordinates of pixels inside a polygon. + + Notes + ----- + This function does not do any border checking. Parts of the polygon that + are outside the coordinate space defined by `image_shape` are not drawn. + + Examples + -------- + >>> import skimage as ski + >>> image_shape = (10, 10) + >>> polygon = np.array([[1, 1], [2, 7], [8, 4]]) + >>> mask = ski.draw.polygon2mask(image_shape, polygon) + >>> mask.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + If vertices / points of the `polygon` are outside the coordinate space + defined by `image_shape`, only a part (or none at all) of the polygon is + drawn in the mask. + + >>> offset = np.array([[2, -4]]) + >>> ski.draw.polygon2mask(image_shape, polygon - offset).astype(int) + array([[0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 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]]) + """ + polygon = np.asarray(polygon) + vertex_row_coords, vertex_col_coords = polygon.T + fill_row_coords, fill_col_coords = draw.polygon( + vertex_row_coords, vertex_col_coords, image_shape + ) + mask = np.zeros(image_shape, dtype=bool) + mask[fill_row_coords, fill_col_coords] = True + return mask diff --git a/lib/python3.10/site-packages/skimage/draw/_random_shapes.py b/lib/python3.10/site-packages/skimage/draw/_random_shapes.py new file mode 100644 index 0000000000000000000000000000000000000000..031816f24dd549629a17ca55a4d6318c1b687921 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/_random_shapes.py @@ -0,0 +1,459 @@ +import math + +import numpy as np + +from .draw import polygon as draw_polygon, disk as draw_disk, ellipse as draw_ellipse +from .._shared.utils import warn + + +def _generate_rectangle_mask(point, image, shape, random): + """Generate a mask for a filled rectangle shape. + + The height and width of the rectangle are generated randomly. + + Parameters + ---------- + point : tuple + The row and column of the top left corner of the rectangle. + image : tuple + The height, width and depth of the image into which the shape + is placed. + shape : tuple + The minimum and maximum size of the shape to fit. + random : `numpy.random.Generator` + + The random state to use for random sampling. + + Raises + ------ + ArithmeticError + When a shape cannot be fit into the image with the given starting + coordinates. This usually means the image dimensions are too small or + shape dimensions too large. + + Returns + ------- + label : tuple + A (category, ((r0, r1), (c0, c1))) tuple specifying the category and + bounding box coordinates of the shape. + indices : 2-D array + A mask of indices that the shape fills. + + """ + available_width = min(image[1] - point[1], shape[1]) - shape[0] + available_height = min(image[0] - point[0], shape[1]) - shape[0] + + # Pick random widths and heights. + r = shape[0] + random.integers(max(1, available_height)) - 1 + c = shape[0] + random.integers(max(1, available_width)) - 1 + rectangle = draw_polygon( + [ + point[0], + point[0] + r, + point[0] + r, + point[0], + ], + [ + point[1], + point[1], + point[1] + c, + point[1] + c, + ], + ) + label = ('rectangle', ((point[0], point[0] + r + 1), (point[1], point[1] + c + 1))) + + return rectangle, label + + +def _generate_circle_mask(point, image, shape, random): + """Generate a mask for a filled circle shape. + + The radius of the circle is generated randomly. + + Parameters + ---------- + point : tuple + The row and column of the top left corner of the rectangle. + image : tuple + The height, width and depth of the image into which the shape is placed. + shape : tuple + The minimum and maximum size and color of the shape to fit. + random : `numpy.random.Generator` + The random state to use for random sampling. + + Raises + ------ + ArithmeticError + When a shape cannot be fit into the image with the given starting + coordinates. This usually means the image dimensions are too small or + shape dimensions too large. + + Returns + ------- + label : tuple + A (category, ((r0, r1), (c0, c1))) tuple specifying the category and + bounding box coordinates of the shape. + indices : 2-D array + A mask of indices that the shape fills. + """ + if shape[0] == 1 or shape[1] == 1: + raise ValueError('size must be > 1 for circles') + min_radius = shape[0] // 2.0 + max_radius = shape[1] // 2.0 + left = point[1] + right = image[1] - point[1] + top = point[0] + bottom = image[0] - point[0] + available_radius = min(left, right, top, bottom, max_radius) - min_radius + if available_radius < 0: + raise ArithmeticError('cannot fit shape to image') + radius = int(min_radius + random.integers(max(1, available_radius))) + # TODO: think about how to deprecate this + # while draw_circle was deprecated in favor of draw_disk + # switching to a label of 'disk' here + # would be a breaking change for downstream libraries + # See discussion on naming convention here + # https://github.com/scikit-image/scikit-image/pull/4428 + disk = draw_disk((point[0], point[1]), radius) + # Until a deprecation path is decided, always return `'circle'` + label = ( + 'circle', + ( + (point[0] - radius + 1, point[0] + radius), + (point[1] - radius + 1, point[1] + radius), + ), + ) + + return disk, label + + +def _generate_triangle_mask(point, image, shape, random): + """Generate a mask for a filled equilateral triangle shape. + + The length of the sides of the triangle is generated randomly. + + Parameters + ---------- + point : tuple + The row and column of the top left corner of a up-pointing triangle. + image : tuple + The height, width and depth of the image into which the shape + is placed. + shape : tuple + The minimum and maximum size and color of the shape to fit. + random : `numpy.random.Generator` + The random state to use for random sampling. + + Raises + ------ + ArithmeticError + When a shape cannot be fit into the image with the given starting + coordinates. This usually means the image dimensions are too small or + shape dimensions too large. + + Returns + ------- + label : tuple + A (category, ((r0, r1), (c0, c1))) tuple specifying the category and + bounding box coordinates of the shape. + indices : 2-D array + A mask of indices that the shape fills. + + """ + if shape[0] == 1 or shape[1] == 1: + raise ValueError('dimension must be > 1 for triangles') + available_side = min(image[1] - point[1], point[0], shape[1]) - shape[0] + side = shape[0] + random.integers(max(1, available_side)) - 1 + triangle_height = int(np.ceil(np.sqrt(3 / 4.0) * side)) + triangle = draw_polygon( + [ + point[0], + point[0] - triangle_height, + point[0], + ], + [ + point[1], + point[1] + side // 2, + point[1] + side, + ], + ) + label = ( + 'triangle', + ((point[0] - triangle_height, point[0] + 1), (point[1], point[1] + side + 1)), + ) + + return triangle, label + + +def _generate_ellipse_mask(point, image, shape, random): + """Generate a mask for a filled ellipse shape. + + The rotation, major and minor semi-axes of the ellipse are generated + randomly. + + Parameters + ---------- + point : tuple + The row and column of the top left corner of the rectangle. + image : tuple + The height, width and depth of the image into which the shape is + placed. + shape : tuple + The minimum and maximum size and color of the shape to fit. + random : `numpy.random.Generator` + The random state to use for random sampling. + + Raises + ------ + ArithmeticError + When a shape cannot be fit into the image with the given starting + coordinates. This usually means the image dimensions are too small or + shape dimensions too large. + + Returns + ------- + label : tuple + A (category, ((r0, r1), (c0, c1))) tuple specifying the category and + bounding box coordinates of the shape. + indices : 2-D array + A mask of indices that the shape fills. + """ + if shape[0] == 1 or shape[1] == 1: + raise ValueError('size must be > 1 for ellipses') + min_radius = shape[0] / 2.0 + max_radius = shape[1] / 2.0 + left = point[1] + right = image[1] - point[1] + top = point[0] + bottom = image[0] - point[0] + available_radius = min(left, right, top, bottom, max_radius) + if available_radius < min_radius: + raise ArithmeticError('cannot fit shape to image') + # NOTE: very conservative because we could take into account the fact that + # we have 2 different radii, but this is a good first approximation. + # Also, we can afford to have a uniform sampling because the ellipse will + # be rotated. + r_radius = random.uniform(min_radius, available_radius + 1) + c_radius = random.uniform(min_radius, available_radius + 1) + rotation = random.uniform(-np.pi, np.pi) + ellipse = draw_ellipse( + point[0], + point[1], + r_radius, + c_radius, + shape=image[:2], + rotation=rotation, + ) + max_radius = math.ceil(max(r_radius, c_radius)) + min_x = np.min(ellipse[0]) + max_x = np.max(ellipse[0]) + 1 + min_y = np.min(ellipse[1]) + max_y = np.max(ellipse[1]) + 1 + label = ('ellipse', ((min_x, max_x), (min_y, max_y))) + + return ellipse, label + + +# Allows lookup by key as well as random selection. +SHAPE_GENERATORS = dict( + rectangle=_generate_rectangle_mask, + circle=_generate_circle_mask, + triangle=_generate_triangle_mask, + ellipse=_generate_ellipse_mask, +) +SHAPE_CHOICES = list(SHAPE_GENERATORS.values()) + + +def _generate_random_colors(num_colors, num_channels, intensity_range, random): + """Generate an array of random colors. + + Parameters + ---------- + num_colors : int + Number of colors to generate. + num_channels : int + Number of elements representing color. + intensity_range : {tuple of tuples of ints, tuple of ints}, optional + The range of values to sample pixel values from. For grayscale images + the format is (min, max). For multichannel - ((min, max),) if the + ranges are equal across the channels, and + ((min_0, max_0), ... (min_N, max_N)) if they differ. + random : `numpy.random.Generator` + The random state to use for random sampling. + + Raises + ------ + ValueError + When the `intensity_range` is not in the interval (0, 255). + + Returns + ------- + colors : array + An array of shape (num_colors, num_channels), where the values for + each channel are drawn from the corresponding `intensity_range`. + + """ + if num_channels == 1: + intensity_range = (intensity_range,) + elif len(intensity_range) == 1: + intensity_range = intensity_range * num_channels + colors = [random.integers(r[0], r[1] + 1, size=num_colors) for r in intensity_range] + return np.transpose(colors) + + +def random_shapes( + image_shape, + max_shapes, + min_shapes=1, + min_size=2, + max_size=None, + num_channels=3, + shape=None, + intensity_range=None, + allow_overlap=False, + num_trials=100, + rng=None, + *, + channel_axis=-1, +): + """Generate an image with random shapes, labeled with bounding boxes. + + The image is populated with random shapes with random sizes, random + locations, and random colors, with or without overlap. + + Shapes have random (row, col) starting coordinates and random sizes bounded + by `min_size` and `max_size`. It can occur that a randomly generated shape + will not fit the image at all. In that case, the algorithm will try again + with new starting coordinates a certain number of times. However, it also + means that some shapes may be skipped altogether. In that case, this + function will generate fewer shapes than requested. + + Parameters + ---------- + image_shape : tuple + The number of rows and columns of the image to generate. + max_shapes : int + The maximum number of shapes to (attempt to) fit into the shape. + min_shapes : int, optional + The minimum number of shapes to (attempt to) fit into the shape. + min_size : int, optional + The minimum dimension of each shape to fit into the image. + max_size : int, optional + The maximum dimension of each shape to fit into the image. + num_channels : int, optional + Number of channels in the generated image. If 1, generate monochrome + images, else color images with multiple channels. Ignored if + ``multichannel`` is set to False. + shape : {rectangle, circle, triangle, ellipse, None} str, optional + The name of the shape to generate or `None` to pick random ones. + intensity_range : {tuple of tuples of uint8, tuple of uint8}, optional + The range of values to sample pixel values from. For grayscale + images the format is (min, max). For multichannel - ((min, max),) + if the ranges are equal across the channels, and + ((min_0, max_0), ... (min_N, max_N)) if they differ. As the + function supports generation of uint8 arrays only, the maximum + range is (0, 255). If None, set to (0, 254) for each channel + reserving color of intensity = 255 for background. + allow_overlap : bool, optional + If `True`, allow shapes to overlap. + num_trials : int, optional + How often to attempt to fit a shape into the image before skipping it. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + image : uint8 array + An image with the fitted shapes. + labels : list + A list of labels, one per shape in the image. Each label is a + (category, ((r0, r1), (c0, c1))) tuple specifying the category and + bounding box coordinates of the shape. + + Examples + -------- + >>> import skimage.draw + >>> image, labels = skimage.draw.random_shapes((32, 32), max_shapes=3) + >>> image # doctest: +SKIP + array([ + [[255, 255, 255], + [255, 255, 255], + [255, 255, 255], + ..., + [255, 255, 255], + [255, 255, 255], + [255, 255, 255]]], dtype=uint8) + >>> labels # doctest: +SKIP + [('circle', ((22, 18), (25, 21))), + ('triangle', ((5, 6), (13, 13)))] + """ + if min_size > image_shape[0] or min_size > image_shape[1]: + raise ValueError('Minimum dimension must be less than ncols and nrows') + max_size = max_size or max(image_shape[0], image_shape[1]) + + if channel_axis is None: + num_channels = 1 + + if intensity_range is None: + intensity_range = (0, 254) if num_channels == 1 else ((0, 254),) + else: + tmp = (intensity_range,) if num_channels == 1 else intensity_range + for intensity_pair in tmp: + for intensity in intensity_pair: + if not (0 <= intensity <= 255): + msg = 'Intensity range must lie within (0, 255) interval' + raise ValueError(msg) + + rng = np.random.default_rng(rng) + user_shape = shape + image_shape = (image_shape[0], image_shape[1], num_channels) + image = np.full(image_shape, 255, dtype=np.uint8) + filled = np.zeros(image_shape, dtype=bool) + labels = [] + + num_shapes = rng.integers(min_shapes, max_shapes + 1) + colors = _generate_random_colors(num_shapes, num_channels, intensity_range, rng) + shape = (min_size, max_size) + for shape_idx in range(num_shapes): + if user_shape is None: + shape_generator = rng.choice(SHAPE_CHOICES) + else: + shape_generator = SHAPE_GENERATORS[user_shape] + for _ in range(num_trials): + # Pick start coordinates. + column = rng.integers(max(1, image_shape[1] - min_size)) + row = rng.integers(max(1, image_shape[0] - min_size)) + point = (row, column) + try: + indices, label = shape_generator(point, image_shape, shape, rng) + except ArithmeticError: + # Couldn't fit the shape, skip it. + indices = [] + continue + # Check if there is an overlap where the mask is nonzero. + if allow_overlap or not filled[indices].any(): + image[indices] = colors[shape_idx] + filled[indices] = True + labels.append(label) + break + else: + warn( + 'Could not fit any shapes to image, ' + 'consider reducing the minimum dimension' + ) + + if channel_axis is None: + image = np.squeeze(image, axis=2) + else: + image = np.moveaxis(image, -1, channel_axis) + + return image, labels diff --git a/lib/python3.10/site-packages/skimage/draw/draw.py b/lib/python3.10/site-packages/skimage/draw/draw.py new file mode 100644 index 0000000000000000000000000000000000000000..c98e65bd4f86d2d5b72cfa1734826bb4878a646f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/draw.py @@ -0,0 +1,973 @@ +import numpy as np + +from .._shared._geometry import polygon_clip +from .._shared.version_requirements import require +from .._shared.compat import NP_COPY_IF_NEEDED +from ._draw import ( + _coords_inside_image, + _line, + _line_aa, + _polygon, + _ellipse_perimeter, + _circle_perimeter, + _circle_perimeter_aa, + _bezier_curve, +) + + +def _ellipse_in_shape(shape, center, radii, rotation=0.0): + """Generate coordinates of points within ellipse bounded by shape. + + Parameters + ---------- + shape : iterable of ints + Shape of the input image. Must be at least length 2. Only the first + two values are used to determine the extent of the input image. + center : iterable of floats + (row, column) position of center inside the given shape. + radii : iterable of floats + Size of two half axes (for row and column) + rotation : float, optional + Rotation of the ellipse defined by the above, in radians + in range (-PI, PI), in contra clockwise direction, + with respect to the column-axis. + + Returns + ------- + rows : iterable of ints + Row coordinates representing values within the ellipse. + cols : iterable of ints + Corresponding column coordinates representing values within the ellipse. + """ + r_lim, c_lim = np.ogrid[0 : float(shape[0]), 0 : float(shape[1])] + r_org, c_org = center + r_rad, c_rad = radii + rotation %= np.pi + sin_alpha, cos_alpha = np.sin(rotation), np.cos(rotation) + r, c = (r_lim - r_org), (c_lim - c_org) + distances = ((r * cos_alpha + c * sin_alpha) / r_rad) ** 2 + ( + (r * sin_alpha - c * cos_alpha) / c_rad + ) ** 2 + return np.nonzero(distances < 1) + + +def ellipse(r, c, r_radius, c_radius, shape=None, rotation=0.0): + """Generate coordinates of pixels within ellipse. + + Parameters + ---------- + r, c : double + Centre coordinate of ellipse. + r_radius, c_radius : double + Minor and major semi-axes. ``(r/r_radius)**2 + (c/c_radius)**2 = 1``. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output pixel + coordinates. This is useful for ellipses which exceed the image size. + By default the full extent of the ellipse are used. Must be at least + length 2. Only the first two values are used to determine the extent. + rotation : float, optional (default 0.) + Set the ellipse rotation (rotation) in range (-PI, PI) + in contra clock wise direction, so PI/2 degree means swap ellipse axis + + Returns + ------- + rr, cc : ndarray of int + Pixel coordinates of ellipse. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Examples + -------- + >>> from skimage.draw import ellipse + >>> img = np.zeros((10, 12), dtype=np.uint8) + >>> rr, cc = ellipse(5, 6, 3, 5, rotation=np.deg2rad(30)) + >>> img[rr, cc] = 1 + >>> img + array([[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, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + Notes + ----- + The ellipse equation:: + + ((x * cos(alpha) + y * sin(alpha)) / x_radius) ** 2 + + ((x * sin(alpha) - y * cos(alpha)) / y_radius) ** 2 = 1 + + + Note that the positions of `ellipse` without specified `shape` can have + also, negative values, as this is correct on the plane. On the other hand + using these ellipse positions for an image afterwards may lead to appearing + on the other side of image, because ``image[-1, -1] = image[end-1, end-1]`` + + >>> rr, cc = ellipse(1, 2, 3, 6) + >>> img = np.zeros((6, 12), dtype=np.uint8) + >>> img[rr, cc] = 1 + >>> img + array([[1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1]], dtype=uint8) + """ + + center = np.array([r, c]) + radii = np.array([r_radius, c_radius]) + # allow just rotation with in range +/- 180 degree + rotation %= np.pi + + # compute rotated radii by given rotation + r_radius_rot = abs(r_radius * np.cos(rotation)) + c_radius * np.sin(rotation) + c_radius_rot = r_radius * np.sin(rotation) + abs(c_radius * np.cos(rotation)) + # The upper_left and lower_right corners of the smallest rectangle + # containing the ellipse. + radii_rot = np.array([r_radius_rot, c_radius_rot]) + upper_left = np.ceil(center - radii_rot).astype(int) + lower_right = np.floor(center + radii_rot).astype(int) + + if shape is not None: + # Constrain upper_left and lower_right by shape boundary. + upper_left = np.maximum(upper_left, np.array([0, 0])) + lower_right = np.minimum(lower_right, np.array(shape[:2]) - 1) + + shifted_center = center - upper_left + bounding_shape = lower_right - upper_left + 1 + + rr, cc = _ellipse_in_shape(bounding_shape, shifted_center, radii, rotation) + rr.flags.writeable = True + cc.flags.writeable = True + rr += upper_left[0] + cc += upper_left[1] + return rr, cc + + +def disk(center, radius, *, shape=None): + """Generate coordinates of pixels within circle. + + Parameters + ---------- + center : tuple + Center coordinate of disk. + radius : double + Radius of disk. + shape : tuple, optional + Image shape as a tuple of size 2. Determines the maximum + extent of output pixel coordinates. This is useful for disks that + exceed the image size. If None, the full extent of the disk is used. + The shape might result in negative coordinates and wraparound + behaviour. + + Returns + ------- + rr, cc : ndarray of int + Pixel coordinates of disk. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import disk + >>> shape = (4, 4) + >>> img = np.zeros(shape, dtype=np.uint8) + >>> rr, cc = disk((0, 0), 2, shape=shape) + >>> img[rr, cc] = 1 + >>> img + array([[1, 1, 0, 0], + [1, 1, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0]], dtype=uint8) + >>> img = np.zeros(shape, dtype=np.uint8) + >>> # Negative coordinates in rr and cc perform a wraparound + >>> rr, cc = disk((0, 0), 2, shape=None) + >>> img[rr, cc] = 1 + >>> img + array([[1, 1, 0, 1], + [1, 1, 0, 1], + [0, 0, 0, 0], + [1, 1, 0, 1]], dtype=uint8) + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = disk((4, 4), 5) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + r, c = center + return ellipse(r, c, radius, radius, shape) + + +@require("matplotlib", ">=3.3") +def polygon_perimeter(r, c, shape=None, clip=False): + """Generate polygon perimeter coordinates. + + Parameters + ---------- + r : (N,) ndarray + Row coordinates of vertices of polygon. + c : (N,) ndarray + Column coordinates of vertices of polygon. + shape : tuple, optional + Image shape which is used to determine maximum extents of output pixel + coordinates. This is useful for polygons that exceed the image size. + If None, the full extents of the polygon is used. Must be at least + length 2. Only the first two values are used to determine the extent of + the input image. + clip : bool, optional + Whether to clip the polygon to the provided shape. If this is set + to True, the drawn figure will always be a closed polygon with all + edges visible. + + Returns + ------- + rr, cc : ndarray of int + Pixel coordinates of polygon. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('matplotlib') + + >>> from skimage.draw import polygon_perimeter + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = polygon_perimeter([5, -1, 5, 10], + ... [-1, 5, 11, 5], + ... shape=img.shape, clip=True) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8) + + """ + if clip: + if shape is None: + raise ValueError("Must specify clipping shape") + clip_box = np.array([0, 0, shape[0] - 1, shape[1] - 1]) + else: + clip_box = np.array([np.min(r), np.min(c), np.max(r), np.max(c)]) + + # Do the clipping irrespective of whether clip is set. This + # ensures that the returned polygon is closed and is an array. + r, c = polygon_clip(r, c, *clip_box) + + r = np.round(r).astype(int) + c = np.round(c).astype(int) + + # Construct line segments + rr, cc = [], [] + for i in range(len(r) - 1): + line_r, line_c = line(r[i], c[i], r[i + 1], c[i + 1]) + rr.extend(line_r) + cc.extend(line_c) + + rr = np.asarray(rr) + cc = np.asarray(cc) + + if shape is None: + return rr, cc + else: + return _coords_inside_image(rr, cc, shape) + + +def set_color(image, coords, color, alpha=1): + """Set pixel color in the image at the given coordinates. + + Note that this function modifies the color of the image in-place. + Coordinates that exceed the shape of the image will be ignored. + + Parameters + ---------- + image : (M, N, C) ndarray + Image + coords : tuple of ((K,) ndarray, (K,) ndarray) + Row and column coordinates of pixels to be colored. + color : (C,) ndarray + Color to be assigned to coordinates in the image. + alpha : scalar or (K,) ndarray + Alpha values used to blend color with image. 0 is transparent, + 1 is opaque. + + Examples + -------- + >>> from skimage.draw import line, set_color + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = line(1, 1, 20, 20) + >>> set_color(img, (rr, cc), 1) + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]], dtype=uint8) + + """ + rr, cc = coords + + if image.ndim == 2: + image = image[..., np.newaxis] + + color = np.array(color, ndmin=1, copy=NP_COPY_IF_NEEDED) + + if image.shape[-1] != color.shape[-1]: + raise ValueError( + f'Color shape ({color.shape[0]}) must match last ' + 'image dimension ({image.shape[-1]}).' + ) + + if np.isscalar(alpha): + # Can be replaced by ``full_like`` when numpy 1.8 becomes + # minimum dependency + alpha = np.ones_like(rr) * alpha + + rr, cc, alpha = _coords_inside_image(rr, cc, image.shape, val=alpha) + + alpha = alpha[..., np.newaxis] + + color = color * alpha + vals = image[rr, cc] * (1 - alpha) + + image[rr, cc] = vals + color + + +def line(r0, c0, r1, c1): + """Generate line pixel coordinates. + + Parameters + ---------- + r0, c0 : int + Starting position (row, column). + r1, c1 : int + End position (row, column). + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the line. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + Anti-aliased line generator is available with `line_aa`. + + Examples + -------- + >>> from skimage.draw import line + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = line(1, 1, 8, 8) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + return _line(r0, c0, r1, c1) + + +def line_aa(r0, c0, r1, c1): + """Generate anti-aliased line pixel coordinates. + + Parameters + ---------- + r0, c0 : int + Starting position (row, column). + r1, c1 : int + End position (row, column). + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> from skimage.draw import line_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = line_aa(1, 1, 8, 8) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 255, 74, 0, 0, 0, 0, 0, 0, 0], + [ 0, 74, 255, 74, 0, 0, 0, 0, 0, 0], + [ 0, 0, 74, 255, 74, 0, 0, 0, 0, 0], + [ 0, 0, 0, 74, 255, 74, 0, 0, 0, 0], + [ 0, 0, 0, 0, 74, 255, 74, 0, 0, 0], + [ 0, 0, 0, 0, 0, 74, 255, 74, 0, 0], + [ 0, 0, 0, 0, 0, 0, 74, 255, 74, 0], + [ 0, 0, 0, 0, 0, 0, 0, 74, 255, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + return _line_aa(r0, c0, r1, c1) + + +def polygon(r, c, shape=None): + """Generate coordinates of pixels inside a polygon. + + Parameters + ---------- + r : (N,) array_like + Row coordinates of the polygon's vertices. + c : (N,) array_like + Column coordinates of the polygon's vertices. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for polygons that exceed the image + size. If None, the full extent of the polygon is used. Must be at + least length 2. Only the first two values are used to determine the + extent of the input image. + + Returns + ------- + rr, cc : ndarray of int + Pixel coordinates of polygon. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + See Also + -------- + polygon2mask: + Create a binary mask from a polygon. + + Notes + ----- + This function ensures that `rr` and `cc` don't contain negative values. + Pixels of the polygon that whose coordinates are smaller 0, are not drawn. + + Examples + -------- + >>> import skimage as ski + >>> r = np.array([1, 2, 8]) + >>> c = np.array([1, 7, 4]) + >>> rr, cc = ski.draw.polygon(r, c) + >>> img = np.zeros((10, 10), dtype=int) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + If the image `shape` is defined and vertices / points of the `polygon` are + outside this coordinate space, only a part (or none at all) of the polygon's + pixels is returned. Shifting the polygon's vertices by an offset can be used + to move the polygon around and potentially draw an arbitrary sub-region of + the polygon. + + >>> offset = (2, -4) + >>> rr, cc = ski.draw.polygon(r - offset[0], c - offset[1], shape=img.shape) + >>> img = np.zeros((10, 10), dtype=int) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 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]]) + """ + return _polygon(r, c, shape) + + +def circle_perimeter(r, c, radius, method='bresenham', shape=None): + """Generate circle perimeter coordinates. + + Parameters + ---------- + r, c : int + Centre coordinate of circle. + radius : int + Radius of circle. + method : {'bresenham', 'andres'}, optional + bresenham : Bresenham method (default) + andres : Andres method + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for circles that exceed the image + size. If None, the full extent of the circle is used. Must be at least + length 2. Only the first two values are used to determine the extent of + the input image. + + Returns + ------- + rr, cc : (N,) ndarray of int + Bresenham and Andres' method: + Indices of pixels that belong to the circle perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + Andres method presents the advantage that concentric + circles create a disc whereas Bresenham can make holes. There + is also less distortions when Andres circles are rotated. + Bresenham method is also known as midpoint circle algorithm. + Anti-aliased circle generator is available with `circle_perimeter_aa`. + + References + ---------- + .. [1] J.E. Bresenham, "Algorithm for computer control of a digital + plotter", IBM Systems journal, 4 (1965) 25-30. + .. [2] E. Andres, "Discrete circles, rings and spheres", Computers & + Graphics, 18 (1994) 695-706. + + Examples + -------- + >>> from skimage.draw import circle_perimeter + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = circle_perimeter(4, 4, 3) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + return _circle_perimeter(r, c, radius, method, shape) + + +def circle_perimeter_aa(r, c, radius, shape=None): + """Generate anti-aliased circle perimeter coordinates. + + Parameters + ---------- + r, c : int + Centre coordinate of circle. + radius : int + Radius of circle. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for circles that exceed the image + size. If None, the full extent of the circle is used. Must be at least + length 2. Only the first two values are used to determine the extent of + the input image. + + Returns + ------- + rr, cc, val : (N,) ndarray (int, int, float) + Indices of pixels (`rr`, `cc`) and intensity values (`val`). + ``img[rr, cc] = val``. + + Notes + ----- + Wu's method draws anti-aliased circle. This implementation doesn't use + lookup table optimization. + + Use the function ``draw.set_color`` to apply ``circle_perimeter_aa`` + results to color images. + + References + ---------- + .. [1] X. Wu, "An efficient antialiasing technique", In ACM SIGGRAPH + Computer Graphics, 25 (1991) 143-152. + + Examples + -------- + >>> from skimage.draw import circle_perimeter_aa + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc, val = circle_perimeter_aa(4, 4, 3) + >>> img[rr, cc] = val * 255 + >>> img + array([[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 255, 0, 0, 0, 0, 0, 255, 0, 0], + [ 0, 211, 43, 0, 0, 0, 43, 211, 0, 0], + [ 0, 60, 194, 43, 0, 43, 194, 60, 0, 0], + [ 0, 0, 60, 211, 255, 211, 60, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + >>> from skimage import data, draw + >>> image = data.chelsea() + >>> rr, cc, val = draw.circle_perimeter_aa(r=100, c=100, radius=75) + >>> draw.set_color(image, (rr, cc), [1, 0, 0], alpha=val) + """ + return _circle_perimeter_aa(r, c, radius, shape) + + +def ellipse_perimeter(r, c, r_radius, c_radius, orientation=0, shape=None): + """Generate ellipse perimeter coordinates. + + Parameters + ---------- + r, c : int + Centre coordinate of ellipse. + r_radius, c_radius : int + Minor and major semi-axes. ``(r/r_radius)**2 + (c/c_radius)**2 = 1``. + orientation : double, optional + Major axis orientation in clockwise direction as radians. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for ellipses that exceed the image + size. If None, the full extent of the ellipse is used. Must be at + least length 2. Only the first two values are used to determine the + extent of the input image. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the ellipse perimeter. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> from skimage.draw import ellipse_perimeter + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(5, 5, 3, 4) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + + Note that the positions of `ellipse` without specified `shape` can have + also, negative values, as this is correct on the plane. On the other hand + using these ellipse positions for an image afterwards may lead to appearing + on the other side of image, because ``image[-1, -1] = image[end-1, end-1]`` + + >>> rr, cc = ellipse_perimeter(2, 3, 4, 5) + >>> img = np.zeros((9, 12), dtype=np.uint8) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1], + [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]], dtype=uint8) + """ + return _ellipse_perimeter(r, c, r_radius, c_radius, orientation, shape) + + +def bezier_curve(r0, c0, r1, c1, r2, c2, weight, shape=None): + """Generate Bezier curve coordinates. + + Parameters + ---------- + r0, c0 : int + Coordinates of the first control point. + r1, c1 : int + Coordinates of the middle control point. + r2, c2 : int + Coordinates of the last control point. + weight : double + Middle control point weight, it describes the line tension. + shape : tuple, optional + Image shape which is used to determine the maximum extent of output + pixel coordinates. This is useful for curves that exceed the image + size. If None, the full extent of the curve is used. + + Returns + ------- + rr, cc : (N,) ndarray of int + Indices of pixels that belong to the Bezier curve. + May be used to directly index into an array, e.g. + ``img[rr, cc] = 1``. + + Notes + ----- + The algorithm is the rational quadratic algorithm presented in + reference [1]_. + + References + ---------- + .. [1] A Rasterizing Algorithm for Drawing Curves, A. Zingl, 2012 + http://members.chello.at/easyfilter/Bresenham.pdf + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import bezier_curve + >>> img = np.zeros((10, 10), dtype=np.uint8) + >>> rr, cc = bezier_curve(1, 5, 5, -2, 8, 8, 2) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + return _bezier_curve(r0, c0, r1, c1, r2, c2, weight, shape) + + +def rectangle(start, end=None, extent=None, shape=None): + """Generate coordinates of pixels within a rectangle. + + Parameters + ---------- + start : tuple + Origin point of the rectangle, e.g., ``([plane,] row, column)``. + end : tuple + End point of the rectangle ``([plane,] row, column)``. + For a 2D matrix, the slice defined by the rectangle is + ``[start:(end+1)]``. + Either `end` or `extent` must be specified. + extent : tuple + The extent (size) of the drawn rectangle. E.g., + ``([num_planes,] num_rows, num_cols)``. + Either `end` or `extent` must be specified. + A negative extent is valid, and will result in a rectangle + going along the opposite direction. If extent is negative, the + `start` point is not included. + shape : tuple, optional + Image shape used to determine the maximum bounds of the output + coordinates. This is useful for clipping rectangles that exceed + the image size. By default, no clipping is done. + + Returns + ------- + coords : array of int, shape (Ndim, Npoints) + The coordinates of all pixels in the rectangle. + + Notes + ----- + This function can be applied to N-dimensional images, by passing `start` and + `end` or `extent` as tuples of length N. + + Examples + -------- + >>> import numpy as np + >>> from skimage.draw import rectangle + >>> img = np.zeros((5, 5), dtype=np.uint8) + >>> start = (1, 1) + >>> extent = (3, 3) + >>> rr, cc = rectangle(start, extent=extent, shape=img.shape) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + + >>> img = np.zeros((5, 5), dtype=np.uint8) + >>> start = (0, 1) + >>> end = (3, 3) + >>> rr, cc = rectangle(start, end=end, shape=img.shape) + >>> img[rr, cc] = 1 + >>> img + array([[0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + >>> import numpy as np + >>> from skimage.draw import rectangle + >>> img = np.zeros((6, 6), dtype=np.uint8) + >>> start = (3, 3) + >>> + >>> rr, cc = rectangle(start, extent=(2, 2)) + >>> img[rr, cc] = 1 + >>> rr, cc = rectangle(start, extent=(-2, 2)) + >>> img[rr, cc] = 2 + >>> rr, cc = rectangle(start, extent=(-2, -2)) + >>> img[rr, cc] = 3 + >>> rr, cc = rectangle(start, extent=(2, -2)) + >>> img[rr, cc] = 4 + >>> print(img) + [[0 0 0 0 0 0] + [0 3 3 2 2 0] + [0 3 3 2 2 0] + [0 4 4 1 1 0] + [0 4 4 1 1 0] + [0 0 0 0 0 0]] + + """ + tl, br = _rectangle_slice(start=start, end=end, extent=extent) + + if shape is not None: + n_dim = len(start) + br = np.minimum(shape[0:n_dim], br) + tl = np.maximum(np.zeros_like(shape[0:n_dim]), tl) + coords = np.meshgrid(*[np.arange(st, en) for st, en in zip(tuple(tl), tuple(br))]) + return coords + + +@require("matplotlib", ">=3.3") +def rectangle_perimeter(start, end=None, extent=None, shape=None, clip=False): + """Generate coordinates of pixels that are exactly around a rectangle. + + Parameters + ---------- + start : tuple + Origin point of the inner rectangle, e.g., ``(row, column)``. + end : tuple + End point of the inner rectangle ``(row, column)``. + For a 2D matrix, the slice defined by inner the rectangle is + ``[start:(end+1)]``. + Either `end` or `extent` must be specified. + extent : tuple + The extent (size) of the inner rectangle. E.g., + ``(num_rows, num_cols)``. + Either `end` or `extent` must be specified. + Negative extents are permitted. See `rectangle` to better + understand how they behave. + shape : tuple, optional + Image shape used to determine the maximum bounds of the output + coordinates. This is useful for clipping perimeters that exceed + the image size. By default, no clipping is done. Must be at least + length 2. Only the first two values are used to determine the extent of + the input image. + clip : bool, optional + Whether to clip the perimeter to the provided shape. If this is set + to True, the drawn figure will always be a closed polygon with all + edges visible. + + Returns + ------- + coords : array of int, shape (2, Npoints) + The coordinates of all pixels in the rectangle. + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('matplotlib') + + >>> import numpy as np + >>> from skimage.draw import rectangle_perimeter + >>> img = np.zeros((5, 6), dtype=np.uint8) + >>> start = (2, 3) + >>> end = (3, 4) + >>> rr, cc = rectangle_perimeter(start, end=end, shape=img.shape) + >>> img[rr, cc] = 1 + >>> img + array([[0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1], + [0, 0, 1, 0, 0, 1], + [0, 0, 1, 0, 0, 1], + [0, 0, 1, 1, 1, 1]], dtype=uint8) + + >>> img = np.zeros((5, 5), dtype=np.uint8) + >>> r, c = rectangle_perimeter(start, (10, 10), shape=img.shape, clip=True) + >>> img[r, c] = 1 + >>> img + array([[0, 0, 0, 0, 0], + [0, 0, 1, 1, 1], + [0, 0, 1, 0, 1], + [0, 0, 1, 0, 1], + [0, 0, 1, 1, 1]], dtype=uint8) + + """ + top_left, bottom_right = _rectangle_slice(start=start, end=end, extent=extent) + + top_left -= 1 + r = [top_left[0], top_left[0], bottom_right[0], bottom_right[0], top_left[0]] + c = [top_left[1], bottom_right[1], bottom_right[1], top_left[1], top_left[1]] + return polygon_perimeter(r, c, shape=shape, clip=clip) + + +def _rectangle_slice(start, end=None, extent=None): + """Return the slice ``(top_left, bottom_right)`` of the rectangle. + + Returns + ------- + (top_left, bottom_right) + The slice you would need to select the region in the rectangle defined + by the parameters. + Select it like: + + ``rect[top_left[0]:bottom_right[0], top_left[1]:bottom_right[1]]`` + """ + if end is None and extent is None: + raise ValueError("Either `end` or `extent` must be given.") + if end is not None and extent is not None: + raise ValueError("Cannot provide both `end` and `extent`.") + + if extent is not None: + end = np.asarray(start) + np.asarray(extent) + top_left = np.minimum(start, end) + bottom_right = np.maximum(start, end) + + top_left = np.round(top_left).astype(int) + bottom_right = np.round(bottom_right).astype(int) + + if extent is None: + bottom_right += 1 + + return (top_left, bottom_right) diff --git a/lib/python3.10/site-packages/skimage/draw/draw3d.py b/lib/python3.10/site-packages/skimage/draw/draw3d.py new file mode 100644 index 0000000000000000000000000000000000000000..22aa389c58d8cc4cd65710ed630df88cb9e28b3c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/draw3d.py @@ -0,0 +1,107 @@ +import numpy as np +from scipy.special import elliprg + + +def ellipsoid(a, b, c, spacing=(1.0, 1.0, 1.0), levelset=False): + """Generate ellipsoid for given semi-axis lengths. + + The respective semi-axis lengths are given along three dimensions in + Cartesian coordinates. Each dimension may use a different grid spacing. + + Parameters + ---------- + a : float + Length of semi-axis along x-axis. + b : float + Length of semi-axis along y-axis. + c : float + Length of semi-axis along z-axis. + spacing : 3-tuple of floats + Grid spacing in three spatial dimensions. + levelset : bool + If True, returns the level set for this ellipsoid (signed level + set about zero, with positive denoting interior) as np.float64. + False returns a binarized version of said level set. + + Returns + ------- + ellipsoid : (M, N, P) array + Ellipsoid centered in a correctly sized array for given `spacing`. + Boolean dtype unless `levelset=True`, in which case a float array is + returned with the level set above 0.0 representing the ellipsoid. + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + offset = np.r_[1, 1, 1] * np.r_[spacing] + + # Calculate limits, and ensure output volume is odd & symmetric + low = np.ceil(-np.r_[a, b, c] - offset) + high = np.floor(np.r_[a, b, c] + offset + 1) + + for dim in range(3): + if (high[dim] - low[dim]) % 2 == 0: + low[dim] -= 1 + num = np.arange(low[dim], high[dim], spacing[dim]) + if 0 not in num: + low[dim] -= np.max(num[num < 0]) + + # Generate (anisotropic) spatial grid + x, y, z = np.mgrid[ + low[0] : high[0] : spacing[0], + low[1] : high[1] : spacing[1], + low[2] : high[2] : spacing[2], + ] + + if not levelset: + arr = ((x / float(a)) ** 2 + (y / float(b)) ** 2 + (z / float(c)) ** 2) <= 1 + else: + arr = ((x / float(a)) ** 2 + (y / float(b)) ** 2 + (z / float(c)) ** 2) - 1 + + return arr + + +def ellipsoid_stats(a, b, c): + """Calculate analytical volume and surface area of an ellipsoid. + + The surface area of an ellipsoid is given by + + .. math:: S=4\\pi b c R_G\\!\\left(1, \\frac{a^2}{b^2}, \\frac{a^2}{c^2}\\right) + + where :math:`R_G` is Carlson's completely symmetric elliptic integral of + the second kind [1]_. The latter is implemented as + :py:func:`scipy.special.elliprg`. + + Parameters + ---------- + a : float + Length of semi-axis along x-axis. + b : float + Length of semi-axis along y-axis. + c : float + Length of semi-axis along z-axis. + + Returns + ------- + vol : float + Calculated volume of ellipsoid. + surf : float + Calculated surface area of ellipsoid. + + References + ---------- + .. [1] Paul Masson (2020). Surface Area of an Ellipsoid. + https://analyticphysics.com/Mathematical%20Methods/Surface%20Area%20of%20an%20Ellipsoid.htm + + """ + if (a <= 0) or (b <= 0) or (c <= 0): + raise ValueError('Parameters a, b, and c must all be > 0') + + # Volume + vol = 4 / 3.0 * np.pi * a * b * c + + # Surface area + surf = 3 * vol * elliprg(1 / a**2, 1 / b**2, 1 / c**2) + + return vol, surf diff --git a/lib/python3.10/site-packages/skimage/draw/draw_nd.py b/lib/python3.10/site-packages/skimage/draw/draw_nd.py new file mode 100644 index 0000000000000000000000000000000000000000..6e552a3a631423fca51413bb9c1403bcf6594d2d --- /dev/null +++ b/lib/python3.10/site-packages/skimage/draw/draw_nd.py @@ -0,0 +1,108 @@ +import numpy as np + + +def _round_safe(coords): + """Round coords while ensuring successive values are less than 1 apart. + + When rounding coordinates for `line_nd`, we want coordinates that are less + than 1 apart (always the case, by design) to remain less than one apart. + However, NumPy rounds values to the nearest *even* integer, so: + + >>> np.round([0.5, 1.5, 2.5, 3.5, 4.5]) + array([0., 2., 2., 4., 4.]) + + So, for our application, we detect whether the above case occurs, and use + ``np.floor`` if so. It is sufficient to detect that the first coordinate + falls on 0.5 and that the second coordinate is 1.0 apart, since we assume + by construction that the inter-point distance is less than or equal to 1 + and that all successive points are equidistant. + + Parameters + ---------- + coords : 1D array of float + The coordinates array. We assume that all successive values are + equidistant (``np.all(np.diff(coords) = coords[1] - coords[0])``) + and that this distance is no more than 1 + (``np.abs(coords[1] - coords[0]) <= 1``). + + Returns + ------- + rounded : 1D array of int + The array correctly rounded for an indexing operation, such that no + successive indices will be more than 1 apart. + + Examples + -------- + >>> coords0 = np.array([0.5, 1.25, 2., 2.75, 3.5]) + >>> _round_safe(coords0) + array([0, 1, 2, 3, 4]) + >>> coords1 = np.arange(0.5, 8, 1) + >>> coords1 + array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]) + >>> _round_safe(coords1) + array([0, 1, 2, 3, 4, 5, 6, 7]) + """ + if len(coords) > 1 and coords[0] % 1 == 0.5 and coords[1] - coords[0] == 1: + _round_function = np.floor + else: + _round_function = np.round + return _round_function(coords).astype(int) + + +def line_nd(start, stop, *, endpoint=False, integer=True): + """Draw a single-pixel thick line in n dimensions. + + The line produced will be ndim-connected. That is, two subsequent + pixels in the line will be either direct or diagonal neighbors in + n dimensions. + + Parameters + ---------- + start : array-like, shape (N,) + The start coordinates of the line. + stop : array-like, shape (N,) + The end coordinates of the line. + endpoint : bool, optional + Whether to include the endpoint in the returned line. Defaults + to False, which allows for easy drawing of multi-point paths. + integer : bool, optional + Whether to round the coordinates to integer. If True (default), + the returned coordinates can be used to directly index into an + array. `False` could be used for e.g. vector drawing. + + Returns + ------- + coords : tuple of arrays + The coordinates of points on the line. + + Examples + -------- + >>> lin = line_nd((1, 1), (5, 2.5), endpoint=False) + >>> lin + (array([1, 2, 3, 4]), array([1, 1, 2, 2])) + >>> im = np.zeros((6, 5), dtype=int) + >>> im[lin] = 1 + >>> im + array([[0, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]]) + >>> line_nd([2, 1, 1], [5, 5, 2.5], endpoint=True) + (array([2, 3, 4, 4, 5]), array([1, 2, 3, 4, 5]), array([1, 1, 2, 2, 2])) + """ + start = np.asarray(start) + stop = np.asarray(stop) + npoints = int(np.ceil(np.max(np.abs(stop - start)))) + if endpoint: + npoints += 1 + + coords = np.linspace(start, stop, num=npoints, endpoint=endpoint).T + if integer: + for dim in range(len(start)): + coords[dim, :] = _round_safe(coords[dim, :]) + + coords = coords.astype(int) + + return tuple(coords) diff --git a/lib/python3.10/site-packages/skimage/exposure/__init__.py b/lib/python3.10/site-packages/skimage/exposure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..45d6ef21e0a88162e58b739a84cb8f6cef2e951a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/exposure/__init__.py @@ -0,0 +1,5 @@ +"""Image intensity adjustment, e.g., histogram equalization, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/exposure/__init__.pyi b/lib/python3.10/site-packages/skimage/exposure/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f058561dc86fb51afc5f444d0af35fde9f8d1d69 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/exposure/__init__.pyi @@ -0,0 +1,29 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'histogram', + 'equalize_hist', + 'equalize_adapthist', + 'rescale_intensity', + 'cumulative_distribution', + 'adjust_gamma', + 'adjust_sigmoid', + 'adjust_log', + 'is_low_contrast', + 'match_histograms', +] + +from ._adapthist import equalize_adapthist +from .histogram_matching import match_histograms +from .exposure import ( + histogram, + equalize_hist, + rescale_intensity, + cumulative_distribution, + adjust_gamma, + adjust_sigmoid, + adjust_log, + is_low_contrast, +) diff --git a/lib/python3.10/site-packages/skimage/exposure/_adapthist.py b/lib/python3.10/site-packages/skimage/exposure/_adapthist.py new file mode 100644 index 0000000000000000000000000000000000000000..5404e32c42b2165075113991e89abd468d810cfd --- /dev/null +++ b/lib/python3.10/site-packages/skimage/exposure/_adapthist.py @@ -0,0 +1,317 @@ +""" +Adapted from "Contrast Limited Adaptive Histogram Equalization" by Karel +Zuiderveld, Graphics Gems IV, Academic Press, 1994. + +http://tog.acm.org/resources/GraphicsGems/ + +Relicensed with permission of the author under the Modified BSD license. +""" + +import math +import numbers + +import numpy as np + +from .._shared.utils import _supported_float_type +from ..color.adapt_rgb import adapt_rgb, hsv_value +from .exposure import rescale_intensity +from ..util import img_as_uint + +NR_OF_GRAY = 2**14 # number of grayscale levels to use in CLAHE algorithm + + +@adapt_rgb(hsv_value) +def equalize_adapthist(image, kernel_size=None, clip_limit=0.01, nbins=256): + """Contrast Limited Adaptive Histogram Equalization (CLAHE). + + An algorithm for local contrast enhancement, that uses histograms computed + over different tile regions of the image. Local details can therefore be + enhanced even in regions that are darker or lighter than most of the image. + + Parameters + ---------- + image : (M[, ...][, C]) ndarray + Input image. + kernel_size : int or array_like, optional + Defines the shape of contextual regions used in the algorithm. If + iterable is passed, it must have the same number of elements as + ``image.ndim`` (without color channel). If integer, it is broadcasted + to each `image` dimension. By default, ``kernel_size`` is 1/8 of + ``image`` height by 1/8 of its width. + clip_limit : float, optional + Clipping limit, normalized between 0 and 1 (higher values give more + contrast). + nbins : int, optional + Number of gray bins for histogram ("data range"). + + Returns + ------- + out : (M[, ...][, C]) ndarray + Equalized image with float64 dtype. + + See Also + -------- + equalize_hist, rescale_intensity + + Notes + ----- + * For color images, the following steps are performed: + - The image is converted to HSV color space + - The CLAHE algorithm is run on the V (Value) channel + - The image is converted back to RGB space and returned + * For RGBA images, the original alpha channel is removed. + + .. versionchanged:: 0.17 + The values returned by this function are slightly shifted upwards + because of an internal change in rounding behavior. + + References + ---------- + .. [1] http://tog.acm.org/resources/GraphicsGems/ + .. [2] https://en.wikipedia.org/wiki/CLAHE#CLAHE + """ + + float_dtype = _supported_float_type(image.dtype) + image = img_as_uint(image) + image = np.round(rescale_intensity(image, out_range=(0, NR_OF_GRAY - 1))).astype( + np.min_scalar_type(NR_OF_GRAY) + ) + + if kernel_size is None: + kernel_size = tuple([max(s // 8, 1) for s in image.shape]) + elif isinstance(kernel_size, numbers.Number): + kernel_size = (kernel_size,) * image.ndim + elif len(kernel_size) != image.ndim: + raise ValueError(f'Incorrect value of `kernel_size`: {kernel_size}') + + kernel_size = [int(k) for k in kernel_size] + + image = _clahe(image, kernel_size, clip_limit, nbins) + image = image.astype(float_dtype, copy=False) + return rescale_intensity(image) + + +def _clahe(image, kernel_size, clip_limit, nbins): + """Contrast Limited Adaptive Histogram Equalization. + + Parameters + ---------- + image : (M[, ...]) ndarray + Input image. + kernel_size : int or N-tuple of int + Defines the shape of contextual regions used in the algorithm. + clip_limit : float + Normalized clipping limit between 0 and 1 (higher values give more + contrast). + nbins : int + Number of gray bins for histogram ("data range"). + + Returns + ------- + out : (M[, ...]) ndarray + Equalized image. + + The number of "effective" graylevels in the output image is set by `nbins`; + selecting a small value (e.g. 128) speeds up processing and still produces + an output image of good quality. A clip limit of 0 or larger than or equal + to 1 results in standard (non-contrast limited) AHE. + """ + ndim = image.ndim + dtype = image.dtype + + # pad the image such that the shape in each dimension + # - is a multiple of the kernel_size and + # - is preceded by half a kernel size + pad_start_per_dim = [k // 2 for k in kernel_size] + + pad_end_per_dim = [ + (k - s % k) % k + int(np.ceil(k / 2.0)) + for k, s in zip(kernel_size, image.shape) + ] + + image = np.pad( + image, + [[p_i, p_f] for p_i, p_f in zip(pad_start_per_dim, pad_end_per_dim)], + mode='reflect', + ) + + # determine gray value bins + bin_size = 1 + NR_OF_GRAY // nbins + lut = np.arange(NR_OF_GRAY, dtype=np.min_scalar_type(NR_OF_GRAY)) + lut //= bin_size + + image = lut[image] + + # calculate graylevel mappings for each contextual region + # rearrange image into flattened contextual regions + ns_hist = [int(s / k) - 1 for s, k in zip(image.shape, kernel_size)] + hist_blocks_shape = np.array([ns_hist, kernel_size]).T.flatten() + hist_blocks_axis_order = np.array( + [np.arange(0, ndim * 2, 2), np.arange(1, ndim * 2, 2)] + ).flatten() + hist_slices = [slice(k // 2, k // 2 + n * k) for k, n in zip(kernel_size, ns_hist)] + hist_blocks = image[tuple(hist_slices)].reshape(hist_blocks_shape) + hist_blocks = np.transpose(hist_blocks, axes=hist_blocks_axis_order) + hist_block_assembled_shape = hist_blocks.shape + hist_blocks = hist_blocks.reshape((math.prod(ns_hist), -1)) + + # Calculate actual clip limit + kernel_elements = math.prod(kernel_size) + if clip_limit > 0.0: + clim = int(np.clip(clip_limit * kernel_elements, 1, None)) + else: + # largest possible value, i.e., do not clip (AHE) + clim = kernel_elements + + hist = np.apply_along_axis(np.bincount, -1, hist_blocks, minlength=nbins) + hist = np.apply_along_axis(clip_histogram, -1, hist, clip_limit=clim) + hist = map_histogram(hist, 0, NR_OF_GRAY - 1, kernel_elements) + hist = hist.reshape(hist_block_assembled_shape[:ndim] + (-1,)) + + # duplicate leading mappings in each dim + map_array = np.pad(hist, [[1, 1] for _ in range(ndim)] + [[0, 0]], mode='edge') + + # Perform multilinear interpolation of graylevel mappings + # using the convention described here: + # https://en.wikipedia.org/w/index.php?title=Adaptive_histogram_ + # equalization&oldid=936814673#Efficient_computation_by_interpolation + + # rearrange image into blocks for vectorized processing + ns_proc = [int(s / k) for s, k in zip(image.shape, kernel_size)] + blocks_shape = np.array([ns_proc, kernel_size]).T.flatten() + blocks_axis_order = np.array( + [np.arange(0, ndim * 2, 2), np.arange(1, ndim * 2, 2)] + ).flatten() + blocks = image.reshape(blocks_shape) + blocks = np.transpose(blocks, axes=blocks_axis_order) + blocks_flattened_shape = blocks.shape + blocks = np.reshape(blocks, (math.prod(ns_proc), math.prod(blocks.shape[ndim:]))) + + # calculate interpolation coefficients + coeffs = np.meshgrid( + *tuple([np.arange(k) / k for k in kernel_size[::-1]]), indexing='ij' + ) + coeffs = [np.transpose(c).flatten() for c in coeffs] + inv_coeffs = [1 - c for dim, c in enumerate(coeffs)] + + # sum over contributions of neighboring contextual + # regions in each direction + result = np.zeros(blocks.shape, dtype=np.float32) + for iedge, edge in enumerate(np.ndindex(*([2] * ndim))): + edge_maps = map_array[tuple([slice(e, e + n) for e, n in zip(edge, ns_proc)])] + edge_maps = edge_maps.reshape((math.prod(ns_proc), -1)) + + # apply map + edge_mapped = np.take_along_axis(edge_maps, blocks, axis=-1) + + # interpolate + edge_coeffs = np.prod( + [[inv_coeffs, coeffs][e][d] for d, e in enumerate(edge[::-1])], 0 + ) + + result += (edge_mapped * edge_coeffs).astype(result.dtype) + + result = result.astype(dtype) + + # rebuild result image from blocks + result = result.reshape(blocks_flattened_shape) + blocks_axis_rebuild_order = np.array( + [np.arange(0, ndim), np.arange(ndim, ndim * 2)] + ).T.flatten() + result = np.transpose(result, axes=blocks_axis_rebuild_order) + result = result.reshape(image.shape) + + # undo padding + unpad_slices = tuple( + [ + slice(p_i, s - p_f) + for p_i, p_f, s in zip(pad_start_per_dim, pad_end_per_dim, image.shape) + ] + ) + result = result[unpad_slices] + + return result + + +def clip_histogram(hist, clip_limit): + """Perform clipping of the histogram and redistribution of bins. + + The histogram is clipped and the number of excess pixels is counted. + Afterwards the excess pixels are equally redistributed across the + whole histogram (providing the bin count is smaller than the cliplimit). + + Parameters + ---------- + hist : ndarray + Histogram array. + clip_limit : int + Maximum allowed bin count. + + Returns + ------- + hist : ndarray + Clipped histogram. + """ + # calculate total number of excess pixels + excess_mask = hist > clip_limit + excess = hist[excess_mask] + n_excess = excess.sum() - excess.size * clip_limit + hist[excess_mask] = clip_limit + + # Second part: clip histogram and redistribute excess pixels in each bin + bin_incr = n_excess // hist.size # average binincrement + upper = clip_limit - bin_incr # Bins larger than upper set to cliplimit + + low_mask = hist < upper + n_excess -= hist[low_mask].size * bin_incr + hist[low_mask] += bin_incr + + mid_mask = np.logical_and(hist >= upper, hist < clip_limit) + mid = hist[mid_mask] + n_excess += mid.sum() - mid.size * clip_limit + hist[mid_mask] = clip_limit + + while n_excess > 0: # Redistribute remaining excess + prev_n_excess = n_excess + for index in range(hist.size): + under_mask = hist < clip_limit + step_size = max(1, np.count_nonzero(under_mask) // n_excess) + under_mask = under_mask[index::step_size] + hist[index::step_size][under_mask] += 1 + n_excess -= np.count_nonzero(under_mask) + if n_excess <= 0: + break + if prev_n_excess == n_excess: + break + + return hist + + +def map_histogram(hist, min_val, max_val, n_pixels): + """Calculate the equalized lookup table (mapping). + + It does so by cumulating the input histogram. + Histogram bins are assumed to be represented by the last array dimension. + + Parameters + ---------- + hist : ndarray + Clipped histogram. + min_val : int + Minimum value for mapping. + max_val : int + Maximum value for mapping. + n_pixels : int + Number of pixels in the region. + + Returns + ------- + out : ndarray + Mapped intensity LUT. + """ + out = np.cumsum(hist, axis=-1).astype(float) + out *= (max_val - min_val) / n_pixels + out += min_val + np.clip(out, a_min=None, a_max=max_val, out=out) + + return out.astype(int) diff --git a/lib/python3.10/site-packages/skimage/exposure/exposure.py b/lib/python3.10/site-packages/skimage/exposure/exposure.py new file mode 100644 index 0000000000000000000000000000000000000000..61b0333afd955b6323ca69ae5da8aabcadde3b51 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/exposure/exposure.py @@ -0,0 +1,849 @@ +import numpy as np + +from ..util.dtype import dtype_range, dtype_limits +from .._shared import utils + + +__all__ = [ + 'histogram', + 'cumulative_distribution', + 'equalize_hist', + 'rescale_intensity', + 'adjust_gamma', + 'adjust_log', + 'adjust_sigmoid', +] + + +DTYPE_RANGE = dtype_range.copy() +DTYPE_RANGE.update((d.__name__, limits) for d, limits in dtype_range.items()) +DTYPE_RANGE.update( + { + 'uint10': (0, 2**10 - 1), + 'uint12': (0, 2**12 - 1), + 'uint14': (0, 2**14 - 1), + 'bool': dtype_range[bool], + 'float': dtype_range[np.float64], + } +) + + +def _offset_array(arr, low_boundary, high_boundary): + """Offset the array to get the lowest value at 0 if negative.""" + if low_boundary < 0: + offset = low_boundary + dyn_range = high_boundary - low_boundary + # get smallest dtype that can hold both minimum and offset maximum + offset_dtype = np.promote_types( + np.min_scalar_type(dyn_range), np.min_scalar_type(low_boundary) + ) + if arr.dtype != offset_dtype: + # prevent overflow errors when offsetting + arr = arr.astype(offset_dtype) + arr = arr - offset + return arr + + +def _bincount_histogram_centers(image, source_range): + """Compute bin centers for bincount-based histogram.""" + if source_range not in ['image', 'dtype']: + raise ValueError(f'Incorrect value for `source_range` argument: {source_range}') + if source_range == 'image': + image_min = int(image.min().astype(np.int64)) + image_max = int(image.max().astype(np.int64)) + elif source_range == 'dtype': + image_min, image_max = dtype_limits(image, clip_negative=False) + bin_centers = np.arange(image_min, image_max + 1) + return bin_centers + + +def _bincount_histogram(image, source_range, bin_centers=None): + """ + Efficient histogram calculation for an image of integers. + + This function is significantly more efficient than np.histogram but + works only on images of integers. It is based on np.bincount. + + Parameters + ---------- + image : array + Input image. + source_range : string + 'image' determines the range from the input image. + 'dtype' determines the range from the expected range of the images + of that data type. + + Returns + ------- + hist : array + The values of the histogram. + bin_centers : array + The values at the center of the bins. + """ + if bin_centers is None: + bin_centers = _bincount_histogram_centers(image, source_range) + image_min, image_max = bin_centers[0], bin_centers[-1] + image = _offset_array(image, image_min, image_max) + hist = np.bincount(image.ravel(), minlength=image_max - min(image_min, 0) + 1) + if source_range == 'image': + idx = max(image_min, 0) + hist = hist[idx:] + return hist, bin_centers + + +def _get_outer_edges(image, hist_range): + """Determine the outer bin edges to use for `numpy.histogram`. + + These are obtained from either the image or hist_range. + + Parameters + ---------- + image : ndarray + Image for which the histogram is to be computed. + hist_range: 2-tuple of int or None + Range of values covered by the histogram bins. If None, the minimum + and maximum values of `image` are used. + + Returns + ------- + first_edge, last_edge : int + The range spanned by the histogram bins. + + Notes + ----- + This function is adapted from ``np.lib.histograms._get_outer_edges``. + """ + if hist_range is not None: + first_edge, last_edge = hist_range + if first_edge > last_edge: + raise ValueError("max must be larger than min in hist_range parameter.") + if not (np.isfinite(first_edge) and np.isfinite(last_edge)): + raise ValueError( + f'supplied hist_range of [{first_edge}, {last_edge}] is ' f'not finite' + ) + elif image.size == 0: + # handle empty arrays. Can't determine hist_range, so use 0-1. + first_edge, last_edge = 0, 1 + else: + first_edge, last_edge = image.min(), image.max() + if not (np.isfinite(first_edge) and np.isfinite(last_edge)): + raise ValueError( + f'autodetected hist_range of [{first_edge}, {last_edge}] is ' + f'not finite' + ) + + # expand empty hist_range to avoid divide by zero + if first_edge == last_edge: + first_edge = first_edge - 0.5 + last_edge = last_edge + 0.5 + + return first_edge, last_edge + + +def _get_bin_edges(image, nbins, hist_range): + """Computes histogram bins for use with `numpy.histogram`. + + Parameters + ---------- + image : ndarray + Image for which the histogram is to be computed. + nbins : int + The number of bins. + hist_range: 2-tuple of int + Range of values covered by the histogram bins. + + Returns + ------- + bin_edges : ndarray + The histogram bin edges. + + Notes + ----- + This function is a simplified version of + ``np.lib.histograms._get_bin_edges`` that only supports uniform bins. + """ + first_edge, last_edge = _get_outer_edges(image, hist_range) + # numpy/gh-10322 means that type resolution rules are dependent on array + # shapes. To avoid this causing problems, we pick a type now and stick + # with it throughout. + bin_type = np.result_type(first_edge, last_edge, image) + if np.issubdtype(bin_type, np.integer): + bin_type = np.result_type(bin_type, float) + + # compute bin edges + bin_edges = np.linspace( + first_edge, last_edge, nbins + 1, endpoint=True, dtype=bin_type + ) + return bin_edges + + +def _get_numpy_hist_range(image, source_range): + if source_range == 'image': + hist_range = None + elif source_range == 'dtype': + hist_range = dtype_limits(image, clip_negative=False) + else: + raise ValueError(f'Incorrect value for `source_range` argument: {source_range}') + return hist_range + + +@utils.channel_as_last_axis(multichannel_output=False) +def histogram( + image, nbins=256, source_range='image', normalize=False, *, channel_axis=None +): + """Return histogram of image. + + Unlike `numpy.histogram`, this function returns the centers of bins and + does not rebin integer arrays. For integer arrays, each integer value has + its own bin, which improves speed and intensity-resolution. + + If `channel_axis` is not set, the histogram is computed on the flattened + image. For color or multichannel images, set ``channel_axis`` to use a + common binning for all channels. Alternatively, one may apply the function + separately on each channel to obtain a histogram for each color channel + with separate binning. + + Parameters + ---------- + image : array + Input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + source_range : string, optional + 'image' (default) determines the range from the input image. + 'dtype' determines the range from the expected range of the images + of that data type. + normalize : bool, optional + If True, normalize the histogram by the sum of its values. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + Returns + ------- + hist : array + The values of the histogram. When ``channel_axis`` is not None, hist + will be a 2D array where the first axis corresponds to channels. + bin_centers : array + The values at the center of the bins. + + See Also + -------- + cumulative_distribution + + Examples + -------- + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.camera()) + >>> np.histogram(image, bins=2) + (array([ 93585, 168559]), array([0. , 0.5, 1. ])) + >>> exposure.histogram(image, nbins=2) + (array([ 93585, 168559]), array([0.25, 0.75])) + """ + sh = image.shape + if len(sh) == 3 and sh[-1] < 4 and channel_axis is None: + utils.warn( + 'This might be a color image. The histogram will be ' + 'computed on the flattened image. You can instead ' + 'apply this function to each color channel, or set ' + 'channel_axis.' + ) + + if channel_axis is not None: + channels = sh[-1] + hist = [] + + # compute bins based on the raveled array + if np.issubdtype(image.dtype, np.integer): + # here bins corresponds to the bin centers + bins = _bincount_histogram_centers(image, source_range) + else: + # determine the bin edges for np.histogram + hist_range = _get_numpy_hist_range(image, source_range) + bins = _get_bin_edges(image, nbins, hist_range) + + for chan in range(channels): + h, bc = _histogram(image[..., chan], bins, source_range, normalize) + hist.append(h) + # Convert to numpy arrays + bin_centers = np.asarray(bc) + hist = np.stack(hist, axis=0) + else: + hist, bin_centers = _histogram(image, nbins, source_range, normalize) + + return hist, bin_centers + + +def _histogram(image, bins, source_range, normalize): + """ + + Parameters + ---------- + image : ndarray + Image for which the histogram is to be computed. + bins : int or ndarray + The number of histogram bins. For images with integer dtype, an array + containing the bin centers can also be provided. For images with + floating point dtype, this can be an array of bin_edges for use by + ``np.histogram``. + source_range : string, optional + 'image' (default) determines the range from the input image. + 'dtype' determines the range from the expected range of the images + of that data type. + normalize : bool, optional + If True, normalize the histogram by the sum of its values. + """ + + image = image.flatten() + # For integer types, histogramming with bincount is more efficient. + if np.issubdtype(image.dtype, np.integer): + bin_centers = bins if isinstance(bins, np.ndarray) else None + hist, bin_centers = _bincount_histogram(image, source_range, bin_centers) + else: + hist_range = _get_numpy_hist_range(image, source_range) + hist, bin_edges = np.histogram(image, bins=bins, range=hist_range) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0 + + if normalize: + hist = hist / np.sum(hist) + return hist, bin_centers + + +def cumulative_distribution(image, nbins=256): + """Return cumulative distribution function (cdf) for the given image. + + Parameters + ---------- + image : array + Image array. + nbins : int, optional + Number of bins for image histogram. + + Returns + ------- + img_cdf : array + Values of cumulative distribution function. + bin_centers : array + Centers of bins. + + See Also + -------- + histogram + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Cumulative_distribution_function + + Examples + -------- + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.camera()) + >>> hi = exposure.histogram(image) + >>> cdf = exposure.cumulative_distribution(image) + >>> all(cdf[0] == np.cumsum(hi[0])/float(image.size)) + True + """ + hist, bin_centers = histogram(image, nbins) + img_cdf = hist.cumsum() + img_cdf = img_cdf / float(img_cdf[-1]) + + # cast img_cdf to single precision for float32 or float16 inputs + cdf_dtype = utils._supported_float_type(image.dtype) + img_cdf = img_cdf.astype(cdf_dtype, copy=False) + + return img_cdf, bin_centers + + +def equalize_hist(image, nbins=256, mask=None): + """Return image after histogram equalization. + + Parameters + ---------- + image : array + Image array. + nbins : int, optional + Number of bins for image histogram. Note: this argument is + ignored for integer images, for which each integer is its own + bin. + mask : ndarray of bools or 0s and 1s, optional + Array of same shape as `image`. Only points at which mask == True + are used for the equalization, which is applied to the whole image. + + Returns + ------- + out : float array + Image array after histogram equalization. + + Notes + ----- + This function is adapted from [1]_ with the author's permission. + + References + ---------- + .. [1] http://www.janeriksolem.net/histogram-equalization-with-python-and.html + .. [2] https://en.wikipedia.org/wiki/Histogram_equalization + + """ + if mask is not None: + mask = np.array(mask, dtype=bool) + cdf, bin_centers = cumulative_distribution(image[mask], nbins) + else: + cdf, bin_centers = cumulative_distribution(image, nbins) + out = np.interp(image.flat, bin_centers, cdf) + out = out.reshape(image.shape) + # Unfortunately, np.interp currently always promotes to float64, so we + # have to cast back to single precision when float32 output is desired + return out.astype(utils._supported_float_type(image.dtype), copy=False) + + +def intensity_range(image, range_values='image', clip_negative=False): + """Return image intensity range (min, max) based on desired value type. + + Parameters + ---------- + image : array + Input image. + range_values : str or 2-tuple, optional + The image intensity range is configured by this parameter. + The possible values for this parameter are enumerated below. + + 'image' + Return image min/max as the range. + 'dtype' + Return min/max of the image's dtype as the range. + dtype-name + Return intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. Note: `image` is ignored for this range type. + 2-tuple + Return `range_values` as min/max intensities. Note that there's no + reason to use this function if you just want to specify the + intensity range explicitly. This option is included for functions + that use `intensity_range` to support all desired range types. + + clip_negative : bool, optional + If True, clip the negative range (i.e. return 0 for min intensity) + even if the image dtype allows negative values. + """ + if range_values == 'dtype': + range_values = image.dtype.type + + if range_values == 'image': + i_min = np.min(image) + i_max = np.max(image) + elif range_values in DTYPE_RANGE: + i_min, i_max = DTYPE_RANGE[range_values] + if clip_negative: + i_min = 0 + else: + i_min, i_max = range_values + return i_min, i_max + + +def _output_dtype(dtype_or_range, image_dtype): + """Determine the output dtype for rescale_intensity. + + The dtype is determined according to the following rules: + - if ``dtype_or_range`` is a dtype, that is the output dtype. + - if ``dtype_or_range`` is a dtype string, that is the dtype used, unless + it is not a NumPy data type (e.g. 'uint12' for 12-bit unsigned integers), + in which case the data type that can contain it will be used + (e.g. uint16 in this case). + - if ``dtype_or_range`` is a pair of values, the output data type will be + ``_supported_float_type(image_dtype)``. This preserves float32 output for + float32 inputs. + + Parameters + ---------- + dtype_or_range : type, string, or 2-tuple of int/float + The desired range for the output, expressed as either a NumPy dtype or + as a (min, max) pair of numbers. + image_dtype : np.dtype + The input image dtype. + + Returns + ------- + out_dtype : type + The data type appropriate for the desired output. + """ + if type(dtype_or_range) in [list, tuple, np.ndarray]: + # pair of values: always return float. + return utils._supported_float_type(image_dtype) + if type(dtype_or_range) == type: + # already a type: return it + return dtype_or_range + if dtype_or_range in DTYPE_RANGE: + # string key in DTYPE_RANGE dictionary + try: + # if it's a canonical numpy dtype, convert + return np.dtype(dtype_or_range).type + except TypeError: # uint10, uint12, uint14 + # otherwise, return uint16 + return np.uint16 + else: + raise ValueError( + 'Incorrect value for out_range, should be a valid image data ' + f'type or a pair of values, got {dtype_or_range}.' + ) + + +def rescale_intensity(image, in_range='image', out_range='dtype'): + """Return image after stretching or shrinking its intensity levels. + + The desired intensity range of the input and output, `in_range` and + `out_range` respectively, are used to stretch or shrink the intensity range + of the input image. See examples below. + + Parameters + ---------- + image : array + Image array. + in_range, out_range : str or 2-tuple, optional + Min and max intensity values of input and output image. + The possible values for this parameter are enumerated below. + + 'image' + Use image min/max as the intensity range. + 'dtype' + Use min/max of the image's dtype as the intensity range. + dtype-name + Use intensity range based on desired `dtype`. Must be valid key + in `DTYPE_RANGE`. + 2-tuple + Use `range_values` as explicit min/max intensities. + + Returns + ------- + out : array + Image array after rescaling its intensity. This image is the same dtype + as the input image. + + Notes + ----- + .. versionchanged:: 0.17 + The dtype of the output array has changed to match the input dtype, or + float if the output range is specified by a pair of values. + + See Also + -------- + equalize_hist + + Examples + -------- + By default, the min/max intensities of the input image are stretched to + the limits allowed by the image's dtype, since `in_range` defaults to + 'image' and `out_range` defaults to 'dtype': + + >>> image = np.array([51, 102, 153], dtype=np.uint8) + >>> rescale_intensity(image) + array([ 0, 127, 255], dtype=uint8) + + It's easy to accidentally convert an image dtype from uint8 to float: + + >>> 1.0 * image + array([ 51., 102., 153.]) + + Use `rescale_intensity` to rescale to the proper range for float dtypes: + + >>> image_float = 1.0 * image + >>> rescale_intensity(image_float) + array([0. , 0.5, 1. ]) + + To maintain the low contrast of the original, use the `in_range` parameter: + + >>> rescale_intensity(image_float, in_range=(0, 255)) + array([0.2, 0.4, 0.6]) + + If the min/max value of `in_range` is more/less than the min/max image + intensity, then the intensity levels are clipped: + + >>> rescale_intensity(image_float, in_range=(0, 102)) + array([0.5, 1. , 1. ]) + + If you have an image with signed integers but want to rescale the image to + just the positive range, use the `out_range` parameter. In that case, the + output dtype will be float: + + >>> image = np.array([-10, 0, 10], dtype=np.int8) + >>> rescale_intensity(image, out_range=(0, 127)) + array([ 0. , 63.5, 127. ]) + + To get the desired range with a specific dtype, use ``.astype()``: + + >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int8) + array([ 0, 63, 127], dtype=int8) + + If the input image is constant, the output will be clipped directly to the + output range: + >>> image = np.array([130, 130, 130], dtype=np.int32) + >>> rescale_intensity(image, out_range=(0, 127)).astype(np.int32) + array([127, 127, 127], dtype=int32) + """ + if out_range in ['dtype', 'image']: + out_dtype = _output_dtype(image.dtype.type, image.dtype) + else: + out_dtype = _output_dtype(out_range, image.dtype) + + imin, imax = map(float, intensity_range(image, in_range)) + omin, omax = map( + float, intensity_range(image, out_range, clip_negative=(imin >= 0)) + ) + + if np.any(np.isnan([imin, imax, omin, omax])): + utils.warn( + "One or more intensity levels are NaN. Rescaling will broadcast " + "NaN to the full image. Provide intensity levels yourself to " + "avoid this. E.g. with np.nanmin(image), np.nanmax(image).", + stacklevel=2, + ) + + image = np.clip(image, imin, imax) + + if imin != imax: + image = (image - imin) / (imax - imin) + return (image * (omax - omin) + omin).astype(out_dtype) + else: + return np.clip(image, omin, omax).astype(out_dtype) + + +def _assert_non_negative(image): + if np.any(image < 0): + raise ValueError( + 'Image Correction methods work correctly only on ' + 'images with non-negative values. Use ' + 'skimage.exposure.rescale_intensity.' + ) + + +def _adjust_gamma_u8(image, gamma, gain): + """LUT based implementation of gamma adjustment.""" + lut = 255 * gain * (np.linspace(0, 1, 256) ** gamma) + lut = np.minimum(np.rint(lut), 255).astype('uint8') + return lut[image] + + +def adjust_gamma(image, gamma=1, gain=1): + """Performs Gamma Correction on the input image. + + Also known as Power Law Transform. + This function transforms the input image pixelwise according to the + equation ``O = I**gamma`` after scaling each pixel to the range 0 to 1. + + Parameters + ---------- + image : ndarray + Input image. + gamma : float, optional + Non negative real number. Default value is 1. + gain : float, optional + The constant multiplier. Default value is 1. + + Returns + ------- + out : ndarray + Gamma corrected output image. + + See Also + -------- + adjust_log + + Notes + ----- + For gamma greater than 1, the histogram will shift towards left and + the output image will be darker than the input image. + + For gamma less than 1, the histogram will shift towards right and + the output image will be brighter than the input image. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Gamma_correction + + Examples + -------- + >>> from skimage import data, exposure, img_as_float + >>> image = img_as_float(data.moon()) + >>> gamma_corrected = exposure.adjust_gamma(image, 2) + >>> # Output is darker for gamma > 1 + >>> image.mean() > gamma_corrected.mean() + True + """ + if gamma < 0: + raise ValueError("Gamma should be a non-negative real number.") + + dtype = image.dtype.type + + if dtype is np.uint8: + out = _adjust_gamma_u8(image, gamma, gain) + else: + _assert_non_negative(image) + + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + out = (((image / scale) ** gamma) * scale * gain).astype(dtype) + + return out + + +def adjust_log(image, gain=1, inv=False): + """Performs Logarithmic correction on the input image. + + This function transforms the input image pixelwise according to the + equation ``O = gain*log(1 + I)`` after scaling each pixel to the range + 0 to 1. For inverse logarithmic correction, the equation is + ``O = gain*(2**I - 1)``. + + Parameters + ---------- + image : ndarray + Input image. + gain : float, optional + The constant multiplier. Default value is 1. + inv : float, optional + If True, it performs inverse logarithmic correction, + else correction will be logarithmic. Defaults to False. + + Returns + ------- + out : ndarray + Logarithm corrected output image. + + See Also + -------- + adjust_gamma + + References + ---------- + .. [1] http://www.ece.ucsb.edu/Faculty/Manjunath/courses/ece178W03/EnhancePart1.pdf + + """ + _assert_non_negative(image) + dtype = image.dtype.type + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + if inv: + out = (2 ** (image / scale) - 1) * scale * gain + return dtype(out) + + out = np.log2(1 + image / scale) * scale * gain + return out.astype(dtype) + + +def adjust_sigmoid(image, cutoff=0.5, gain=10, inv=False): + """Performs Sigmoid Correction on the input image. + + Also known as Contrast Adjustment. + This function transforms the input image pixelwise according to the + equation ``O = 1/(1 + exp*(gain*(cutoff - I)))`` after scaling each pixel + to the range 0 to 1. + + Parameters + ---------- + image : ndarray + Input image. + cutoff : float, optional + Cutoff of the sigmoid function that shifts the characteristic curve + in horizontal direction. Default value is 0.5. + gain : float, optional + The constant multiplier in exponential's power of sigmoid function. + Default value is 10. + inv : bool, optional + If True, returns the negative sigmoid correction. Defaults to False. + + Returns + ------- + out : ndarray + Sigmoid corrected output image. + + See Also + -------- + adjust_gamma + + References + ---------- + .. [1] Gustav J. Braun, "Image Lightness Rescaling Using Sigmoidal Contrast + Enhancement Functions", + http://markfairchild.org/PDFs/PAP07.pdf + + """ + _assert_non_negative(image) + dtype = image.dtype.type + scale = float(dtype_limits(image, True)[1] - dtype_limits(image, True)[0]) + + if inv: + out = (1 - 1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale + return dtype(out) + + out = (1 / (1 + np.exp(gain * (cutoff - image / scale)))) * scale + return out.astype(dtype) + + +def is_low_contrast( + image, + fraction_threshold=0.05, + lower_percentile=1, + upper_percentile=99, + method='linear', +): + """Determine if an image is low contrast. + + Parameters + ---------- + image : array-like + The image under test. + fraction_threshold : float, optional + The low contrast fraction threshold. An image is considered low- + contrast when its range of brightness spans less than this + fraction of its data type's full range. [1]_ + lower_percentile : float, optional + Disregard values below this percentile when computing image contrast. + upper_percentile : float, optional + Disregard values above this percentile when computing image contrast. + method : str, optional + The contrast determination method. Right now the only available + option is "linear". + + Returns + ------- + out : bool + True when the image is determined to be low contrast. + + Notes + ----- + For boolean images, this function returns False only if all values are + the same (the method, threshold, and percentile arguments are ignored). + + References + ---------- + .. [1] https://scikit-image.org/docs/dev/user_guide/data_types.html + + Examples + -------- + >>> image = np.linspace(0, 0.04, 100) + >>> is_low_contrast(image) + True + >>> image[-1] = 1 + >>> is_low_contrast(image) + True + >>> is_low_contrast(image, upper_percentile=100) + False + """ + image = np.asanyarray(image) + + if image.dtype == bool: + return not ((image.max() == 1) and (image.min() == 0)) + + if image.ndim == 3: + from ..color import rgb2gray, rgba2rgb # avoid circular import + + if image.shape[2] == 4: + image = rgba2rgb(image) + if image.shape[2] == 3: + image = rgb2gray(image) + + dlimits = dtype_limits(image, clip_negative=False) + limits = np.percentile(image, [lower_percentile, upper_percentile]) + ratio = (limits[1] - limits[0]) / (dlimits[1] - dlimits[0]) + + return ratio < fraction_threshold diff --git a/lib/python3.10/site-packages/skimage/exposure/histogram_matching.py b/lib/python3.10/site-packages/skimage/exposure/histogram_matching.py new file mode 100644 index 0000000000000000000000000000000000000000..1c475d91a6a84cf20ffbc2f22f095563de56511a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/exposure/histogram_matching.py @@ -0,0 +1,93 @@ +import numpy as np + +from .._shared import utils + + +def _match_cumulative_cdf(source, template): + """ + Return modified source array so that the cumulative density function of + its values matches the cumulative density function of the template. + """ + if source.dtype.kind == 'u': + src_lookup = source.reshape(-1) + src_counts = np.bincount(src_lookup) + tmpl_counts = np.bincount(template.reshape(-1)) + + # omit values where the count was 0 + tmpl_values = np.nonzero(tmpl_counts)[0] + tmpl_counts = tmpl_counts[tmpl_values] + else: + src_values, src_lookup, src_counts = np.unique( + source.reshape(-1), return_inverse=True, return_counts=True + ) + tmpl_values, tmpl_counts = np.unique(template.reshape(-1), return_counts=True) + + # calculate normalized quantiles for each array + src_quantiles = np.cumsum(src_counts) / source.size + tmpl_quantiles = np.cumsum(tmpl_counts) / template.size + + interp_a_values = np.interp(src_quantiles, tmpl_quantiles, tmpl_values) + return interp_a_values[src_lookup].reshape(source.shape) + + +@utils.channel_as_last_axis(channel_arg_positions=(0, 1)) +def match_histograms(image, reference, *, channel_axis=None): + """Adjust an image so that its cumulative histogram matches that of another. + + The adjustment is applied separately for each channel. + + Parameters + ---------- + image : ndarray + Input image. Can be gray-scale or in color. + reference : ndarray + Image to match histogram of. Must have the same number of channels as + image. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + Returns + ------- + matched : ndarray + Transformed input image. + + Raises + ------ + ValueError + Thrown when the number of channels in the input image and the reference + differ. + + References + ---------- + .. [1] http://paulbourke.net/miscellaneous/equalisation/ + + """ + if image.ndim != reference.ndim: + raise ValueError( + 'Image and reference must have the same number ' 'of channels.' + ) + + if channel_axis is not None: + if image.shape[-1] != reference.shape[-1]: + raise ValueError( + 'Number of channels in the input image and ' + 'reference image must match!' + ) + + matched = np.empty(image.shape, dtype=image.dtype) + for channel in range(image.shape[-1]): + matched_channel = _match_cumulative_cdf( + image[..., channel], reference[..., channel] + ) + matched[..., channel] = matched_channel + else: + # _match_cumulative_cdf will always return float64 due to np.interp + matched = _match_cumulative_cdf(image, reference) + + if matched.dtype.kind == 'f': + # output a float32 result when the input is float16 or float32 + out_dtype = utils._supported_float_type(image.dtype) + matched = matched.astype(out_dtype, copy=False) + return matched diff --git a/lib/python3.10/site-packages/skimage/feature/__init__.py b/lib/python3.10/site-packages/skimage/feature/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5a30fa68b4859a9b3b1aefe20e9185c00eaa6c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/__init__.py @@ -0,0 +1,5 @@ +"""Feature detection and extraction, e.g., texture analysis, corners, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/feature/__init__.pyi b/lib/python3.10/site-packages/skimage/feature/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..40c54fb43ca313da733b6c0577037d06f1c3b566 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/__init__.pyi @@ -0,0 +1,88 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'canny', + 'Cascade', + 'daisy', + 'hog', + 'graycomatrix', + 'graycoprops', + 'local_binary_pattern', + 'multiblock_lbp', + 'draw_multiblock_lbp', + 'peak_local_max', + 'structure_tensor', + 'structure_tensor_eigenvalues', + 'hessian_matrix', + 'hessian_matrix_det', + 'hessian_matrix_eigvals', + 'shape_index', + 'corner_kitchen_rosenfeld', + 'corner_harris', + 'corner_shi_tomasi', + 'corner_foerstner', + 'corner_subpix', + 'corner_peaks', + 'corner_moravec', + 'corner_fast', + 'corner_orientations', + 'match_template', + 'BRIEF', + 'CENSURE', + 'ORB', + 'SIFT', + 'match_descriptors', + 'plot_matched_features', + 'blob_dog', + 'blob_doh', + 'blob_log', + 'haar_like_feature', + 'haar_like_feature_coord', + 'draw_haar_like_feature', + 'multiscale_basic_features', + 'learn_gmm', + 'fisher_vector', +] + +from ._canny import canny +from ._cascade import Cascade +from ._daisy import daisy +from ._hog import hog +from .texture import ( + graycomatrix, + graycoprops, + local_binary_pattern, + multiblock_lbp, + draw_multiblock_lbp, +) +from .peak import peak_local_max +from .corner import ( + corner_kitchen_rosenfeld, + corner_harris, + corner_shi_tomasi, + corner_foerstner, + corner_subpix, + corner_peaks, + corner_fast, + structure_tensor, + structure_tensor_eigenvalues, + hessian_matrix, + hessian_matrix_eigvals, + hessian_matrix_det, + corner_moravec, + corner_orientations, + shape_index, +) +from .template import match_template +from .brief import BRIEF +from .censure import CENSURE +from .orb import ORB +from .sift import SIFT +from .match import match_descriptors +from .util import plot_matched_features +from .blob import blob_dog, blob_log, blob_doh +from .haar import haar_like_feature, haar_like_feature_coord, draw_haar_like_feature +from ._basic_features import multiscale_basic_features +from ._fisher_vector import learn_gmm, fisher_vector diff --git a/lib/python3.10/site-packages/skimage/feature/_basic_features.py b/lib/python3.10/site-packages/skimage/feature/_basic_features.py new file mode 100644 index 0000000000000000000000000000000000000000..86b70ca28bb80c8d675910fabe1e9d8b2c01f331 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_basic_features.py @@ -0,0 +1,198 @@ +from itertools import combinations_with_replacement +import itertools +import numpy as np +from skimage import filters, feature +from skimage.util.dtype import img_as_float32 +from .._shared._dependency_checks import is_wasm + +if not is_wasm: + from concurrent.futures import ThreadPoolExecutor as PoolExecutor +else: + from contextlib import AbstractContextManager + + # Threading isn't supported on WASM, mock ThreadPoolExecutor as a fallback + class PoolExecutor(AbstractContextManager): + def __init__(self, *_, **__): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def map(self, fn, iterables): + return map(fn, iterables) + + +def _texture_filter(gaussian_filtered): + H_elems = [ + np.gradient(np.gradient(gaussian_filtered)[ax0], axis=ax1) + for ax0, ax1 in combinations_with_replacement(range(gaussian_filtered.ndim), 2) + ] + eigvals = feature.hessian_matrix_eigvals(H_elems) + return eigvals + + +def _singlescale_basic_features_singlechannel( + img, sigma, intensity=True, edges=True, texture=True +): + results = () + gaussian_filtered = filters.gaussian(img, sigma=sigma, preserve_range=False) + if intensity: + results += (gaussian_filtered,) + if edges: + results += (filters.sobel(gaussian_filtered),) + if texture: + results += (*_texture_filter(gaussian_filtered),) + return results + + +def _mutiscale_basic_features_singlechannel( + img, + intensity=True, + edges=True, + texture=True, + sigma_min=0.5, + sigma_max=16, + num_sigma=None, + num_workers=None, +): + """Features for a single channel nd image. + + Parameters + ---------- + img : ndarray + Input image, which can be grayscale or multichannel. + intensity : bool, default True + If True, pixel intensities averaged over the different scales + are added to the feature set. + edges : bool, default True + If True, intensities of local gradients averaged over the different + scales are added to the feature set. + texture : bool, default True + If True, eigenvalues of the Hessian matrix after Gaussian blurring + at different scales are added to the feature set. + sigma_min : float, optional + Smallest value of the Gaussian kernel used to average local + neighborhoods before extracting features. + sigma_max : float, optional + Largest value of the Gaussian kernel used to average local + neighborhoods before extracting features. + num_sigma : int, optional + Number of values of the Gaussian kernel between sigma_min and sigma_max. + If None, sigma_min multiplied by powers of 2 are used. + num_workers : int or None, optional + The number of parallel threads to use. If set to ``None``, the full + set of available cores are used. + + Returns + ------- + features : list + List of features, each element of the list is an array of shape as img. + """ + # computations are faster as float32 + img = np.ascontiguousarray(img_as_float32(img)) + if num_sigma is None: + num_sigma = int(np.log2(sigma_max) - np.log2(sigma_min) + 1) + sigmas = np.logspace( + np.log2(sigma_min), + np.log2(sigma_max), + num=num_sigma, + base=2, + endpoint=True, + ) + with PoolExecutor(max_workers=num_workers) as ex: + out_sigmas = list( + ex.map( + lambda s: _singlescale_basic_features_singlechannel( + img, s, intensity=intensity, edges=edges, texture=texture + ), + sigmas, + ) + ) + features = itertools.chain.from_iterable(out_sigmas) + return features + + +def multiscale_basic_features( + image, + intensity=True, + edges=True, + texture=True, + sigma_min=0.5, + sigma_max=16, + num_sigma=None, + num_workers=None, + *, + channel_axis=None, +): + """Local features for a single- or multi-channel nd image. + + Intensity, gradient intensity and local structure are computed at + different scales thanks to Gaussian blurring. + + Parameters + ---------- + image : ndarray + Input image, which can be grayscale or multichannel. + intensity : bool, default True + If True, pixel intensities averaged over the different scales + are added to the feature set. + edges : bool, default True + If True, intensities of local gradients averaged over the different + scales are added to the feature set. + texture : bool, default True + If True, eigenvalues of the Hessian matrix after Gaussian blurring + at different scales are added to the feature set. + sigma_min : float, optional + Smallest value of the Gaussian kernel used to average local + neighborhoods before extracting features. + sigma_max : float, optional + Largest value of the Gaussian kernel used to average local + neighborhoods before extracting features. + num_sigma : int, optional + Number of values of the Gaussian kernel between sigma_min and sigma_max. + If None, sigma_min multiplied by powers of 2 are used. + num_workers : int or None, optional + The number of parallel threads to use. If set to ``None``, the full + set of available cores are used. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + features : np.ndarray + Array of shape ``image.shape + (n_features,)``. When `channel_axis` is + not None, all channels are concatenated along the features dimension. + (i.e. ``n_features == n_features_singlechannel * n_channels``) + """ + if not any([intensity, edges, texture]): + raise ValueError( + "At least one of `intensity`, `edges` or `textures`" + "must be True for features to be computed." + ) + if channel_axis is None: + image = image[..., np.newaxis] + channel_axis = -1 + elif channel_axis != -1: + image = np.moveaxis(image, channel_axis, -1) + + all_results = ( + _mutiscale_basic_features_singlechannel( + image[..., dim], + intensity=intensity, + edges=edges, + texture=texture, + sigma_min=sigma_min, + sigma_max=sigma_max, + num_sigma=num_sigma, + num_workers=num_workers, + ) + for dim in range(image.shape[-1]) + ) + features = list(itertools.chain.from_iterable(all_results)) + out = np.stack(features, axis=-1) + return out diff --git a/lib/python3.10/site-packages/skimage/feature/_canny.py b/lib/python3.10/site-packages/skimage/feature/_canny.py new file mode 100644 index 0000000000000000000000000000000000000000..a0fe8275148d155949dda8bc0229760d408d5d89 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_canny.py @@ -0,0 +1,262 @@ +""" +canny.py - Canny Edge detector + +Reference: Canny, J., A Computational Approach To Edge Detection, IEEE Trans. + Pattern Analysis and Machine Intelligence, 8:679-714, 1986 +""" + +import numpy as np +import scipy.ndimage as ndi + +from ..util.dtype import dtype_limits +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type, check_nD +from ._canny_cy import _nonmaximum_suppression_bilinear + + +def _preprocess(image, mask, sigma, mode, cval): + """Generate a smoothed image and an eroded mask. + + The image is smoothed using a gaussian filter ignoring masked + pixels and the mask is eroded. + + Parameters + ---------- + image : array + Image to be smoothed. + mask : array + Mask with 1's for significant pixels, 0's for masked pixels. + sigma : scalar or sequence of scalars + Standard deviation for Gaussian kernel. The standard + deviations of the Gaussian filter are given for each axis as a + sequence, or as a single number, in which case it is equal for + all axes. + mode : str, {'reflect', 'constant', 'nearest', 'mirror', 'wrap'} + The ``mode`` parameter determines how the array borders are + handled, where ``cval`` is the value when mode is equal to + 'constant'. + cval : float, optional + Value to fill past edges of input if `mode` is 'constant'. + + Returns + ------- + smoothed_image : ndarray + The smoothed array + eroded_mask : ndarray + The eroded mask. + + Notes + ----- + This function calculates the fractional contribution of masked pixels + by applying the function to the mask (which gets you the fraction of + the pixel data that's due to significant points). We then mask the image + and apply the function. The resulting values will be lower by the + bleed-over fraction, so you can recalibrate by dividing by the function + on the mask to recover the effect of smoothing from just the significant + pixels. + """ + gaussian_kwargs = dict(sigma=sigma, mode=mode, cval=cval, preserve_range=False) + compute_bleedover = mode == 'constant' or mask is not None + float_type = _supported_float_type(image.dtype) + if mask is None: + if compute_bleedover: + mask = np.ones(image.shape, dtype=float_type) + masked_image = image + + eroded_mask = np.ones(image.shape, dtype=bool) + eroded_mask[:1, :] = 0 + eroded_mask[-1:, :] = 0 + eroded_mask[:, :1] = 0 + eroded_mask[:, -1:] = 0 + + else: + mask = mask.astype(bool, copy=False) + masked_image = np.zeros_like(image) + masked_image[mask] = image[mask] + + # Make the eroded mask. Setting the border value to zero will wipe + # out the image edges for us. + s = ndi.generate_binary_structure(2, 2) + eroded_mask = ndi.binary_erosion(mask, s, border_value=0) + + if compute_bleedover: + # Compute the fractional contribution of masked pixels by applying + # the function to the mask (which gets you the fraction of the + # pixel data that's due to significant points) + bleed_over = ( + gaussian(mask.astype(float_type, copy=False), **gaussian_kwargs) + + np.finfo(float_type).eps + ) + + # Smooth the masked image + smoothed_image = gaussian(masked_image, **gaussian_kwargs) + + # Lower the result by the bleed-over fraction, so you can + # recalibrate by dividing by the function on the mask to recover + # the effect of smoothing from just the significant pixels. + if compute_bleedover: + smoothed_image /= bleed_over + + return smoothed_image, eroded_mask + + +def canny( + image, + sigma=1.0, + low_threshold=None, + high_threshold=None, + mask=None, + use_quantiles=False, + *, + mode='constant', + cval=0.0, +): + """Edge filter an image using the Canny algorithm. + + Parameters + ---------- + image : 2D array + Grayscale input image to detect edges on; can be of any dtype. + sigma : float, optional + Standard deviation of the Gaussian filter. + low_threshold : float, optional + Lower bound for hysteresis thresholding (linking edges). + If None, low_threshold is set to 10% of dtype's max. + high_threshold : float, optional + Upper bound for hysteresis thresholding (linking edges). + If None, high_threshold is set to 20% of dtype's max. + mask : array, dtype=bool, optional + Mask to limit the application of Canny to a certain area. + use_quantiles : bool, optional + If ``True`` then treat low_threshold and high_threshold as + quantiles of the edge magnitude image, rather than absolute + edge magnitude values. If ``True`` then the thresholds must be + in the range [0, 1]. + mode : str, {'reflect', 'constant', 'nearest', 'mirror', 'wrap'} + The ``mode`` parameter determines how the array borders are + handled during Gaussian filtering, where ``cval`` is the value when + mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if `mode` is 'constant'. + + Returns + ------- + output : 2D array (image) + The binary edge map. + + See also + -------- + skimage.filters.sobel + + Notes + ----- + The steps of the algorithm are as follows: + + * Smooth the image using a Gaussian with ``sigma`` width. + + * Apply the horizontal and vertical Sobel operators to get the gradients + within the image. The edge strength is the norm of the gradient. + + * Thin potential edges to 1-pixel wide curves. First, find the normal + to the edge at each point. This is done by looking at the + signs and the relative magnitude of the X-Sobel and Y-Sobel + to sort the points into 4 categories: horizontal, vertical, + diagonal and antidiagonal. Then look in the normal and reverse + directions to see if the values in either of those directions are + greater than the point in question. Use interpolation to get a mix of + points instead of picking the one that's the closest to the normal. + + * Perform a hysteresis thresholding: first label all points above the + high threshold as edges. Then recursively label any point above the + low threshold that is 8-connected to a labeled point as an edge. + + References + ---------- + .. [1] Canny, J., A Computational Approach To Edge Detection, IEEE Trans. + Pattern Analysis and Machine Intelligence, 8:679-714, 1986 + :DOI:`10.1109/TPAMI.1986.4767851` + .. [2] William Green's Canny tutorial + https://en.wikipedia.org/wiki/Canny_edge_detector + + Examples + -------- + >>> from skimage import feature + >>> rng = np.random.default_rng() + >>> # Generate noisy image of a square + >>> im = np.zeros((256, 256)) + >>> im[64:-64, 64:-64] = 1 + >>> im += 0.2 * rng.random(im.shape) + >>> # First trial with the Canny filter, with the default smoothing + >>> edges1 = feature.canny(im) + >>> # Increase the smoothing for better results + >>> edges2 = feature.canny(im, sigma=3) + + """ + + # Regarding masks, any point touching a masked point will have a gradient + # that is "infected" by the masked point, so it's enough to erode the + # mask by one and then mask the output. We also mask out the border points + # because who knows what lies beyond the edge of the image? + + if np.issubdtype(image.dtype, np.int64) or np.issubdtype(image.dtype, np.uint64): + raise ValueError("64-bit integer images are not supported") + + check_nD(image, 2) + dtype_max = dtype_limits(image, clip_negative=False)[1] + + if low_threshold is None: + low_threshold = 0.1 + elif use_quantiles: + if not (0.0 <= low_threshold <= 1.0): + raise ValueError("Quantile thresholds must be between 0 and 1.") + else: + low_threshold /= dtype_max + + if high_threshold is None: + high_threshold = 0.2 + elif use_quantiles: + if not (0.0 <= high_threshold <= 1.0): + raise ValueError("Quantile thresholds must be between 0 and 1.") + else: + high_threshold /= dtype_max + + if high_threshold < low_threshold: + raise ValueError("low_threshold should be lower then high_threshold") + + # Image filtering + smoothed, eroded_mask = _preprocess(image, mask, sigma, mode, cval) + + # Gradient magnitude estimation + jsobel = ndi.sobel(smoothed, axis=1) + isobel = ndi.sobel(smoothed, axis=0) + magnitude = isobel * isobel + magnitude += jsobel * jsobel + np.sqrt(magnitude, out=magnitude) + + if use_quantiles: + low_threshold, high_threshold = np.percentile( + magnitude, [100.0 * low_threshold, 100.0 * high_threshold] + ) + + # Non-maximum suppression + low_masked = _nonmaximum_suppression_bilinear( + isobel, jsobel, magnitude, eroded_mask, low_threshold + ) + + # Double thresholding and edge tracking + # + # Segment the low-mask, then only keep low-segments that have + # some high_mask component in them + # + low_mask = low_masked > 0 + strel = np.ones((3, 3), bool) + labels, count = ndi.label(low_mask, strel) + if count == 0: + return low_mask + + high_mask = low_mask & (low_masked >= high_threshold) + nonzero_sums = np.unique(labels[high_mask]) + good_label = np.zeros((count + 1,), bool) + good_label[nonzero_sums] = True + output_mask = good_label[labels] + return output_mask diff --git a/lib/python3.10/site-packages/skimage/feature/_daisy.py b/lib/python3.10/site-packages/skimage/feature/_daisy.py new file mode 100644 index 0000000000000000000000000000000000000000..74f0e122ca1c626132587a28e574e9c10955b115 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_daisy.py @@ -0,0 +1,249 @@ +import math + +import numpy as np +from numpy import arctan2, exp, pi, sqrt + +from .. import draw +from ..util.dtype import img_as_float +from .._shared.filters import gaussian +from .._shared.utils import check_nD +from ..color import gray2rgb + + +def daisy( + image, + step=4, + radius=15, + rings=3, + histograms=8, + orientations=8, + normalization='l1', + sigmas=None, + ring_radii=None, + visualize=False, +): + '''Extract DAISY feature descriptors densely for the given image. + + DAISY is a feature descriptor similar to SIFT formulated in a way that + allows for fast dense extraction. Typically, this is practical for + bag-of-features image representations. + + The implementation follows Tola et al. [1]_ but deviate on the following + points: + + * Histogram bin contribution are smoothed with a circular Gaussian + window over the tonal range (the angular range). + * The sigma values of the spatial Gaussian smoothing in this code do not + match the sigma values in the original code by Tola et al. [2]_. In + their code, spatial smoothing is applied to both the input image and + the center histogram. However, this smoothing is not documented in [1]_ + and, therefore, it is omitted. + + Parameters + ---------- + image : (M, N) array + Input image (grayscale). + step : int, optional + Distance between descriptor sampling points. + radius : int, optional + Radius (in pixels) of the outermost ring. + rings : int, optional + Number of rings. + histograms : int, optional + Number of histograms sampled per ring. + orientations : int, optional + Number of orientations (bins) per histogram. + normalization : [ 'l1' | 'l2' | 'daisy' | 'off' ], optional + How to normalize the descriptors + + * 'l1': L1-normalization of each descriptor. + * 'l2': L2-normalization of each descriptor. + * 'daisy': L2-normalization of individual histograms. + * 'off': Disable normalization. + + sigmas : 1D array of float, optional + Standard deviation of spatial Gaussian smoothing for the center + histogram and for each ring of histograms. The array of sigmas should + be sorted from the center and out. I.e. the first sigma value defines + the spatial smoothing of the center histogram and the last sigma value + defines the spatial smoothing of the outermost ring. Specifying sigmas + overrides the following parameter. + + ``rings = len(sigmas) - 1`` + + ring_radii : 1D array of int, optional + Radius (in pixels) for each ring. Specifying ring_radii overrides the + following two parameters. + + ``rings = len(ring_radii)`` + ``radius = ring_radii[-1]`` + + If both sigmas and ring_radii are given, they must satisfy the + following predicate since no radius is needed for the center + histogram. + + ``len(ring_radii) == len(sigmas) + 1`` + + visualize : bool, optional + Generate a visualization of the DAISY descriptors + + Returns + ------- + descs : array + Grid of DAISY descriptors for the given image as an array + dimensionality (P, Q, R) where + + ``P = ceil((M - radius*2) / step)`` + ``Q = ceil((N - radius*2) / step)`` + ``R = (rings * histograms + 1) * orientations`` + + descs_img : (M, N, 3) array (only if visualize==True) + Visualization of the DAISY descriptors. + + References + ---------- + .. [1] Tola et al. "Daisy: An efficient dense descriptor applied to wide- + baseline stereo." Pattern Analysis and Machine Intelligence, IEEE + Transactions on 32.5 (2010): 815-830. + .. [2] http://cvlab.epfl.ch/software/daisy + ''' + + check_nD(image, 2, 'img') + + image = img_as_float(image) + float_dtype = image.dtype + + # Validate parameters. + if ( + sigmas is not None + and ring_radii is not None + and len(sigmas) - 1 != len(ring_radii) + ): + raise ValueError('`len(sigmas)-1 != len(ring_radii)`') + if ring_radii is not None: + rings = len(ring_radii) + radius = ring_radii[-1] + if sigmas is not None: + rings = len(sigmas) - 1 + if sigmas is None: + sigmas = [radius * (i + 1) / float(2 * rings) for i in range(rings)] + if ring_radii is None: + ring_radii = [radius * (i + 1) / float(rings) for i in range(rings)] + if normalization not in ['l1', 'l2', 'daisy', 'off']: + raise ValueError('Invalid normalization method.') + + # Compute image derivatives. + dx = np.zeros(image.shape, dtype=float_dtype) + dy = np.zeros(image.shape, dtype=float_dtype) + dx[:, :-1] = np.diff(image, n=1, axis=1) + dy[:-1, :] = np.diff(image, n=1, axis=0) + + # Compute gradient orientation and magnitude and their contribution + # to the histograms. + grad_mag = sqrt(dx**2 + dy**2) + grad_ori = arctan2(dy, dx) + orientation_kappa = orientations / pi + orientation_angles = [2 * o * pi / orientations - pi for o in range(orientations)] + hist = np.empty((orientations,) + image.shape, dtype=float_dtype) + for i, o in enumerate(orientation_angles): + # Weigh bin contribution by the circular normal distribution + hist[i, :, :] = exp(orientation_kappa * np.cos(grad_ori - o)) + # Weigh bin contribution by the gradient magnitude + hist[i, :, :] = np.multiply(hist[i, :, :], grad_mag) + + # Smooth orientation histograms for the center and all rings. + sigmas = [sigmas[0]] + sigmas + hist_smooth = np.empty((rings + 1,) + hist.shape, dtype=float_dtype) + for i in range(rings + 1): + for j in range(orientations): + hist_smooth[i, j, :, :] = gaussian( + hist[j, :, :], sigma=sigmas[i], mode='reflect' + ) + + # Assemble descriptor grid. + theta = [2 * pi * j / histograms for j in range(histograms)] + desc_dims = (rings * histograms + 1) * orientations + descs = np.empty( + (desc_dims, image.shape[0] - 2 * radius, image.shape[1] - 2 * radius), + dtype=float_dtype, + ) + descs[:orientations, :, :] = hist_smooth[0, :, radius:-radius, radius:-radius] + idx = orientations + for i in range(rings): + for j in range(histograms): + y_min = radius + int(round(ring_radii[i] * math.sin(theta[j]))) + y_max = descs.shape[1] + y_min + x_min = radius + int(round(ring_radii[i] * math.cos(theta[j]))) + x_max = descs.shape[2] + x_min + descs[idx : idx + orientations, :, :] = hist_smooth[ + i + 1, :, y_min:y_max, x_min:x_max + ] + idx += orientations + descs = descs[:, ::step, ::step] + descs = descs.swapaxes(0, 1).swapaxes(1, 2) + + # Normalize descriptors. + if normalization != 'off': + descs += 1e-10 + if normalization == 'l1': + descs /= np.sum(descs, axis=2)[:, :, np.newaxis] + elif normalization == 'l2': + descs /= sqrt(np.sum(descs**2, axis=2))[:, :, np.newaxis] + elif normalization == 'daisy': + for i in range(0, desc_dims, orientations): + norms = sqrt(np.sum(descs[:, :, i : i + orientations] ** 2, axis=2)) + descs[:, :, i : i + orientations] /= norms[:, :, np.newaxis] + + if visualize: + descs_img = gray2rgb(image) + for i in range(descs.shape[0]): + for j in range(descs.shape[1]): + # Draw center histogram sigma + color = [1, 0, 0] + desc_y = i * step + radius + desc_x = j * step + radius + rows, cols, val = draw.circle_perimeter_aa( + desc_y, desc_x, int(sigmas[0]) + ) + draw.set_color(descs_img, (rows, cols), color, alpha=val) + max_bin = np.max(descs[i, j, :]) + for o_num, o in enumerate(orientation_angles): + # Draw center histogram bins + bin_size = descs[i, j, o_num] / max_bin + dy = sigmas[0] * bin_size * math.sin(o) + dx = sigmas[0] * bin_size * math.cos(o) + rows, cols, val = draw.line_aa( + desc_y, desc_x, int(desc_y + dy), int(desc_x + dx) + ) + draw.set_color(descs_img, (rows, cols), color, alpha=val) + for r_num, r in enumerate(ring_radii): + color_offset = float(1 + r_num) / rings + color = (1 - color_offset, 1, color_offset) + for t_num, t in enumerate(theta): + # Draw ring histogram sigmas + hist_y = desc_y + int(round(r * math.sin(t))) + hist_x = desc_x + int(round(r * math.cos(t))) + rows, cols, val = draw.circle_perimeter_aa( + hist_y, hist_x, int(sigmas[r_num + 1]) + ) + draw.set_color(descs_img, (rows, cols), color, alpha=val) + for o_num, o in enumerate(orientation_angles): + # Draw histogram bins + bin_size = descs[ + i, + j, + orientations + + r_num * histograms * orientations + + t_num * orientations + + o_num, + ] + bin_size /= max_bin + dy = sigmas[r_num + 1] * bin_size * math.sin(o) + dx = sigmas[r_num + 1] * bin_size * math.cos(o) + rows, cols, val = draw.line_aa( + hist_y, hist_x, int(hist_y + dy), int(hist_x + dx) + ) + draw.set_color(descs_img, (rows, cols), color, alpha=val) + return descs, descs_img + else: + return descs diff --git a/lib/python3.10/site-packages/skimage/feature/_fisher_vector.py b/lib/python3.10/site-packages/skimage/feature/_fisher_vector.py new file mode 100644 index 0000000000000000000000000000000000000000..7645d1d2f05b141cf08f4ee7836d11cae246ee73 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_fisher_vector.py @@ -0,0 +1,265 @@ +""" +fisher_vector.py - Implementation of the Fisher vector encoding algorithm + +This module contains the source code for Fisher vector computation. The +computation is separated into two distinct steps, which are called separately +by the user, namely: + +learn_gmm: Used to estimate the GMM for all vectors/descriptors computed for + all examples in the dataset (e.g. estimated using all the SIFT + vectors computed for all images in the dataset, or at least a subset + of this). + +fisher_vector: Used to compute the Fisher vector representation for a + single set of descriptors/vector (e.g. the SIFT + descriptors for a single image in your dataset, or + perhaps a test image). + +Reference: Perronnin, F. and Dance, C. Fisher kernels on Visual Vocabularies + for Image Categorization, IEEE Conference on Computer Vision and + Pattern Recognition, 2007 + +Origin Author: Dan Oneata (Author of the original implementation for the Fisher +vector computation using scikit-learn and NumPy. Subsequently ported to +scikit-image (here) by other authors.) +""" + +import numpy as np + + +class FisherVectorException(Exception): + pass + + +class DescriptorException(FisherVectorException): + pass + + +def learn_gmm(descriptors, *, n_modes=32, gm_args=None): + """Estimate a Gaussian mixture model (GMM) given a set of descriptors and + number of modes (i.e. Gaussians). This function is essentially a wrapper + around the scikit-learn implementation of GMM, namely the + :class:`sklearn.mixture.GaussianMixture` class. + + Due to the nature of the Fisher vector, the only enforced parameter of the + underlying scikit-learn class is the covariance_type, which must be 'diag'. + + There is no simple way to know what value to use for `n_modes` a-priori. + Typically, the value is usually one of ``{16, 32, 64, 128}``. One may train + a few GMMs and choose the one that maximises the log probability of the + GMM, or choose `n_modes` such that the downstream classifier trained on + the resultant Fisher vectors has maximal performance. + + Parameters + ---------- + descriptors : np.ndarray (N, M) or list [(N1, M), (N2, M), ...] + List of NumPy arrays, or a single NumPy array, of the descriptors + used to estimate the GMM. The reason a list of NumPy arrays is + permissible is because often when using a Fisher vector encoding, + descriptors/vectors are computed separately for each sample/image in + the dataset, such as SIFT vectors for each image. If a list if passed + in, then each element must be a NumPy array in which the number of + rows may differ (e.g. different number of SIFT vector for each image), + but the number of columns for each must be the same (i.e. the + dimensionality must be the same). + n_modes : int + The number of modes/Gaussians to estimate during the GMM estimate. + gm_args : dict + Keyword arguments that can be passed into the underlying scikit-learn + :class:`sklearn.mixture.GaussianMixture` class. + + Returns + ------- + gmm : :class:`sklearn.mixture.GaussianMixture` + The estimated GMM object, which contains the necessary parameters + needed to compute the Fisher vector. + + References + ---------- + .. [1] https://scikit-learn.org/stable/modules/generated/sklearn.mixture.GaussianMixture.html + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('sklearn') + + >>> from skimage.feature import fisher_vector + >>> rng = np.random.Generator(np.random.PCG64()) + >>> sift_for_images = [rng.standard_normal((10, 128)) for _ in range(10)] + >>> num_modes = 16 + >>> # Estimate 16-mode GMM with these synthetic SIFT vectors + >>> gmm = learn_gmm(sift_for_images, n_modes=num_modes) + """ + + try: + from sklearn.mixture import GaussianMixture + except ImportError: + raise ImportError( + 'scikit-learn is not installed. Please ensure it is installed in ' + 'order to use the Fisher vector functionality.' + ) + + if not isinstance(descriptors, (list, np.ndarray)): + raise DescriptorException( + 'Please ensure descriptors are either a NumPy array, ' + 'or a list of NumPy arrays.' + ) + + d_mat_1 = descriptors[0] + if isinstance(descriptors, list) and not isinstance(d_mat_1, np.ndarray): + raise DescriptorException( + 'Please ensure descriptors are a list of NumPy arrays.' + ) + + if isinstance(descriptors, list): + expected_shape = descriptors[0].shape + ranks = [len(e.shape) == len(expected_shape) for e in descriptors] + if not all(ranks): + raise DescriptorException( + 'Please ensure all elements of your descriptor list ' 'are of rank 2.' + ) + dims = [e.shape[1] == descriptors[0].shape[1] for e in descriptors] + if not all(dims): + raise DescriptorException( + 'Please ensure all descriptors are of the same dimensionality.' + ) + + if not isinstance(n_modes, int) or n_modes <= 0: + raise FisherVectorException('Please ensure n_modes is a positive integer.') + + if gm_args: + has_cov_type = 'covariance_type' in gm_args + cov_type_not_diag = gm_args['covariance_type'] != 'diag' + if has_cov_type and cov_type_not_diag: + raise FisherVectorException('Covariance type must be "diag".') + + if isinstance(descriptors, list): + descriptors = np.vstack(descriptors) + + if gm_args: + has_cov_type = 'covariance_type' in gm_args + if has_cov_type: + gmm = GaussianMixture(n_components=n_modes, **gm_args) + else: + gmm = GaussianMixture( + n_components=n_modes, covariance_type='diag', **gm_args + ) + else: + gmm = GaussianMixture(n_components=n_modes, covariance_type='diag') + + gmm.fit(descriptors) + + return gmm + + +def fisher_vector(descriptors, gmm, *, improved=False, alpha=0.5): + """Compute the Fisher vector given some descriptors/vectors, + and an associated estimated GMM. + + Parameters + ---------- + descriptors : np.ndarray, shape=(n_descriptors, descriptor_length) + NumPy array of the descriptors for which the Fisher vector + representation is to be computed. + gmm : :class:`sklearn.mixture.GaussianMixture` + An estimated GMM object, which contains the necessary parameters needed + to compute the Fisher vector. + improved : bool, default=False + Flag denoting whether to compute improved Fisher vectors or not. + Improved Fisher vectors are L2 and power normalized. Power + normalization is simply f(z) = sign(z) pow(abs(z), alpha) for some + 0 <= alpha <= 1. + alpha : float, default=0.5 + The parameter for the power normalization step. Ignored if + improved=False. + + Returns + ------- + fisher_vector : np.ndarray + The computation Fisher vector, which is given by a concatenation of the + gradients of a GMM with respect to its parameters (mixture weights, + means, and covariance matrices). For D-dimensional input descriptors or + vectors, and a K-mode GMM, the Fisher vector dimensionality will be + 2KD + K. Thus, its dimensionality is invariant to the number of + descriptors/vectors. + + References + ---------- + .. [1] Perronnin, F. and Dance, C. Fisher kernels on Visual Vocabularies + for Image Categorization, IEEE Conference on Computer Vision and + Pattern Recognition, 2007 + .. [2] Perronnin, F. and Sanchez, J. and Mensink T. Improving the Fisher + Kernel for Large-Scale Image Classification, ECCV, 2010 + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('sklearn') + + >>> from skimage.feature import fisher_vector, learn_gmm + >>> sift_for_images = [np.random.random((10, 128)) for _ in range(10)] + >>> num_modes = 16 + >>> # Estimate 16-mode GMM with these synthetic SIFT vectors + >>> gmm = learn_gmm(sift_for_images, n_modes=num_modes) + >>> test_image_descriptors = np.random.random((25, 128)) + >>> # Compute the Fisher vector + >>> fv = fisher_vector(test_image_descriptors, gmm) + """ + try: + from sklearn.mixture import GaussianMixture + except ImportError: + raise ImportError( + 'scikit-learn is not installed. Please ensure it is installed in ' + 'order to use the Fisher vector functionality.' + ) + + if not isinstance(descriptors, np.ndarray): + raise DescriptorException('Please ensure descriptors is a NumPy array.') + + if not isinstance(gmm, GaussianMixture): + raise FisherVectorException( + 'Please ensure gmm is a sklearn.mixture.GaussianMixture object.' + ) + + if improved and not isinstance(alpha, float): + raise FisherVectorException( + 'Please ensure that the alpha parameter is a float.' + ) + + num_descriptors = len(descriptors) + + mixture_weights = gmm.weights_ + means = gmm.means_ + covariances = gmm.covariances_ + + posterior_probabilities = gmm.predict_proba(descriptors) + + # Statistics necessary to compute GMM gradients wrt its parameters + pp_sum = posterior_probabilities.mean(axis=0, keepdims=True).T + pp_x = posterior_probabilities.T.dot(descriptors) / num_descriptors + pp_x_2 = posterior_probabilities.T.dot(np.power(descriptors, 2)) / num_descriptors + + # Compute GMM gradients wrt its parameters + d_pi = pp_sum.squeeze() - mixture_weights + + d_mu = pp_x - pp_sum * means + + d_sigma_t1 = pp_sum * np.power(means, 2) + d_sigma_t2 = pp_sum * covariances + d_sigma_t3 = 2 * pp_x * means + d_sigma = -pp_x_2 - d_sigma_t1 + d_sigma_t2 + d_sigma_t3 + + # Apply analytical diagonal normalization + sqrt_mixture_weights = np.sqrt(mixture_weights) + d_pi /= sqrt_mixture_weights + d_mu /= sqrt_mixture_weights[:, np.newaxis] * np.sqrt(covariances) + d_sigma /= np.sqrt(2) * sqrt_mixture_weights[:, np.newaxis] * covariances + + # Concatenate GMM gradients to form Fisher vector representation + fisher_vector = np.hstack((d_pi, d_mu.ravel(), d_sigma.ravel())) + + if improved: + fisher_vector = np.sign(fisher_vector) * np.power(np.abs(fisher_vector), alpha) + fisher_vector = fisher_vector / np.linalg.norm(fisher_vector) + + return fisher_vector diff --git a/lib/python3.10/site-packages/skimage/feature/_hessian_det_appx.cpython-310-x86_64-linux-gnu.so b/lib/python3.10/site-packages/skimage/feature/_hessian_det_appx.cpython-310-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..c7dcc2c9238b700d8293abfe9c28af89f22ee917 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/feature/_hessian_det_appx.cpython-310-x86_64-linux-gnu.so differ diff --git a/lib/python3.10/site-packages/skimage/feature/_hog.py b/lib/python3.10/site-packages/skimage/feature/_hog.py new file mode 100644 index 0000000000000000000000000000000000000000..206361a3d4684361f0de401afa4b9c211cff5e94 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_hog.py @@ -0,0 +1,341 @@ +import numpy as np + +from . import _hoghistogram +from .._shared import utils + + +def _hog_normalize_block(block, method, eps=1e-5): + if method == 'L1': + out = block / (np.sum(np.abs(block)) + eps) + elif method == 'L1-sqrt': + out = np.sqrt(block / (np.sum(np.abs(block)) + eps)) + elif method == 'L2': + out = block / np.sqrt(np.sum(block**2) + eps**2) + elif method == 'L2-Hys': + out = block / np.sqrt(np.sum(block**2) + eps**2) + out = np.minimum(out, 0.2) + out = out / np.sqrt(np.sum(out**2) + eps**2) + else: + raise ValueError('Selected block normalization method is invalid.') + + return out + + +def _hog_channel_gradient(channel): + """Compute unnormalized gradient image along `row` and `col` axes. + + Parameters + ---------- + channel : (M, N) ndarray + Grayscale image or one of image channel. + + Returns + ------- + g_row, g_col : channel gradient along `row` and `col` axes correspondingly. + """ + g_row = np.empty(channel.shape, dtype=channel.dtype) + g_row[0, :] = 0 + g_row[-1, :] = 0 + g_row[1:-1, :] = channel[2:, :] - channel[:-2, :] + g_col = np.empty(channel.shape, dtype=channel.dtype) + g_col[:, 0] = 0 + g_col[:, -1] = 0 + g_col[:, 1:-1] = channel[:, 2:] - channel[:, :-2] + + return g_row, g_col + + +@utils.channel_as_last_axis(multichannel_output=False) +def hog( + image, + orientations=9, + pixels_per_cell=(8, 8), + cells_per_block=(3, 3), + block_norm='L2-Hys', + visualize=False, + transform_sqrt=False, + feature_vector=True, + *, + channel_axis=None, +): + """Extract Histogram of Oriented Gradients (HOG) for a given image. + + Compute a Histogram of Oriented Gradients (HOG) by + + 1. (optional) global image normalization + 2. computing the gradient image in `row` and `col` + 3. computing gradient histograms + 4. normalizing across blocks + 5. flattening into a feature vector + + Parameters + ---------- + image : (M, N[, C]) ndarray + Input image. + orientations : int, optional + Number of orientation bins. + pixels_per_cell : 2-tuple (int, int), optional + Size (in pixels) of a cell. + cells_per_block : 2-tuple (int, int), optional + Number of cells in each block. + block_norm : str {'L1', 'L1-sqrt', 'L2', 'L2-Hys'}, optional + Block normalization method: + + ``L1`` + Normalization using L1-norm. + ``L1-sqrt`` + Normalization using L1-norm, followed by square root. + ``L2`` + Normalization using L2-norm. + ``L2-Hys`` + Normalization using L2-norm, followed by limiting the + maximum values to 0.2 (`Hys` stands for `hysteresis`) and + renormalization using L2-norm. (default) + For details, see [3]_, [4]_. + + visualize : bool, optional + Also return an image of the HOG. For each cell and orientation bin, + the image contains a line segment that is centered at the cell center, + is perpendicular to the midpoint of the range of angles spanned by the + orientation bin, and has intensity proportional to the corresponding + histogram value. + transform_sqrt : bool, optional + Apply power law compression to normalize the image before + processing. DO NOT use this if the image contains negative + values. Also see `notes` section below. + feature_vector : bool, optional + Return the data as a feature vector by calling .ravel() on the result + just before returning. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + `channel_axis` was added in 0.19. + + Returns + ------- + out : (n_blocks_row, n_blocks_col, n_cells_row, n_cells_col, n_orient) ndarray + HOG descriptor for the image. If `feature_vector` is True, a 1D + (flattened) array is returned. + hog_image : (M, N) ndarray, optional + A visualisation of the HOG image. Only provided if `visualize` is True. + + Raises + ------ + ValueError + If the image is too small given the values of pixels_per_cell and + cells_per_block. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Histogram_of_oriented_gradients + + .. [2] Dalal, N and Triggs, B, Histograms of Oriented Gradients for + Human Detection, IEEE Computer Society Conference on Computer + Vision and Pattern Recognition 2005 San Diego, CA, USA, + https://lear.inrialpes.fr/people/triggs/pubs/Dalal-cvpr05.pdf, + :DOI:`10.1109/CVPR.2005.177` + + .. [3] Lowe, D.G., Distinctive image features from scale-invatiant + keypoints, International Journal of Computer Vision (2004) 60: 91, + http://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf, + :DOI:`10.1023/B:VISI.0000029664.99615.94` + + .. [4] Dalal, N, Finding People in Images and Videos, + Human-Computer Interaction [cs.HC], Institut National Polytechnique + de Grenoble - INPG, 2006, + https://tel.archives-ouvertes.fr/tel-00390303/file/NavneetDalalThesis.pdf + + Notes + ----- + The presented code implements the HOG extraction method from [2]_ with + the following changes: (I) blocks of (3, 3) cells are used ((2, 2) in the + paper); (II) no smoothing within cells (Gaussian spatial window with sigma=8pix + in the paper); (III) L1 block normalization is used (L2-Hys in the paper). + + Power law compression, also known as Gamma correction, is used to reduce + the effects of shadowing and illumination variations. The compression makes + the dark regions lighter. When the kwarg `transform_sqrt` is set to + ``True``, the function computes the square root of each color channel + and then applies the hog algorithm to the image. + """ + image = np.atleast_2d(image) + float_dtype = utils._supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + multichannel = channel_axis is not None + ndim_spatial = image.ndim - 1 if multichannel else image.ndim + if ndim_spatial != 2: + raise ValueError( + 'Only images with two spatial dimensions are ' + 'supported. If using with color/multichannel ' + 'images, specify `channel_axis`.' + ) + + """ + The first stage applies an optional global image normalization + equalisation that is designed to reduce the influence of illumination + effects. In practice we use gamma (power law) compression, either + computing the square root or the log of each color channel. + Image texture strength is typically proportional to the local surface + illumination so this compression helps to reduce the effects of local + shadowing and illumination variations. + """ + + if transform_sqrt: + image = np.sqrt(image) + + """ + The second stage computes first order image gradients. These capture + contour, silhouette and some texture information, while providing + further resistance to illumination variations. The locally dominant + color channel is used, which provides color invariance to a large + extent. Variant methods may also include second order image derivatives, + which act as primitive bar detectors - a useful feature for capturing, + e.g. bar like structures in bicycles and limbs in humans. + """ + + if multichannel: + g_row_by_ch = np.empty_like(image, dtype=float_dtype) + g_col_by_ch = np.empty_like(image, dtype=float_dtype) + g_magn = np.empty_like(image, dtype=float_dtype) + + for idx_ch in range(image.shape[2]): + ( + g_row_by_ch[:, :, idx_ch], + g_col_by_ch[:, :, idx_ch], + ) = _hog_channel_gradient(image[:, :, idx_ch]) + g_magn[:, :, idx_ch] = np.hypot( + g_row_by_ch[:, :, idx_ch], g_col_by_ch[:, :, idx_ch] + ) + + # For each pixel select the channel with the highest gradient magnitude + idcs_max = g_magn.argmax(axis=2) + rr, cc = np.meshgrid( + np.arange(image.shape[0]), + np.arange(image.shape[1]), + indexing='ij', + sparse=True, + ) + g_row = g_row_by_ch[rr, cc, idcs_max] + g_col = g_col_by_ch[rr, cc, idcs_max] + else: + g_row, g_col = _hog_channel_gradient(image) + + """ + The third stage aims to produce an encoding that is sensitive to + local image content while remaining resistant to small changes in + pose or appearance. The adopted method pools gradient orientation + information locally in the same way as the SIFT [Lowe 2004] + feature. The image window is divided into small spatial regions, + called "cells". For each cell we accumulate a local 1-D histogram + of gradient or edge orientations over all the pixels in the + cell. This combined cell-level 1-D histogram forms the basic + "orientation histogram" representation. Each orientation histogram + divides the gradient angle range into a fixed number of + predetermined bins. The gradient magnitudes of the pixels in the + cell are used to vote into the orientation histogram. + """ + + s_row, s_col = image.shape[:2] + c_row, c_col = pixels_per_cell + b_row, b_col = cells_per_block + + n_cells_row = int(s_row // c_row) # number of cells along row-axis + n_cells_col = int(s_col // c_col) # number of cells along col-axis + + # compute orientations integral images + orientation_histogram = np.zeros( + (n_cells_row, n_cells_col, orientations), dtype=float + ) + g_row = g_row.astype(float, copy=False) + g_col = g_col.astype(float, copy=False) + + _hoghistogram.hog_histograms( + g_col, + g_row, + c_col, + c_row, + s_col, + s_row, + n_cells_col, + n_cells_row, + orientations, + orientation_histogram, + ) + + # now compute the histogram for each cell + hog_image = None + + if visualize: + from .. import draw + + radius = min(c_row, c_col) // 2 - 1 + orientations_arr = np.arange(orientations) + # set dr_arr, dc_arr to correspond to midpoints of orientation bins + orientation_bin_midpoints = np.pi * (orientations_arr + 0.5) / orientations + dr_arr = radius * np.sin(orientation_bin_midpoints) + dc_arr = radius * np.cos(orientation_bin_midpoints) + hog_image = np.zeros((s_row, s_col), dtype=float_dtype) + for r in range(n_cells_row): + for c in range(n_cells_col): + for o, dr, dc in zip(orientations_arr, dr_arr, dc_arr): + centre = tuple([r * c_row + c_row // 2, c * c_col + c_col // 2]) + rr, cc = draw.line( + int(centre[0] - dc), + int(centre[1] + dr), + int(centre[0] + dc), + int(centre[1] - dr), + ) + hog_image[rr, cc] += orientation_histogram[r, c, o] + + """ + The fourth stage computes normalization, which takes local groups of + cells and contrast normalizes their overall responses before passing + to next stage. Normalization introduces better invariance to illumination, + shadowing, and edge contrast. It is performed by accumulating a measure + of local histogram "energy" over local groups of cells that we call + "blocks". The result is used to normalize each cell in the block. + Typically each individual cell is shared between several blocks, but + its normalizations are block dependent and thus different. The cell + thus appears several times in the final output vector with different + normalizations. This may seem redundant but it improves the performance. + We refer to the normalized block descriptors as Histogram of Oriented + Gradient (HOG) descriptors. + """ + + n_blocks_row = (n_cells_row - b_row) + 1 + n_blocks_col = (n_cells_col - b_col) + 1 + if n_blocks_col <= 0 or n_blocks_row <= 0: + min_row = b_row * c_row + min_col = b_col * c_col + raise ValueError( + 'The input image is too small given the values of ' + 'pixels_per_cell and cells_per_block. ' + 'It should have at least: ' + f'{min_row} rows and {min_col} cols.' + ) + normalized_blocks = np.zeros( + (n_blocks_row, n_blocks_col, b_row, b_col, orientations), dtype=float_dtype + ) + + for r in range(n_blocks_row): + for c in range(n_blocks_col): + block = orientation_histogram[r : r + b_row, c : c + b_col, :] + normalized_blocks[r, c, :] = _hog_normalize_block(block, method=block_norm) + + """ + The final step collects the HOG descriptors from all blocks of a dense + overlapping grid of blocks covering the detection window into a combined + feature vector for use in the window classifier. + """ + + if feature_vector: + normalized_blocks = normalized_blocks.ravel() + + if visualize: + return normalized_blocks, hog_image + else: + return normalized_blocks diff --git a/lib/python3.10/site-packages/skimage/feature/_orb_descriptor_positions.py b/lib/python3.10/site-packages/skimage/feature/_orb_descriptor_positions.py new file mode 100644 index 0000000000000000000000000000000000000000..74e05058c6d71d8459f41dcc26521f2a4af86e5a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/_orb_descriptor_positions.py @@ -0,0 +1,10 @@ +import os +import numpy as np + +# Putting this in cython was giving strange bugs for different versions +# of cython which seemed to indicate troubles with the __file__ variable +# not being defined. Keeping it in pure python makes it more reliable +this_dir = os.path.dirname(__file__) +POS = np.loadtxt(os.path.join(this_dir, "orb_descriptor_positions.txt"), dtype=np.int8) +POS0 = np.ascontiguousarray(POS[:, :2]) +POS1 = np.ascontiguousarray(POS[:, 2:]) diff --git a/lib/python3.10/site-packages/skimage/feature/blob.py b/lib/python3.10/site-packages/skimage/feature/blob.py new file mode 100644 index 0000000000000000000000000000000000000000..601898162e639981da58d9f67b593de78f1c74de --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/blob.py @@ -0,0 +1,715 @@ +import math + +import numpy as np +import scipy.ndimage as ndi +from scipy import spatial + +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type, check_nD +from ..transform import integral_image +from ..util import img_as_float +from ._hessian_det_appx import _hessian_matrix_det +from .peak import peak_local_max + +# This basic blob detection algorithm is based on: +# http://www.cs.utah.edu/~jfishbau/advimproc/project1/ (04.04.2013) +# Theory behind: https://en.wikipedia.org/wiki/Blob_detection (04.04.2013) + + +def _compute_disk_overlap(d, r1, r2): + """ + Compute fraction of surface overlap between two disks of radii + ``r1`` and ``r2``, with centers separated by a distance ``d``. + + Parameters + ---------- + d : float + Distance between centers. + r1 : float + Radius of the first disk. + r2 : float + Radius of the second disk. + + Returns + ------- + fraction: float + Fraction of area of the overlap between the two disks. + """ + + ratio1 = (d**2 + r1**2 - r2**2) / (2 * d * r1) + ratio1 = np.clip(ratio1, -1, 1) + acos1 = math.acos(ratio1) + + ratio2 = (d**2 + r2**2 - r1**2) / (2 * d * r2) + ratio2 = np.clip(ratio2, -1, 1) + acos2 = math.acos(ratio2) + + a = -d + r2 + r1 + b = d - r2 + r1 + c = d + r2 - r1 + d = d + r2 + r1 + area = r1**2 * acos1 + r2**2 * acos2 - 0.5 * math.sqrt(abs(a * b * c * d)) + return area / (math.pi * (min(r1, r2) ** 2)) + + +def _compute_sphere_overlap(d, r1, r2): + """ + Compute volume overlap fraction between two spheres of radii + ``r1`` and ``r2``, with centers separated by a distance ``d``. + + Parameters + ---------- + d : float + Distance between centers. + r1 : float + Radius of the first sphere. + r2 : float + Radius of the second sphere. + + Returns + ------- + fraction: float + Fraction of volume of the overlap between the two spheres. + + Notes + ----- + See for example http://mathworld.wolfram.com/Sphere-SphereIntersection.html + for more details. + """ + vol = ( + math.pi + / (12 * d) + * (r1 + r2 - d) ** 2 + * (d**2 + 2 * d * (r1 + r2) - 3 * (r1**2 + r2**2) + 6 * r1 * r2) + ) + return vol / (4.0 / 3 * math.pi * min(r1, r2) ** 3) + + +def _blob_overlap(blob1, blob2, *, sigma_dim=1): + """Finds the overlapping area fraction between two blobs. + + Returns a float representing fraction of overlapped area. Note that 0.0 + is *always* returned for dimension greater than 3. + + Parameters + ---------- + blob1 : sequence of arrays + A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``, + where ``row, col`` (or ``(pln, row, col)``) are coordinates + of blob and ``sigma`` is the standard deviation of the Gaussian kernel + which detected the blob. + blob2 : sequence of arrays + A sequence of ``(row, col, sigma)`` or ``(pln, row, col, sigma)``, + where ``row, col`` (or ``(pln, row, col)``) are coordinates + of blob and ``sigma`` is the standard deviation of the Gaussian kernel + which detected the blob. + sigma_dim : int, optional + The dimensionality of the sigma value. Can be 1 or the same as the + dimensionality of the blob space (2 or 3). + + Returns + ------- + f : float + Fraction of overlapped area (or volume in 3D). + """ + ndim = len(blob1) - sigma_dim + if ndim > 3: + return 0.0 + root_ndim = math.sqrt(ndim) + + # we divide coordinates by sigma * sqrt(ndim) to rescale space to isotropy, + # giving spheres of radius = 1 or < 1. + if blob1[-1] == blob2[-1] == 0: + return 0.0 + elif blob1[-1] > blob2[-1]: + max_sigma = blob1[-sigma_dim:] + r1 = 1 + r2 = blob2[-1] / blob1[-1] + else: + max_sigma = blob2[-sigma_dim:] + r2 = 1 + r1 = blob1[-1] / blob2[-1] + pos1 = blob1[:ndim] / (max_sigma * root_ndim) + pos2 = blob2[:ndim] / (max_sigma * root_ndim) + + d = np.sqrt(np.sum((pos2 - pos1) ** 2)) + if d > r1 + r2: # centers farther than sum of radii, so no overlap + return 0.0 + + # one blob is inside the other + if d <= abs(r1 - r2): + return 1.0 + + if ndim == 2: + return _compute_disk_overlap(d, r1, r2) + + else: # ndim=3 http://mathworld.wolfram.com/Sphere-SphereIntersection.html + return _compute_sphere_overlap(d, r1, r2) + + +def _prune_blobs(blobs_array, overlap, *, sigma_dim=1): + """Eliminated blobs with area overlap. + + Parameters + ---------- + blobs_array : ndarray + A 2d array with each row representing 3 (or 4) values, + ``(row, col, sigma)`` or ``(pln, row, col, sigma)`` in 3D, + where ``(row, col)`` (``(pln, row, col)``) are coordinates of the blob + and ``sigma`` is the standard deviation of the Gaussian kernel which + detected the blob. + This array must not have a dimension of size 0. + overlap : float + A value between 0 and 1. If the fraction of area overlapping for 2 + blobs is greater than `overlap` the smaller blob is eliminated. + sigma_dim : int, optional + The number of columns in ``blobs_array`` corresponding to sigmas rather + than positions. + + Returns + ------- + A : ndarray + `array` with overlapping blobs removed. + """ + sigma = blobs_array[:, -sigma_dim:].max() + distance = 2 * sigma * math.sqrt(blobs_array.shape[1] - sigma_dim) + tree = spatial.cKDTree(blobs_array[:, :-sigma_dim]) + pairs = np.array(list(tree.query_pairs(distance))) + if len(pairs) == 0: + return blobs_array + else: + for i, j in pairs: + blob1, blob2 = blobs_array[i], blobs_array[j] + if _blob_overlap(blob1, blob2, sigma_dim=sigma_dim) > overlap: + # note: this test works even in the anisotropic case because + # all sigmas increase together. + if blob1[-1] > blob2[-1]: + blob2[-1] = 0 + else: + blob1[-1] = 0 + + return np.stack([b for b in blobs_array if b[-1] > 0]) + + +def _format_exclude_border(img_ndim, exclude_border): + """Format an ``exclude_border`` argument as a tuple of ints for calling + ``peak_local_max``. + """ + if isinstance(exclude_border, tuple): + if len(exclude_border) != img_ndim: + raise ValueError( + "`exclude_border` should have the same length as the " + "dimensionality of the image." + ) + for exclude in exclude_border: + if not isinstance(exclude, int): + raise ValueError( + "exclude border, when expressed as a tuple, must only " + "contain ints." + ) + return exclude_border + (0,) + elif isinstance(exclude_border, int): + return (exclude_border,) * img_ndim + (0,) + elif exclude_border is True: + raise ValueError("exclude_border cannot be True") + elif exclude_border is False: + return (0,) * (img_ndim + 1) + else: + raise ValueError(f'Unsupported value ({exclude_border}) for exclude_border') + + +def blob_dog( + image, + min_sigma=1, + max_sigma=50, + sigma_ratio=1.6, + threshold=0.5, + overlap=0.5, + *, + threshold_rel=None, + exclude_border=False, +): + r"""Finds blobs in the given grayscale image. + + Blobs are found using the Difference of Gaussian (DoG) method [1]_, [2]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : scalar or sequence of scalars, optional + The minimum standard deviation for Gaussian kernel. Keep this low to + detect smaller blobs. The standard deviations of the Gaussian filter + are given for each axis as a sequence, or as a single number, in + which case it is equal for all axes. + max_sigma : scalar or sequence of scalars, optional + The maximum standard deviation for Gaussian kernel. Keep this high to + detect larger blobs. The standard deviations of the Gaussian filter + are given for each axis as a sequence, or as a single number, in + which case it is equal for all axes. + sigma_ratio : float, optional + The ratio between the standard deviation of Gaussian Kernels used for + computing the Difference of Gaussians + threshold : float or None, optional + The absolute lower bound for scale space maxima. Local maxima smaller + than `threshold` are ignored. Reduce this to detect blobs with lower + intensities. If `threshold_rel` is also specified, whichever threshold + is larger will be used. If None, `threshold_rel` is used instead. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + threshold_rel : float or None, optional + Minimum intensity of peaks, calculated as + ``max(dog_space) * threshold_rel``, where ``dog_space`` refers to the + stack of Difference-of-Gaussian (DoG) images computed internally. This + should have a value between 0 and 1. If None, `threshold` is used + instead. + exclude_border : tuple of ints, int, or False, optional + If tuple of ints, the length of the tuple must match the input array's + dimensionality. Each element of the tuple will exclude peaks from + within `exclude_border`-pixels of the border of the image along that + dimension. + If nonzero int, `exclude_border` excludes peaks from within + `exclude_border`-pixels of the border of the image. + If zero or False, peaks are identified regardless of their + distance from the border. + + Returns + ------- + A : (n, image.ndim + sigma) ndarray + A 2d array with each row representing 2 coordinate values for a 2D + image, or 3 coordinate values for a 3D image, plus the sigma(s) used. + When a single sigma is passed, outputs are: + ``(r, c, sigma)`` or ``(p, r, c, sigma)`` where ``(r, c)`` or + ``(p, r, c)`` are coordinates of the blob and ``sigma`` is the standard + deviation of the Gaussian kernel which detected the blob. When an + anisotropic gaussian is used (sigmas per dimension), the detected sigma + is returned for each dimension. + + See also + -------- + skimage.filters.difference_of_gaussians + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Blob_detection#The_difference_of_Gaussians_approach + .. [2] Lowe, D. G. "Distinctive Image Features from Scale-Invariant + Keypoints." International Journal of Computer Vision 60, 91–110 (2004). + https://www.cs.ubc.ca/~lowe/papers/ijcv04.pdf + :DOI:`10.1023/B:VISI.0000029664.99615.94` + + Examples + -------- + >>> from skimage import data, feature + >>> coins = data.coins() + >>> feature.blob_dog(coins, threshold=.05, min_sigma=10, max_sigma=40) + array([[128., 155., 10.], + [198., 155., 10.], + [124., 338., 10.], + [127., 102., 10.], + [193., 281., 10.], + [126., 208., 10.], + [267., 115., 10.], + [197., 102., 10.], + [198., 215., 10.], + [123., 279., 10.], + [126., 46., 10.], + [259., 247., 10.], + [196., 43., 10.], + [ 54., 276., 10.], + [267., 358., 10.], + [ 58., 100., 10.], + [259., 305., 10.], + [185., 347., 16.], + [261., 174., 16.], + [ 46., 336., 16.], + [ 54., 217., 10.], + [ 55., 157., 10.], + [ 57., 41., 10.], + [260., 47., 16.]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}\sigma` for + a 2-D image and :math:`\sqrt{3}\sigma` for a 3-D image. + """ + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + # if both min and max sigma are scalar, function returns only one sigma + scalar_sigma = np.isscalar(max_sigma) and np.isscalar(min_sigma) + + # Gaussian filter requires that sequence-type sigmas have same + # dimensionality as image. This broadcasts scalar kernels + if np.isscalar(max_sigma): + max_sigma = np.full(image.ndim, max_sigma, dtype=float_dtype) + if np.isscalar(min_sigma): + min_sigma = np.full(image.ndim, min_sigma, dtype=float_dtype) + + # Convert sequence types to array + min_sigma = np.asarray(min_sigma, dtype=float_dtype) + max_sigma = np.asarray(max_sigma, dtype=float_dtype) + + if sigma_ratio <= 1.0: + raise ValueError('sigma_ratio must be > 1.0') + + # k such that min_sigma*(sigma_ratio**k) > max_sigma + k = int(np.mean(np.log(max_sigma / min_sigma) / np.log(sigma_ratio) + 1)) + + # a geometric progression of standard deviations for gaussian kernels + sigma_list = np.array([min_sigma * (sigma_ratio**i) for i in range(k + 1)]) + + # computing difference between two successive Gaussian blurred images + # to obtain an approximation of the scale invariant Laplacian of the + # Gaussian operator + dog_image_cube = np.empty(image.shape + (k,), dtype=float_dtype) + gaussian_previous = gaussian(image, sigma=sigma_list[0], mode='reflect') + for i, s in enumerate(sigma_list[1:]): + gaussian_current = gaussian(image, sigma=s, mode='reflect') + dog_image_cube[..., i] = gaussian_previous - gaussian_current + gaussian_previous = gaussian_current + + # normalization factor for consistency in DoG magnitude + sf = 1 / (sigma_ratio - 1) + dog_image_cube *= sf + + exclude_border = _format_exclude_border(image.ndim, exclude_border) + local_maxima = peak_local_max( + dog_image_cube, + threshold_abs=threshold, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + footprint=np.ones((3,) * (image.ndim + 1)), + ) + + # Catch no peaks + if local_maxima.size == 0: + return np.empty((0, image.ndim + (1 if scalar_sigma else image.ndim))) + + # Convert local_maxima to float64 + lm = local_maxima.astype(float_dtype) + + # translate final column of lm, which contains the index of the + # sigma that produced the maximum intensity value, into the sigma + sigmas_of_peaks = sigma_list[local_maxima[:, -1]] + + if scalar_sigma: + # select one sigma column, keeping dimension + sigmas_of_peaks = sigmas_of_peaks[:, 0:1] + + # Remove sigma index and replace with sigmas + lm = np.hstack([lm[:, :-1], sigmas_of_peaks]) + + sigma_dim = sigmas_of_peaks.shape[1] + + return _prune_blobs(lm, overlap, sigma_dim=sigma_dim) + + +def blob_log( + image, + min_sigma=1, + max_sigma=50, + num_sigma=10, + threshold=0.2, + overlap=0.5, + log_scale=False, + *, + threshold_rel=None, + exclude_border=False, +): + r"""Finds blobs in the given grayscale image. + + Blobs are found using the Laplacian of Gaussian (LoG) method [1]_. + For each blob found, the method returns its coordinates and the standard + deviation of the Gaussian kernel that detected the blob. + + Parameters + ---------- + image : ndarray + Input grayscale image, blobs are assumed to be light on dark + background (white on black). + min_sigma : scalar or sequence of scalars, optional + the minimum standard deviation for Gaussian kernel. Keep this low to + detect smaller blobs. The standard deviations of the Gaussian filter + are given for each axis as a sequence, or as a single number, in + which case it is equal for all axes. + max_sigma : scalar or sequence of scalars, optional + The maximum standard deviation for Gaussian kernel. Keep this high to + detect larger blobs. The standard deviations of the Gaussian filter + are given for each axis as a sequence, or as a single number, in + which case it is equal for all axes. + num_sigma : int, optional + The number of intermediate values of standard deviations to consider + between `min_sigma` and `max_sigma`. + threshold : float or None, optional + The absolute lower bound for scale space maxima. Local maxima smaller + than `threshold` are ignored. Reduce this to detect blobs with lower + intensities. If `threshold_rel` is also specified, whichever threshold + is larger will be used. If None, `threshold_rel` is used instead. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + log_scale : bool, optional + If set intermediate values of standard deviations are interpolated + using a logarithmic scale to the base `10`. If not, linear + interpolation is used. + threshold_rel : float or None, optional + Minimum intensity of peaks, calculated as + ``max(log_space) * threshold_rel``, where ``log_space`` refers to the + stack of Laplacian-of-Gaussian (LoG) images computed internally. This + should have a value between 0 and 1. If None, `threshold` is used + instead. + exclude_border : tuple of ints, int, or False, optional + If tuple of ints, the length of the tuple must match the input array's + dimensionality. Each element of the tuple will exclude peaks from + within `exclude_border`-pixels of the border of the image along that + dimension. + If nonzero int, `exclude_border` excludes peaks from within + `exclude_border`-pixels of the border of the image. + If zero or False, peaks are identified regardless of their + distance from the border. + + Returns + ------- + A : (n, image.ndim + sigma) ndarray + A 2d array with each row representing 2 coordinate values for a 2D + image, or 3 coordinate values for a 3D image, plus the sigma(s) used. + When a single sigma is passed, outputs are: + ``(r, c, sigma)`` or ``(p, r, c, sigma)`` where ``(r, c)`` or + ``(p, r, c)`` are coordinates of the blob and ``sigma`` is the standard + deviation of the Gaussian kernel which detected the blob. When an + anisotropic gaussian is used (sigmas per dimension), the detected sigma + is returned for each dimension. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Blob_detection#The_Laplacian_of_Gaussian + + Examples + -------- + >>> from skimage import data, feature, exposure + >>> img = data.coins() + >>> img = exposure.equalize_hist(img) # improves detection + >>> feature.blob_log(img, threshold = .3) + array([[124. , 336. , 11.88888889], + [198. , 155. , 11.88888889], + [194. , 213. , 17.33333333], + [121. , 272. , 17.33333333], + [263. , 244. , 17.33333333], + [194. , 276. , 17.33333333], + [266. , 115. , 11.88888889], + [128. , 154. , 11.88888889], + [260. , 174. , 17.33333333], + [198. , 103. , 11.88888889], + [126. , 208. , 11.88888889], + [127. , 102. , 11.88888889], + [263. , 302. , 17.33333333], + [197. , 44. , 11.88888889], + [185. , 344. , 17.33333333], + [126. , 46. , 11.88888889], + [113. , 323. , 1. ]]) + + Notes + ----- + The radius of each blob is approximately :math:`\sqrt{2}\sigma` for + a 2-D image and :math:`\sqrt{3}\sigma` for a 3-D image. + """ + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + # if both min and max sigma are scalar, function returns only one sigma + scalar_sigma = True if np.isscalar(max_sigma) and np.isscalar(min_sigma) else False + + # Gaussian filter requires that sequence-type sigmas have same + # dimensionality as image. This broadcasts scalar kernels + if np.isscalar(max_sigma): + max_sigma = np.full(image.ndim, max_sigma, dtype=float_dtype) + if np.isscalar(min_sigma): + min_sigma = np.full(image.ndim, min_sigma, dtype=float_dtype) + + # Convert sequence types to array + min_sigma = np.asarray(min_sigma, dtype=float_dtype) + max_sigma = np.asarray(max_sigma, dtype=float_dtype) + + if log_scale: + start = np.log10(min_sigma) + stop = np.log10(max_sigma) + sigma_list = np.logspace(start, stop, num_sigma) + else: + sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) + + # computing gaussian laplace + image_cube = np.empty(image.shape + (len(sigma_list),), dtype=float_dtype) + for i, s in enumerate(sigma_list): + # average s**2 provides scale invariance + image_cube[..., i] = -ndi.gaussian_laplace(image, s) * np.mean(s) ** 2 + + exclude_border = _format_exclude_border(image.ndim, exclude_border) + local_maxima = peak_local_max( + image_cube, + threshold_abs=threshold, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + footprint=np.ones((3,) * (image.ndim + 1)), + ) + + # Catch no peaks + if local_maxima.size == 0: + return np.empty((0, image.ndim + (1 if scalar_sigma else image.ndim))) + + # Convert local_maxima to float64 + lm = local_maxima.astype(float_dtype) + + # translate final column of lm, which contains the index of the + # sigma that produced the maximum intensity value, into the sigma + sigmas_of_peaks = sigma_list[local_maxima[:, -1]] + + if scalar_sigma: + # select one sigma column, keeping dimension + sigmas_of_peaks = sigmas_of_peaks[:, 0:1] + + # Remove sigma index and replace with sigmas + lm = np.hstack([lm[:, :-1], sigmas_of_peaks]) + + sigma_dim = sigmas_of_peaks.shape[1] + + return _prune_blobs(lm, overlap, sigma_dim=sigma_dim) + + +def blob_doh( + image, + min_sigma=1, + max_sigma=30, + num_sigma=10, + threshold=0.01, + overlap=0.5, + log_scale=False, + *, + threshold_rel=None, +): + """Finds blobs in the given grayscale image. + + Blobs are found using the Determinant of Hessian method [1]_. For each blob + found, the method returns its coordinates and the standard deviation + of the Gaussian Kernel used for the Hessian matrix whose determinant + detected the blob. Determinant of Hessians is approximated using [2]_. + + Parameters + ---------- + image : 2D ndarray + Input grayscale image.Blobs can either be light on dark or vice versa. + min_sigma : float, optional + The minimum standard deviation for Gaussian Kernel used to compute + Hessian matrix. Keep this low to detect smaller blobs. + max_sigma : float, optional + The maximum standard deviation for Gaussian Kernel used to compute + Hessian matrix. Keep this high to detect larger blobs. + num_sigma : int, optional + The number of intermediate values of standard deviations to consider + between `min_sigma` and `max_sigma`. + threshold : float or None, optional + The absolute lower bound for scale space maxima. Local maxima smaller + than `threshold` are ignored. Reduce this to detect blobs with lower + intensities. If `threshold_rel` is also specified, whichever threshold + is larger will be used. If None, `threshold_rel` is used instead. + overlap : float, optional + A value between 0 and 1. If the area of two blobs overlaps by a + fraction greater than `threshold`, the smaller blob is eliminated. + log_scale : bool, optional + If set intermediate values of standard deviations are interpolated + using a logarithmic scale to the base `10`. If not, linear + interpolation is used. + threshold_rel : float or None, optional + Minimum intensity of peaks, calculated as + ``max(doh_space) * threshold_rel``, where ``doh_space`` refers to the + stack of Determinant-of-Hessian (DoH) images computed internally. This + should have a value between 0 and 1. If None, `threshold` is used + instead. + + Returns + ------- + A : (n, 3) ndarray + A 2d array with each row representing 3 values, ``(y,x,sigma)`` + where ``(y,x)`` are coordinates of the blob and ``sigma`` is the + standard deviation of the Gaussian kernel of the Hessian Matrix whose + determinant detected the blob. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Blob_detection#The_determinant_of_the_Hessian + .. [2] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool, + "SURF: Speeded Up Robust Features" + ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf + + Examples + -------- + >>> from skimage import data, feature + >>> img = data.coins() + >>> feature.blob_doh(img) + array([[197. , 153. , 20.33333333], + [124. , 336. , 20.33333333], + [126. , 153. , 20.33333333], + [195. , 100. , 23.55555556], + [192. , 212. , 23.55555556], + [121. , 271. , 30. ], + [126. , 101. , 20.33333333], + [193. , 275. , 23.55555556], + [123. , 205. , 20.33333333], + [270. , 363. , 30. ], + [265. , 113. , 23.55555556], + [262. , 243. , 23.55555556], + [185. , 348. , 30. ], + [156. , 302. , 30. ], + [123. , 44. , 23.55555556], + [260. , 173. , 30. ], + [197. , 44. , 20.33333333]]) + + Notes + ----- + The radius of each blob is approximately `sigma`. + Computation of Determinant of Hessians is independent of the standard + deviation. Therefore detecting larger blobs won't take more time. In + methods line :py:meth:`blob_dog` and :py:meth:`blob_log` the computation + of Gaussians for larger `sigma` takes more time. The downside is that + this method can't be used for detecting blobs of radius less than `3px` + due to the box filters used in the approximation of Hessian Determinant. + """ + check_nD(image, 2) + + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + image = integral_image(image) + + if log_scale: + start, stop = math.log(min_sigma, 10), math.log(max_sigma, 10) + sigma_list = np.logspace(start, stop, num_sigma) + else: + sigma_list = np.linspace(min_sigma, max_sigma, num_sigma) + + image_cube = np.empty(shape=image.shape + (len(sigma_list),), dtype=float_dtype) + for j, s in enumerate(sigma_list): + image_cube[..., j] = _hessian_matrix_det(image, s) + + local_maxima = peak_local_max( + image_cube, + threshold_abs=threshold, + threshold_rel=threshold_rel, + exclude_border=False, + footprint=np.ones((3,) * image_cube.ndim), + ) + + # Catch no peaks + if local_maxima.size == 0: + return np.empty((0, 3)) + # Convert local_maxima to float64 + lm = local_maxima.astype(np.float64) + # Convert the last index to its corresponding scale value + lm[:, -1] = sigma_list[local_maxima[:, -1]] + return _prune_blobs(lm, overlap) diff --git a/lib/python3.10/site-packages/skimage/feature/brief.py b/lib/python3.10/site-packages/skimage/feature/brief.py new file mode 100644 index 0000000000000000000000000000000000000000..634efb2c892842f0925dec470670d1f2090b5da8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/brief.py @@ -0,0 +1,216 @@ +import copy + +import numpy as np +from packaging.version import Version + +from .._shared.filters import gaussian +from .._shared.utils import check_nD +from .brief_cy import _brief_loop +from .util import ( + DescriptorExtractor, + _mask_border_keypoints, + _prepare_grayscale_input_2D, +) + + +np2 = Version(np.__version__) >= Version('2') + + +class BRIEF(DescriptorExtractor): + """BRIEF binary descriptor extractor. + + BRIEF (Binary Robust Independent Elementary Features) is an efficient + feature point descriptor. It is highly discriminative even when using + relatively few bits and is computed using simple intensity difference + tests. + + For each keypoint, intensity comparisons are carried out for a specifically + distributed number N of pixel-pairs resulting in a binary descriptor of + length N. For binary descriptors the Hamming distance can be used for + feature matching, which leads to lower computational cost in comparison to + the L2 norm. + + Parameters + ---------- + descriptor_size : int, optional + Size of BRIEF descriptor for each keypoint. Sizes 128, 256 and 512 + recommended by the authors. Default is 256. + patch_size : int, optional + Length of the two dimensional square patch sampling region around + the keypoints. Default is 49. + mode : {'normal', 'uniform'}, optional + Probability distribution for sampling location of decision pixel-pairs + around keypoints. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator (RNG). + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + The PRNG is used for the random sampling of the decision + pixel-pairs. From a square window with length `patch_size`, + pixel pairs are sampled using the `mode` parameter to build + the descriptors using intensity comparison. + + For matching across images, the same `rng` should be used to construct + descriptors. To facilitate this: + + (a) `rng` defaults to 1 + (b) Subsequent calls of the ``extract`` method will use the same rng/seed. + sigma : float, optional + Standard deviation of the Gaussian low-pass filter applied to the image + to alleviate noise sensitivity, which is strongly recommended to obtain + discriminative and good descriptors. + + Attributes + ---------- + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D ndarray of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + mask : (N,) array of dtype bool + Mask indicating whether a keypoint has been filtered out + (``False``) or is described in the `descriptors` array (``True``). + + Examples + -------- + >>> from skimage.feature import (corner_harris, corner_peaks, BRIEF, + ... match_descriptors) + >>> import numpy as np + >>> square1 = np.zeros((8, 8), dtype=np.int32) + >>> square1[2:6, 2:6] = 1 + >>> square1 + array([[0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> square2 = np.zeros((9, 9), dtype=np.int32) + >>> square2[2:7, 2:7] = 1 + >>> square2 + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=int32) + >>> keypoints1 = corner_peaks(corner_harris(square1), min_distance=1) + >>> keypoints2 = corner_peaks(corner_harris(square2), min_distance=1) + >>> extractor = BRIEF(patch_size=5) + >>> extractor.extract(square1, keypoints1) + >>> descriptors1 = extractor.descriptors + >>> extractor.extract(square2, keypoints2) + >>> descriptors2 = extractor.descriptors + >>> matches = match_descriptors(descriptors1, descriptors2) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 3]]) + >>> keypoints1[matches[:, 0]] + array([[2, 2], + [2, 5], + [5, 2], + [5, 5]]) + >>> keypoints2[matches[:, 1]] + array([[2, 2], + [2, 6], + [6, 2], + [6, 6]]) + + """ + + def __init__( + self, descriptor_size=256, patch_size=49, mode='normal', sigma=1, rng=1 + ): + mode = mode.lower() + if mode not in ('normal', 'uniform'): + raise ValueError("`mode` must be 'normal' or 'uniform'.") + + self.descriptor_size = descriptor_size + self.patch_size = patch_size + self.mode = mode + self.sigma = sigma + + if isinstance(rng, np.random.Generator): + # Spawn an independent RNG from parent RNG provided by the user. + # This is necessary so that we can safely deepcopy the RNG. + # See https://github.com/scikit-learn/scikit-learn/issues/16988#issuecomment-1518037853 + bg = rng._bit_generator + ss = bg._seed_seq + (child_ss,) = ss.spawn(1) + self.rng = np.random.Generator(type(bg)(child_ss)) + elif rng is None: + self.rng = np.random.default_rng(np.random.SeedSequence()) + else: + self.rng = np.random.default_rng(rng) + + self.descriptors = None + self.mask = None + + def extract(self, image, keypoints): + """Extract BRIEF binary descriptors for given keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + + """ + check_nD(image, 2) + + # Copy RNG so we can repeatedly call extract with the same random values + rng = copy.deepcopy(self.rng) + + image = _prepare_grayscale_input_2D(image) + + # Gaussian low-pass filtering to alleviate noise sensitivity + image = np.ascontiguousarray(gaussian(image, sigma=self.sigma, mode='reflect')) + + # Sampling pairs of decision pixels in patch_size x patch_size window + desc_size = self.descriptor_size + patch_size = self.patch_size + if self.mode == 'normal': + samples = (patch_size / 5.0) * rng.standard_normal(desc_size * 8) + samples = np.array(samples, dtype=np.int32) + samples = samples[ + (samples < (patch_size // 2)) & (samples > -(patch_size - 2) // 2) + ] + + pos1 = samples[: desc_size * 2].reshape(desc_size, 2) + pos2 = samples[desc_size * 2 : desc_size * 4].reshape(desc_size, 2) + elif self.mode == 'uniform': + samples = rng.integers( + -(patch_size - 2) // 2, (patch_size // 2) + 1, (desc_size * 2, 2) + ) + samples = np.array(samples, dtype=np.int32) + pos1, pos2 = np.split(samples, 2) + + pos1 = np.ascontiguousarray(pos1) + pos2 = np.ascontiguousarray(pos2) + + # Removing keypoints that are within (patch_size / 2) distance from the + # image border + self.mask = _mask_border_keypoints(image.shape, keypoints, patch_size // 2) + + keypoints = np.array( + keypoints[self.mask, :], + dtype=np.int64, + order='C', + copy=None if np2 else False, + ) + + self.descriptors = np.zeros( + (keypoints.shape[0], desc_size), dtype=bool, order='C' + ) + + _brief_loop(image, self.descriptors.view(np.uint8), keypoints, pos1, pos2) diff --git a/lib/python3.10/site-packages/skimage/feature/censure.py b/lib/python3.10/site-packages/skimage/feature/censure.py new file mode 100644 index 0000000000000000000000000000000000000000..7aaeab90877c08617936f0671692f6ecab4418b1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/censure.py @@ -0,0 +1,343 @@ +import numpy as np +from scipy.ndimage import maximum_filter, minimum_filter, convolve + +from ..transform import integral_image +from .corner import structure_tensor +from ..morphology import octagon, star +from .censure_cy import _censure_dob_loop +from ..feature.util import ( + FeatureDetector, + _prepare_grayscale_input_2D, + _mask_border_keypoints, +) +from .._shared.utils import check_nD + +# The paper(Reference [1]) mentions the sizes of the Octagon shaped filter +# kernel for the first seven scales only. The sizes of the later scales +# have been extrapolated based on the following statement in the paper. +# "These octagons scale linearly and were experimentally chosen to correspond +# to the seven DOBs described in the previous section." +OCTAGON_OUTER_SHAPE = [ + (5, 2), + (5, 3), + (7, 3), + (9, 4), + (9, 7), + (13, 7), + (15, 10), + (15, 11), + (15, 12), + (17, 13), + (17, 14), +] +OCTAGON_INNER_SHAPE = [ + (3, 0), + (3, 1), + (3, 2), + (5, 2), + (5, 3), + (5, 4), + (5, 5), + (7, 5), + (7, 6), + (9, 6), + (9, 7), +] + +# The sizes for the STAR shaped filter kernel for different scales have been +# taken from the OpenCV implementation. +STAR_SHAPE = [1, 2, 3, 4, 6, 8, 11, 12, 16, 22, 23, 32, 45, 46, 64, 90, 128] +STAR_FILTER_SHAPE = [ + (1, 0), + (3, 1), + (4, 2), + (5, 3), + (7, 4), + (8, 5), + (9, 6), + (11, 8), + (13, 10), + (14, 11), + (15, 12), + (16, 14), +] + + +def _filter_image(image, min_scale, max_scale, mode): + response = np.zeros( + (image.shape[0], image.shape[1], max_scale - min_scale + 1), dtype=np.float64 + ) + + if mode == 'dob': + # make response[:, :, i] contiguous memory block + item_size = response.itemsize + response.strides = ( + item_size * response.shape[1], + item_size, + item_size * response.shape[0] * response.shape[1], + ) + + integral_img = integral_image(image) + + for i in range(max_scale - min_scale + 1): + n = min_scale + i + + # Constant multipliers for the outer region and the inner region + # of the bi-level filters with the constraint of keeping the + # DC bias 0. + inner_weight = 1.0 / (2 * n + 1) ** 2 + outer_weight = 1.0 / (12 * n**2 + 4 * n) + + _censure_dob_loop( + n, integral_img, response[:, :, i], inner_weight, outer_weight + ) + + # NOTE : For the Octagon shaped filter, we implemented and evaluated the + # slanted integral image based image filtering but the performance was + # more or less equal to image filtering using + # scipy.ndimage.filters.convolve(). Hence we have decided to use the + # later for a much cleaner implementation. + elif mode == 'octagon': + # TODO : Decide the shapes of Octagon filters for scales > 7 + + for i in range(max_scale - min_scale + 1): + mo, no = OCTAGON_OUTER_SHAPE[min_scale + i - 1] + mi, ni = OCTAGON_INNER_SHAPE[min_scale + i - 1] + response[:, :, i] = convolve(image, _octagon_kernel(mo, no, mi, ni)) + + elif mode == 'star': + for i in range(max_scale - min_scale + 1): + m = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][0]] + n = STAR_SHAPE[STAR_FILTER_SHAPE[min_scale + i - 1][1]] + response[:, :, i] = convolve(image, _star_kernel(m, n)) + + return response + + +def _octagon_kernel(mo, no, mi, ni): + outer = (mo + 2 * no) ** 2 - 2 * no * (no + 1) + inner = (mi + 2 * ni) ** 2 - 2 * ni * (ni + 1) + outer_weight = 1.0 / (outer - inner) + inner_weight = 1.0 / inner + c = ((mo + 2 * no) - (mi + 2 * ni)) // 2 + outer_oct = octagon(mo, no) + inner_oct = np.zeros((mo + 2 * no, mo + 2 * no)) + inner_oct[c:-c, c:-c] = octagon(mi, ni) + bfilter = outer_weight * outer_oct - (outer_weight + inner_weight) * inner_oct + return bfilter + + +def _star_kernel(m, n): + c = m + m // 2 - n - n // 2 + outer_star = star(m) + inner_star = np.zeros_like(outer_star) + inner_star[c:-c, c:-c] = star(n) + outer_weight = 1.0 / (np.sum(outer_star - inner_star)) + inner_weight = 1.0 / np.sum(inner_star) + bfilter = outer_weight * outer_star - (outer_weight + inner_weight) * inner_star + return bfilter + + +def _suppress_lines(feature_mask, image, sigma, line_threshold): + Arr, Arc, Acc = structure_tensor(image, sigma, order='rc') + feature_mask[(Arr + Acc) ** 2 > line_threshold * (Arr * Acc - Arc**2)] = False + + +class CENSURE(FeatureDetector): + """CENSURE keypoint detector. + + min_scale : int, optional + Minimum scale to extract keypoints from. + max_scale : int, optional + Maximum scale to extract keypoints from. The keypoints will be + extracted from all the scales except the first and the last i.e. + from the scales in the range [min_scale + 1, max_scale - 1]. The filter + sizes for different scales is such that the two adjacent scales + comprise of an octave. + mode : {'DoB', 'Octagon', 'STAR'}, optional + Type of bi-level filter used to get the scales of the input image. + Possible values are 'DoB', 'Octagon' and 'STAR'. The three modes + represent the shape of the bi-level filters i.e. box(square), octagon + and star respectively. For instance, a bi-level octagon filter consists + of a smaller inner octagon and a larger outer octagon with the filter + weights being uniformly negative in both the inner octagon while + uniformly positive in the difference region. Use STAR and Octagon for + better features and DoB for better performance. + non_max_threshold : float, optional + Threshold value used to suppress maximas and minimas with a weak + magnitude response obtained after Non-Maximal Suppression. + line_threshold : float, optional + Threshold for rejecting interest points which have ratio of principal + curvatures greater than this value. + + Attributes + ---------- + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N,) array + Corresponding scales. + + References + ---------- + .. [1] Motilal Agrawal, Kurt Konolige and Morten Rufus Blas + "CENSURE: Center Surround Extremas for Realtime Feature + Detection and Matching", + https://link.springer.com/chapter/10.1007/978-3-540-88693-8_8 + :DOI:`10.1007/978-3-540-88693-8_8` + + .. [2] Adam Schmidt, Marek Kraft, Michal Fularz and Zuzanna Domagala + "Comparative Assessment of Point Feature Detectors and + Descriptors in the Context of Robot Navigation" + http://yadda.icm.edu.pl/yadda/element/bwmeta1.element.baztech-268aaf28-0faf-4872-a4df-7e2e61cb364c/c/Schmidt_comparative.pdf + :DOI:`10.1.1.465.1117` + + Examples + -------- + >>> from skimage.data import astronaut + >>> from skimage.color import rgb2gray + >>> from skimage.feature import CENSURE + >>> img = rgb2gray(astronaut()[100:300, 100:300]) + >>> censure = CENSURE() + >>> censure.detect(img) + >>> censure.keypoints + array([[ 4, 148], + [ 12, 73], + [ 21, 176], + [ 91, 22], + [ 93, 56], + [ 94, 22], + [ 95, 54], + [100, 51], + [103, 51], + [106, 67], + [108, 15], + [117, 20], + [122, 60], + [125, 37], + [129, 37], + [133, 76], + [145, 44], + [146, 94], + [150, 114], + [153, 33], + [154, 156], + [155, 151], + [184, 63]]) + >>> censure.scales + array([2, 6, 6, 2, 4, 3, 2, 3, 2, 6, 3, 2, 2, 3, 2, 2, 2, 3, 2, 2, 4, 2, + 2]) + + """ + + def __init__( + self, + min_scale=1, + max_scale=7, + mode='DoB', + non_max_threshold=0.15, + line_threshold=10, + ): + mode = mode.lower() + if mode not in ('dob', 'octagon', 'star'): + raise ValueError("`mode` must be one of 'DoB', 'Octagon', 'STAR'.") + + if min_scale < 1 or max_scale < 1 or max_scale - min_scale < 2: + raise ValueError( + 'The scales must be >= 1 and the number of ' 'scales should be >= 3.' + ) + + self.min_scale = min_scale + self.max_scale = max_scale + self.mode = mode + self.non_max_threshold = non_max_threshold + self.line_threshold = line_threshold + + self.keypoints = None + self.scales = None + + def detect(self, image): + """Detect CENSURE keypoints along with the corresponding scale. + + Parameters + ---------- + image : 2D ndarray + Input image. + + """ + + # (1) First we generate the required scales on the input grayscale + # image using a bi-level filter and stack them up in `filter_response`. + + # (2) We then perform Non-Maximal suppression in 3 x 3 x 3 window on + # the filter_response to suppress points that are neither minima or + # maxima in 3 x 3 x 3 neighborhood. We obtain a boolean ndarray + # `feature_mask` containing all the minimas and maximas in + # `filter_response` as True. + # (3) Then we suppress all the points in the `feature_mask` for which + # the corresponding point in the image at a particular scale has the + # ratio of principal curvatures greater than `line_threshold`. + # (4) Finally, we remove the border keypoints and return the keypoints + # along with its corresponding scale. + + check_nD(image, 2) + + num_scales = self.max_scale - self.min_scale + + image = np.ascontiguousarray(_prepare_grayscale_input_2D(image)) + + # Generating all the scales + filter_response = _filter_image( + image, self.min_scale, self.max_scale, self.mode + ) + + # Suppressing points that are neither minima or maxima in their + # 3 x 3 x 3 neighborhood to zero + minimas = minimum_filter(filter_response, (3, 3, 3)) == filter_response + maximas = maximum_filter(filter_response, (3, 3, 3)) == filter_response + + feature_mask = minimas | maximas + feature_mask[filter_response < self.non_max_threshold] = False + + for i in range(1, num_scales): + # sigma = (window_size - 1) / 6.0, so the window covers > 99% of + # the kernel's distribution + # window_size = 7 + 2 * (min_scale - 1 + i) + # Hence sigma = 1 + (min_scale - 1 + i)/ 3.0 + _suppress_lines( + feature_mask[:, :, i], + image, + (1 + (self.min_scale + i - 1) / 3.0), + self.line_threshold, + ) + + rows, cols, scales = np.nonzero(feature_mask[..., 1:num_scales]) + keypoints = np.column_stack([rows, cols]) + scales = scales + self.min_scale + 1 + + if self.mode == 'dob': + self.keypoints = keypoints + self.scales = scales + return + + cumulative_mask = np.zeros(keypoints.shape[0], dtype=bool) + + if self.mode == 'octagon': + for i in range(self.min_scale + 1, self.max_scale): + c = (OCTAGON_OUTER_SHAPE[i - 1][0] - 1) // 2 + OCTAGON_OUTER_SHAPE[ + i - 1 + ][1] + cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & ( + scales == i + ) + elif self.mode == 'star': + for i in range(self.min_scale + 1, self.max_scale): + c = ( + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] + + STAR_SHAPE[STAR_FILTER_SHAPE[i - 1][0]] // 2 + ) + cumulative_mask |= _mask_border_keypoints(image.shape, keypoints, c) & ( + scales == i + ) + + self.keypoints = keypoints[cumulative_mask] + self.scales = scales[cumulative_mask] diff --git a/lib/python3.10/site-packages/skimage/feature/corner.py b/lib/python3.10/site-packages/skimage/feature/corner.py new file mode 100644 index 0000000000000000000000000000000000000000..0c61d5b60c5230cc8a8be3c171823031014fce4e --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/corner.py @@ -0,0 +1,1355 @@ +import functools +import math +from itertools import combinations_with_replacement + +import numpy as np +from scipy import ndimage as ndi +from scipy import spatial, stats + +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type, safe_as_int, warn +from ..transform import integral_image +from ..util import img_as_float +from ._hessian_det_appx import _hessian_matrix_det +from .corner_cy import _corner_fast, _corner_moravec, _corner_orientations +from .peak import peak_local_max +from .util import _prepare_grayscale_input_2D, _prepare_grayscale_input_nD + + +def _compute_derivatives(image, mode='constant', cval=0): + """Compute derivatives in axis directions using the Sobel operator. + + Parameters + ---------- + image : ndarray + Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + derivatives : list of ndarray + Derivatives in each axis direction. + + """ + + derivatives = [ + ndi.sobel(image, axis=i, mode=mode, cval=cval) for i in range(image.ndim) + ] + + return derivatives + + +def structure_tensor(image, sigma=1, mode='constant', cval=0, order='rc'): + """Compute structure tensor using sum of squared differences. + + The (2-dimensional) structure tensor A is defined as:: + + A = [Arr Arc] + [Arc Acc] + + which is approximated by the weighted sum of squared differences in a local + window around each pixel in the image. This formula can be extended to a + larger number of dimensions (see [1]_). + + Parameters + ---------- + image : ndarray + Input image. + sigma : float or array-like of float, optional + Standard deviation used for the Gaussian kernel, which is used as a + weighting function for the local summation of squared differences. + If sigma is an iterable, its length must be equal to `image.ndim` and + each element is used for the Gaussian kernel applied along its + respective axis. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + order : {'rc', 'xy'}, optional + NOTE: 'xy' is only an option for 2D images, higher dimensions must + always use 'rc' order. This parameter allows for the use of reverse or + forward order of the image axes in gradient computation. 'rc' indicates + the use of the first axis initially (Arr, Arc, Acc), whilst 'xy' + indicates the usage of the last axis initially (Axx, Axy, Ayy). + + Returns + ------- + A_elems : list of ndarray + Upper-diagonal elements of the structure tensor for each pixel in the + input image. + + Examples + -------- + >>> from skimage.feature import structure_tensor + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> Arr, Arc, Acc = structure_tensor(square, sigma=0.1, order='rc') + >>> Acc + array([[0., 0., 0., 0., 0.], + [0., 1., 0., 1., 0.], + [0., 4., 0., 4., 0.], + [0., 1., 0., 1., 0.], + [0., 0., 0., 0., 0.]]) + + See also + -------- + structure_tensor_eigenvalues + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Structure_tensor + """ + if order == 'xy' and image.ndim > 2: + raise ValueError('Only "rc" order is supported for dim > 2.') + + if order not in ['rc', 'xy']: + raise ValueError(f'order {order} is invalid. Must be either "rc" or "xy"') + + if not np.isscalar(sigma): + sigma = tuple(sigma) + if len(sigma) != image.ndim: + raise ValueError('sigma must have as many elements as image ' 'has axes') + + image = _prepare_grayscale_input_nD(image) + + derivatives = _compute_derivatives(image, mode=mode, cval=cval) + + if order == 'xy': + derivatives = reversed(derivatives) + + # structure tensor + A_elems = [ + gaussian(der0 * der1, sigma=sigma, mode=mode, cval=cval) + for der0, der1 in combinations_with_replacement(derivatives, 2) + ] + + return A_elems + + +def _hessian_matrix_with_gaussian(image, sigma=1, mode='reflect', cval=0, order='rc'): + """Compute the Hessian via convolutions with Gaussian derivatives. + + In 2D, the Hessian matrix is defined as: + H = [Hrr Hrc] + [Hrc Hcc] + + which is computed by convolving the image with the second derivatives + of the Gaussian kernel in the respective r- and c-directions. + + The implementation here also supports n-dimensional data. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float or sequence of float, optional + Standard deviation used for the Gaussian kernel, which sets the + amount of smoothing in terms of pixel-distances. It is + advised to not choose a sigma much less than 1.0, otherwise + aliasing artifacts may occur. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + order : {'rc', 'xy'}, optional + This parameter allows for the use of reverse or forward order of + the image axes in gradient computation. 'rc' indicates the use of + the first axis initially (Hrr, Hrc, Hcc), whilst 'xy' indicates the + usage of the last axis initially (Hxx, Hxy, Hyy) + + Returns + ------- + H_elems : list of ndarray + Upper-diagonal elements of the hessian matrix for each pixel in the + input image. In 2D, this will be a three element list containing [Hrr, + Hrc, Hcc]. In nD, the list will contain ``(n**2 + n) / 2`` arrays. + + """ + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + if image.ndim > 2 and order == "xy": + raise ValueError("order='xy' is only supported for 2D images.") + if order not in ["rc", "xy"]: + raise ValueError(f"unrecognized order: {order}") + + if np.isscalar(sigma): + sigma = (sigma,) * image.ndim + + # This function uses `scipy.ndimage.gaussian_filter` with the order + # argument to compute convolutions. For example, specifying + # ``order=[1, 0]`` would apply convolution with a first-order derivative of + # the Gaussian along the first axis and simple Gaussian smoothing along the + # second. + + # For small sigma, the SciPy Gaussian filter suffers from aliasing and edge + # artifacts, given that the filter will approximate a sinc or sinc + # derivative which only goes to 0 very slowly (order 1/n**2). Thus, we use + # a much larger truncate value to reduce any edge artifacts. + truncate = 8 if all(s > 1 for s in sigma) else 100 + sq1_2 = 1 / math.sqrt(2) + sigma_scaled = tuple(sq1_2 * s for s in sigma) + common_kwargs = dict(sigma=sigma_scaled, mode=mode, cval=cval, truncate=truncate) + gaussian_ = functools.partial(ndi.gaussian_filter, **common_kwargs) + + # Apply two successive first order Gaussian derivative operations, as + # detailed in: + # https://dsp.stackexchange.com/questions/78280/are-scipy-second-order-gaussian-derivatives-correct + + # 1.) First order along one axis while smoothing (order=0) along the other + ndim = image.ndim + + # orders in 2D = ([1, 0], [0, 1]) + # in 3D = ([1, 0, 0], [0, 1, 0], [0, 0, 1]) + # etc. + orders = tuple([0] * d + [1] + [0] * (ndim - d - 1) for d in range(ndim)) + gradients = [gaussian_(image, order=orders[d]) for d in range(ndim)] + + # 2.) apply the derivative along another axis as well + axes = range(ndim) + if order == 'xy': + axes = reversed(axes) + H_elems = [ + gaussian_(gradients[ax0], order=orders[ax1]) + for ax0, ax1 in combinations_with_replacement(axes, 2) + ] + return H_elems + + +def hessian_matrix( + image, sigma=1, mode='constant', cval=0, order='rc', use_gaussian_derivatives=None +): + r"""Compute the Hessian matrix. + + In 2D, the Hessian matrix is defined as:: + + H = [Hrr Hrc] + [Hrc Hcc] + + which is computed by convolving the image with the second derivatives + of the Gaussian kernel in the respective r- and c-directions. + + The implementation here also supports n-dimensional data. + + Parameters + ---------- + image : ndarray + Input image. + sigma : float + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + order : {'rc', 'xy'}, optional + For 2D images, this parameter allows for the use of reverse or forward + order of the image axes in gradient computation. 'rc' indicates the use + of the first axis initially (Hrr, Hrc, Hcc), whilst 'xy' indicates the + usage of the last axis initially (Hxx, Hxy, Hyy). Images with higher + dimension must always use 'rc' order. + use_gaussian_derivatives : boolean, optional + Indicates whether the Hessian is computed by convolving with Gaussian + derivatives, or by a simple finite-difference operation. + + Returns + ------- + H_elems : list of ndarray + Upper-diagonal elements of the hessian matrix for each pixel in the + input image. In 2D, this will be a three element list containing [Hrr, + Hrc, Hcc]. In nD, the list will contain ``(n**2 + n) / 2`` arrays. + + + Notes + ----- + The distributive property of derivatives and convolutions allows us to + restate the derivative of an image, I, smoothed with a Gaussian kernel, G, + as the convolution of the image with the derivative of G. + + .. math:: + + \frac{\partial }{\partial x_i}(I * G) = + I * \left( \frac{\partial }{\partial x_i} G \right) + + When ``use_gaussian_derivatives`` is ``True``, this property is used to + compute the second order derivatives that make up the Hessian matrix. + + When ``use_gaussian_derivatives`` is ``False``, simple finite differences + on a Gaussian-smoothed image are used instead. + + Examples + -------- + >>> from skimage.feature import hessian_matrix + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 4 + >>> Hrr, Hrc, Hcc = hessian_matrix(square, sigma=0.1, order='rc', + ... use_gaussian_derivatives=False) + >>> Hrc + array([[ 0., 0., 0., 0., 0.], + [ 0., 1., 0., -1., 0.], + [ 0., 0., 0., 0., 0.], + [ 0., -1., 0., 1., 0.], + [ 0., 0., 0., 0., 0.]]) + + """ + + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + if image.ndim > 2 and order == "xy": + raise ValueError("order='xy' is only supported for 2D images.") + if order not in ["rc", "xy"]: + raise ValueError(f"unrecognized order: {order}") + + if use_gaussian_derivatives is None: + use_gaussian_derivatives = False + warn( + "use_gaussian_derivatives currently defaults to False, but will " + "change to True in a future version. Please specify this " + "argument explicitly to maintain the current behavior", + category=FutureWarning, + stacklevel=2, + ) + + if use_gaussian_derivatives: + return _hessian_matrix_with_gaussian( + image, sigma=sigma, mode=mode, cval=cval, order=order + ) + + gaussian_filtered = gaussian(image, sigma=sigma, mode=mode, cval=cval) + + gradients = np.gradient(gaussian_filtered) + axes = range(image.ndim) + + if order == 'xy': + axes = reversed(axes) + + H_elems = [ + np.gradient(gradients[ax0], axis=ax1) + for ax0, ax1 in combinations_with_replacement(axes, 2) + ] + return H_elems + + +def hessian_matrix_det(image, sigma=1, approximate=True): + """Compute the approximate Hessian Determinant over an image. + + The 2D approximate method uses box filters over integral images to + compute the approximate Hessian Determinant. + + Parameters + ---------- + image : ndarray + The image over which to compute the Hessian Determinant. + sigma : float, optional + Standard deviation of the Gaussian kernel used for the Hessian + matrix. + approximate : bool, optional + If ``True`` and the image is 2D, use a much faster approximate + computation. This argument has no effect on 3D and higher images. + + Returns + ------- + out : array + The array of the Determinant of Hessians. + + References + ---------- + .. [1] Herbert Bay, Andreas Ess, Tinne Tuytelaars, Luc Van Gool, + "SURF: Speeded Up Robust Features" + ftp://ftp.vision.ee.ethz.ch/publications/articles/eth_biwi_00517.pdf + + Notes + ----- + For 2D images when ``approximate=True``, the running time of this method + only depends on size of the image. It is independent of `sigma` as one + would expect. The downside is that the result for `sigma` less than `3` + is not accurate, i.e., not similar to the result obtained if someone + computed the Hessian and took its determinant. + """ + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + if image.ndim == 2 and approximate: + integral = integral_image(image) + return np.array(_hessian_matrix_det(integral, sigma)) + else: # slower brute-force implementation for nD images + hessian_mat_array = _symmetric_image( + hessian_matrix(image, sigma, use_gaussian_derivatives=False) + ) + return np.linalg.det(hessian_mat_array) + + +def _symmetric_compute_eigenvalues(S_elems): + """Compute eigenvalues from the upper-diagonal entries of a symmetric + matrix. + + Parameters + ---------- + S_elems : list of ndarray + The upper-diagonal elements of the matrix, as returned by + `hessian_matrix` or `structure_tensor`. + + Returns + ------- + eigs : ndarray + The eigenvalues of the matrix, in decreasing order. The eigenvalues are + the leading dimension. That is, ``eigs[i, j, k]`` contains the + ith-largest eigenvalue at position (j, k). + """ + + if len(S_elems) == 3: # Fast explicit formulas for 2D. + M00, M01, M11 = S_elems + eigs = np.empty((2, *M00.shape), M00.dtype) + eigs[:] = (M00 + M11) / 2 + hsqrtdet = np.sqrt(M01**2 + ((M00 - M11) / 2) ** 2) + eigs[0] += hsqrtdet + eigs[1] -= hsqrtdet + return eigs + else: + matrices = _symmetric_image(S_elems) + # eigvalsh returns eigenvalues in increasing order. We want decreasing + eigs = np.linalg.eigvalsh(matrices)[..., ::-1] + leading_axes = tuple(range(eigs.ndim - 1)) + return np.transpose(eigs, (eigs.ndim - 1,) + leading_axes) + + +def _symmetric_image(S_elems): + """Convert the upper-diagonal elements of a matrix to the full + symmetric matrix. + + Parameters + ---------- + S_elems : list of array + The upper-diagonal elements of the matrix, as returned by + `hessian_matrix` or `structure_tensor`. + + Returns + ------- + image : array + An array of shape ``(M, N[, ...], image.ndim, image.ndim)``, + containing the matrix corresponding to each coordinate. + """ + image = S_elems[0] + symmetric_image = np.zeros( + image.shape + (image.ndim, image.ndim), dtype=S_elems[0].dtype + ) + for idx, (row, col) in enumerate( + combinations_with_replacement(range(image.ndim), 2) + ): + symmetric_image[..., row, col] = S_elems[idx] + symmetric_image[..., col, row] = S_elems[idx] + return symmetric_image + + +def structure_tensor_eigenvalues(A_elems): + """Compute eigenvalues of structure tensor. + + Parameters + ---------- + A_elems : list of ndarray + The upper-diagonal elements of the structure tensor, as returned + by `structure_tensor`. + + Returns + ------- + ndarray + The eigenvalues of the structure tensor, in decreasing order. The + eigenvalues are the leading dimension. That is, the coordinate + [i, j, k] corresponds to the ith-largest eigenvalue at position (j, k). + + Examples + -------- + >>> from skimage.feature import structure_tensor + >>> from skimage.feature import structure_tensor_eigenvalues + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 1 + >>> A_elems = structure_tensor(square, sigma=0.1, order='rc') + >>> structure_tensor_eigenvalues(A_elems)[0] + array([[0., 0., 0., 0., 0.], + [0., 2., 4., 2., 0.], + [0., 4., 0., 4., 0.], + [0., 2., 4., 2., 0.], + [0., 0., 0., 0., 0.]]) + + See also + -------- + structure_tensor + """ + return _symmetric_compute_eigenvalues(A_elems) + + +def hessian_matrix_eigvals(H_elems): + """Compute eigenvalues of Hessian matrix. + + Parameters + ---------- + H_elems : list of ndarray + The upper-diagonal elements of the Hessian matrix, as returned + by `hessian_matrix`. + + Returns + ------- + eigs : ndarray + The eigenvalues of the Hessian matrix, in decreasing order. The + eigenvalues are the leading dimension. That is, ``eigs[i, j, k]`` + contains the ith-largest eigenvalue at position (j, k). + + Examples + -------- + >>> from skimage.feature import hessian_matrix, hessian_matrix_eigvals + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 4 + >>> H_elems = hessian_matrix(square, sigma=0.1, order='rc', + ... use_gaussian_derivatives=False) + >>> hessian_matrix_eigvals(H_elems)[0] + array([[ 0., 0., 2., 0., 0.], + [ 0., 1., 0., 1., 0.], + [ 2., 0., -2., 0., 2.], + [ 0., 1., 0., 1., 0.], + [ 0., 0., 2., 0., 0.]]) + """ + return _symmetric_compute_eigenvalues(H_elems) + + +def shape_index(image, sigma=1, mode='constant', cval=0): + """Compute the shape index. + + The shape index, as defined by Koenderink & van Doorn [1]_, is a + single valued measure of local curvature, assuming the image as a 3D plane + with intensities representing heights. + + It is derived from the eigenvalues of the Hessian, and its + value ranges from -1 to 1 (and is undefined (=NaN) in *flat* regions), + with following ranges representing following shapes: + + .. table:: Ranges of the shape index and corresponding shapes. + + =================== ============= + Interval (s in ...) Shape + =================== ============= + [ -1, -7/8) Spherical cup + [-7/8, -5/8) Through + [-5/8, -3/8) Rut + [-3/8, -1/8) Saddle rut + [-1/8, +1/8) Saddle + [+1/8, +3/8) Saddle ridge + [+3/8, +5/8) Ridge + [+5/8, +7/8) Dome + [+7/8, +1] Spherical cap + =================== ============= + + Parameters + ---------- + image : (M, N) ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used for + smoothing the input data before Hessian eigen value calculation. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + s : ndarray + Shape index + + References + ---------- + .. [1] Koenderink, J. J. & van Doorn, A. J., + "Surface shape and curvature scales", + Image and Vision Computing, 1992, 10, 557-564. + :DOI:`10.1016/0262-8856(92)90076-F` + + Examples + -------- + >>> from skimage.feature import shape_index + >>> square = np.zeros((5, 5)) + >>> square[2, 2] = 4 + >>> s = shape_index(square, sigma=0.1) + >>> s + array([[ nan, nan, -0.5, nan, nan], + [ nan, -0. , nan, -0. , nan], + [-0.5, nan, -1. , nan, -0.5], + [ nan, -0. , nan, -0. , nan], + [ nan, nan, -0.5, nan, nan]]) + """ + + H = hessian_matrix( + image, + sigma=sigma, + mode=mode, + cval=cval, + order='rc', + use_gaussian_derivatives=False, + ) + l1, l2 = hessian_matrix_eigvals(H) + + # don't warn on divide by 0 as occurs in the docstring example + with np.errstate(divide='ignore', invalid='ignore'): + return (2.0 / np.pi) * np.arctan((l2 + l1) / (l2 - l1)) + + +def corner_kitchen_rosenfeld(image, mode='constant', cval=0): + """Compute Kitchen and Rosenfeld corner measure response image. + + The corner measure is calculated as follows:: + + (imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy) + / (imx**2 + imy**2) + + Where imx and imy are the first and imxx, imxy, imyy the second + derivatives. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + response : ndarray + Kitchen and Rosenfeld response image. + + References + ---------- + .. [1] Kitchen, L., & Rosenfeld, A. (1982). Gray-level corner detection. + Pattern recognition letters, 1(2), 95-102. + :DOI:`10.1016/0167-8655(82)90020-4` + """ + + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + imy, imx = _compute_derivatives(image, mode=mode, cval=cval) + imxy, imxx = _compute_derivatives(imx, mode=mode, cval=cval) + imyy, imyx = _compute_derivatives(imy, mode=mode, cval=cval) + + numerator = imxx * imy**2 + imyy * imx**2 - 2 * imxy * imx * imy + denominator = imx**2 + imy**2 + + response = np.zeros_like(image, dtype=float_dtype) + + mask = denominator != 0 + response[mask] = numerator[mask] / denominator[mask] + + return response + + +def corner_harris(image, method='k', k=0.05, eps=1e-6, sigma=1): + """Compute Harris corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are first derivatives, averaged with a gaussian filter. + The corner measure is then defined as:: + + det(A) - k * trace(A)**2 + + or:: + + 2 * det(A) / (trace(A) + eps) + + Parameters + ---------- + image : (M, N) ndarray + Input image. + method : {'k', 'eps'}, optional + Method to compute the response image from the auto-correlation matrix. + k : float, optional + Sensitivity factor to separate corners from edges, typically in range + `[0, 0.2]`. Small values of k result in detection of sharp corners. + eps : float, optional + Normalisation factor (Noble's corner measure). + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Harris response image. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_harris, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corner_peaks(corner_harris(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Arr, Arc, Acc = structure_tensor(image, sigma, order='rc') + + # determinant + detA = Arr * Acc - Arc**2 + # trace + traceA = Arr + Acc + + if method == 'k': + response = detA - k * traceA**2 + else: + response = 2 * detA / (traceA + eps) + + return response + + +def corner_shi_tomasi(image, sigma=1): + """Compute Shi-Tomasi (Kanade-Tomasi) corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are first derivatives, averaged with a gaussian filter. + The corner measure is then defined as the smaller eigenvalue of A:: + + ((Axx + Ayy) - sqrt((Axx - Ayy)**2 + 4 * Axy**2)) / 2 + + Parameters + ---------- + image : (M, N) ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + response : ndarray + Shi-Tomasi response image. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_shi_tomasi, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> corner_peaks(corner_shi_tomasi(square), min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Arr, Arc, Acc = structure_tensor(image, sigma, order='rc') + + # minimum eigenvalue of A + response = ((Arr + Acc) - np.sqrt((Arr - Acc) ** 2 + 4 * Arc**2)) / 2 + + return response + + +def corner_foerstner(image, sigma=1): + """Compute Foerstner corner measure response image. + + This corner detector uses information from the auto-correlation matrix A:: + + A = [(imx**2) (imx*imy)] = [Axx Axy] + [(imx*imy) (imy**2)] [Axy Ayy] + + Where imx and imy are first derivatives, averaged with a gaussian filter. + The corner measure is then defined as:: + + w = det(A) / trace(A) (size of error ellipse) + q = 4 * det(A) / trace(A)**2 (roundness of error ellipse) + + Parameters + ---------- + image : (M, N) ndarray + Input image. + sigma : float, optional + Standard deviation used for the Gaussian kernel, which is used as + weighting function for the auto-correlation matrix. + + Returns + ------- + w : ndarray + Error ellipse sizes. + q : ndarray + Roundness of error ellipse. + + References + ---------- + .. [1] Förstner, W., & Gülch, E. (1987, June). A fast operator for + detection and precise location of distinct points, corners and + centres of circular features. In Proc. ISPRS intercommission + conference on fast processing of photogrammetric data (pp. 281-305). + https://cseweb.ucsd.edu/classes/sp02/cse252/foerstner/foerstner.pdf + .. [2] https://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_foerstner, corner_peaks + >>> square = np.zeros([10, 10]) + >>> square[2:8, 2:8] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> w, q = corner_foerstner(square) + >>> accuracy_thresh = 0.5 + >>> roundness_thresh = 0.3 + >>> foerstner = (q > roundness_thresh) * (w > accuracy_thresh) * w + >>> corner_peaks(foerstner, min_distance=1) + array([[2, 2], + [2, 7], + [7, 2], + [7, 7]]) + + """ + + Arr, Arc, Acc = structure_tensor(image, sigma, order='rc') + + # determinant + detA = Arr * Acc - Arc**2 + # trace + traceA = Arr + Acc + + w = np.zeros_like(image, dtype=detA.dtype) + q = np.zeros_like(w) + + mask = traceA != 0 + + w[mask] = detA[mask] / traceA[mask] + q[mask] = 4 * detA[mask] / traceA[mask] ** 2 + + return w, q + + +def corner_fast(image, n=12, threshold=0.15): + """Extract FAST corners for a given image. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + n : int, optional + Minimum number of consecutive pixels out of 16 pixels on the circle + that should all be either brighter or darker w.r.t testpixel. + A point c on the circle is darker w.r.t test pixel p if + `Ic < Ip - threshold` and brighter if `Ic > Ip + threshold`. Also + stands for the n in `FAST-n` corner detector. + threshold : float, optional + Threshold used in deciding whether the pixels on the circle are + brighter, darker or similar w.r.t. the test pixel. Decrease the + threshold when more corners are desired and vice-versa. + + Returns + ------- + response : ndarray + FAST corner response image. + + References + ---------- + .. [1] Rosten, E., & Drummond, T. (2006, May). Machine learning for + high-speed corner detection. In European conference on computer + vision (pp. 430-443). Springer, Berlin, Heidelberg. + :DOI:`10.1007/11744023_34` + http://www.edwardrosten.com/work/rosten_2006_machine.pdf + .. [2] Wikipedia, "Features from accelerated segment test", + https://en.wikipedia.org/wiki/Features_from_accelerated_segment_test + + Examples + -------- + >>> from skimage.feature import corner_fast, corner_peaks + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + array([[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, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 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]]) + >>> corner_peaks(corner_fast(square, 9), min_distance=1) + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + + """ + image = _prepare_grayscale_input_2D(image) + + image = np.ascontiguousarray(image) + response = _corner_fast(image, n, threshold) + return response + + +def corner_subpix(image, corners, window_size=11, alpha=0.99): + """Determine subpixel position of corners. + + A statistical test decides whether the corner is defined as the + intersection of two edges or a single peak. Depending on the classification + result, the subpixel corner location is determined based on the local + covariance of the grey-values. If the significance level for either + statistical test is not sufficient, the corner cannot be classified, and + the output subpixel position is set to NaN. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + corners : (K, 2) ndarray + Corner coordinates `(row, col)`. + window_size : int, optional + Search window size for subpixel estimation. + alpha : float, optional + Significance level for corner classification. + + Returns + ------- + positions : (K, 2) ndarray + Subpixel corner positions. NaN for "not classified" corners. + + References + ---------- + .. [1] Förstner, W., & Gülch, E. (1987, June). A fast operator for + detection and precise location of distinct points, corners and + centres of circular features. In Proc. ISPRS intercommission + conference on fast processing of photogrammetric data (pp. 281-305). + https://cseweb.ucsd.edu/classes/sp02/cse252/foerstner/foerstner.pdf + .. [2] https://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_harris, corner_peaks, corner_subpix + >>> img = np.zeros((10, 10)) + >>> img[:5, :5] = 1 + >>> img[5:, 5:] = 1 + >>> img.astype(int) + array([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]]) + >>> coords = corner_peaks(corner_harris(img), min_distance=2) + >>> coords_subpix = corner_subpix(img, coords, window_size=7) + >>> coords_subpix + array([[4.5, 4.5]]) + + """ + + # window extent in one direction + wext = (window_size - 1) // 2 + + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + image = np.pad(image, pad_width=wext, mode='constant', constant_values=0) + + # add pad width, make sure to not modify the input values in-place + corners = safe_as_int(corners + wext) + + # normal equation arrays + N_dot = np.zeros((2, 2), dtype=float_dtype) + N_edge = np.zeros((2, 2), dtype=float_dtype) + b_dot = np.zeros((2,), dtype=float_dtype) + b_edge = np.zeros((2,), dtype=float_dtype) + + # critical statistical test values + redundancy = window_size**2 - 2 + t_crit_dot = stats.f.isf(1 - alpha, redundancy, redundancy) + t_crit_edge = stats.f.isf(alpha, redundancy, redundancy) + + # coordinates of pixels within window + y, x = np.mgrid[-wext : wext + 1, -wext : wext + 1] + + corners_subpix = np.zeros_like(corners, dtype=float_dtype) + + for i, (y0, x0) in enumerate(corners): + # crop window around corner + border for sobel operator + miny = y0 - wext - 1 + maxy = y0 + wext + 2 + minx = x0 - wext - 1 + maxx = x0 + wext + 2 + window = image[miny:maxy, minx:maxx] + + winy, winx = _compute_derivatives(window, mode='constant', cval=0) + + # compute gradient squares and remove border + winx_winx = (winx * winx)[1:-1, 1:-1] + winx_winy = (winx * winy)[1:-1, 1:-1] + winy_winy = (winy * winy)[1:-1, 1:-1] + + # sum of squared differences (mean instead of gaussian filter) + Axx = np.sum(winx_winx) + Axy = np.sum(winx_winy) + Ayy = np.sum(winy_winy) + + # sum of squared differences weighted with coordinates + # (mean instead of gaussian filter) + bxx_x = np.sum(winx_winx * x) + bxx_y = np.sum(winx_winx * y) + bxy_x = np.sum(winx_winy * x) + bxy_y = np.sum(winx_winy * y) + byy_x = np.sum(winy_winy * x) + byy_y = np.sum(winy_winy * y) + + # normal equations for subpixel position + N_dot[0, 0] = Axx + N_dot[0, 1] = N_dot[1, 0] = -Axy + N_dot[1, 1] = Ayy + + N_edge[0, 0] = Ayy + N_edge[0, 1] = N_edge[1, 0] = Axy + N_edge[1, 1] = Axx + + b_dot[:] = bxx_y - bxy_x, byy_x - bxy_y + b_edge[:] = byy_y + bxy_x, bxx_x + bxy_y + + # estimated positions + try: + est_dot = np.linalg.solve(N_dot, b_dot) + est_edge = np.linalg.solve(N_edge, b_edge) + except np.linalg.LinAlgError: + # if image is constant the system is singular + corners_subpix[i, :] = np.nan, np.nan + continue + + # residuals + ry_dot = y - est_dot[0] + rx_dot = x - est_dot[1] + ry_edge = y - est_edge[0] + rx_edge = x - est_edge[1] + # squared residuals + rxx_dot = rx_dot * rx_dot + rxy_dot = rx_dot * ry_dot + ryy_dot = ry_dot * ry_dot + rxx_edge = rx_edge * rx_edge + rxy_edge = rx_edge * ry_edge + ryy_edge = ry_edge * ry_edge + + # determine corner class (dot or edge) + # variance for different models + var_dot = np.sum( + winx_winx * ryy_dot - 2 * winx_winy * rxy_dot + winy_winy * rxx_dot + ) + var_edge = np.sum( + winy_winy * ryy_edge + 2 * winx_winy * rxy_edge + winx_winx * rxx_edge + ) + + # test value (F-distributed) + if var_dot < np.spacing(1) and var_edge < np.spacing(1): + t = np.nan + elif var_dot == 0: + t = np.inf + else: + t = var_edge / var_dot + + # 1 for edge, -1 for dot, 0 for "not classified" + corner_class = int(t < t_crit_edge) - int(t > t_crit_dot) + + if corner_class == -1: + corners_subpix[i, :] = y0 + est_dot[0], x0 + est_dot[1] + elif corner_class == 0: + corners_subpix[i, :] = np.nan, np.nan + elif corner_class == 1: + corners_subpix[i, :] = y0 + est_edge[0], x0 + est_edge[1] + + # subtract pad width + corners_subpix -= wext + + return corners_subpix + + +def corner_peaks( + image, + min_distance=1, + threshold_abs=None, + threshold_rel=None, + exclude_border=True, + indices=True, + num_peaks=np.inf, + footprint=None, + labels=None, + *, + num_peaks_per_label=np.inf, + p_norm=np.inf, +): + """Find peaks in corner measure response image. + + This differs from `skimage.feature.peak_local_max` in that it suppresses + multiple connected peaks with the same accumulator value. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + min_distance : int, optional + The minimal allowed distance separating peaks. + * : * + See :py:meth:`skimage.feature.peak_local_max`. + p_norm : float + Which Minkowski p-norm to use. Should be in the range [1, inf]. + A finite large p may cause a ValueError if overflow can occur. + ``inf`` corresponds to the Chebyshev distance and 2 to the + Euclidean distance. + + Returns + ------- + output : ndarray or ndarray of bools + + * If `indices = True` : (row, column, ...) coordinates of peaks. + * If `indices = False` : Boolean array shaped like `image`, with peaks + represented by True values. + + See also + -------- + skimage.feature.peak_local_max + + Notes + ----- + .. versionchanged:: 0.18 + The default value of `threshold_rel` has changed to None, which + corresponds to letting `skimage.feature.peak_local_max` decide on the + default. This is equivalent to `threshold_rel=0`. + + The `num_peaks` limit is applied before suppression of connected peaks. + To limit the number of peaks after suppression, set `num_peaks=np.inf` and + post-process the output of this function. + + Examples + -------- + >>> from skimage.feature import peak_local_max + >>> response = np.zeros((5, 5)) + >>> response[2:4, 2:4] = 1 + >>> response + array([[0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 1., 1., 0.], + [0., 0., 1., 1., 0.], + [0., 0., 0., 0., 0.]]) + >>> peak_local_max(response) + array([[2, 2], + [2, 3], + [3, 2], + [3, 3]]) + >>> corner_peaks(response) + array([[2, 2]]) + + """ + if np.isinf(num_peaks): + num_peaks = None + + # Get the coordinates of the detected peaks + coords = peak_local_max( + image, + min_distance=min_distance, + threshold_abs=threshold_abs, + threshold_rel=threshold_rel, + exclude_border=exclude_border, + num_peaks=np.inf, + footprint=footprint, + labels=labels, + num_peaks_per_label=num_peaks_per_label, + ) + + if len(coords): + # Use KDtree to find the peaks that are too close to each other + tree = spatial.cKDTree(coords) + + rejected_peaks_indices = set() + for idx, point in enumerate(coords): + if idx not in rejected_peaks_indices: + candidates = tree.query_ball_point(point, r=min_distance, p=p_norm) + candidates.remove(idx) + rejected_peaks_indices.update(candidates) + + # Remove the peaks that are too close to each other + coords = np.delete(coords, tuple(rejected_peaks_indices), axis=0)[:num_peaks] + + if indices: + return coords + + peaks = np.zeros_like(image, dtype=bool) + peaks[tuple(coords.T)] = True + + return peaks + + +def corner_moravec(image, window_size=1): + """Compute Moravec corner measure response image. + + This is one of the simplest corner detectors and is comparatively fast but + has several limitations (e.g. not rotation invariant). + + Parameters + ---------- + image : (M, N) ndarray + Input image. + window_size : int, optional + Window size. + + Returns + ------- + response : ndarray + Moravec response image. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Corner_detection + + Examples + -------- + >>> from skimage.feature import corner_moravec + >>> square = np.zeros([7, 7]) + >>> square[3, 3] = 1 + >>> square.astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) + >>> corner_moravec(square).astype(int) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 2, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]]) + """ + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + return _corner_moravec(np.ascontiguousarray(image), window_size) + + +def corner_orientations(image, corners, mask): + """Compute the orientation of corners. + + The orientation of corners is computed using the first order central moment + i.e. the center of mass approach. The corner orientation is the angle of + the vector from the corner coordinate to the intensity centroid in the + local neighborhood around the corner calculated using first order central + moment. + + Parameters + ---------- + image : (M, N) array + Input grayscale image. + corners : (K, 2) array + Corner coordinates as ``(row, col)``. + mask : 2D array + Mask defining the local neighborhood of the corner used for the + calculation of the central moment. + + Returns + ------- + orientations : (K, 1) array + Orientations of corners in the range [-pi, pi]. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB : An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + .. [2] Paul L. Rosin, "Measuring Corner Properties" + http://users.cs.cf.ac.uk/Paul.Rosin/corner2.pdf + + Examples + -------- + >>> from skimage.morphology import octagon + >>> from skimage.feature import (corner_fast, corner_peaks, + ... corner_orientations) + >>> square = np.zeros((12, 12)) + >>> square[3:9, 3:9] = 1 + >>> square.astype(int) + array([[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, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 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]]) + >>> corners = corner_peaks(corner_fast(square, 9), min_distance=1) + >>> corners + array([[3, 3], + [3, 8], + [8, 3], + [8, 8]]) + >>> orientations = corner_orientations(square, corners, octagon(3, 2)) + >>> np.rad2deg(orientations) + array([ 45., 135., -45., -135.]) + + """ + image = _prepare_grayscale_input_2D(image) + return _corner_orientations(image, corners, mask) diff --git a/lib/python3.10/site-packages/skimage/feature/haar.py b/lib/python3.10/site-packages/skimage/feature/haar.py new file mode 100644 index 0000000000000000000000000000000000000000..6cdc4078da91e90c0e5a53b1233f3e2c5003d1ba --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/haar.py @@ -0,0 +1,339 @@ +from itertools import chain +from operator import add + +import numpy as np + +from ._haar import haar_like_feature_coord_wrapper +from ._haar import haar_like_feature_wrapper +from ..color import gray2rgb +from ..draw import rectangle +from ..util import img_as_float + +FEATURE_TYPE = ('type-2-x', 'type-2-y', 'type-3-x', 'type-3-y', 'type-4') + + +def _validate_feature_type(feature_type): + """Transform feature type to an iterable and check that it exists.""" + if feature_type is None: + feature_type_ = FEATURE_TYPE + else: + if isinstance(feature_type, str): + feature_type_ = [feature_type] + else: + feature_type_ = feature_type + for feat_t in feature_type_: + if feat_t not in FEATURE_TYPE: + raise ValueError( + f'The given feature type is unknown. Got {feat_t} instead of one ' + f'of {FEATURE_TYPE}.' + ) + return feature_type_ + + +def haar_like_feature_coord(width, height, feature_type=None): + """Compute the coordinates of Haar-like features. + + Parameters + ---------- + width : int + Width of the detection window. + height : int + Height of the detection window. + feature_type : str or list of str or None, optional + The type of feature to consider: + + - 'type-2-x': 2 rectangles varying along the x axis; + - 'type-2-y': 2 rectangles varying along the y axis; + - 'type-3-x': 3 rectangles varying along the x axis; + - 'type-3-y': 3 rectangles varying along the y axis; + - 'type-4': 4 rectangles varying along x and y axis. + + By default all features are extracted. + + Returns + ------- + feature_coord : (n_features, n_rectangles, 2, 2), ndarray of list of \ +tuple coord + Coordinates of the rectangles for each feature. + feature_type : (n_features,), ndarray of str + The corresponding type for each feature. + + Examples + -------- + >>> import numpy as np + >>> from skimage.transform import integral_image + >>> from skimage.feature import haar_like_feature_coord + >>> feat_coord, feat_type = haar_like_feature_coord(2, 2, 'type-4') + >>> feat_coord # doctest: +SKIP + array([ list([[(0, 0), (0, 0)], [(0, 1), (0, 1)], + [(1, 1), (1, 1)], [(1, 0), (1, 0)]])], dtype=object) + >>> feat_type + array(['type-4'], dtype=object) + + """ + feature_type_ = _validate_feature_type(feature_type) + + feat_coord, feat_type = zip( + *[ + haar_like_feature_coord_wrapper(width, height, feat_t) + for feat_t in feature_type_ + ] + ) + + return np.concatenate(feat_coord), np.hstack(feat_type) + + +def haar_like_feature( + int_image, r, c, width, height, feature_type=None, feature_coord=None +): + """Compute the Haar-like features for a region of interest (ROI) of an + integral image. + + Haar-like features have been successfully used for image classification and + object detection [1]_. It has been used for real-time face detection + algorithm proposed in [2]_. + + Parameters + ---------- + int_image : (M, N) ndarray + Integral image for which the features need to be computed. + r : int + Row-coordinate of top left corner of the detection window. + c : int + Column-coordinate of top left corner of the detection window. + width : int + Width of the detection window. + height : int + Height of the detection window. + feature_type : str or list of str or None, optional + The type of feature to consider: + + - 'type-2-x': 2 rectangles varying along the x axis; + - 'type-2-y': 2 rectangles varying along the y axis; + - 'type-3-x': 3 rectangles varying along the x axis; + - 'type-3-y': 3 rectangles varying along the y axis; + - 'type-4': 4 rectangles varying along x and y axis. + + By default all features are extracted. + + If using with `feature_coord`, it should correspond to the feature + type of each associated coordinate feature. + feature_coord : ndarray of list of tuples or None, optional + The array of coordinates to be extracted. This is useful when you want + to recompute only a subset of features. In this case `feature_type` + needs to be an array containing the type of each feature, as returned + by :func:`haar_like_feature_coord`. By default, all coordinates are + computed. + + Returns + ------- + haar_features : (n_features,) ndarray of int or float + Resulting Haar-like features. Each value is equal to the subtraction of + sums of the positive and negative rectangles. The data type depends of + the data type of `int_image`: `int` when the data type of `int_image` + is `uint` or `int` and `float` when the data type of `int_image` is + `float`. + + Notes + ----- + When extracting those features in parallel, be aware that the choice of the + backend (i.e. multiprocessing vs threading) will have an impact on the + performance. The rule of thumb is as follows: use multiprocessing when + extracting features for all possible ROI in an image; use threading when + extracting the feature at specific location for a limited number of ROIs. + Refer to the example + :ref:`sphx_glr_auto_examples_applications_plot_haar_extraction_selection_classification.py` + for more insights. + + Examples + -------- + >>> import numpy as np + >>> from skimage.transform import integral_image + >>> from skimage.feature import haar_like_feature + >>> img = np.ones((5, 5), dtype=np.uint8) + >>> img_ii = integral_image(img) + >>> feature = haar_like_feature(img_ii, 0, 0, 5, 5, 'type-3-x') + >>> feature + array([-1, -2, -3, -4, -5, -1, -2, -3, -4, -5, -1, -2, -3, -4, -5, -1, -2, + -3, -4, -1, -2, -3, -4, -1, -2, -3, -4, -1, -2, -3, -1, -2, -3, -1, + -2, -3, -1, -2, -1, -2, -1, -2, -1, -1, -1]) + + You can compute the feature for some pre-computed coordinates. + + >>> from skimage.feature import haar_like_feature_coord + >>> feature_coord, feature_type = zip( + ... *[haar_like_feature_coord(5, 5, feat_t) + ... for feat_t in ('type-2-x', 'type-3-x')]) + >>> # only select one feature over two + >>> feature_coord = np.concatenate([x[::2] for x in feature_coord]) + >>> feature_type = np.concatenate([x[::2] for x in feature_type]) + >>> feature = haar_like_feature(img_ii, 0, 0, 5, 5, + ... feature_type=feature_type, + ... feature_coord=feature_coord) + >>> feature + array([ 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, -1, -3, -5, -2, -4, -1, + -3, -5, -2, -4, -2, -4, -2, -4, -2, -1, -3, -2, -1, -1, -1, -1, -1]) + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Haar-like_feature + .. [2] Oren, M., Papageorgiou, C., Sinha, P., Osuna, E., & Poggio, T. + (1997, June). Pedestrian detection using wavelet templates. + In Computer Vision and Pattern Recognition, 1997. Proceedings., + 1997 IEEE Computer Society Conference on (pp. 193-199). IEEE. + http://tinyurl.com/y6ulxfta + :DOI:`10.1109/CVPR.1997.609319` + .. [3] Viola, Paul, and Michael J. Jones. "Robust real-time face + detection." International journal of computer vision 57.2 + (2004): 137-154. + https://www.merl.com/publications/docs/TR2004-043.pdf + :DOI:`10.1109/CVPR.2001.990517` + + """ + if feature_coord is None: + feature_type_ = _validate_feature_type(feature_type) + + return np.hstack( + list( + chain.from_iterable( + haar_like_feature_wrapper( + int_image, r, c, width, height, feat_t, feature_coord + ) + for feat_t in feature_type_ + ) + ) + ) + else: + if feature_coord.shape[0] != feature_type.shape[0]: + raise ValueError( + "Inconsistent size between feature coordinates" "and feature types." + ) + + mask_feature = [feature_type == feat_t for feat_t in FEATURE_TYPE] + haar_feature_idx, haar_feature = zip( + *[ + ( + np.flatnonzero(mask), + haar_like_feature_wrapper( + int_image, r, c, width, height, feat_t, feature_coord[mask] + ), + ) + for mask, feat_t in zip(mask_feature, FEATURE_TYPE) + if np.count_nonzero(mask) + ] + ) + + haar_feature_idx = np.concatenate(haar_feature_idx) + haar_feature = np.concatenate(haar_feature) + + haar_feature[haar_feature_idx] = haar_feature.copy() + return haar_feature + + +def draw_haar_like_feature( + image, + r, + c, + width, + height, + feature_coord, + color_positive_block=(1.0, 0.0, 0.0), + color_negative_block=(0.0, 1.0, 0.0), + alpha=0.5, + max_n_features=None, + rng=None, +): + """Visualization of Haar-like features. + + Parameters + ---------- + image : (M, N) ndarray + The region of an integral image for which the features need to be + computed. + r : int + Row-coordinate of top left corner of the detection window. + c : int + Column-coordinate of top left corner of the detection window. + width : int + Width of the detection window. + height : int + Height of the detection window. + feature_coord : ndarray of list of tuples or None, optional + The array of coordinates to be extracted. This is useful when you want + to recompute only a subset of features. In this case `feature_type` + needs to be an array containing the type of each feature, as returned + by :func:`haar_like_feature_coord`. By default, all coordinates are + computed. + color_positive_block : tuple of 3 floats + Floats specifying the color for the positive block. Corresponding + values define (R, G, B) values. Default value is red (1, 0, 0). + color_negative_block : tuple of 3 floats + Floats specifying the color for the negative block Corresponding values + define (R, G, B) values. Default value is blue (0, 1, 0). + alpha : float + Value in the range [0, 1] that specifies opacity of visualization. 1 - + fully transparent, 0 - opaque. + max_n_features : int, default=None + The maximum number of features to be returned. + By default, all features are returned. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + The rng is used when generating a set of features smaller than + the total number of available features. + + Returns + ------- + features : (M, N), ndarray + An image in which the different features will be added. + + Examples + -------- + >>> import numpy as np + >>> from skimage.feature import haar_like_feature_coord + >>> from skimage.feature import draw_haar_like_feature + >>> feature_coord, _ = haar_like_feature_coord(2, 2, 'type-4') + >>> image = draw_haar_like_feature(np.zeros((2, 2)), + ... 0, 0, 2, 2, + ... feature_coord, + ... max_n_features=1) + >>> image + array([[[0. , 0.5, 0. ], + [0.5, 0. , 0. ]], + + [[0.5, 0. , 0. ], + [0. , 0.5, 0. ]]]) + + """ + rng = np.random.default_rng(rng) + color_positive_block = np.asarray(color_positive_block, dtype=np.float64) + color_negative_block = np.asarray(color_negative_block, dtype=np.float64) + + if max_n_features is None: + feature_coord_ = feature_coord + else: + feature_coord_ = rng.choice(feature_coord, size=max_n_features, replace=False) + + output = np.copy(image) + if len(image.shape) < 3: + output = gray2rgb(image) + output = img_as_float(output) + + for coord in feature_coord_: + for idx_rect, rect in enumerate(coord): + coord_start, coord_end = rect + coord_start = tuple(map(add, coord_start, [r, c])) + coord_end = tuple(map(add, coord_end, [r, c])) + rr, cc = rectangle(coord_start, coord_end) + + if ((idx_rect + 1) % 2) == 0: + new_value = (1 - alpha) * output[rr, cc] + alpha * color_positive_block + else: + new_value = (1 - alpha) * output[rr, cc] + alpha * color_negative_block + output[rr, cc] = new_value + + return output diff --git a/lib/python3.10/site-packages/skimage/feature/match.py b/lib/python3.10/site-packages/skimage/feature/match.py new file mode 100644 index 0000000000000000000000000000000000000000..c6c429170156e939e6ecd34e56c6ea6b2c0ee897 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/match.py @@ -0,0 +1,103 @@ +import numpy as np +from scipy.spatial.distance import cdist + + +def match_descriptors( + descriptors1, + descriptors2, + metric=None, + p=2, + max_distance=np.inf, + cross_check=True, + max_ratio=1.0, +): + """Brute-force matching of descriptors. + + For each descriptor in the first set this matcher finds the closest + descriptor in the second set (and vice-versa in the case of enabled + cross-checking). + + Parameters + ---------- + descriptors1 : (M, P) array + Descriptors of size P about M keypoints in the first image. + descriptors2 : (N, P) array + Descriptors of size P about N keypoints in the second image. + metric : {'euclidean', 'cityblock', 'minkowski', 'hamming', ...} , optional + The metric to compute the distance between two descriptors. See + `scipy.spatial.distance.cdist` for all possible types. The hamming + distance should be used for binary descriptors. By default the L2-norm + is used for all descriptors of dtype float or double and the Hamming + distance is used for binary descriptors automatically. + p : int, optional + The p-norm to apply for ``metric='minkowski'``. + max_distance : float, optional + Maximum allowed distance between descriptors of two keypoints + in separate images to be regarded as a match. + cross_check : bool, optional + If True, the matched keypoints are returned after cross checking i.e. a + matched pair (keypoint1, keypoint2) is returned if keypoint2 is the + best match for keypoint1 in second image and keypoint1 is the best + match for keypoint2 in first image. + max_ratio : float, optional + Maximum ratio of distances between first and second closest descriptor + in the second set of descriptors. This threshold is useful to filter + ambiguous matches between the two descriptor sets. The choice of this + value depends on the statistics of the chosen descriptor, e.g., + for SIFT descriptors a value of 0.8 is usually chosen, see + D.G. Lowe, "Distinctive Image Features from Scale-Invariant Keypoints", + International Journal of Computer Vision, 2004. + + Returns + ------- + matches : (Q, 2) array + Indices of corresponding matches in first and second set of + descriptors, where ``matches[:, 0]`` denote the indices in the first + and ``matches[:, 1]`` the indices in the second set of descriptors. + + """ + + if descriptors1.shape[1] != descriptors2.shape[1]: + raise ValueError("Descriptor length must equal.") + + if metric is None: + if np.issubdtype(descriptors1.dtype, bool): + metric = 'hamming' + else: + metric = 'euclidean' + + kwargs = {} + # Scipy raises an error if p is passed as an extra argument when it isn't + # necessary for the chosen metric. + if metric == 'minkowski': + kwargs['p'] = p + distances = cdist(descriptors1, descriptors2, metric=metric, **kwargs) + + indices1 = np.arange(descriptors1.shape[0]) + indices2 = np.argmin(distances, axis=1) + + if cross_check: + matches1 = np.argmin(distances, axis=0) + mask = indices1 == matches1[indices2] + indices1 = indices1[mask] + indices2 = indices2[mask] + + if max_distance < np.inf: + mask = distances[indices1, indices2] < max_distance + indices1 = indices1[mask] + indices2 = indices2[mask] + + if max_ratio < 1.0: + best_distances = distances[indices1, indices2] + distances[indices1, indices2] = np.inf + second_best_indices2 = np.argmin(distances[indices1], axis=1) + second_best_distances = distances[indices1, second_best_indices2] + second_best_distances[second_best_distances == 0] = np.finfo(np.float64).eps + ratio = best_distances / second_best_distances + mask = ratio < max_ratio + indices1 = indices1[mask] + indices2 = indices2[mask] + + matches = np.column_stack((indices1, indices2)) + + return matches diff --git a/lib/python3.10/site-packages/skimage/feature/orb.py b/lib/python3.10/site-packages/skimage/feature/orb.py new file mode 100644 index 0000000000000000000000000000000000000000..c9fa5f23bc222eb0da9d660134ded0b1b7a7695b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/orb.py @@ -0,0 +1,366 @@ +import numpy as np + +from ..feature.util import ( + FeatureDetector, + DescriptorExtractor, + _mask_border_keypoints, + _prepare_grayscale_input_2D, +) + +from .corner import corner_fast, corner_orientations, corner_peaks, corner_harris +from ..transform import pyramid_gaussian +from .._shared.utils import check_nD +from .._shared.compat import NP_COPY_IF_NEEDED + +from .orb_cy import _orb_loop + + +OFAST_MASK = np.zeros((31, 31)) +OFAST_UMAX = [15, 15, 15, 15, 14, 14, 14, 13, 13, 12, 11, 10, 9, 8, 6, 3] +for i in range(-15, 16): + for j in range(-OFAST_UMAX[abs(i)], OFAST_UMAX[abs(i)] + 1): + OFAST_MASK[15 + j, 15 + i] = 1 + + +class ORB(FeatureDetector, DescriptorExtractor): + """Oriented FAST and rotated BRIEF feature detector and binary descriptor + extractor. + + Parameters + ---------- + n_keypoints : int, optional + Number of keypoints to be returned. The function will return the best + `n_keypoints` according to the Harris corner response if more than + `n_keypoints` are detected. If not, then all the detected keypoints + are returned. + fast_n : int, optional + The `n` parameter in `skimage.feature.corner_fast`. Minimum number of + consecutive pixels out of 16 pixels on the circle that should all be + either brighter or darker w.r.t test-pixel. A point c on the circle is + darker w.r.t test pixel p if ``Ic < Ip - threshold`` and brighter if + ``Ic > Ip + threshold``. Also stands for the n in ``FAST-n`` corner + detector. + fast_threshold : float, optional + The ``threshold`` parameter in ``feature.corner_fast``. Threshold used + to decide whether the pixels on the circle are brighter, darker or + similar w.r.t. the test pixel. Decrease the threshold when more + corners are desired and vice-versa. + harris_k : float, optional + The `k` parameter in `skimage.feature.corner_harris`. Sensitivity + factor to separate corners from edges, typically in range ``[0, 0.2]``. + Small values of `k` result in detection of sharp corners. + downscale : float, optional + Downscale factor for the image pyramid. Default value 1.2 is chosen so + that there are more dense scales which enable robust scale invariance + for a subsequent feature description. + n_scales : int, optional + Maximum number of scales from the bottom of the image pyramid to + extract the features from. + + Attributes + ---------- + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N,) array + Corresponding scales. + orientations : (N,) array + Corresponding orientations in radians. + responses : (N,) array + Corresponding Harris corner responses. + descriptors : (Q, `descriptor_size`) array of dtype bool + 2D array of binary descriptors of size `descriptor_size` for Q + keypoints after filtering out border keypoints with value at an + index ``(i, j)`` either being ``True`` or ``False`` representing + the outcome of the intensity comparison for i-th keypoint on j-th + decision pixel-pair. It is ``Q == np.sum(mask)``. + + References + ---------- + .. [1] Ethan Rublee, Vincent Rabaud, Kurt Konolige and Gary Bradski + "ORB: An efficient alternative to SIFT and SURF" + http://www.vision.cs.chubu.ac.jp/CV-R/pdf/Rublee_iccv2011.pdf + + Examples + -------- + >>> from skimage.feature import ORB, match_descriptors + >>> img1 = np.zeros((100, 100)) + >>> img2 = np.zeros_like(img1) + >>> rng = np.random.default_rng(19481137) # do not copy this value + >>> square = rng.random((20, 20)) + >>> img1[40:60, 40:60] = square + >>> img2[53:73, 53:73] = square + >>> detector_extractor1 = ORB(n_keypoints=5) + >>> detector_extractor2 = ORB(n_keypoints=5) + >>> detector_extractor1.detect_and_extract(img1) + >>> detector_extractor2.detect_and_extract(img2) + >>> matches = match_descriptors(detector_extractor1.descriptors, + ... detector_extractor2.descriptors) + >>> matches + array([[0, 0], + [1, 1], + [2, 2], + [3, 4], + [4, 3]]) + >>> detector_extractor1.keypoints[matches[:, 0]] + array([[59. , 59. ], + [40. , 40. ], + [57. , 40. ], + [46. , 58. ], + [58.8, 58.8]]) + >>> detector_extractor2.keypoints[matches[:, 1]] + array([[72., 72.], + [53., 53.], + [70., 53.], + [59., 71.], + [72., 72.]]) + + """ + + def __init__( + self, + downscale=1.2, + n_scales=8, + n_keypoints=500, + fast_n=9, + fast_threshold=0.08, + harris_k=0.04, + ): + self.downscale = downscale + self.n_scales = n_scales + self.n_keypoints = n_keypoints + self.fast_n = fast_n + self.fast_threshold = fast_threshold + self.harris_k = harris_k + + self.keypoints = None + self.scales = None + self.responses = None + self.orientations = None + self.descriptors = None + + def _build_pyramid(self, image): + image = _prepare_grayscale_input_2D(image) + return list( + pyramid_gaussian( + image, self.n_scales - 1, self.downscale, channel_axis=None + ) + ) + + def _detect_octave(self, octave_image): + dtype = octave_image.dtype + # Extract keypoints for current octave + fast_response = corner_fast(octave_image, self.fast_n, self.fast_threshold) + keypoints = corner_peaks(fast_response, min_distance=1) + + if len(keypoints) == 0: + return ( + np.zeros((0, 2), dtype=dtype), + np.zeros((0,), dtype=dtype), + np.zeros((0,), dtype=dtype), + ) + + mask = _mask_border_keypoints(octave_image.shape, keypoints, distance=16) + keypoints = keypoints[mask] + + orientations = corner_orientations(octave_image, keypoints, OFAST_MASK) + + harris_response = corner_harris(octave_image, method='k', k=self.harris_k) + responses = harris_response[keypoints[:, 0], keypoints[:, 1]] + + return keypoints, orientations, responses + + def detect(self, image): + """Detect oriented FAST keypoints along with the corresponding scale. + + Parameters + ---------- + image : 2D array + Input image. + + """ + check_nD(image, 2) + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + orientations_list = [] + scales_list = [] + responses_list = [] + + for octave in range(len(pyramid)): + octave_image = np.ascontiguousarray(pyramid[octave]) + + if np.squeeze(octave_image).ndim < 2: + # No further keypoints can be detected if the image is not really 2d + break + + keypoints, orientations, responses = self._detect_octave(octave_image) + + keypoints_list.append(keypoints * self.downscale**octave) + orientations_list.append(orientations) + scales_list.append( + np.full( + keypoints.shape[0], + self.downscale**octave, + dtype=octave_image.dtype, + ) + ) + responses_list.append(responses) + + keypoints = np.vstack(keypoints_list) + orientations = np.hstack(orientations_list) + scales = np.hstack(scales_list) + responses = np.hstack(responses_list) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][: self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + + def _extract_octave(self, octave_image, keypoints, orientations): + mask = _mask_border_keypoints(octave_image.shape, keypoints, distance=20) + keypoints = np.array( + keypoints[mask], dtype=np.intp, order='C', copy=NP_COPY_IF_NEEDED + ) + orientations = np.array(orientations[mask], order='C', copy=False) + + descriptors = _orb_loop(octave_image, keypoints, orientations) + + return descriptors, mask + + def extract(self, image, keypoints, scales, orientations): + """Extract rBRIEF binary descriptors for given keypoints in image. + + Note that the keypoints must be extracted using the same `downscale` + and `n_scales` parameters. Additionally, if you want to extract both + keypoints and descriptors you should use the faster + `detect_and_extract`. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + scales : (N,) array + Corresponding scales. + orientations : (N,) array + Corresponding orientations in radians. + + """ + check_nD(image, 2) + + pyramid = self._build_pyramid(image) + + descriptors_list = [] + mask_list = [] + + # Determine octaves from scales + octaves = (np.log(scales) / np.log(self.downscale)).astype(np.intp) + + for octave in range(len(pyramid)): + # Mask for all keypoints in current octave + octave_mask = octaves == octave + + if np.sum(octave_mask) > 0: + octave_image = np.ascontiguousarray(pyramid[octave]) + + octave_keypoints = keypoints[octave_mask] + octave_keypoints /= self.downscale**octave + octave_orientations = orientations[octave_mask] + + descriptors, mask = self._extract_octave( + octave_image, octave_keypoints, octave_orientations + ) + + descriptors_list.append(descriptors) + mask_list.append(mask) + + self.descriptors = np.vstack(descriptors_list).view(bool) + self.mask_ = np.hstack(mask_list) + + def detect_and_extract(self, image): + """Detect oriented FAST keypoints and extract rBRIEF descriptors. + + Note that this is faster than first calling `detect` and then + `extract`. + + Parameters + ---------- + image : 2D array + Input image. + + """ + check_nD(image, 2) + + pyramid = self._build_pyramid(image) + + keypoints_list = [] + responses_list = [] + scales_list = [] + orientations_list = [] + descriptors_list = [] + + for octave in range(len(pyramid)): + octave_image = np.ascontiguousarray(pyramid[octave]) + + if np.squeeze(octave_image).ndim < 2: + # No further keypoints can be detected if the image is not really 2d + break + + keypoints, orientations, responses = self._detect_octave(octave_image) + + if len(keypoints) == 0: + keypoints_list.append(keypoints) + responses_list.append(responses) + descriptors_list.append(np.zeros((0, 256), dtype=bool)) + continue + + descriptors, mask = self._extract_octave( + octave_image, keypoints, orientations + ) + + scaled_keypoints = keypoints[mask] * self.downscale**octave + keypoints_list.append(scaled_keypoints) + responses_list.append(responses[mask]) + orientations_list.append(orientations[mask]) + scales_list.append( + self.downscale**octave + * np.ones(scaled_keypoints.shape[0], dtype=np.intp) + ) + descriptors_list.append(descriptors) + + if len(scales_list) == 0: + raise RuntimeError( + "ORB found no features. Try passing in an image containing " + "greater intensity contrasts between adjacent pixels." + ) + + keypoints = np.vstack(keypoints_list) + responses = np.hstack(responses_list) + scales = np.hstack(scales_list) + orientations = np.hstack(orientations_list) + descriptors = np.vstack(descriptors_list).view(bool) + + if keypoints.shape[0] < self.n_keypoints: + self.keypoints = keypoints + self.scales = scales + self.orientations = orientations + self.responses = responses + self.descriptors = descriptors + else: + # Choose best n_keypoints according to Harris corner response + best_indices = responses.argsort()[::-1][: self.n_keypoints] + self.keypoints = keypoints[best_indices] + self.scales = scales[best_indices] + self.orientations = orientations[best_indices] + self.responses = responses[best_indices] + self.descriptors = descriptors[best_indices] diff --git a/lib/python3.10/site-packages/skimage/feature/orb_descriptor_positions.txt b/lib/python3.10/site-packages/skimage/feature/orb_descriptor_positions.txt new file mode 100644 index 0000000000000000000000000000000000000000..a78b2eb421572adf2349fc0758e12ddef70f1b45 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/orb_descriptor_positions.txt @@ -0,0 +1,256 @@ +8 -3 9 5 +4 2 7 -12 +-11 9 -8 2 +7 -12 12 -13 +2 -13 2 12 +1 -7 1 6 +-2 -10 -2 -4 +-13 -13 -11 -8 +-13 -3 -12 -9 +10 4 11 9 +-13 -8 -8 -9 +-11 7 -9 12 +7 7 12 6 +-4 -5 -3 0 +-13 2 -12 -3 +-9 0 -7 5 +12 -6 12 -1 +-3 6 -2 12 +-6 -13 -4 -8 +11 -13 12 -8 +4 7 5 1 +5 -3 10 -3 +3 -7 6 12 +-8 -7 -6 -2 +-2 11 -1 -10 +-13 12 -8 10 +-7 3 -5 -3 +-4 2 -3 7 +-10 -12 -6 11 +5 -12 6 -7 +5 -6 7 -1 +1 0 4 -5 +9 11 11 -13 +4 7 4 12 +2 -1 4 4 +-4 -12 -2 7 +-8 -5 -7 -10 +4 11 9 12 +0 -8 1 -13 +-13 -2 -8 2 +-3 -2 -2 3 +-6 9 -4 -9 +8 12 10 7 +0 9 1 3 +7 -5 11 -10 +-13 -6 -11 0 +10 7 12 1 +-6 -3 -6 12 +10 -9 12 -4 +-13 8 -8 -12 +-13 0 -8 -4 +3 3 7 8 +5 7 10 -7 +-1 7 1 -12 +3 -10 5 6 +2 -4 3 -10 +-13 0 -13 5 +-13 -7 -12 12 +-13 3 -11 8 +-7 12 -4 7 +6 -10 12 8 +-9 -1 -7 -6 +-2 -5 0 12 +-12 5 -7 5 +3 -10 8 -13 +-7 -7 -4 5 +-3 -2 -1 -7 +2 9 5 -11 +-11 -13 -5 -13 +-1 6 0 -1 +5 -3 5 2 +-4 -13 -4 12 +-9 -6 -9 6 +-12 -10 -8 -4 +10 2 12 -3 +7 12 12 12 +-7 -13 -6 5 +-4 9 -3 4 +7 -1 12 2 +-7 6 -5 1 +-13 11 -12 5 +-3 7 -2 -6 +7 -8 12 -7 +-13 -7 -11 -12 +1 -3 12 12 +2 -6 3 0 +-4 3 -2 -13 +-1 -13 1 9 +7 1 8 -6 +1 -1 3 12 +9 1 12 6 +-1 -9 -1 3 +-13 -13 -10 5 +7 7 10 12 +12 -5 12 9 +6 3 7 11 +5 -13 6 10 +2 -12 2 3 +3 8 4 -6 +2 6 12 -13 +9 -12 10 3 +-8 4 -7 9 +-11 12 -4 -6 +1 12 2 -8 +6 -9 7 -4 +2 3 3 -2 +6 3 11 0 +3 -3 8 -8 +7 8 9 3 +-11 -5 -6 -4 +-10 11 -5 10 +-5 -8 -3 12 +-10 5 -9 0 +8 -1 12 -6 +4 -6 6 -11 +-10 12 -8 7 +4 -2 6 7 +-2 0 -2 12 +-5 -8 -5 2 +7 -6 10 12 +-9 -13 -8 -8 +-5 -13 -5 -2 +8 -8 9 -13 +-9 -11 -9 0 +1 -8 1 -2 +7 -4 9 1 +-2 1 -1 -4 +11 -6 12 -11 +-12 -9 -6 4 +3 7 7 12 +5 5 10 8 +0 -4 2 8 +-9 12 -5 -13 +0 7 2 12 +-1 2 1 7 +5 11 7 -9 +3 5 6 -8 +-13 -4 -8 9 +-5 9 -3 -3 +-4 -7 -3 -12 +6 5 8 0 +-7 6 -6 12 +-13 6 -5 -2 +1 -10 3 10 +4 1 8 -4 +-2 -2 2 -13 +2 -12 12 12 +-2 -13 0 -6 +4 1 9 3 +-6 -10 -3 -5 +-3 -13 -1 1 +7 5 12 -11 +4 -2 5 -7 +-13 9 -9 -5 +7 1 8 6 +7 -8 7 6 +-7 -4 -7 1 +-8 11 -7 -8 +-13 6 -12 -8 +2 4 3 9 +10 -5 12 3 +-6 -5 -6 7 +8 -3 9 -8 +2 -12 2 8 +-11 -2 -10 3 +-12 -13 -7 -9 +-11 0 -10 -5 +5 -3 11 8 +-2 -13 -1 12 +-1 -8 0 9 +-13 -11 -12 -5 +-10 -2 -10 11 +-3 9 -2 -13 +2 -3 3 2 +-9 -13 -4 0 +-4 6 -3 -10 +-4 12 -2 -7 +-6 -11 -4 9 +6 -3 6 11 +-13 11 -5 5 +11 11 12 6 +7 -5 12 -2 +-1 12 0 7 +-4 -8 -3 -2 +-7 1 -6 7 +-13 -12 -8 -13 +-7 -2 -6 -8 +-8 5 -6 -9 +-5 -1 -4 5 +-13 7 -8 10 +1 5 5 -13 +1 0 10 -13 +9 12 10 -1 +5 -8 10 -9 +-1 11 1 -13 +-9 -3 -6 2 +-1 -10 1 12 +-13 1 -8 -10 +8 -11 10 -6 +2 -13 3 -6 +7 -13 12 -9 +-10 -10 -5 -7 +-10 -8 -8 -13 +4 -6 8 5 +3 12 8 -13 +-4 2 -3 -3 +5 -13 10 -12 +4 -13 5 -1 +-9 9 -4 3 +0 3 3 -9 +-12 1 -6 1 +3 2 4 -8 +-10 -10 -10 9 +8 -13 12 12 +-8 -12 -6 -5 +2 2 3 7 +10 6 11 -8 +6 8 8 -12 +-7 10 -6 5 +-3 -9 -3 9 +-1 -13 -1 5 +-3 -7 -3 4 +-8 -2 -8 3 +4 2 12 12 +2 -5 3 11 +6 -9 11 -13 +3 -1 7 12 +11 -1 12 4 +-3 0 -3 6 +4 -11 4 12 +2 -4 2 1 +-10 -6 -8 1 +-13 7 -11 1 +-13 12 -11 -13 +6 0 11 -13 +0 -1 1 4 +-13 3 -9 -2 +-9 8 -6 -3 +-13 -6 -8 -2 +5 -9 8 10 +2 7 3 -9 +-1 -6 -1 -1 +9 5 11 -2 +11 -3 12 -8 +3 0 3 5 +-1 4 0 10 +3 -6 4 5 +-13 0 -10 5 +5 8 12 11 +8 9 9 -6 +7 -4 8 -12 +-10 4 -10 9 +7 3 12 4 +9 -7 10 -2 +7 0 12 -2 +-1 -6 0 -11 diff --git a/lib/python3.10/site-packages/skimage/feature/peak.py b/lib/python3.10/site-packages/skimage/feature/peak.py new file mode 100644 index 0000000000000000000000000000000000000000..6e8493174f11abac15e13876c323072e3ed6c744 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/peak.py @@ -0,0 +1,420 @@ +from warnings import warn + +import numpy as np +import scipy.ndimage as ndi + +from .. import measure +from .._shared.coord import ensure_spacing + + +def _get_high_intensity_peaks(image, mask, num_peaks, min_distance, p_norm): + """ + Return the highest intensity peak coordinates. + """ + # get coordinates of peaks + coord = np.nonzero(mask) + intensities = image[coord] + # Highest peak first + idx_maxsort = np.argsort(-intensities, kind="stable") + coord = np.transpose(coord)[idx_maxsort] + + if np.isfinite(num_peaks): + max_out = int(num_peaks) + else: + max_out = None + + if min_distance > 1: + coord = ensure_spacing( + coord, spacing=min_distance, p_norm=p_norm, max_out=max_out + ) + + if len(coord) > num_peaks: + coord = coord[:num_peaks] + + return coord + + +def _get_peak_mask(image, footprint, threshold, mask=None): + """ + Return the mask containing all peak candidates above thresholds. + """ + if footprint.size == 1 or image.size == 1: + return image > threshold + + image_max = ndi.maximum_filter(image, footprint=footprint, mode='nearest') + + out = image == image_max + + # no peak for a trivial image + image_is_trivial = np.all(out) if mask is None else np.all(out[mask]) + if image_is_trivial: + out[:] = False + if mask is not None: + # isolated pixels in masked area are returned as peaks + isolated_px = np.logical_xor(mask, ndi.binary_opening(mask)) + out[isolated_px] = True + + out &= image > threshold + return out + + +def _exclude_border(label, border_width): + """Set label border values to 0.""" + # zero out label borders + for i, width in enumerate(border_width): + if width == 0: + continue + label[(slice(None),) * i + (slice(None, width),)] = 0 + label[(slice(None),) * i + (slice(-width, None),)] = 0 + return label + + +def _get_threshold(image, threshold_abs, threshold_rel): + """Return the threshold value according to an absolute and a relative + value. + + """ + threshold = threshold_abs if threshold_abs is not None else image.min() + + if threshold_rel is not None: + threshold = max(threshold, threshold_rel * image.max()) + + return threshold + + +def _get_excluded_border_width(image, min_distance, exclude_border): + """Return border_width values relative to a min_distance if requested.""" + + if isinstance(exclude_border, bool): + border_width = (min_distance if exclude_border else 0,) * image.ndim + elif isinstance(exclude_border, int): + if exclude_border < 0: + raise ValueError("`exclude_border` cannot be a negative value") + border_width = (exclude_border,) * image.ndim + elif isinstance(exclude_border, tuple): + if len(exclude_border) != image.ndim: + raise ValueError( + "`exclude_border` should have the same length as the " + "dimensionality of the image." + ) + for exclude in exclude_border: + if not isinstance(exclude, int): + raise ValueError( + "`exclude_border`, when expressed as a tuple, must only " + "contain ints." + ) + if exclude < 0: + raise ValueError("`exclude_border` can not be a negative value") + border_width = exclude_border + else: + raise TypeError( + "`exclude_border` must be bool, int, or tuple with the same " + "length as the dimensionality of the image." + ) + + return border_width + + +def peak_local_max( + image, + min_distance=1, + threshold_abs=None, + threshold_rel=None, + exclude_border=True, + num_peaks=np.inf, + footprint=None, + labels=None, + num_peaks_per_label=np.inf, + p_norm=np.inf, +): + """Find peaks in an image as coordinate list. + + Peaks are the local maxima in a region of `2 * min_distance + 1` + (i.e. peaks are separated by at least `min_distance`). + + If both `threshold_abs` and `threshold_rel` are provided, the maximum + of the two is chosen as the minimum intensity threshold of peaks. + + .. versionchanged:: 0.18 + Prior to version 0.18, peaks of the same height within a radius of + `min_distance` were all returned, but this could cause unexpected + behaviour. From 0.18 onwards, an arbitrary peak within the region is + returned. See issue gh-2592. + + Parameters + ---------- + image : ndarray + Input image. + min_distance : int, optional + The minimal allowed distance separating peaks. To find the + maximum number of peaks, use `min_distance=1`. + threshold_abs : float or None, optional + Minimum intensity of peaks. By default, the absolute threshold is + the minimum intensity of the image. + threshold_rel : float or None, optional + Minimum intensity of peaks, calculated as + ``max(image) * threshold_rel``. + exclude_border : int, tuple of ints, or bool, optional + If positive integer, `exclude_border` excludes peaks from within + `exclude_border`-pixels of the border of the image. + If tuple of non-negative ints, the length of the tuple must match the + input array's dimensionality. Each element of the tuple will exclude + peaks from within `exclude_border`-pixels of the border of the image + along that dimension. + If True, takes the `min_distance` parameter as value. + If zero or False, peaks are identified regardless of their distance + from the border. + num_peaks : int, optional + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` peaks based on highest peak intensity. + footprint : ndarray of bools, optional + If provided, `footprint == 1` represents the local region within which + to search for peaks at every point in `image`. + labels : ndarray of ints, optional + If provided, each unique region `labels == value` represents a unique + region to search for peaks. Zero is reserved for background. + num_peaks_per_label : int, optional + Maximum number of peaks for each label. + p_norm : float + Which Minkowski p-norm to use. Should be in the range [1, inf]. + A finite large p may cause a ValueError if overflow can occur. + ``inf`` corresponds to the Chebyshev distance and 2 to the + Euclidean distance. + + Returns + ------- + output : ndarray + The coordinates of the peaks. + + Notes + ----- + The peak local maximum function returns the coordinates of local peaks + (maxima) in an image. Internally, a maximum filter is used for finding + local maxima. This operation dilates the original image. After comparison + of the dilated and original images, this function returns the coordinates + of the peaks where the dilated image equals the original image. + + See also + -------- + skimage.feature.corner_peaks + + Examples + -------- + >>> img1 = np.zeros((7, 7)) + >>> img1[3, 4] = 1 + >>> img1[3, 2] = 1.5 + >>> img1 + array([[0. , 0. , 0. , 0. , 0. , 0. , 0. ], + [0. , 0. , 0. , 0. , 0. , 0. , 0. ], + [0. , 0. , 0. , 0. , 0. , 0. , 0. ], + [0. , 0. , 1.5, 0. , 1. , 0. , 0. ], + [0. , 0. , 0. , 0. , 0. , 0. , 0. ], + [0. , 0. , 0. , 0. , 0. , 0. , 0. ], + [0. , 0. , 0. , 0. , 0. , 0. , 0. ]]) + + >>> peak_local_max(img1, min_distance=1) + array([[3, 2], + [3, 4]]) + + >>> peak_local_max(img1, min_distance=2) + array([[3, 2]]) + + >>> img2 = np.zeros((20, 20, 20)) + >>> img2[10, 10, 10] = 1 + >>> img2[15, 15, 15] = 1 + >>> peak_idx = peak_local_max(img2, exclude_border=0) + >>> peak_idx + array([[10, 10, 10], + [15, 15, 15]]) + + >>> peak_mask = np.zeros_like(img2, dtype=bool) + >>> peak_mask[tuple(peak_idx.T)] = True + >>> np.argwhere(peak_mask) + array([[10, 10, 10], + [15, 15, 15]]) + + """ + if (footprint is None or footprint.size == 1) and min_distance < 1: + warn( + "When min_distance < 1, peak_local_max acts as finding " + "image > max(threshold_abs, threshold_rel * max(image)).", + RuntimeWarning, + stacklevel=2, + ) + + border_width = _get_excluded_border_width(image, min_distance, exclude_border) + + threshold = _get_threshold(image, threshold_abs, threshold_rel) + + if footprint is None: + size = 2 * min_distance + 1 + footprint = np.ones((size,) * image.ndim, dtype=bool) + else: + footprint = np.asarray(footprint) + + if labels is None: + # Non maximum filter + mask = _get_peak_mask(image, footprint, threshold) + + mask = _exclude_border(mask, border_width) + + # Select highest intensities (num_peaks) + coordinates = _get_high_intensity_peaks( + image, mask, num_peaks, min_distance, p_norm + ) + + else: + _labels = _exclude_border(labels.astype(int, casting="safe"), border_width) + + if np.issubdtype(image.dtype, np.floating): + bg_val = np.finfo(image.dtype).min + else: + bg_val = np.iinfo(image.dtype).min + + # For each label, extract a smaller image enclosing the object of + # interest, identify num_peaks_per_label peaks + labels_peak_coord = [] + + for label_idx, roi in enumerate(ndi.find_objects(_labels)): + if roi is None: + continue + + # Get roi mask + label_mask = labels[roi] == label_idx + 1 + # Extract image roi + img_object = image[roi].copy() + # Ensure masked values don't affect roi's local peaks + img_object[np.logical_not(label_mask)] = bg_val + + mask = _get_peak_mask(img_object, footprint, threshold, label_mask) + + coordinates = _get_high_intensity_peaks( + img_object, mask, num_peaks_per_label, min_distance, p_norm + ) + + # transform coordinates in global image indices space + for idx, s in enumerate(roi): + coordinates[:, idx] += s.start + + labels_peak_coord.append(coordinates) + + if labels_peak_coord: + coordinates = np.vstack(labels_peak_coord) + else: + coordinates = np.empty((0, 2), dtype=int) + + if len(coordinates) > num_peaks: + out = np.zeros_like(image, dtype=bool) + out[tuple(coordinates.T)] = True + coordinates = _get_high_intensity_peaks( + image, out, num_peaks, min_distance, p_norm + ) + + return coordinates + + +def _prominent_peaks( + image, min_xdistance=1, min_ydistance=1, threshold=None, num_peaks=np.inf +): + """Return peaks with non-maximum suppression. + + Identifies most prominent features separated by certain distances. + Non-maximum suppression with different sizes is applied separately + in the first and second dimension of the image to identify peaks. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + min_xdistance : int + Minimum distance separating features in the x dimension. + min_ydistance : int + Minimum distance separating features in the y dimension. + threshold : float + Minimum intensity of peaks. Default is `0.5 * max(image)`. + num_peaks : int + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` coordinates based on peak intensity. + + Returns + ------- + intensity, xcoords, ycoords : tuple of array + Peak intensity values, x and y indices. + """ + + img = image.copy() + rows, cols = img.shape + + if threshold is None: + threshold = 0.5 * np.max(img) + + ycoords_size = 2 * min_ydistance + 1 + xcoords_size = 2 * min_xdistance + 1 + img_max = ndi.maximum_filter1d( + img, size=ycoords_size, axis=0, mode='constant', cval=0 + ) + img_max = ndi.maximum_filter1d( + img_max, size=xcoords_size, axis=1, mode='constant', cval=0 + ) + mask = img == img_max + img *= mask + img_t = img > threshold + + label_img = measure.label(img_t) + props = measure.regionprops(label_img, img_max) + + # Sort the list of peaks by intensity, not left-right, so larger peaks + # in Hough space cannot be arbitrarily suppressed by smaller neighbors + props = sorted(props, key=lambda x: x.intensity_max)[::-1] + coords = np.array([np.round(p.centroid) for p in props], dtype=int) + + img_peaks = [] + ycoords_peaks = [] + xcoords_peaks = [] + + # relative coordinate grid for local neighborhood suppression + ycoords_ext, xcoords_ext = np.mgrid[ + -min_ydistance : min_ydistance + 1, -min_xdistance : min_xdistance + 1 + ] + + for ycoords_idx, xcoords_idx in coords: + accum = img_max[ycoords_idx, xcoords_idx] + if accum > threshold: + # absolute coordinate grid for local neighborhood suppression + ycoords_nh = ycoords_idx + ycoords_ext + xcoords_nh = xcoords_idx + xcoords_ext + + # no reflection for distance neighborhood + ycoords_in = np.logical_and(ycoords_nh > 0, ycoords_nh < rows) + ycoords_nh = ycoords_nh[ycoords_in] + xcoords_nh = xcoords_nh[ycoords_in] + + # reflect xcoords and assume xcoords are continuous, + # e.g. for angles: + # (..., 88, 89, -90, -89, ..., 89, -90, -89, ...) + xcoords_low = xcoords_nh < 0 + ycoords_nh[xcoords_low] = rows - ycoords_nh[xcoords_low] + xcoords_nh[xcoords_low] += cols + xcoords_high = xcoords_nh >= cols + ycoords_nh[xcoords_high] = rows - ycoords_nh[xcoords_high] + xcoords_nh[xcoords_high] -= cols + + # suppress neighborhood + img_max[ycoords_nh, xcoords_nh] = 0 + + # add current feature to peaks + img_peaks.append(accum) + ycoords_peaks.append(ycoords_idx) + xcoords_peaks.append(xcoords_idx) + + img_peaks = np.array(img_peaks) + ycoords_peaks = np.array(ycoords_peaks) + xcoords_peaks = np.array(xcoords_peaks) + + if num_peaks < len(img_peaks): + idx_maxsort = np.argsort(img_peaks)[::-1][:num_peaks] + img_peaks = img_peaks[idx_maxsort] + ycoords_peaks = ycoords_peaks[idx_maxsort] + xcoords_peaks = xcoords_peaks[idx_maxsort] + + return img_peaks, xcoords_peaks, ycoords_peaks diff --git a/lib/python3.10/site-packages/skimage/feature/sift.py b/lib/python3.10/site-packages/skimage/feature/sift.py new file mode 100644 index 0000000000000000000000000000000000000000..e5dab0f4523779ca9307015ba80506aa4f171914 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/sift.py @@ -0,0 +1,771 @@ +import math + +import numpy as np +import scipy.ndimage as ndi + +from .._shared.utils import check_nD, _supported_float_type +from ..feature.util import DescriptorExtractor, FeatureDetector +from .._shared.filters import gaussian +from ..transform import rescale +from ..util import img_as_float +from ._sift import _local_max, _ori_distances, _update_histogram + + +def _edgeness(hxx, hyy, hxy): + """Compute edgeness (eq. 18 of Otero et. al. IPOL paper)""" + trace = hxx + hyy + determinant = hxx * hyy - hxy * hxy + return (trace * trace) / determinant + + +def _sparse_gradient(vol, positions): + """Gradient of a 3D volume at the provided `positions`. + + For SIFT we only need the gradient at specific positions and do not need + the gradient at the edge positions, so can just use this simple + implementation instead of numpy.gradient. + """ + p0 = positions[..., 0] + p1 = positions[..., 1] + p2 = positions[..., 2] + g0 = vol[p0 + 1, p1, p2] - vol[p0 - 1, p1, p2] + g0 *= 0.5 + g1 = vol[p0, p1 + 1, p2] - vol[p0, p1 - 1, p2] + g1 *= 0.5 + g2 = vol[p0, p1, p2 + 1] - vol[p0, p1, p2 - 1] + g2 *= 0.5 + return g0, g1, g2 + + +def _hessian(d, positions): + """Compute the non-redundant 3D Hessian terms at the requested positions. + + Source: "Anatomy of the SIFT Method" p.380 (13) + """ + p0 = positions[..., 0] + p1 = positions[..., 1] + p2 = positions[..., 2] + two_d0 = 2 * d[p0, p1, p2] + # 0 = row, 1 = col, 2 = octave + h00 = d[p0 - 1, p1, p2] + d[p0 + 1, p1, p2] - two_d0 + h11 = d[p0, p1 - 1, p2] + d[p0, p1 + 1, p2] - two_d0 + h22 = d[p0, p1, p2 - 1] + d[p0, p1, p2 + 1] - two_d0 + h01 = 0.25 * ( + d[p0 + 1, p1 + 1, p2] + - d[p0 - 1, p1 + 1, p2] + - d[p0 + 1, p1 - 1, p2] + + d[p0 - 1, p1 - 1, p2] + ) + h02 = 0.25 * ( + d[p0 + 1, p1, p2 + 1] + - d[p0 + 1, p1, p2 - 1] + + d[p0 - 1, p1, p2 - 1] + - d[p0 - 1, p1, p2 + 1] + ) + h12 = 0.25 * ( + d[p0, p1 + 1, p2 + 1] + - d[p0, p1 + 1, p2 - 1] + + d[p0, p1 - 1, p2 - 1] + - d[p0, p1 - 1, p2 + 1] + ) + return (h00, h11, h22, h01, h02, h12) + + +def _offsets(grad, hess): + """Compute position refinement offsets from gradient and Hessian. + + This is equivalent to np.linalg.solve(-H, J) where H is the Hessian + matrix and J is the gradient (Jacobian). + + This analytical solution is adapted from (BSD-licensed) C code by + Otero et. al (see SIFT docstring References). + """ + h00, h11, h22, h01, h02, h12 = hess + g0, g1, g2 = grad + det = h00 * h11 * h22 + det -= h00 * h12 * h12 + det -= h01 * h01 * h22 + det += 2 * h01 * h02 * h12 + det -= h02 * h02 * h11 + aa = (h11 * h22 - h12 * h12) / det + ab = (h02 * h12 - h01 * h22) / det + ac = (h01 * h12 - h02 * h11) / det + bb = (h00 * h22 - h02 * h02) / det + bc = (h01 * h02 - h00 * h12) / det + cc = (h00 * h11 - h01 * h01) / det + offset0 = -aa * g0 - ab * g1 - ac * g2 + offset1 = -ab * g0 - bb * g1 - bc * g2 + offset2 = -ac * g0 - bc * g1 - cc * g2 + return np.stack((offset0, offset1, offset2), axis=-1) + + +class SIFT(FeatureDetector, DescriptorExtractor): + """SIFT feature detection and descriptor extraction. + + Parameters + ---------- + upsampling : int, optional + Prior to the feature detection the image is upscaled by a factor + of 1 (no upscaling), 2 or 4. Method: Bi-cubic interpolation. + n_octaves : int, optional + Maximum number of octaves. With every octave the image size is + halved and the sigma doubled. The number of octaves will be + reduced as needed to keep at least 12 pixels along each dimension + at the smallest scale. + n_scales : int, optional + Maximum number of scales in every octave. + sigma_min : float, optional + The blur level of the seed image. If upsampling is enabled + sigma_min is scaled by factor 1/upsampling + sigma_in : float, optional + The assumed blur level of the input image. + c_dog : float, optional + Threshold to discard low contrast extrema in the DoG. It's final + value is dependent on n_scales by the relation: + final_c_dog = (2^(1/n_scales)-1) / (2^(1/3)-1) * c_dog + c_edge : float, optional + Threshold to discard extrema that lie in edges. If H is the + Hessian of an extremum, its "edgeness" is described by + tr(H)²/det(H). If the edgeness is higher than + (c_edge + 1)²/c_edge, the extremum is discarded. + n_bins : int, optional + Number of bins in the histogram that describes the gradient + orientations around keypoint. + lambda_ori : float, optional + The window used to find the reference orientation of a keypoint + has a width of 6 * lambda_ori * sigma and is weighted by a + standard deviation of 2 * lambda_ori * sigma. + c_max : float, optional + The threshold at which a secondary peak in the orientation + histogram is accepted as orientation + lambda_descr : float, optional + The window used to define the descriptor of a keypoint has a width + of 2 * lambda_descr * sigma * (n_hist+1)/n_hist and is weighted by + a standard deviation of lambda_descr * sigma. + n_hist : int, optional + The window used to define the descriptor of a keypoint consists of + n_hist * n_hist histograms. + n_ori : int, optional + The number of bins in the histograms of the descriptor patch. + + Attributes + ---------- + delta_min : float + The sampling distance of the first octave. It's final value is + 1/upsampling. + float_dtype : type + The datatype of the image. + scalespace_sigmas : (n_octaves, n_scales + 3) array + The sigma value of all scales in all octaves. + keypoints : (N, 2) array + Keypoint coordinates as ``(row, col)``. + positions : (N, 2) array + Subpixel-precision keypoint coordinates as ``(row, col)``. + sigmas : (N,) array + The corresponding sigma (blur) value of a keypoint. + scales : (N,) array + The corresponding scale of a keypoint. + orientations : (N,) array + The orientations of the gradient around every keypoint. + octaves : (N,) array + The corresponding octave of a keypoint. + descriptors : (N, n_hist*n_hist*n_ori) array + The descriptors of a keypoint. + + Notes + ----- + The SIFT algorithm was developed by David Lowe [1]_, [2]_ and later + patented by the University of British Columbia. Since the patent expired in + 2020 it's free to use. The implementation here closely follows the + detailed description in [3]_, including use of the same default parameters. + + References + ---------- + .. [1] D.G. Lowe. "Object recognition from local scale-invariant + features", Proceedings of the Seventh IEEE International + Conference on Computer Vision, 1999, vol.2, pp. 1150-1157. + :DOI:`10.1109/ICCV.1999.790410` + + .. [2] D.G. Lowe. "Distinctive Image Features from Scale-Invariant + Keypoints", International Journal of Computer Vision, 2004, + vol. 60, pp. 91–110. + :DOI:`10.1023/B:VISI.0000029664.99615.94` + + .. [3] I. R. Otero and M. Delbracio. "Anatomy of the SIFT Method", + Image Processing On Line, 4 (2014), pp. 370–396. + :DOI:`10.5201/ipol.2014.82` + + Examples + -------- + >>> from skimage.feature import SIFT, match_descriptors + >>> from skimage.data import camera + >>> from skimage.transform import rotate + >>> img1 = camera() + >>> img2 = rotate(camera(), 90) + >>> detector_extractor1 = SIFT() + >>> detector_extractor2 = SIFT() + >>> detector_extractor1.detect_and_extract(img1) + >>> detector_extractor2.detect_and_extract(img2) + >>> matches = match_descriptors(detector_extractor1.descriptors, + ... detector_extractor2.descriptors, + ... max_ratio=0.6) + >>> matches[10:15] + array([[ 10, 412], + [ 11, 417], + [ 12, 407], + [ 13, 411], + [ 14, 406]]) + >>> detector_extractor1.keypoints[matches[10:15, 0]] + array([[ 95, 214], + [ 97, 211], + [ 97, 218], + [102, 215], + [104, 218]]) + >>> detector_extractor2.keypoints[matches[10:15, 1]] + array([[297, 95], + [301, 97], + [294, 97], + [297, 102], + [293, 104]]) + + """ + + def __init__( + self, + upsampling=2, + n_octaves=8, + n_scales=3, + sigma_min=1.6, + sigma_in=0.5, + c_dog=0.04 / 3, + c_edge=10, + n_bins=36, + lambda_ori=1.5, + c_max=0.8, + lambda_descr=6, + n_hist=4, + n_ori=8, + ): + if upsampling in [1, 2, 4]: + self.upsampling = upsampling + else: + raise ValueError("upsampling must be 1, 2 or 4") + self.n_octaves = n_octaves + self.n_scales = n_scales + self.sigma_min = sigma_min / upsampling + self.sigma_in = sigma_in + self.c_dog = (2 ** (1 / n_scales) - 1) / (2 ** (1 / 3) - 1) * c_dog + self.c_edge = c_edge + self.n_bins = n_bins + self.lambda_ori = lambda_ori + self.c_max = c_max + self.lambda_descr = lambda_descr + self.n_hist = n_hist + self.n_ori = n_ori + self.delta_min = 1 / upsampling + self.float_dtype = None + self.scalespace_sigmas = None + self.keypoints = None + self.positions = None + self.sigmas = None + self.scales = None + self.orientations = None + self.octaves = None + self.descriptors = None + + @property + def deltas(self): + """The sampling distances of all octaves""" + deltas = self.delta_min * np.power( + 2, np.arange(self.n_octaves), dtype=self.float_dtype + ) + return deltas + + def _set_number_of_octaves(self, image_shape): + size_min = 12 # minimum size of last octave + s0 = min(image_shape) * self.upsampling + max_octaves = int(math.log2(s0 / size_min) + 1) + if max_octaves < self.n_octaves: + self.n_octaves = max_octaves + + def _create_scalespace(self, image): + """Source: "Anatomy of the SIFT Method" Alg. 1 + Construction of the scalespace by gradually blurring (scales) and + downscaling (octaves) the image. + """ + scalespace = [] + if self.upsampling > 1: + image = rescale(image, self.upsampling, order=1) + + # smooth to sigma_min, assuming sigma_in + image = gaussian( + image, + sigma=self.upsampling * math.sqrt(self.sigma_min**2 - self.sigma_in**2), + mode='reflect', + ) + + # Eq. 10: sigmas.shape = (n_octaves, n_scales + 3). + # The three extra scales are: + # One for the differences needed for DoG and two auxiliary + # images (one at either end) for peak_local_max with exclude + # border = True (see Fig. 5) + # The smoothing doubles after n_scales steps. + tmp = np.power(2, np.arange(self.n_scales + 3) / self.n_scales) + tmp *= self.sigma_min + # all sigmas for the gaussian scalespace + sigmas = self.deltas[:, np.newaxis] / self.deltas[0] * tmp[np.newaxis, :] + self.scalespace_sigmas = sigmas + + # Eq. 7: Gaussian smoothing depends on difference with previous sigma + # gaussian_sigmas.shape = (n_octaves, n_scales + 2) + var_diff = np.diff(sigmas * sigmas, axis=1) + gaussian_sigmas = np.sqrt(var_diff) / self.deltas[:, np.newaxis] + + # one octave is represented by a 3D image with depth (n_scales+x) + for o in range(self.n_octaves): + # Temporarily put scales axis first so octave[i] is C-contiguous + # (this makes Gaussian filtering faster). + octave = np.empty( + (self.n_scales + 3,) + image.shape, dtype=self.float_dtype, order='C' + ) + octave[0] = image + for s in range(1, self.n_scales + 3): + # blur new scale assuming sigma of the last one + gaussian( + octave[s - 1], + sigma=gaussian_sigmas[o, s - 1], + mode='reflect', + out=octave[s], + ) + # move scales to last axis as expected by other methods + scalespace.append(np.moveaxis(octave, 0, -1)) + if o < self.n_octaves - 1: + # downscale the image by taking every second pixel + image = octave[self.n_scales][::2, ::2] + return scalespace + + def _inrange(self, a, dim): + return ( + (a[:, 0] > 0) + & (a[:, 0] < dim[0] - 1) + & (a[:, 1] > 0) + & (a[:, 1] < dim[1] - 1) + ) + + def _find_localize_evaluate(self, dogspace, img_shape): + """Source: "Anatomy of the SIFT Method" Alg. 4-9 + 1) first find all extrema of a (3, 3, 3) neighborhood + 2) use second order Taylor development to refine the positions to + sub-pixel precision + 3) filter out extrema that have low contrast and lie on edges or close + to the image borders + """ + extrema_pos = [] + extrema_scales = [] + extrema_sigmas = [] + threshold = self.c_dog * 0.8 + for o, (octave, delta) in enumerate(zip(dogspace, self.deltas)): + # find extrema + keys = _local_max(np.ascontiguousarray(octave), threshold) + if keys.size == 0: + extrema_pos.append(np.empty((0, 2))) + continue + + # localize extrema + oshape = octave.shape + refinement_iterations = 5 + offset_max = 0.6 + for i in range(refinement_iterations): + if i > 0: + # exclude any keys that have moved out of bounds + keys = keys[self._inrange(keys, oshape), :] + + # Jacobian and Hessian of all extrema + grad = _sparse_gradient(octave, keys) + hess = _hessian(octave, keys) + + # solve for offset of the extremum + off = _offsets(grad, hess) + if i == refinement_iterations - 1: + break + # offset is too big and an increase would not bring us out of + # bounds + wrong_position_pos = np.logical_and( + off > offset_max, keys + 1 < tuple([a - 1 for a in oshape]) + ) + wrong_position_neg = np.logical_and(off < -offset_max, keys - 1 > 0) + if not np.any(np.logical_or(wrong_position_neg, wrong_position_pos)): + break + keys[wrong_position_pos] += 1 + keys[wrong_position_neg] -= 1 + + # mask for all extrema that have been localized successfully + finished = np.all(np.abs(off) < offset_max, axis=1) + keys = keys[finished] + off = off[finished] + grad = [g[finished] for g in grad] + + # value of extremum in octave + vals = octave[keys[:, 0], keys[:, 1], keys[:, 2]] + # values at interpolated point + w = vals + for i in range(3): + w += 0.5 * grad[i] * off[:, i] + + h00, h11, h01 = hess[0][finished], hess[1][finished], hess[3][finished] + + sigmaratio = self.scalespace_sigmas[0, 1] / self.scalespace_sigmas[0, 0] + + # filter for contrast, edgeness and borders + contrast_threshold = self.c_dog + contrast_filter = np.abs(w) > contrast_threshold + + edge_threshold = np.square(self.c_edge + 1) / self.c_edge + edge_response = _edgeness( + h00[contrast_filter], h11[contrast_filter], h01[contrast_filter] + ) + edge_filter = np.abs(edge_response) <= edge_threshold + + keys = keys[contrast_filter][edge_filter] + off = off[contrast_filter][edge_filter] + yx = ((keys[:, :2] + off[:, :2]) * delta).astype(self.float_dtype) + + sigmas = self.scalespace_sigmas[o, keys[:, 2]] * np.power( + sigmaratio, off[:, 2] + ) + border_filter = np.all( + np.logical_and( + (yx - sigmas[:, np.newaxis]) > 0.0, + (yx + sigmas[:, np.newaxis]) < img_shape, + ), + axis=1, + ) + extrema_pos.append(yx[border_filter]) + extrema_scales.append(keys[border_filter, 2]) + extrema_sigmas.append(sigmas[border_filter]) + + octave_indices = np.concatenate( + [np.full(len(p), i) for i, p in enumerate(extrema_pos)] + ) + + if len(octave_indices) == 0: + raise RuntimeError( + "SIFT found no features. Try passing in an image containing " + "greater intensity contrasts between adjacent pixels." + ) + + extrema_pos = np.concatenate(extrema_pos) + extrema_scales = np.concatenate(extrema_scales) + extrema_sigmas = np.concatenate(extrema_sigmas) + return extrema_pos, extrema_scales, extrema_sigmas, octave_indices + + def _fit(self, h): + """Refine the position of the peak by fitting it to a parabola""" + return (h[0] - h[2]) / (2 * (h[0] + h[2] - 2 * h[1])) + + def _compute_orientation( + self, positions_oct, scales_oct, sigmas_oct, octaves, gaussian_scalespace + ): + """Source: "Anatomy of the SIFT Method" Alg. 11 + Calculates the orientation of the gradient around every keypoint + """ + gradient_space = [] + # list for keypoints that have more than one reference orientation + keypoint_indices = [] + keypoint_angles = [] + keypoint_octave = [] + orientations = np.zeros_like(sigmas_oct, dtype=self.float_dtype) + key_count = 0 + for o, (octave, delta) in enumerate(zip(gaussian_scalespace, self.deltas)): + gradient_space.append(np.gradient(octave)) + + in_oct = octaves == o + if not np.any(in_oct): + continue + positions = positions_oct[in_oct] + scales = scales_oct[in_oct] + sigmas = sigmas_oct[in_oct] + + oshape = octave.shape[:2] + # convert to octave's dimensions + yx = positions / delta + sigma = sigmas / delta + + # dimensions of the patch + radius = 3 * self.lambda_ori * sigma + p_min = np.maximum(0, yx - radius[:, np.newaxis] + 0.5).astype(int) + p_max = np.minimum( + yx + radius[:, np.newaxis] + 0.5, (oshape[0] - 1, oshape[1] - 1) + ).astype(int) + # orientation histogram + hist = np.empty(self.n_bins, dtype=self.float_dtype) + avg_kernel = np.full((3,), 1 / 3, dtype=self.float_dtype) + for k in range(len(yx)): + hist[:] = 0 + + # use the patch coordinates to get the gradient and then + # normalize them + r, c = np.meshgrid( + np.arange(p_min[k, 0], p_max[k, 0] + 1), + np.arange(p_min[k, 1], p_max[k, 1] + 1), + indexing='ij', + sparse=True, + ) + gradient_row = gradient_space[o][0][r, c, scales[k]] + gradient_col = gradient_space[o][1][r, c, scales[k]] + r = r.astype(self.float_dtype, copy=False) + c = c.astype(self.float_dtype, copy=False) + r -= yx[k, 0] + c -= yx[k, 1] + + # gradient magnitude and angles + magnitude = np.sqrt(np.square(gradient_row) + np.square(gradient_col)) + theta = np.mod(np.arctan2(gradient_col, gradient_row), 2 * np.pi) + + # more weight to center values + kernel = np.exp( + np.divide(r * r + c * c, -2 * (self.lambda_ori * sigma[k]) ** 2) + ) + + # fill the histogram + bins = np.floor( + (theta / (2 * np.pi) * self.n_bins + 0.5) % self.n_bins + ).astype(int) + np.add.at(hist, bins, kernel * magnitude) + + # smooth the histogram and find the maximum + hist = np.concatenate((hist[-6:], hist, hist[:6])) + for _ in range(6): # number of smoothings + hist = np.convolve(hist, avg_kernel, mode='same') + hist = hist[6:-6] + max_filter = ndi.maximum_filter(hist, [3], mode='wrap') + + # if an angle is in 80% percent range of the maximum, a + # new keypoint is created for it + maxima = np.nonzero( + np.logical_and( + hist >= (self.c_max * np.max(hist)), max_filter == hist + ) + ) + + # save the angles + for c, m in enumerate(maxima[0]): + neigh = np.arange(m - 1, m + 2) % len(hist) + # use neighbors to fit a parabola, to get more accurate + # result + ori = (m + self._fit(hist[neigh]) + 0.5) * 2 * np.pi / self.n_bins + if ori > np.pi: + ori -= 2 * np.pi + if c == 0: + orientations[key_count] = ori + else: + keypoint_indices.append(key_count) + keypoint_angles.append(ori) + keypoint_octave.append(o) + key_count += 1 + self.positions = np.concatenate( + (positions_oct, positions_oct[keypoint_indices]) + ) + self.scales = np.concatenate((scales_oct, scales_oct[keypoint_indices])) + self.sigmas = np.concatenate((sigmas_oct, sigmas_oct[keypoint_indices])) + self.orientations = np.concatenate((orientations, keypoint_angles)) + self.octaves = np.concatenate((octaves, keypoint_octave)) + # return the gradient_space to reuse it to find the descriptor + return gradient_space + + def _rotate(self, row, col, angle): + c = math.cos(angle) + s = math.sin(angle) + rot_row = c * row + s * col + rot_col = -s * row + c * col + return rot_row, rot_col + + def _compute_descriptor(self, gradient_space): + """Source: "Anatomy of the SIFT Method" Alg. 12 + Calculates the descriptor for every keypoint + """ + n_key = len(self.scales) + self.descriptors = np.empty( + (n_key, self.n_hist**2 * self.n_ori), dtype=np.uint8 + ) + + # indices of the histograms + hists = np.arange(1, self.n_hist + 1, dtype=self.float_dtype) + # indices of the bins + bins = np.arange(1, self.n_ori + 1, dtype=self.float_dtype) + + key_numbers = np.arange(n_key) + for o, (gradient, delta) in enumerate(zip(gradient_space, self.deltas)): + in_oct = self.octaves == o + if not np.any(in_oct): + continue + positions = self.positions[in_oct] + scales = self.scales[in_oct] + sigmas = self.sigmas[in_oct] + orientations = self.orientations[in_oct] + numbers = key_numbers[in_oct] + + dim = gradient[0].shape[:2] + center_pos = positions / delta + sigma = sigmas / delta + + # dimensions of the patch + radius = self.lambda_descr * (1 + 1 / self.n_hist) * sigma + radius_patch = math.sqrt(2) * radius + p_min = np.asarray( + np.maximum(0, center_pos - radius_patch[:, np.newaxis] + 0.5), dtype=int + ) + p_max = np.asarray( + np.minimum( + center_pos + radius_patch[:, np.newaxis] + 0.5, + (dim[0] - 1, dim[1] - 1), + ), + dtype=int, + ) + + for k in range(len(p_max)): + rad_k = float(radius[k]) + ori = float(orientations[k]) + histograms = np.zeros( + (self.n_hist, self.n_hist, self.n_ori), dtype=self.float_dtype + ) + # the patch + r, c = np.meshgrid( + np.arange(p_min[k, 0], p_max[k, 0]), + np.arange(p_min[k, 1], p_max[k, 1]), + indexing='ij', + sparse=True, + ) + # normalized coordinates + r_norm = np.subtract(r, center_pos[k, 0], dtype=self.float_dtype) + c_norm = np.subtract(c, center_pos[k, 1], dtype=self.float_dtype) + r_norm, c_norm = self._rotate(r_norm, c_norm, ori) + + # select coordinates and gradient values within the patch + inside = np.maximum(np.abs(r_norm), np.abs(c_norm)) < rad_k + r_norm, c_norm = r_norm[inside], c_norm[inside] + r_idx, c_idx = np.nonzero(inside) + r = r[r_idx, 0] + c = c[0, c_idx] + gradient_row = gradient[0][r, c, scales[k]] + gradient_col = gradient[1][r, c, scales[k]] + # compute the (relative) gradient orientation + theta = np.arctan2(gradient_col, gradient_row) - ori + lam_sig = self.lambda_descr * float(sigma[k]) + # Gaussian weighted kernel magnitude + kernel = np.exp((r_norm * r_norm + c_norm * c_norm) / (-2 * lam_sig**2)) + magnitude = ( + np.sqrt(gradient_row * gradient_row + gradient_col * gradient_col) + * kernel + ) + + lam_sig_ratio = 2 * lam_sig / self.n_hist + rc_bins = (hists - (1 + self.n_hist) / 2) * lam_sig_ratio + rc_bin_spacing = lam_sig_ratio + ori_bins = (2 * np.pi * bins) / self.n_ori + + # distances to the histograms and bins + dist_r = np.abs(np.subtract.outer(rc_bins, r_norm)) + dist_c = np.abs(np.subtract.outer(rc_bins, c_norm)) + + # the orientation histograms/bins that get the contribution + near_t, near_t_val = _ori_distances(ori_bins, theta) + + # create the histogram + _update_histogram( + histograms, + near_t, + near_t_val, + magnitude, + dist_r, + dist_c, + rc_bin_spacing, + ) + + # convert the histograms to a 1d descriptor + histograms = histograms.reshape(-1) + # saturate the descriptor + histograms = np.minimum(histograms, 0.2 * np.linalg.norm(histograms)) + # normalize the descriptor + descriptor = (512 * histograms) / np.linalg.norm(histograms) + # quantize the descriptor + descriptor = np.minimum(np.floor(descriptor), 255) + self.descriptors[numbers[k], :] = descriptor + + def _preprocess(self, image): + check_nD(image, 2) + image = img_as_float(image) + self.float_dtype = _supported_float_type(image.dtype) + image = image.astype(self.float_dtype, copy=False) + + self._set_number_of_octaves(image.shape) + return image + + def detect(self, image): + """Detect the keypoints. + + Parameters + ---------- + image : 2D array + Input image. + + """ + image = self._preprocess(image) + + gaussian_scalespace = self._create_scalespace(image) + + dog_scalespace = [np.diff(layer, axis=2) for layer in gaussian_scalespace] + + positions, scales, sigmas, octaves = self._find_localize_evaluate( + dog_scalespace, image.shape + ) + + self._compute_orientation( + positions, scales, sigmas, octaves, gaussian_scalespace + ) + + self.keypoints = self.positions.round().astype(int) + + def extract(self, image): + """Extract the descriptors for all keypoints in the image. + + Parameters + ---------- + image : 2D array + Input image. + + """ + image = self._preprocess(image) + + gaussian_scalespace = self._create_scalespace(image) + + gradient_space = [np.gradient(octave) for octave in gaussian_scalespace] + + self._compute_descriptor(gradient_space) + + def detect_and_extract(self, image): + """Detect the keypoints and extract their descriptors. + + Parameters + ---------- + image : 2D array + Input image. + + """ + image = self._preprocess(image) + + gaussian_scalespace = self._create_scalespace(image) + + dog_scalespace = [np.diff(layer, axis=2) for layer in gaussian_scalespace] + + positions, scales, sigmas, octaves = self._find_localize_evaluate( + dog_scalespace, image.shape + ) + + gradient_space = self._compute_orientation( + positions, scales, sigmas, octaves, gaussian_scalespace + ) + + self._compute_descriptor(gradient_space) + + self.keypoints = self.positions.round().astype(int) diff --git a/lib/python3.10/site-packages/skimage/feature/template.py b/lib/python3.10/site-packages/skimage/feature/template.py new file mode 100644 index 0000000000000000000000000000000000000000..06defe96f96b48d0dea6c66b4974e4f3ef1daedc --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/template.py @@ -0,0 +1,186 @@ +import math + +import numpy as np +from scipy.signal import fftconvolve + +from .._shared.utils import check_nD, _supported_float_type + + +def _window_sum_2d(image, window_shape): + window_sum = np.cumsum(image, axis=0) + window_sum = window_sum[window_shape[0] : -1] - window_sum[: -window_shape[0] - 1] + + window_sum = np.cumsum(window_sum, axis=1) + window_sum = ( + window_sum[:, window_shape[1] : -1] - window_sum[:, : -window_shape[1] - 1] + ) + + return window_sum + + +def _window_sum_3d(image, window_shape): + window_sum = _window_sum_2d(image, window_shape) + + window_sum = np.cumsum(window_sum, axis=2) + window_sum = ( + window_sum[:, :, window_shape[2] : -1] + - window_sum[:, :, : -window_shape[2] - 1] + ) + + return window_sum + + +def match_template( + image, template, pad_input=False, mode='constant', constant_values=0 +): + """Match a template to a 2-D or 3-D image using normalized correlation. + + The output is an array with values between -1.0 and 1.0. The value at a + given position corresponds to the correlation coefficient between the image + and the template. + + For `pad_input=True` matches correspond to the center and otherwise to the + top-left corner of the template. To find the best match you must search for + peaks in the response (output) image. + + Parameters + ---------- + image : (M, N[, P]) array + 2-D or 3-D input image. + template : (m, n[, p]) array + Template to locate. It must be `(m <= M, n <= N[, p <= P])`. + pad_input : bool + If True, pad `image` so that output is the same size as the image, and + output values correspond to the template center. Otherwise, the output + is an array with shape `(M - m + 1, N - n + 1)` for an `(M, N)` image + and an `(m, n)` template, and matches correspond to origin + (top-left corner) of the template. + mode : see `numpy.pad`, optional + Padding mode. + constant_values : see `numpy.pad`, optional + Constant values used in conjunction with ``mode='constant'``. + + Returns + ------- + output : array + Response image with correlation coefficients. + + Notes + ----- + Details on the cross-correlation are presented in [1]_. This implementation + uses FFT convolutions of the image and the template. Reference [2]_ + presents similar derivations but the approximation presented in this + reference is not used in our implementation. + + References + ---------- + .. [1] J. P. Lewis, "Fast Normalized Cross-Correlation", Industrial Light + and Magic. + .. [2] Briechle and Hanebeck, "Template Matching using Fast Normalized + Cross Correlation", Proceedings of the SPIE (2001). + :DOI:`10.1117/12.421129` + + Examples + -------- + >>> template = np.zeros((3, 3)) + >>> template[1, 1] = 1 + >>> template + array([[0., 0., 0.], + [0., 1., 0.], + [0., 0., 0.]]) + >>> image = np.zeros((6, 6)) + >>> image[1, 1] = 1 + >>> image[4, 4] = -1 + >>> image + array([[ 0., 0., 0., 0., 0., 0.], + [ 0., 1., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., -1., 0.], + [ 0., 0., 0., 0., 0., 0.]]) + >>> result = match_template(image, template) + >>> np.round(result, 3) + array([[ 1. , -0.125, 0. , 0. ], + [-0.125, -0.125, 0. , 0. ], + [ 0. , 0. , 0.125, 0.125], + [ 0. , 0. , 0.125, -1. ]]) + >>> result = match_template(image, template, pad_input=True) + >>> np.round(result, 3) + array([[-0.125, -0.125, -0.125, 0. , 0. , 0. ], + [-0.125, 1. , -0.125, 0. , 0. , 0. ], + [-0.125, -0.125, -0.125, 0. , 0. , 0. ], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125], + [ 0. , 0. , 0. , 0.125, -1. , 0.125], + [ 0. , 0. , 0. , 0.125, 0.125, 0.125]]) + """ + check_nD(image, (2, 3)) + + if image.ndim < template.ndim: + raise ValueError( + "Dimensionality of template must be less than or " + "equal to the dimensionality of image." + ) + if np.any(np.less(image.shape, template.shape)): + raise ValueError("Image must be larger than template.") + + image_shape = image.shape + + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + pad_width = tuple((width, width) for width in template.shape) + if mode == 'constant': + image = np.pad( + image, pad_width=pad_width, mode=mode, constant_values=constant_values + ) + else: + image = np.pad(image, pad_width=pad_width, mode=mode) + + # Use special case for 2-D images for much better performance in + # computation of integral images + if image.ndim == 2: + image_window_sum = _window_sum_2d(image, template.shape) + image_window_sum2 = _window_sum_2d(image**2, template.shape) + elif image.ndim == 3: + image_window_sum = _window_sum_3d(image, template.shape) + image_window_sum2 = _window_sum_3d(image**2, template.shape) + + template_mean = template.mean() + template_volume = math.prod(template.shape) + template_ssd = np.sum((template - template_mean) ** 2) + + if image.ndim == 2: + xcorr = fftconvolve(image, template[::-1, ::-1], mode="valid")[1:-1, 1:-1] + elif image.ndim == 3: + xcorr = fftconvolve(image, template[::-1, ::-1, ::-1], mode="valid")[ + 1:-1, 1:-1, 1:-1 + ] + + numerator = xcorr - image_window_sum * template_mean + + denominator = image_window_sum2 + np.multiply(image_window_sum, image_window_sum, out=image_window_sum) + np.divide(image_window_sum, template_volume, out=image_window_sum) + denominator -= image_window_sum + denominator *= template_ssd + np.maximum(denominator, 0, out=denominator) # sqrt of negative number not allowed + np.sqrt(denominator, out=denominator) + + response = np.zeros_like(xcorr, dtype=float_dtype) + + # avoid zero-division + mask = denominator > np.finfo(float_dtype).eps + + response[mask] = numerator[mask] / denominator[mask] + + slices = [] + for i in range(template.ndim): + if pad_input: + d0 = (template.shape[i] - 1) // 2 + d1 = d0 + image_shape[i] + else: + d0 = template.shape[i] - 1 + d1 = d0 + image_shape[i] - template.shape[i] + 1 + slices.append(slice(d0, d1)) + + return response[tuple(slices)] diff --git a/lib/python3.10/site-packages/skimage/feature/texture.py b/lib/python3.10/site-packages/skimage/feature/texture.py new file mode 100644 index 0000000000000000000000000000000000000000..200aca450147045b9a015c67db182c5542756484 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/texture.py @@ -0,0 +1,562 @@ +""" +Methods to characterize image textures. +""" + +import warnings + +import numpy as np + +from .._shared.utils import check_nD +from ..color import gray2rgb +from ..util import img_as_float +from ._texture import _glcm_loop, _local_binary_pattern, _multiblock_lbp + + +def graycomatrix(image, distances, angles, levels=None, symmetric=False, normed=False): + """Calculate the gray-level co-occurrence matrix. + + A gray level co-occurrence matrix is a histogram of co-occurring + grayscale values at a given offset over an image. + + .. versionchanged:: 0.19 + `greymatrix` was renamed to `graymatrix` in 0.19. + + Parameters + ---------- + image : array_like + Integer typed input image. Only positive valued images are supported. + If type is other than uint8, the argument `levels` needs to be set. + distances : array_like + List of pixel pair distance offsets. + angles : array_like + List of pixel pair angles in radians. + levels : int, optional + The input image should contain integers in [0, `levels`-1], + where levels indicate the number of gray-levels counted + (typically 256 for an 8-bit image). This argument is required for + 16-bit images or higher and is typically the maximum of the image. + As the output matrix is at least `levels` x `levels`, it might + be preferable to use binning of the input image rather than + large values for `levels`. + symmetric : bool, optional + If True, the output matrix `P[:, :, d, theta]` is symmetric. This + is accomplished by ignoring the order of value pairs, so both + (i, j) and (j, i) are accumulated when (i, j) is encountered + for a given offset. The default is False. + normed : bool, optional + If True, normalize each matrix `P[:, :, d, theta]` by dividing + by the total number of accumulated co-occurrences for the given + offset. The elements of the resulting matrix sum to 1. The + default is False. + + Returns + ------- + P : 4-D ndarray + The gray-level co-occurrence histogram. The value + `P[i,j,d,theta]` is the number of times that gray-level `j` + occurs at a distance `d` and at an angle `theta` from + gray-level `i`. If `normed` is `False`, the output is of + type uint32, otherwise it is float64. The dimensions are: + levels x levels x number of distances x number of angles. + + References + ---------- + .. [1] M. Hall-Beyer, 2007. GLCM Texture: A Tutorial + https://prism.ucalgary.ca/handle/1880/51900 + DOI:`10.11575/PRISM/33280` + .. [2] R.M. Haralick, K. Shanmugam, and I. Dinstein, "Textural features for + image classification", IEEE Transactions on Systems, Man, and + Cybernetics, vol. SMC-3, no. 6, pp. 610-621, Nov. 1973. + :DOI:`10.1109/TSMC.1973.4309314` + .. [3] M. Nadler and E.P. Smith, Pattern Recognition Engineering, + Wiley-Interscience, 1993. + .. [4] Wikipedia, https://en.wikipedia.org/wiki/Co-occurrence_matrix + + + Examples + -------- + Compute 4 GLCMs using 1-pixel distance and 4 different angles. For example, + an angle of 0 radians refers to the neighboring pixel to the right; + pi/4 radians to the top-right diagonal neighbor; pi/2 radians to the pixel + above, and so forth. + + >>> image = np.array([[0, 0, 1, 1], + ... [0, 0, 1, 1], + ... [0, 2, 2, 2], + ... [2, 2, 3, 3]], dtype=np.uint8) + >>> result = graycomatrix(image, [1], [0, np.pi/4, np.pi/2, 3*np.pi/4], + ... levels=4) + >>> result[:, :, 0, 0] + array([[2, 2, 1, 0], + [0, 2, 0, 0], + [0, 0, 3, 1], + [0, 0, 0, 1]], dtype=uint32) + >>> result[:, :, 0, 1] + array([[1, 1, 3, 0], + [0, 1, 1, 0], + [0, 0, 0, 2], + [0, 0, 0, 0]], dtype=uint32) + >>> result[:, :, 0, 2] + array([[3, 0, 2, 0], + [0, 2, 2, 0], + [0, 0, 1, 2], + [0, 0, 0, 0]], dtype=uint32) + >>> result[:, :, 0, 3] + array([[2, 0, 0, 0], + [1, 1, 2, 0], + [0, 0, 2, 1], + [0, 0, 0, 0]], dtype=uint32) + + """ + check_nD(image, 2) + check_nD(distances, 1, 'distances') + check_nD(angles, 1, 'angles') + + image = np.ascontiguousarray(image) + + image_max = image.max() + + if np.issubdtype(image.dtype, np.floating): + raise ValueError( + "Float images are not supported by graycomatrix. " + "Convert the image to an unsigned integer type." + ) + + # for image type > 8bit, levels must be set. + if image.dtype not in (np.uint8, np.int8) and levels is None: + raise ValueError( + "The levels argument is required for data types " + "other than uint8. The resulting matrix will be at " + "least levels ** 2 in size." + ) + + if np.issubdtype(image.dtype, np.signedinteger) and np.any(image < 0): + raise ValueError("Negative-valued images are not supported.") + + if levels is None: + levels = 256 + + if image_max >= levels: + raise ValueError( + "The maximum grayscale value in the image should be " + "smaller than the number of levels." + ) + + distances = np.ascontiguousarray(distances, dtype=np.float64) + angles = np.ascontiguousarray(angles, dtype=np.float64) + + P = np.zeros( + (levels, levels, len(distances), len(angles)), dtype=np.uint32, order='C' + ) + + # count co-occurences + _glcm_loop(image, distances, angles, levels, P) + + # make each GLMC symmetric + if symmetric: + Pt = np.transpose(P, (1, 0, 2, 3)) + P = P + Pt + + # normalize each GLCM + if normed: + P = P.astype(np.float64) + glcm_sums = np.sum(P, axis=(0, 1), keepdims=True) + glcm_sums[glcm_sums == 0] = 1 + P /= glcm_sums + + return P + + +def graycoprops(P, prop='contrast'): + """Calculate texture properties of a GLCM. + + Compute a feature of a gray level co-occurrence matrix to serve as + a compact summary of the matrix. The properties are computed as + follows: + + - 'contrast': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}(i-j)^2` + - 'dissimilarity': :math:`\\sum_{i,j=0}^{levels-1}P_{i,j}|i-j|` + - 'homogeneity': :math:`\\sum_{i,j=0}^{levels-1}\\frac{P_{i,j}}{1+(i-j)^2}` + - 'ASM': :math:`\\sum_{i,j=0}^{levels-1} P_{i,j}^2` + - 'energy': :math:`\\sqrt{ASM}` + - 'correlation': + .. math:: \\sum_{i,j=0}^{levels-1} P_{i,j}\\left[\\frac{(i-\\mu_i) \\ + (j-\\mu_j)}{\\sqrt{(\\sigma_i^2)(\\sigma_j^2)}}\\right] + - 'mean': :math:`\\sum_{i=0}^{levels-1} i*P_{i}` + - 'variance': :math:`\\sum_{i=0}^{levels-1} P_{i}*(i-mean)^2` + - 'std': :math:`\\sqrt{variance}` + - 'entropy': :math:`\\sum_{i,j=0}^{levels-1} -P_{i,j}*log(P_{i,j})` + + Each GLCM is normalized to have a sum of 1 before the computation of + texture properties. + + .. versionchanged:: 0.19 + `greycoprops` was renamed to `graycoprops` in 0.19. + + Parameters + ---------- + P : ndarray + Input array. `P` is the gray-level co-occurrence histogram + for which to compute the specified property. The value + `P[i,j,d,theta]` is the number of times that gray-level j + occurs at a distance d and at an angle theta from + gray-level i. + prop : {'contrast', 'dissimilarity', 'homogeneity', 'energy', \ + 'correlation', 'ASM', 'mean', 'variance', 'std', 'entropy'}, optional + The property of the GLCM to compute. The default is 'contrast'. + + Returns + ------- + results : 2-D ndarray + 2-dimensional array. `results[d, a]` is the property 'prop' for + the d'th distance and the a'th angle. + + References + ---------- + .. [1] M. Hall-Beyer, 2007. GLCM Texture: A Tutorial v. 1.0 through 3.0. + The GLCM Tutorial Home Page, + https://prism.ucalgary.ca/handle/1880/51900 + DOI:`10.11575/PRISM/33280` + + Examples + -------- + Compute the contrast for GLCMs with distances [1, 2] and angles + [0 degrees, 90 degrees] + + >>> image = np.array([[0, 0, 1, 1], + ... [0, 0, 1, 1], + ... [0, 2, 2, 2], + ... [2, 2, 3, 3]], dtype=np.uint8) + >>> g = graycomatrix(image, [1, 2], [0, np.pi/2], levels=4, + ... normed=True, symmetric=True) + >>> contrast = graycoprops(g, 'contrast') + >>> contrast + array([[0.58333333, 1. ], + [1.25 , 2.75 ]]) + + """ + + def glcm_mean(): + I = np.arange(num_level).reshape((num_level, 1, 1, 1)) + mean = np.sum(I * P, axis=(0, 1)) + return I, mean + + check_nD(P, 4, 'P') + + (num_level, num_level2, num_dist, num_angle) = P.shape + if num_level != num_level2: + raise ValueError('num_level and num_level2 must be equal.') + if num_dist <= 0: + raise ValueError('num_dist must be positive.') + if num_angle <= 0: + raise ValueError('num_angle must be positive.') + + # normalize each GLCM + P = P.astype(np.float64) + glcm_sums = np.sum(P, axis=(0, 1), keepdims=True) + glcm_sums[glcm_sums == 0] = 1 + P /= glcm_sums + + # create weights for specified property + I, J = np.ogrid[0:num_level, 0:num_level] + if prop == 'contrast': + weights = (I - J) ** 2 + elif prop == 'dissimilarity': + weights = np.abs(I - J) + elif prop == 'homogeneity': + weights = 1.0 / (1.0 + (I - J) ** 2) + elif prop in ['ASM', 'energy', 'correlation', 'entropy', 'variance', 'mean', 'std']: + pass + else: + raise ValueError(f'{prop} is an invalid property') + + # compute property for each GLCM + if prop == 'energy': + asm = np.sum(P**2, axis=(0, 1)) + results = np.sqrt(asm) + elif prop == 'ASM': + results = np.sum(P**2, axis=(0, 1)) + elif prop == 'mean': + _, results = glcm_mean() + elif prop == 'variance': + I, mean = glcm_mean() + results = np.sum(P * ((I - mean) ** 2), axis=(0, 1)) + elif prop == 'std': + I, mean = glcm_mean() + var = np.sum(P * ((I - mean) ** 2), axis=(0, 1)) + results = np.sqrt(var) + elif prop == 'entropy': + ln = -np.log(P, where=(P != 0), out=np.zeros_like(P)) + results = np.sum(P * ln, axis=(0, 1)) + + elif prop == 'correlation': + results = np.zeros((num_dist, num_angle), dtype=np.float64) + I = np.array(range(num_level)).reshape((num_level, 1, 1, 1)) + J = np.array(range(num_level)).reshape((1, num_level, 1, 1)) + diff_i = I - np.sum(I * P, axis=(0, 1)) + diff_j = J - np.sum(J * P, axis=(0, 1)) + + std_i = np.sqrt(np.sum(P * (diff_i) ** 2, axis=(0, 1))) + std_j = np.sqrt(np.sum(P * (diff_j) ** 2, axis=(0, 1))) + cov = np.sum(P * (diff_i * diff_j), axis=(0, 1)) + + # handle the special case of standard deviations near zero + mask_0 = std_i < 1e-15 + mask_0[std_j < 1e-15] = True + results[mask_0] = 1 + + # handle the standard case + mask_1 = ~mask_0 + results[mask_1] = cov[mask_1] / (std_i[mask_1] * std_j[mask_1]) + elif prop in ['contrast', 'dissimilarity', 'homogeneity']: + weights = weights.reshape((num_level, num_level, 1, 1)) + results = np.sum(P * weights, axis=(0, 1)) + + return results + + +def local_binary_pattern(image, P, R, method='default'): + """Compute the local binary patterns (LBP) of an image. + + LBP is a visual descriptor often used in texture classification. + + Parameters + ---------- + image : (M, N) array + 2D grayscale image. + P : int + Number of circularly symmetric neighbor set points (quantization of + the angular space). + R : float + Radius of circle (spatial resolution of the operator). + method : str {'default', 'ror', 'uniform', 'nri_uniform', 'var'}, optional + Method to determine the pattern: + + ``default`` + Original local binary pattern which is grayscale invariant but not + rotation invariant. + ``ror`` + Extension of default pattern which is grayscale invariant and + rotation invariant. + ``uniform`` + Uniform pattern which is grayscale invariant and rotation + invariant, offering finer quantization of the angular space. + For details, see [1]_. + ``nri_uniform`` + Variant of uniform pattern which is grayscale invariant but not + rotation invariant. For details, see [2]_ and [3]_. + ``var`` + Variance of local image texture (related to contrast) + which is rotation invariant but not grayscale invariant. + + Returns + ------- + output : (M, N) array + LBP image. + + References + ---------- + .. [1] T. Ojala, M. Pietikainen, T. Maenpaa, "Multiresolution gray-scale + and rotation invariant texture classification with local binary + patterns", IEEE Transactions on Pattern Analysis and Machine + Intelligence, vol. 24, no. 7, pp. 971-987, July 2002 + :DOI:`10.1109/TPAMI.2002.1017623` + .. [2] T. Ahonen, A. Hadid and M. Pietikainen. "Face recognition with + local binary patterns", in Proc. Eighth European Conf. Computer + Vision, Prague, Czech Republic, May 11-14, 2004, pp. 469-481, 2004. + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.214.6851 + :DOI:`10.1007/978-3-540-24670-1_36` + .. [3] T. Ahonen, A. Hadid and M. Pietikainen, "Face Description with + Local Binary Patterns: Application to Face Recognition", + IEEE Transactions on Pattern Analysis and Machine Intelligence, + vol. 28, no. 12, pp. 2037-2041, Dec. 2006 + :DOI:`10.1109/TPAMI.2006.244` + """ + check_nD(image, 2) + + methods = { + 'default': ord('D'), + 'ror': ord('R'), + 'uniform': ord('U'), + 'nri_uniform': ord('N'), + 'var': ord('V'), + } + if np.issubdtype(image.dtype, np.floating): + warnings.warn( + "Applying `local_binary_pattern` to floating-point images may " + "give unexpected results when small numerical differences between " + "adjacent pixels are present. It is recommended to use this " + "function with images of integer dtype." + ) + image = np.ascontiguousarray(image, dtype=np.float64) + output = _local_binary_pattern(image, P, R, methods[method.lower()]) + return output + + +def multiblock_lbp(int_image, r, c, width, height): + """Multi-block local binary pattern (MB-LBP). + + The features are calculated similarly to local binary patterns (LBPs), + (See :py:meth:`local_binary_pattern`) except that summed blocks are + used instead of individual pixel values. + + MB-LBP is an extension of LBP that can be computed on multiple scales + in constant time using the integral image. Nine equally-sized rectangles + are used to compute a feature. For each rectangle, the sum of the pixel + intensities is computed. Comparisons of these sums to that of the central + rectangle determine the feature, similarly to LBP. + + Parameters + ---------- + int_image : (N, M) array + Integral image. + r : int + Row-coordinate of top left corner of a rectangle containing feature. + c : int + Column-coordinate of top left corner of a rectangle containing feature. + width : int + Width of one of the 9 equal rectangles that will be used to compute + a feature. + height : int + Height of one of the 9 equal rectangles that will be used to compute + a feature. + + Returns + ------- + output : int + 8-bit MB-LBP feature descriptor. + + References + ---------- + .. [1] L. Zhang, R. Chu, S. Xiang, S. Liao, S.Z. Li. "Face Detection Based + on Multi-Block LBP Representation", In Proceedings: Advances in + Biometrics, International Conference, ICB 2007, Seoul, Korea. + http://www.cbsr.ia.ac.cn/users/scliao/papers/Zhang-ICB07-MBLBP.pdf + :DOI:`10.1007/978-3-540-74549-5_2` + """ + + int_image = np.ascontiguousarray(int_image, dtype=np.float32) + lbp_code = _multiblock_lbp(int_image, r, c, width, height) + return lbp_code + + +def draw_multiblock_lbp( + image, + r, + c, + width, + height, + lbp_code=0, + color_greater_block=(1, 1, 1), + color_less_block=(0, 0.69, 0.96), + alpha=0.5, +): + """Multi-block local binary pattern visualization. + + Blocks with higher sums are colored with alpha-blended white rectangles, + whereas blocks with lower sums are colored alpha-blended cyan. Colors + and the `alpha` parameter can be changed. + + Parameters + ---------- + image : ndarray of float or uint + Image on which to visualize the pattern. + r : int + Row-coordinate of top left corner of a rectangle containing feature. + c : int + Column-coordinate of top left corner of a rectangle containing feature. + width : int + Width of one of 9 equal rectangles that will be used to compute + a feature. + height : int + Height of one of 9 equal rectangles that will be used to compute + a feature. + lbp_code : int + The descriptor of feature to visualize. If not provided, the + descriptor with 0 value will be used. + color_greater_block : tuple of 3 floats + Floats specifying the color for the block that has greater + intensity value. They should be in the range [0, 1]. + Corresponding values define (R, G, B) values. Default value + is white (1, 1, 1). + color_greater_block : tuple of 3 floats + Floats specifying the color for the block that has greater intensity + value. They should be in the range [0, 1]. Corresponding values define + (R, G, B) values. Default value is cyan (0, 0.69, 0.96). + alpha : float + Value in the range [0, 1] that specifies opacity of visualization. + 1 - fully transparent, 0 - opaque. + + Returns + ------- + output : ndarray of float + Image with MB-LBP visualization. + + References + ---------- + .. [1] L. Zhang, R. Chu, S. Xiang, S. Liao, S.Z. Li. "Face Detection Based + on Multi-Block LBP Representation", In Proceedings: Advances in + Biometrics, International Conference, ICB 2007, Seoul, Korea. + http://www.cbsr.ia.ac.cn/users/scliao/papers/Zhang-ICB07-MBLBP.pdf + :DOI:`10.1007/978-3-540-74549-5_2` + """ + + # Default colors for regions. + # White is for the blocks that are brighter. + # Cyan is for the blocks that has less intensity. + color_greater_block = np.asarray(color_greater_block, dtype=np.float64) + color_less_block = np.asarray(color_less_block, dtype=np.float64) + + # Copy array to avoid the changes to the original one. + output = np.copy(image) + + # As the visualization uses RGB color we need 3 bands. + if len(image.shape) < 3: + output = gray2rgb(image) + + # Colors are specified in floats. + output = img_as_float(output) + + # Offsets of neighbor rectangles relative to central one. + # It has order starting from top left and going clockwise. + neighbor_rect_offsets = ( + (-1, -1), + (-1, 0), + (-1, 1), + (0, 1), + (1, 1), + (1, 0), + (1, -1), + (0, -1), + ) + + # Pre-multiply the offsets with width and height. + neighbor_rect_offsets = np.array(neighbor_rect_offsets) + neighbor_rect_offsets[:, 0] *= height + neighbor_rect_offsets[:, 1] *= width + + # Top-left coordinates of central rectangle. + central_rect_r = r + height + central_rect_c = c + width + + for element_num, offset in enumerate(neighbor_rect_offsets): + offset_r, offset_c = offset + + curr_r = central_rect_r + offset_r + curr_c = central_rect_c + offset_c + + has_greater_value = lbp_code & (1 << (7 - element_num)) + + # Mix-in the visualization colors. + if has_greater_value: + new_value = (1 - alpha) * output[ + curr_r : curr_r + height, curr_c : curr_c + width + ] + alpha * color_greater_block + output[curr_r : curr_r + height, curr_c : curr_c + width] = new_value + else: + new_value = (1 - alpha) * output[ + curr_r : curr_r + height, curr_c : curr_c + width + ] + alpha * color_less_block + output[curr_r : curr_r + height, curr_c : curr_c + width] = new_value + + return output diff --git a/lib/python3.10/site-packages/skimage/feature/util.py b/lib/python3.10/site-packages/skimage/feature/util.py new file mode 100644 index 0000000000000000000000000000000000000000..72e6d292205ffde2ae38bf1c760dbcb32e4933ab --- /dev/null +++ b/lib/python3.10/site-packages/skimage/feature/util.py @@ -0,0 +1,232 @@ +import numpy as np + +from ..util import img_as_float +from .._shared.utils import ( + _supported_float_type, + check_nD, +) + + +class FeatureDetector: + def __init__(self): + self.keypoints_ = np.array([]) + + def detect(self, image): + """Detect keypoints in image. + + Parameters + ---------- + image : 2D array + Input image. + + """ + raise NotImplementedError() + + +class DescriptorExtractor: + def __init__(self): + self.descriptors_ = np.array([]) + + def extract(self, image, keypoints): + """Extract feature descriptors in image for given keypoints. + + Parameters + ---------- + image : 2D array + Input image. + keypoints : (N, 2) array + Keypoint locations as ``(row, col)``. + + """ + raise NotImplementedError() + + +def plot_matched_features( + image0, + image1, + *, + keypoints0, + keypoints1, + matches, + ax, + keypoints_color='k', + matches_color=None, + only_matches=False, + alignment='horizontal', +): + """Plot matched features between two images. + + .. versionadded:: 0.23 + + Parameters + ---------- + image0 : (N, M [, 3]) array + First image. + image1 : (N, M [, 3]) array + Second image. + keypoints0 : (K1, 2) array + First keypoint coordinates as ``(row, col)``. + keypoints1 : (K2, 2) array + Second keypoint coordinates as ``(row, col)``. + matches : (Q, 2) array + Indices of corresponding matches in first and second sets of + descriptors, where `matches[:, 0]` (resp. `matches[:, 1]`) contains + the indices in the first (resp. second) set of descriptors. + ax : matplotlib.axes.Axes + The Axes object where the images and their matched features are drawn. + keypoints_color : matplotlib color, optional + Color for keypoint locations. + matches_color : matplotlib color or sequence thereof, optional + Single color or sequence of colors for each line defined by `matches`, + which connect keypoint matches. See [1]_ for an overview of supported + color formats. By default, colors are picked randomly. + only_matches : bool, optional + Set to True to plot matches only and not the keypoint locations. + alignment : {'horizontal', 'vertical'}, optional + Whether to show the two images side by side (`'horizontal'`), or one above + the other (`'vertical'`). + + References + ---------- + .. [1] https://matplotlib.org/stable/users/explain/colors/colors.html#specifying-colors + + Notes + ----- + To make a sequence of colors passed to `matches_color` work for any number of + `matches`, you can wrap that sequence in :func:`itertools.cycle`. + """ + image0 = img_as_float(image0) + image1 = img_as_float(image1) + + new_shape0 = list(image0.shape) + new_shape1 = list(image1.shape) + + if image0.shape[0] < image1.shape[0]: + new_shape0[0] = image1.shape[0] + elif image0.shape[0] > image1.shape[0]: + new_shape1[0] = image0.shape[0] + + if image0.shape[1] < image1.shape[1]: + new_shape0[1] = image1.shape[1] + elif image0.shape[1] > image1.shape[1]: + new_shape1[1] = image0.shape[1] + + if new_shape0 != image0.shape: + new_image0 = np.zeros(new_shape0, dtype=image0.dtype) + new_image0[: image0.shape[0], : image0.shape[1]] = image0 + image0 = new_image0 + + if new_shape1 != image1.shape: + new_image1 = np.zeros(new_shape1, dtype=image1.dtype) + new_image1[: image1.shape[0], : image1.shape[1]] = image1 + image1 = new_image1 + + offset = np.array(image0.shape) + if alignment == 'horizontal': + image = np.concatenate([image0, image1], axis=1) + offset[0] = 0 + elif alignment == 'vertical': + image = np.concatenate([image0, image1], axis=0) + offset[1] = 0 + else: + mesg = ( + f"`plot_matched_features` accepts either 'horizontal' or 'vertical' for " + f"alignment, but '{alignment}' was given. See " + f"https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.plot_matched_features " + f"for details." + ) + raise ValueError(mesg) + + if not only_matches: + ax.scatter( + keypoints0[:, 1], + keypoints0[:, 0], + facecolors='none', + edgecolors=keypoints_color, + ) + ax.scatter( + keypoints1[:, 1] + offset[1], + keypoints1[:, 0] + offset[0], + facecolors='none', + edgecolors=keypoints_color, + ) + + ax.imshow(image, cmap='gray') + ax.axis((0, image0.shape[1] + offset[1], image0.shape[0] + offset[0], 0)) + + number_of_matches = matches.shape[0] + + from matplotlib.colors import is_color_like + + if matches_color is None: + rng = np.random.default_rng(seed=0) + colors = [rng.random(3) for _ in range(number_of_matches)] + elif is_color_like(matches_color): + colors = [matches_color for _ in range(number_of_matches)] + elif hasattr(matches_color, "__len__") and len(matches_color) == number_of_matches: + # No need to check each color, matplotlib does so for us + colors = matches_color + else: + error_message = ( + '`matches_color` needs to be a single color ' + 'or a sequence of length equal to the number of matches.' + ) + raise ValueError(error_message) + + for i, match in enumerate(matches): + idx0, idx1 = match + ax.plot( + (keypoints0[idx0, 1], keypoints1[idx1, 1] + offset[1]), + (keypoints0[idx0, 0], keypoints1[idx1, 0] + offset[0]), + '-', + color=colors[i], + ) + + +def _prepare_grayscale_input_2D(image): + image = np.squeeze(image) + check_nD(image, 2) + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + return image.astype(float_dtype, copy=False) + + +def _prepare_grayscale_input_nD(image): + image = np.squeeze(image) + check_nD(image, range(2, 6)) + image = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + return image.astype(float_dtype, copy=False) + + +def _mask_border_keypoints(image_shape, keypoints, distance): + """Mask coordinates that are within certain distance from the image border. + + Parameters + ---------- + image_shape : (2,) array_like + Shape of the image as ``(rows, cols)``. + keypoints : (N, 2) array + Keypoint coordinates as ``(rows, cols)``. + distance : int + Image border distance. + + Returns + ------- + mask : (N,) bool array + Mask indicating if pixels are within the image (``True``) or in the + border region of the image (``False``). + + """ + + rows = image_shape[0] + cols = image_shape[1] + + mask = ( + ((distance - 1) < keypoints[:, 0]) + & (keypoints[:, 0] < (rows - distance + 1)) + & ((distance - 1) < keypoints[:, 1]) + & (keypoints[:, 1] < (cols - distance + 1)) + ) + + return mask diff --git a/lib/python3.10/site-packages/skimage/filters/__init__.py b/lib/python3.10/site-packages/skimage/filters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2777d8f618ffbd6bb478157bd61f4a7d655db251 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/__init__.py @@ -0,0 +1,5 @@ +"""Sharpening, edge finding, rank filters, thresholding, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/filters/__init__.pyi b/lib/python3.10/site-packages/skimage/filters/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5c39465fa80f70b0c76a935cfd03678c9beeb64d --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/__init__.pyi @@ -0,0 +1,109 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + "LPIFilter2D", + "apply_hysteresis_threshold", + "butterworth", + "correlate_sparse", + "difference_of_gaussians", + "farid", + "farid_h", + "farid_v", + "filter_inverse", + "filter_forward", + "frangi", + "gabor", + "gabor_kernel", + "gaussian", + "hessian", + "laplace", + "median", + "meijering", + "prewitt", + "prewitt_h", + "prewitt_v", + "rank", + "rank_order", + "roberts", + "roberts_neg_diag", + "roberts_pos_diag", + "sato", + "scharr", + "scharr_h", + "scharr_v", + "sobel", + "sobel_h", + "sobel_v", + "threshold_isodata", + "threshold_li", + "threshold_local", + "threshold_mean", + "threshold_minimum", + "threshold_multiotsu", + "threshold_niblack", + "threshold_otsu", + "threshold_sauvola", + "threshold_triangle", + "threshold_yen", + "try_all_threshold", + "unsharp_mask", + "wiener", + "window", +] + +from . import rank +from ._fft_based import butterworth +from ._gabor import gabor, gabor_kernel +from ._gaussian import difference_of_gaussians, gaussian +from ._median import median +from ._rank_order import rank_order +from ._sparse import correlate_sparse +from ._unsharp_mask import unsharp_mask +from ._window import window +from .edges import ( + farid, + farid_h, + farid_v, + laplace, + prewitt, + prewitt_h, + prewitt_v, + roberts, + roberts_neg_diag, + roberts_pos_diag, + scharr, + scharr_h, + scharr_v, + sobel, + sobel_h, + sobel_v, +) +from .lpi_filter import ( + LPIFilter2D, + filter_inverse, + filter_forward, + wiener, +) +from .ridges import ( + frangi, + hessian, + meijering, + sato, +) +from .thresholding import ( + apply_hysteresis_threshold, + threshold_isodata, + threshold_li, + threshold_local, + threshold_mean, + threshold_minimum, + threshold_multiotsu, + threshold_niblack, + threshold_otsu, + threshold_sauvola, + threshold_triangle, + threshold_yen, + try_all_threshold, +) diff --git a/lib/python3.10/site-packages/skimage/filters/_fft_based.py b/lib/python3.10/site-packages/skimage/filters/_fft_based.py new file mode 100644 index 0000000000000000000000000000000000000000..12af9a44283f6121cb8c3967e005e8a64689d221 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_fft_based.py @@ -0,0 +1,189 @@ +import functools + +import numpy as np +import scipy.fft as fft + +from .._shared.utils import _supported_float_type + + +def _get_nd_butterworth_filter( + shape, factor, order, high_pass, real, dtype=np.float64, squared_butterworth=True +): + """Create a N-dimensional Butterworth mask for an FFT + + Parameters + ---------- + shape : tuple of int + Shape of the n-dimensional FFT and mask. + factor : float + Fraction of mask dimensions where the cutoff should be. + order : float + Controls the slope in the cutoff region. + high_pass : bool + Whether the filter is high pass (low frequencies attenuated) or + low pass (high frequencies are attenuated). + real : bool + Whether the FFT is of a real (True) or complex (False) image + squared_butterworth : bool, optional + When True, the square of the Butterworth filter is used. + + Returns + ------- + wfilt : ndarray + The FFT mask. + + """ + ranges = [] + for i, d in enumerate(shape): + # start and stop ensures center of mask aligns with center of FFT + axis = np.arange(-(d - 1) // 2, (d - 1) // 2 + 1) / (d * factor) + ranges.append(fft.ifftshift(axis**2)) + # for real image FFT, halve the last axis + if real: + limit = d // 2 + 1 + ranges[-1] = ranges[-1][:limit] + # q2 = squared Euclidean distance grid + q2 = functools.reduce(np.add, np.meshgrid(*ranges, indexing="ij", sparse=True)) + q2 = q2.astype(dtype) + q2 = np.power(q2, order) + wfilt = 1 / (1 + q2) + if high_pass: + wfilt *= q2 + if not squared_butterworth: + np.sqrt(wfilt, out=wfilt) + return wfilt + + +def butterworth( + image, + cutoff_frequency_ratio=0.005, + high_pass=True, + order=2.0, + channel_axis=None, + *, + squared_butterworth=True, + npad=0, +): + """Apply a Butterworth filter to enhance high or low frequency features. + + This filter is defined in the Fourier domain. + + Parameters + ---------- + image : (M[, N[, ..., P]][, C]) ndarray + Input image. + cutoff_frequency_ratio : float, optional + Determines the position of the cut-off relative to the shape of the + FFT. Receives a value between [0, 0.5]. + high_pass : bool, optional + Whether to perform a high pass filter. If False, a low pass filter is + performed. + order : float, optional + Order of the filter which affects the slope near the cut-off. Higher + order means steeper slope in frequency space. + channel_axis : int, optional + If there is a channel dimension, provide the index here. If None + (default) then all axes are assumed to be spatial dimensions. + squared_butterworth : bool, optional + When True, the square of a Butterworth filter is used. See notes below + for more details. + npad : int, optional + Pad each edge of the image by `npad` pixels using `numpy.pad`'s + ``mode='edge'`` extension. + + Returns + ------- + result : ndarray + The Butterworth-filtered image. + + Notes + ----- + A band-pass filter can be achieved by combining a high-pass and low-pass + filter. The user can increase `npad` if boundary artifacts are apparent. + + The "Butterworth filter" used in image processing textbooks (e.g. [1]_, + [2]_) is often the square of the traditional Butterworth filters as + described by [3]_, [4]_. The squared version will be used here if + `squared_butterworth` is set to ``True``. The lowpass, squared Butterworth + filter is given by the following expression for the lowpass case: + + .. math:: + H_{low}(f) = \\frac{1}{1 + \\left(\\frac{f}{c f_s}\\right)^{2n}} + + with the highpass case given by + + .. math:: + H_{hi}(f) = 1 - H_{low}(f) + + where :math:`f=\\sqrt{\\sum_{d=0}^{\\mathrm{ndim}} f_{d}^{2}}` is the + absolute value of the spatial frequency, :math:`f_s` is the sampling + frequency, :math:`c` the ``cutoff_frequency_ratio``, and :math:`n` is the + filter `order` [1]_. When ``squared_butterworth=False``, the square root of + the above expressions are used instead. + + Note that ``cutoff_frequency_ratio`` is defined in terms of the sampling + frequency, :math:`f_s`. The FFT spectrum covers the Nyquist range + (:math:`[-f_s/2, f_s/2]`) so ``cutoff_frequency_ratio`` should have a value + between 0 and 0.5. The frequency response (gain) at the cutoff is 0.5 when + ``squared_butterworth`` is true and :math:`1/\\sqrt{2}` when it is false. + + Examples + -------- + Apply a high-pass and low-pass Butterworth filter to a grayscale and + color image respectively: + + >>> from skimage.data import camera, astronaut + >>> from skimage.filters import butterworth + >>> high_pass = butterworth(camera(), 0.07, True, 8) + >>> low_pass = butterworth(astronaut(), 0.01, False, 4, channel_axis=-1) + + References + ---------- + .. [1] Russ, John C., et al. The Image Processing Handbook, 3rd. Ed. + 1999, CRC Press, LLC. + .. [2] Birchfield, Stan. Image Processing and Analysis. 2018. Cengage + Learning. + .. [3] Butterworth, Stephen. "On the theory of filter amplifiers." + Wireless Engineer 7.6 (1930): 536-541. + .. [4] https://en.wikipedia.org/wiki/Butterworth_filter + + """ + if npad < 0: + raise ValueError("npad must be >= 0") + elif npad > 0: + center_slice = tuple(slice(npad, s + npad) for s in image.shape) + image = np.pad(image, npad, mode='edge') + fft_shape = ( + image.shape if channel_axis is None else np.delete(image.shape, channel_axis) + ) + is_real = np.isrealobj(image) + float_dtype = _supported_float_type(image.dtype, allow_complex=True) + if cutoff_frequency_ratio < 0 or cutoff_frequency_ratio > 0.5: + raise ValueError("cutoff_frequency_ratio should be in the range [0, 0.5]") + wfilt = _get_nd_butterworth_filter( + fft_shape, + cutoff_frequency_ratio, + order, + high_pass, + is_real, + float_dtype, + squared_butterworth, + ) + axes = np.arange(image.ndim) + if channel_axis is not None: + axes = np.delete(axes, channel_axis) + abs_channel = channel_axis % image.ndim + post = image.ndim - abs_channel - 1 + sl = (slice(None),) * abs_channel + (np.newaxis,) + (slice(None),) * post + wfilt = wfilt[sl] + if is_real: + butterfilt = fft.irfftn( + wfilt * fft.rfftn(image, axes=axes), s=fft_shape, axes=axes + ) + else: + butterfilt = fft.ifftn( + wfilt * fft.fftn(image, axes=axes), s=fft_shape, axes=axes + ) + if npad > 0: + butterfilt = butterfilt[center_slice] + return butterfilt diff --git a/lib/python3.10/site-packages/skimage/filters/_gabor.py b/lib/python3.10/site-packages/skimage/filters/_gabor.py new file mode 100644 index 0000000000000000000000000000000000000000..f6e035b7fc2c8fd29f7a2d33efb92d92274b5cd7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_gabor.py @@ -0,0 +1,220 @@ +import math + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import _supported_float_type, check_nD + +__all__ = ['gabor_kernel', 'gabor'] + + +def _sigma_prefactor(bandwidth): + b = bandwidth + # See http://www.cs.rug.nl/~imaging/simplecell.html + return 1.0 / np.pi * math.sqrt(math.log(2) / 2.0) * (2.0**b + 1) / (2.0**b - 1) + + +def gabor_kernel( + frequency, + theta=0, + bandwidth=1, + sigma_x=None, + sigma_y=None, + n_stds=3, + offset=0, + dtype=np.complex128, +): + """Return complex 2D Gabor filter kernel. + + Gabor kernel is a Gaussian kernel modulated by a complex harmonic function. + Harmonic function consists of an imaginary sine function and a real + cosine function. Spatial frequency is inversely proportional to the + wavelength of the harmonic and to the standard deviation of a Gaussian + kernel. The bandwidth is also inversely proportional to the standard + deviation. + + Parameters + ---------- + frequency : float + Spatial frequency of the harmonic function. Specified in pixels. + theta : float, optional + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float, optional + The bandwidth captured by the filter. For fixed bandwidth, ``sigma_x`` + and ``sigma_y`` will decrease with increasing frequency. This value is + ignored if ``sigma_x`` and ``sigma_y`` are set by the user. + sigma_x, sigma_y : float, optional + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that ``sigma_x`` controls the *vertical* + direction. + n_stds : scalar, optional + The linear size of the kernel is n_stds (3 by default) standard + deviations + offset : float, optional + Phase offset of harmonic function in radians. + dtype : {np.complex64, np.complex128} + Specifies if the filter is single or double precision complex. + + Returns + ------- + g : complex array + Complex filter kernel. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Gabor_filter + .. [2] https://web.archive.org/web/20180127125930/http://mplab.ucsd.edu/tutorials/gabor.pdf + + Examples + -------- + >>> from skimage.filters import gabor_kernel + >>> from matplotlib import pyplot as plt # doctest: +SKIP + + >>> gk = gabor_kernel(frequency=0.2) + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(gk.real) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + + >>> # more ripples (equivalent to increasing the size of the + >>> # Gaussian spread) + >>> gk = gabor_kernel(frequency=0.2, bandwidth=0.1) + >>> fig, ax = plt.suplots() # doctest: +SKIP + >>> ax.imshow(gk.real) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + """ + if sigma_x is None: + sigma_x = _sigma_prefactor(bandwidth) / frequency + if sigma_y is None: + sigma_y = _sigma_prefactor(bandwidth) / frequency + + if np.dtype(dtype).kind != 'c': + raise ValueError("dtype must be complex") + + ct = math.cos(theta) + st = math.sin(theta) + x0 = math.ceil(max(abs(n_stds * sigma_x * ct), abs(n_stds * sigma_y * st), 1)) + y0 = math.ceil(max(abs(n_stds * sigma_y * ct), abs(n_stds * sigma_x * st), 1)) + y, x = np.meshgrid( + np.arange(-y0, y0 + 1), np.arange(-x0, x0 + 1), indexing='ij', sparse=True + ) + rotx = x * ct + y * st + roty = -x * st + y * ct + + g = np.empty(roty.shape, dtype=dtype) + np.exp( + -0.5 * (rotx**2 / sigma_x**2 + roty**2 / sigma_y**2) + + 1j * (2 * np.pi * frequency * rotx + offset), + out=g, + ) + g *= 1 / (2 * np.pi * sigma_x * sigma_y) + + return g + + +def gabor( + image, + frequency, + theta=0, + bandwidth=1, + sigma_x=None, + sigma_y=None, + n_stds=3, + offset=0, + mode='reflect', + cval=0, +): + """Return real and imaginary responses to Gabor filter. + + The real and imaginary parts of the Gabor filter kernel are applied to the + image and the response is returned as a pair of arrays. + + Gabor filter is a linear filter with a Gaussian kernel which is modulated + by a sinusoidal plane wave. Frequency and orientation representations of + the Gabor filter are similar to those of the human visual system. + Gabor filter banks are commonly used in computer vision and image + processing. They are especially suitable for edge detection and texture + classification. + + Parameters + ---------- + image : 2-D array + Input image. + frequency : float + Spatial frequency of the harmonic function. Specified in pixels. + theta : float, optional + Orientation in radians. If 0, the harmonic is in the x-direction. + bandwidth : float, optional + The bandwidth captured by the filter. For fixed bandwidth, ``sigma_x`` + and ``sigma_y`` will decrease with increasing frequency. This value is + ignored if ``sigma_x`` and ``sigma_y`` are set by the user. + sigma_x, sigma_y : float, optional + Standard deviation in x- and y-directions. These directions apply to + the kernel *before* rotation. If `theta = pi/2`, then the kernel is + rotated 90 degrees so that ``sigma_x`` controls the *vertical* + direction. + n_stds : scalar, optional + The linear size of the kernel is n_stds (3 by default) standard + deviations. + offset : float, optional + Phase offset of harmonic function in radians. + mode : {'constant', 'nearest', 'reflect', 'mirror', 'wrap'}, optional + Mode used to convolve image with a kernel, passed to `ndi.convolve` + cval : scalar, optional + Value to fill past edges of input if ``mode`` of convolution is + 'constant'. The parameter is passed to `ndi.convolve`. + + Returns + ------- + real, imag : arrays + Filtered images using the real and imaginary parts of the Gabor filter + kernel. Images are of the same dimensions as the input one. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Gabor_filter + .. [2] https://web.archive.org/web/20180127125930/http://mplab.ucsd.edu/tutorials/gabor.pdf + + Examples + -------- + >>> from skimage.filters import gabor + >>> from skimage import data + >>> from matplotlib import pyplot as plt # doctest: +SKIP + + >>> image = data.coins() + >>> # detecting edges in a coin image + >>> filt_real, filt_imag = gabor(image, frequency=0.6) + >>> fix, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(filt_real) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + + >>> # less sensitivity to finer details with the lower frequency kernel + >>> filt_real, filt_imag = gabor(image, frequency=0.1) + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(filt_real) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + """ + check_nD(image, 2) + # do not cast integer types to float! + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + kernel_dtype = np.promote_types(image.dtype, np.complex64) + else: + kernel_dtype = np.complex128 + + g = gabor_kernel( + frequency, + theta, + bandwidth, + sigma_x, + sigma_y, + n_stds, + offset, + dtype=kernel_dtype, + ) + + filtered_real = ndi.convolve(image, np.real(g), mode=mode, cval=cval) + filtered_imag = ndi.convolve(image, np.imag(g), mode=mode, cval=cval) + + return filtered_real, filtered_imag diff --git a/lib/python3.10/site-packages/skimage/filters/_gaussian.py b/lib/python3.10/site-packages/skimage/filters/_gaussian.py new file mode 100644 index 0000000000000000000000000000000000000000..193b77dc67e611b23e3d5c33ab30bad19fa527e9 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_gaussian.py @@ -0,0 +1,168 @@ +import numpy as np + +from .._shared.filters import gaussian +from ..util import img_as_float + +__all__ = ['gaussian', 'difference_of_gaussians'] + + +def difference_of_gaussians( + image, + low_sigma, + high_sigma=None, + *, + mode='nearest', + cval=0, + channel_axis=None, + truncate=4.0, +): + """Find features between ``low_sigma`` and ``high_sigma`` in size. + + This function uses the Difference of Gaussians method for applying + band-pass filters to multi-dimensional arrays. The input array is + blurred with two Gaussian kernels of differing sigmas to produce two + intermediate, filtered images. The more-blurred image is then subtracted + from the less-blurred image. The final output image will therefore have + had high-frequency components attenuated by the smaller-sigma Gaussian, and + low frequency components will have been removed due to their presence in + the more-blurred intermediate. + + Parameters + ---------- + image : ndarray + Input array to filter. + low_sigma : scalar or sequence of scalars + Standard deviation(s) for the Gaussian kernel with the smaller sigmas + across all axes. The standard deviations are given for each axis as a + sequence, or as a single number, in which case the single number is + used as the standard deviation value for all axes. + high_sigma : scalar or sequence of scalars, optional (default is None) + Standard deviation(s) for the Gaussian kernel with the larger sigmas + across all axes. The standard deviations are given for each axis as a + sequence, or as a single number, in which case the single number is + used as the standard deviation value for all axes. If None is given + (default), sigmas for all axes are calculated as 1.6 * low_sigma. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The ``mode`` parameter determines how the array borders are + handled, where ``cval`` is the value when mode is equal to + 'constant'. Default is 'nearest'. + cval : scalar, optional + Value to fill past edges of input if ``mode`` is 'constant'. Default + is 0.0 + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + truncate : float, optional (default is 4.0) + Truncate the filter at this many standard deviations. + + Returns + ------- + filtered_image : ndarray + the filtered array. + + See also + -------- + skimage.feature.blob_dog + + Notes + ----- + This function will subtract an array filtered with a Gaussian kernel + with sigmas given by ``high_sigma`` from an array filtered with a + Gaussian kernel with sigmas provided by ``low_sigma``. The values for + ``high_sigma`` must always be greater than or equal to the corresponding + values in ``low_sigma``, or a ``ValueError`` will be raised. + + When ``high_sigma`` is none, the values for ``high_sigma`` will be + calculated as 1.6x the corresponding values in ``low_sigma``. This ratio + was originally proposed by Marr and Hildreth (1980) [1]_ and is commonly + used when approximating the inverted Laplacian of Gaussian, which is used + in edge and blob detection. + + Input image is converted according to the conventions of ``img_as_float``. + + Except for sigma values, all parameters are used for both filters. + + Examples + -------- + Apply a simple Difference of Gaussians filter to a color image: + + >>> from skimage.data import astronaut + >>> from skimage.filters import difference_of_gaussians + >>> filtered_image = difference_of_gaussians(astronaut(), 2, 10, + ... channel_axis=-1) + + Apply a Laplacian of Gaussian filter as approximated by the Difference + of Gaussians filter: + + >>> filtered_image = difference_of_gaussians(astronaut(), 2, + ... channel_axis=-1) + + Apply a Difference of Gaussians filter to a grayscale image using different + sigma values for each axis: + + >>> from skimage.data import camera + >>> filtered_image = difference_of_gaussians(camera(), (2,5), (3,20)) + + References + ---------- + .. [1] Marr, D. and Hildreth, E. Theory of Edge Detection. Proc. R. Soc. + Lond. Series B 207, 187-217 (1980). + https://doi.org/10.1098/rspb.1980.0020 + + """ + image = img_as_float(image) + low_sigma = np.array(low_sigma, dtype='float', ndmin=1) + if high_sigma is None: + high_sigma = low_sigma * 1.6 + else: + high_sigma = np.array(high_sigma, dtype='float', ndmin=1) + + if channel_axis is not None: + spatial_dims = image.ndim - 1 + else: + spatial_dims = image.ndim + + if len(low_sigma) != 1 and len(low_sigma) != spatial_dims: + raise ValueError( + 'low_sigma must have length equal to number of' + ' spatial dimensions of input' + ) + if len(high_sigma) != 1 and len(high_sigma) != spatial_dims: + raise ValueError( + 'high_sigma must have length equal to number of' + ' spatial dimensions of input' + ) + + low_sigma = low_sigma * np.ones(spatial_dims) + high_sigma = high_sigma * np.ones(spatial_dims) + + if any(high_sigma < low_sigma): + raise ValueError( + 'high_sigma must be equal to or larger than' 'low_sigma for all axes' + ) + + im1 = gaussian( + image, + sigma=low_sigma, + mode=mode, + cval=cval, + channel_axis=channel_axis, + truncate=truncate, + preserve_range=False, + ) + + im2 = gaussian( + image, + sigma=high_sigma, + mode=mode, + cval=cval, + channel_axis=channel_axis, + truncate=truncate, + preserve_range=False, + ) + + return im1 - im2 diff --git a/lib/python3.10/site-packages/skimage/filters/_median.py b/lib/python3.10/site-packages/skimage/filters/_median.py new file mode 100644 index 0000000000000000000000000000000000000000..e6d1459ed896e69ed3c2252a472999398e0dbc42 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_median.py @@ -0,0 +1,82 @@ +from warnings import warn + +import numpy as np +from scipy import ndimage as ndi + +from .rank import generic + + +def median( + image, footprint=None, out=None, mode='nearest', cval=0.0, behavior='ndimage' +): + """Return local median of an image. + + Parameters + ---------- + image : array-like + Input image. + footprint : ndarray, optional + If ``behavior=='rank'``, ``footprint`` is a 2-D array of 1's and 0's. + If ``behavior=='ndimage'``, ``footprint`` is a N-D array of 1's and 0's + with the same number of dimension than ``image``. + If None, ``footprint`` will be a N-D array with 3 elements for each + dimension (e.g., vector, square, cube, etc.) + out : ndarray, (same dtype as image), optional + If None, a new array is allocated. + mode : {'reflect', 'constant', 'nearest', 'mirror','‘wrap'}, optional + The mode parameter determines how the array borders are handled, where + ``cval`` is the value when mode is equal to 'constant'. + Default is 'nearest'. + + .. versionadded:: 0.15 + ``mode`` is used when ``behavior='ndimage'``. + cval : scalar, optional + Value to fill past edges of input if mode is 'constant'. Default is 0.0 + + .. versionadded:: 0.15 + ``cval`` was added in 0.15 is used when ``behavior='ndimage'``. + behavior : {'ndimage', 'rank'}, optional + Either to use the old behavior (i.e., < 0.15) or the new behavior. + The old behavior will call the :func:`skimage.filters.rank.median`. + The new behavior will call the :func:`scipy.ndimage.median_filter`. + Default is 'ndimage'. + + .. versionadded:: 0.15 + ``behavior`` is introduced in 0.15 + .. versionchanged:: 0.16 + Default ``behavior`` has been changed from 'rank' to 'ndimage' + + Returns + ------- + out : 2-D array (same dtype as input image) + Output image. + + See also + -------- + skimage.filters.rank.median : Rank-based implementation of the median + filtering offering more flexibility with additional parameters but + dedicated for unsigned integer images. + + Examples + -------- + >>> from skimage import data + >>> from skimage.morphology import disk + >>> from skimage.filters import median + >>> img = data.camera() + >>> med = median(img, disk(5)) + + """ + if behavior == 'rank': + if mode != 'nearest' or not np.isclose(cval, 0.0): + warn( + "Change 'behavior' to 'ndimage' if you want to use the " + "parameters 'mode' or 'cval'. They will be discarded " + "otherwise.", + stacklevel=2, + ) + return generic.median(image, footprint=footprint, out=out) + if footprint is None: + footprint = ndi.generate_binary_structure(image.ndim, image.ndim) + return ndi.median_filter( + image, footprint=footprint, output=out, mode=mode, cval=cval + ) diff --git a/lib/python3.10/site-packages/skimage/filters/_rank_order.py b/lib/python3.10/site-packages/skimage/filters/_rank_order.py new file mode 100644 index 0000000000000000000000000000000000000000..9c4ba888a2454a1dbf854cf07a8c0b9e08c04f70 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_rank_order.py @@ -0,0 +1,57 @@ +""" +_rank_order.py - convert an image of any type to an image of ints whose +pixels have an identical rank order compared to the original image +""" + +import numpy as np + + +def rank_order(image): + """Return an image of the same shape where each pixel is the + index of the pixel value in the ascending order of the unique + values of ``image``, aka the rank-order value. + + Parameters + ---------- + image : ndarray + + Returns + ------- + labels : ndarray of unsigned integers, of shape image.shape + New array where each pixel has the rank-order value of the + corresponding pixel in ``image``. Pixel values are between 0 and + n - 1, where n is the number of distinct unique values in + ``image``. The dtype of this array will be determined by + ``np.min_scalar_type(image.size)``. + original_values : 1-D ndarray + Unique original values of ``image``. This will have the same dtype as + ``image``. + + Examples + -------- + >>> a = np.array([[1, 4, 5], [4, 4, 1], [5, 1, 1]]) + >>> a + array([[1, 4, 5], + [4, 4, 1], + [5, 1, 1]]) + >>> rank_order(a) + (array([[0, 1, 2], + [1, 1, 0], + [2, 0, 0]], dtype=uint8), array([1, 4, 5])) + >>> b = np.array([-1., 2.5, 3.1, 2.5]) + >>> rank_order(b) + (array([0, 1, 2, 1], dtype=uint8), array([-1. , 2.5, 3.1])) + """ + flat_image = image.reshape(-1) + unsigned_dtype = np.min_scalar_type(flat_image.size) + sort_order = flat_image.argsort().astype(unsigned_dtype, copy=False) + flat_image = flat_image[sort_order] + sort_rank = np.zeros_like(sort_order) + is_different = flat_image[:-1] != flat_image[1:] + np.cumsum(is_different, out=sort_rank[1:], dtype=sort_rank.dtype) + original_values = np.zeros((int(sort_rank[-1]) + 1,), image.dtype) + original_values[0] = flat_image[0] + original_values[1:] = flat_image[1:][is_different] + int_image = np.zeros_like(sort_order) + int_image[sort_order] = sort_rank + return (int_image.reshape(image.shape), original_values) diff --git a/lib/python3.10/site-packages/skimage/filters/_sparse.py b/lib/python3.10/site-packages/skimage/filters/_sparse.py new file mode 100644 index 0000000000000000000000000000000000000000..d7831a3fea8e0cce437f34fb36c6d14393be7b42 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_sparse.py @@ -0,0 +1,139 @@ +import numpy as np + +from .._shared.utils import _supported_float_type, _to_np_mode + + +def _validate_window_size(axis_sizes): + """Ensure all sizes in ``axis_sizes`` are odd. + + Parameters + ---------- + axis_sizes : iterable of int + + Raises + ------ + ValueError + If any given axis size is even. + """ + for axis_size in axis_sizes: + if axis_size % 2 == 0: + msg = ( + f'Window size for `threshold_sauvola` or ' + f'`threshold_niblack` must not be even on any dimension. ' + f'Got {axis_sizes}' + ) + raise ValueError(msg) + + +def _get_view(padded, kernel_shape, idx, val): + """Get a view into `padded` that is offset by `idx` and scaled by `val`. + + If `padded` was created by padding the original image by `kernel_shape` as + in correlate_sparse, then the view created here will match the size of the + original image. + """ + sl_shift = tuple( + [ + slice(c, s - (w_ - 1 - c)) + for c, w_, s in zip(idx, kernel_shape, padded.shape) + ] + ) + v = padded[sl_shift] + if val == 1: + return v + return val * v + + +def _correlate_sparse(image, kernel_shape, kernel_indices, kernel_values): + """Perform correlation with a sparse kernel. + + Parameters + ---------- + image : ndarray + The (prepadded) image to be correlated. + kernel_shape : tuple of int + The shape of the sparse filter kernel. + kernel_indices : list of coordinate tuples + The indices of each non-zero kernel entry. + kernel_values : list of float + The kernel values at each location in kernel_indices. + + Returns + ------- + out : ndarray + The filtered image. + + Notes + ----- + This function only returns results for the 'valid' region of the + convolution, and thus `out` will be smaller than `image` by an amount + equal to the kernel size along each axis. + """ + idx, val = kernel_indices[0], kernel_values[0] + # implementation assumes this corner is first in kernel_indices_in_values + if tuple(idx) != (0,) * image.ndim: + raise RuntimeError("Unexpected initial index in kernel_indices") + # make a copy to avoid modifying the input image + out = _get_view(image, kernel_shape, idx, val).copy() + for idx, val in zip(kernel_indices[1:], kernel_values[1:]): + out += _get_view(image, kernel_shape, idx, val) + return out + + +def correlate_sparse(image, kernel, mode='reflect'): + """Compute valid cross-correlation of `padded_array` and `kernel`. + + This function is *fast* when `kernel` is large with many zeros. + + See ``scipy.ndimage.correlate`` for a description of cross-correlation. + + Parameters + ---------- + image : ndarray, dtype float, shape (M, N[, ...], P) + The input array. If mode is 'valid', this array should already be + padded, as a margin of the same shape as kernel will be stripped + off. + kernel : ndarray, dtype float, shape (Q, R[, ...], S) + The kernel to be correlated. Must have the same number of + dimensions as `padded_array`. For high performance, it should + be sparse (few nonzero entries). + mode : string, optional + See `scipy.ndimage.correlate` for valid modes. + Additionally, mode 'valid' is accepted, in which case no padding is + applied and the result is the result for the smaller image for which + the kernel is entirely inside the original data. + + Returns + ------- + result : array of float, shape (M, N[, ...], P) + The result of cross-correlating `image` with `kernel`. If mode + 'valid' is used, the resulting shape is (M-Q+1, N-R+1[, ...], P-S+1). + """ + kernel = np.asarray(kernel) + + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + if mode == 'valid': + padded_image = image + else: + np_mode = _to_np_mode(mode) + _validate_window_size(kernel.shape) + padded_image = np.pad( + image, + [(w // 2, w // 2) for w in kernel.shape], + mode=np_mode, + ) + + # extract the kernel's non-zero indices and corresponding values + indices = np.nonzero(kernel) + values = list(kernel[indices].astype(float_dtype, copy=False)) + indices = list(zip(*indices)) + + # _correlate_sparse requires an index at (0,) * kernel.ndim to be present + corner_index = (0,) * kernel.ndim + if corner_index not in indices: + indices = [corner_index] + indices + values = [0.0] + values + + return _correlate_sparse(padded_image, kernel.shape, indices, values) diff --git a/lib/python3.10/site-packages/skimage/filters/_unsharp_mask.py b/lib/python3.10/site-packages/skimage/filters/_unsharp_mask.py new file mode 100644 index 0000000000000000000000000000000000000000..0ebf2f90f0aaa9587e487098baea538cdf4217e1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_unsharp_mask.py @@ -0,0 +1,141 @@ +import numpy as np + +from ..util.dtype import img_as_float +from .._shared import utils +from .._shared.filters import gaussian + + +def _unsharp_mask_single_channel(image, radius, amount, vrange): + """Single channel implementation of the unsharp masking filter.""" + + blurred = gaussian(image, sigma=radius, mode='reflect') + + result = image + (image - blurred) * amount + if vrange is not None: + return np.clip(result, vrange[0], vrange[1], out=result) + return result + + +def unsharp_mask( + image, radius=1.0, amount=1.0, preserve_range=False, *, channel_axis=None +): + """Unsharp masking filter. + + The sharp details are identified as the difference between the original + image and its blurred version. These details are then scaled, and added + back to the original image. + + Parameters + ---------- + image : (M[, ...][, C]) ndarray + Input image. + radius : scalar or sequence of scalars, optional + If a scalar is given, then its value is used for all dimensions. + If sequence is given, then there must be exactly one radius + for each dimension except the last dimension for multichannel images. + Note that 0 radius means no blurring, and negative values are + not allowed. + amount : scalar, optional + The details will be amplified with this factor. The factor could be 0 + or negative. Typically, it is a small positive number, e.g. 1.0. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of ``img_as_float``. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + output : (M[, ...][, C]) ndarray of float + Image with unsharp mask applied. + + Notes + ----- + Unsharp masking is an image sharpening technique. It is a linear image + operation, and numerically stable, unlike deconvolution which is an + ill-posed problem. Because of this stability, it is often + preferred over deconvolution. + + The main idea is as follows: sharp details are identified as the + difference between the original image and its blurred version. + These details are added back to the original image after a scaling step: + + enhanced image = original + amount * (original - blurred) + + When applying this filter to several color layers independently, + color bleeding may occur. More visually pleasing result can be + achieved by processing only the brightness/lightness/intensity + channel in a suitable color space such as HSV, HSL, YUV, or YCbCr. + + Unsharp masking is described in most introductory digital image + processing books. This implementation is based on [1]_. + + Examples + -------- + >>> array = np.ones(shape=(5,5), dtype=np.uint8)*100 + >>> array[2,2] = 120 + >>> array + array([[100, 100, 100, 100, 100], + [100, 100, 100, 100, 100], + [100, 100, 120, 100, 100], + [100, 100, 100, 100, 100], + [100, 100, 100, 100, 100]], dtype=uint8) + >>> np.around(unsharp_mask(array, radius=0.5, amount=2),2) + array([[0.39, 0.39, 0.39, 0.39, 0.39], + [0.39, 0.39, 0.38, 0.39, 0.39], + [0.39, 0.38, 0.53, 0.38, 0.39], + [0.39, 0.39, 0.38, 0.39, 0.39], + [0.39, 0.39, 0.39, 0.39, 0.39]]) + + >>> array = np.ones(shape=(5,5), dtype=np.int8)*100 + >>> array[2,2] = 127 + >>> np.around(unsharp_mask(array, radius=0.5, amount=2),2) + array([[0.79, 0.79, 0.79, 0.79, 0.79], + [0.79, 0.78, 0.75, 0.78, 0.79], + [0.79, 0.75, 1. , 0.75, 0.79], + [0.79, 0.78, 0.75, 0.78, 0.79], + [0.79, 0.79, 0.79, 0.79, 0.79]]) + + >>> np.around(unsharp_mask(array, radius=0.5, amount=2, preserve_range=True), 2) + array([[100. , 100. , 99.99, 100. , 100. ], + [100. , 99.39, 95.48, 99.39, 100. ], + [ 99.99, 95.48, 147.59, 95.48, 99.99], + [100. , 99.39, 95.48, 99.39, 100. ], + [100. , 100. , 99.99, 100. , 100. ]]) + + + References + ---------- + .. [1] Maria Petrou, Costas Petrou + "Image Processing: The Fundamentals", (2010), ed ii., page 357, + ISBN 13: 9781119994398 :DOI:`10.1002/9781119994398` + .. [2] Wikipedia. Unsharp masking + https://en.wikipedia.org/wiki/Unsharp_masking + + """ + vrange = None # Range for valid values; used for clipping. + float_dtype = utils._supported_float_type(image.dtype) + if preserve_range: + fimg = image.astype(float_dtype, copy=False) + else: + fimg = img_as_float(image).astype(float_dtype, copy=False) + negative = np.any(fimg < 0) + if negative: + vrange = [-1.0, 1.0] + else: + vrange = [0.0, 1.0] + + if channel_axis is not None: + result = np.empty_like(fimg, dtype=float_dtype) + for channel in range(image.shape[channel_axis]): + sl = utils.slice_at_axis(channel, channel_axis) + result[sl] = _unsharp_mask_single_channel(fimg[sl], radius, amount, vrange) + return result + else: + return _unsharp_mask_single_channel(fimg, radius, amount, vrange) diff --git a/lib/python3.10/site-packages/skimage/filters/_window.py b/lib/python3.10/site-packages/skimage/filters/_window.py new file mode 100644 index 0000000000000000000000000000000000000000..edd60c81d0baf74e75f111188abdb36dfaa119e6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/_window.py @@ -0,0 +1,131 @@ +import functools + +import numpy as np +from scipy.signal import get_window + +from .._shared.utils import safe_as_int +from ..transform import warp + + +def window(window_type, shape, warp_kwargs=None): + """Return an n-dimensional window of a given size and dimensionality. + + Parameters + ---------- + window_type : string, float, or tuple + The type of window to be created. Any window type supported by + ``scipy.signal.get_window`` is allowed here. See notes below for a + current list, or the SciPy documentation for the version of SciPy + on your machine. + shape : tuple of int or int + The shape of the window along each axis. If an integer is provided, + a 1D window is generated. + warp_kwargs : dict + Keyword arguments passed to `skimage.transform.warp` (e.g., + ``warp_kwargs={'order':3}`` to change interpolation method). + + Returns + ------- + nd_window : ndarray + A window of the specified ``shape``. ``dtype`` is ``np.float64``. + + Notes + ----- + This function is based on ``scipy.signal.get_window`` and thus can access + all of the window types available to that function + (e.g., ``"hann"``, ``"boxcar"``). Note that certain window types require + parameters that have to be supplied with the window name as a tuple + (e.g., ``("tukey", 0.8)``). If only a float is supplied, it is interpreted + as the beta parameter of the Kaiser window. + + See https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.windows.get_window.html + for more details. + + Note that this function generates a double precision array of the specified + ``shape`` and can thus generate very large arrays that consume a large + amount of available memory. + + The approach taken here to create nD windows is to first calculate the + Euclidean distance from the center of the intended nD window to each + position in the array. That distance is used to sample, with + interpolation, from a 1D window returned from ``scipy.signal.get_window``. + The method of interpolation can be changed with the ``order`` keyword + argument passed to `skimage.transform.warp`. + + Some coordinates in the output window will be outside of the original + signal; these will be filled in with zeros. + + Window types: + - boxcar + - triang + - blackman + - hamming + - hann + - bartlett + - flattop + - parzen + - bohman + - blackmanharris + - nuttall + - barthann + - kaiser (needs beta) + - gaussian (needs standard deviation) + - general_gaussian (needs power, width) + - slepian (needs width) + - dpss (needs normalized half-bandwidth) + - chebwin (needs attenuation) + - exponential (needs decay scale) + - tukey (needs taper fraction) + + Examples + -------- + Return a Hann window with shape (512, 512): + + >>> from skimage.filters import window + >>> w = window('hann', (512, 512)) + + Return a Kaiser window with beta parameter of 16 and shape (256, 256, 35): + + >>> w = window(16, (256, 256, 35)) + + Return a Tukey window with an alpha parameter of 0.8 and shape (100, 300): + + >>> w = window(('tukey', 0.8), (100, 300)) + + References + ---------- + .. [1] Two-dimensional window design, Wikipedia, + https://en.wikipedia.org/wiki/Two_dimensional_window_design + """ + + if np.isscalar(shape): + shape = (safe_as_int(shape),) + else: + shape = tuple(safe_as_int(shape)) + if any(s < 0 for s in shape): + raise ValueError("invalid shape") + + ndim = len(shape) + if ndim <= 0: + raise ValueError("Number of dimensions must be greater than zero") + + max_size = functools.reduce(max, shape) + w = get_window(window_type, max_size, fftbins=False) + w = np.reshape(w, (-1,) + (1,) * (ndim - 1)) + + # Create coords for warping following `ndimage.map_coordinates` convention. + L = [np.arange(s, dtype=np.float32) * (max_size / s) for s in shape] + + center = (max_size / 2) - 0.5 + dist = 0 + for g in np.meshgrid(*L, sparse=True, indexing='ij'): + g -= center + dist = dist + g * g + dist = np.sqrt(dist) + coords = np.zeros((ndim,) + dist.shape, dtype=np.float32) + coords[0] = dist + center + + if warp_kwargs is None: + warp_kwargs = {} + + return warp(w, coords, mode='constant', cval=0.0, **warp_kwargs) diff --git a/lib/python3.10/site-packages/skimage/filters/edges.py b/lib/python3.10/site-packages/skimage/filters/edges.py new file mode 100644 index 0000000000000000000000000000000000000000..050ed5a30d90795cafa7e6c0db04b72872bf9917 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/edges.py @@ -0,0 +1,863 @@ +import numpy as np +from scipy import ndimage as ndi +from scipy.ndimage import binary_erosion, convolve + +from .._shared.utils import _supported_float_type, check_nD +from ..restoration.uft import laplacian +from ..util.dtype import img_as_float + +# n-dimensional filter weights +SOBEL_EDGE = np.array([1, 0, -1]) +SOBEL_SMOOTH = np.array([1, 2, 1]) / 4 +HSOBEL_WEIGHTS = SOBEL_EDGE.reshape((3, 1)) * SOBEL_SMOOTH.reshape((1, 3)) +VSOBEL_WEIGHTS = HSOBEL_WEIGHTS.T + +SCHARR_EDGE = np.array([1, 0, -1]) +SCHARR_SMOOTH = np.array([3, 10, 3]) / 16 +HSCHARR_WEIGHTS = SCHARR_EDGE.reshape((3, 1)) * SCHARR_SMOOTH.reshape((1, 3)) +VSCHARR_WEIGHTS = HSCHARR_WEIGHTS.T + +PREWITT_EDGE = np.array([1, 0, -1]) +PREWITT_SMOOTH = np.full((3,), 1 / 3) +HPREWITT_WEIGHTS = PREWITT_EDGE.reshape((3, 1)) * PREWITT_SMOOTH.reshape((1, 3)) +VPREWITT_WEIGHTS = HPREWITT_WEIGHTS.T + +# 2D-only filter weights +ROBERTS_PD_WEIGHTS = np.array([[1, 0], [0, -1]], dtype=np.float64) +ROBERTS_ND_WEIGHTS = np.array([[0, 1], [-1, 0]], dtype=np.float64) + +# These filter weights can be found in Farid & Simoncelli (2004), +# Table 1 (3rd and 4th row). Additional decimal places were computed +# using the code found at https://www.cs.dartmouth.edu/farid/ +farid_smooth = np.array( + [ + [ + 0.0376593171958126, + 0.249153396177344, + 0.426374573253687, + 0.249153396177344, + 0.0376593171958126, + ] + ] +) +farid_edge = np.array( + [[0.109603762960254, 0.276690988455557, 0, -0.276690988455557, -0.109603762960254]] +) +HFARID_WEIGHTS = farid_edge.T * farid_smooth +VFARID_WEIGHTS = np.copy(HFARID_WEIGHTS.T) + + +def _mask_filter_result(result, mask): + """Return result after masking. + + Input masks are eroded so that mask areas in the original image don't + affect values in the result. + """ + if mask is not None: + erosion_footprint = ndi.generate_binary_structure(mask.ndim, mask.ndim) + mask = binary_erosion(mask, erosion_footprint, border_value=0) + result *= mask + return result + + +def _kernel_shape(ndim, dim): + """Return list of `ndim` 1s except at position `dim`, where value is -1. + + Parameters + ---------- + ndim : int + The number of dimensions of the kernel shape. + dim : int + The axis of the kernel to expand to shape -1. + + Returns + ------- + shape : list of int + The requested shape. + + Examples + -------- + >>> _kernel_shape(2, 0) + [-1, 1] + >>> _kernel_shape(3, 1) + [1, -1, 1] + >>> _kernel_shape(4, -1) + [1, 1, 1, -1] + """ + shape = [ + 1, + ] * ndim + shape[dim] = -1 + return shape + + +def _reshape_nd(arr, ndim, dim): + """Reshape a 1D array to have n dimensions, all singletons but one. + + Parameters + ---------- + arr : array, shape (N,) + Input array + ndim : int + Number of desired dimensions of reshaped array. + dim : int + Which dimension/axis will not be singleton-sized. + + Returns + ------- + arr_reshaped : array, shape ([1, ...], N, [1,...]) + View of `arr` reshaped to the desired shape. + + Examples + -------- + >>> rng = np.random.default_rng() + >>> arr = rng.random(7) + >>> _reshape_nd(arr, 2, 0).shape + (7, 1) + >>> _reshape_nd(arr, 3, 1).shape + (1, 7, 1) + >>> _reshape_nd(arr, 4, -1).shape + (1, 1, 1, 7) + """ + kernel_shape = _kernel_shape(ndim, dim) + return np.reshape(arr, kernel_shape) + + +def _generic_edge_filter( + image, + *, + smooth_weights, + edge_weights=[1, 0, -1], + axis=None, + mode='reflect', + cval=0.0, + mask=None, +): + """Apply a generic, n-dimensional edge filter. + + The filter is computed by applying the edge weights along one dimension + and the smoothing weights along all other dimensions. If no axis is given, + or a tuple of axes is given the filter is computed along all axes in turn, + and the magnitude is computed as the square root of the average square + magnitude of all the axes. + + Parameters + ---------- + image : array + The input image. + smooth_weights : array of float + The smoothing weights for the filter. These are applied to dimensions + orthogonal to the edge axis. + edge_weights : 1D array of float, optional + The weights to compute the edge along the chosen axes. + axis : int or sequence of int, optional + Compute the edge filter along this axis. If not provided, the edge + magnitude is computed. This is defined as:: + + edge_mag = np.sqrt(sum([_generic_edge_filter(image, ..., axis=i)**2 + for i in range(image.ndim)]) / image.ndim) + + The magnitude is also computed if axis is a sequence. + mode : str or sequence of str, optional + The boundary mode for the convolution. See `scipy.ndimage.convolve` + for a description of the modes. This can be either a single boundary + mode or one boundary mode per axis. + cval : float, optional + When `mode` is ``'constant'``, this is the constant used in values + outside the boundary of the image data. + """ + ndim = image.ndim + if axis is None: + axes = list(range(ndim)) + elif np.isscalar(axis): + axes = [axis] + else: + axes = axis + return_magnitude = len(axes) > 1 + + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + output = np.zeros(image.shape, dtype=image.dtype) + + for edge_dim in axes: + kernel = _reshape_nd(edge_weights, ndim, edge_dim) + smooth_axes = list(set(range(ndim)) - {edge_dim}) + for smooth_dim in smooth_axes: + kernel = kernel * _reshape_nd(smooth_weights, ndim, smooth_dim) + ax_output = ndi.convolve(image, kernel, mode=mode) + if return_magnitude: + ax_output *= ax_output + output += ax_output + + if return_magnitude: + output = np.sqrt(output) / np.sqrt(ndim, dtype=output.dtype) + return output + + +def sobel(image, mask=None, *, axis=None, mode='reflect', cval=0.0): + """Find edges in an image using the Sobel filter. + + Parameters + ---------- + image : array + The input image. + mask : array of bool, optional + Clip the output image to this mask. (Values where mask=0 will be set + to 0.) + axis : int or sequence of int, optional + Compute the edge filter along this axis. If not provided, the edge + magnitude is computed. This is defined as:: + + sobel_mag = np.sqrt(sum([sobel(image, axis=i)**2 + for i in range(image.ndim)]) / image.ndim) + + The magnitude is also computed if axis is a sequence. + mode : str or sequence of str, optional + The boundary mode for the convolution. See `scipy.ndimage.convolve` + for a description of the modes. This can be either a single boundary + mode or one boundary mode per axis. + cval : float, optional + When `mode` is ``'constant'``, this is the constant used in values + outside the boundary of the image data. + + Returns + ------- + output : array of float + The Sobel edge map. + + See also + -------- + sobel_h, sobel_v : horizontal and vertical edge detection. + scharr, prewitt, farid, skimage.feature.canny + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + + .. [2] https://en.wikipedia.org/wiki/Sobel_operator + + Examples + -------- + >>> from skimage import data + >>> from skimage import filters + >>> camera = data.camera() + >>> edges = filters.sobel(camera) + """ + output = _generic_edge_filter( + image, smooth_weights=SOBEL_SMOOTH, axis=axis, mode=mode, cval=cval + ) + output = _mask_filter_result(output, mask) + return output + + +def sobel_h(image, mask=None): + """Find the horizontal edges of an image using the Sobel transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Sobel edge map. + + Notes + ----- + We use the following kernel:: + + 1 2 1 + 0 0 0 + -1 -2 -1 + + """ + check_nD(image, 2) + return sobel(image, mask=mask, axis=0) + + +def sobel_v(image, mask=None): + """Find the vertical edges of an image using the Sobel transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Sobel edge map. + + Notes + ----- + We use the following kernel:: + + 1 0 -1 + 2 0 -2 + 1 0 -1 + + """ + check_nD(image, 2) + return sobel(image, mask=mask, axis=1) + + +def scharr(image, mask=None, *, axis=None, mode='reflect', cval=0.0): + """Find the edge magnitude using the Scharr transform. + + Parameters + ---------- + image : array + The input image. + mask : array of bool, optional + Clip the output image to this mask. (Values where mask=0 will be set + to 0.) + axis : int or sequence of int, optional + Compute the edge filter along this axis. If not provided, the edge + magnitude is computed. This is defined as:: + + sch_mag = np.sqrt(sum([scharr(image, axis=i)**2 + for i in range(image.ndim)]) / image.ndim) + + The magnitude is also computed if axis is a sequence. + mode : str or sequence of str, optional + The boundary mode for the convolution. See `scipy.ndimage.convolve` + for a description of the modes. This can be either a single boundary + mode or one boundary mode per axis. + cval : float, optional + When `mode` is ``'constant'``, this is the constant used in values + outside the boundary of the image data. + + Returns + ------- + output : array of float + The Scharr edge map. + + See also + -------- + scharr_h, scharr_v : horizontal and vertical edge detection. + sobel, prewitt, farid, skimage.feature.canny + + Notes + ----- + The Scharr operator has a better rotation invariance than + other edge filters such as the Sobel or the Prewitt operators. + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + + .. [2] https://en.wikipedia.org/wiki/Sobel_operator#Alternative_operators + + Examples + -------- + >>> from skimage import data + >>> from skimage import filters + >>> camera = data.camera() + >>> edges = filters.scharr(camera) + """ + output = _generic_edge_filter( + image, smooth_weights=SCHARR_SMOOTH, axis=axis, mode=mode, cval=cval + ) + output = _mask_filter_result(output, mask) + return output + + +def scharr_h(image, mask=None): + """Find the horizontal edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Scharr edge map. + + Notes + ----- + We use the following kernel:: + + 3 10 3 + 0 0 0 + -3 -10 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + + """ + check_nD(image, 2) + return scharr(image, mask=mask, axis=0) + + +def scharr_v(image, mask=None): + """Find the vertical edges of an image using the Scharr transform. + + Parameters + ---------- + image : 2-D array + Image to process + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Scharr edge map. + + Notes + ----- + We use the following kernel:: + + 3 0 -3 + 10 0 -10 + 3 0 -3 + + References + ---------- + .. [1] D. Kroon, 2009, Short Paper University Twente, Numerical + Optimization of Kernel Based Image Derivatives. + """ + check_nD(image, 2) + return scharr(image, mask=mask, axis=1) + + +def prewitt(image, mask=None, *, axis=None, mode='reflect', cval=0.0): + """Find the edge magnitude using the Prewitt transform. + + Parameters + ---------- + image : array + The input image. + mask : array of bool, optional + Clip the output image to this mask. (Values where mask=0 will be set + to 0.) + axis : int or sequence of int, optional + Compute the edge filter along this axis. If not provided, the edge + magnitude is computed. This is defined as:: + + prw_mag = np.sqrt(sum([prewitt(image, axis=i)**2 + for i in range(image.ndim)]) / image.ndim) + + The magnitude is also computed if axis is a sequence. + mode : str or sequence of str, optional + The boundary mode for the convolution. See `scipy.ndimage.convolve` + for a description of the modes. This can be either a single boundary + mode or one boundary mode per axis. + cval : float, optional + When `mode` is ``'constant'``, this is the constant used in values + outside the boundary of the image data. + + Returns + ------- + output : array of float + The Prewitt edge map. + + See also + -------- + prewitt_h, prewitt_v : horizontal and vertical edge detection. + sobel, scharr, farid, skimage.feature.canny + + Notes + ----- + The edge magnitude depends slightly on edge directions, since the + approximation of the gradient operator by the Prewitt operator is not + completely rotation invariant. For a better rotation invariance, the Scharr + operator should be used. The Sobel operator has a better rotation + invariance than the Prewitt operator, but a worse rotation invariance than + the Scharr operator. + + Examples + -------- + >>> from skimage import data + >>> from skimage import filters + >>> camera = data.camera() + >>> edges = filters.prewitt(camera) + """ + output = _generic_edge_filter( + image, smooth_weights=PREWITT_SMOOTH, axis=axis, mode=mode, cval=cval + ) + output = _mask_filter_result(output, mask) + return output + + +def prewitt_h(image, mask=None): + """Find the horizontal edges of an image using the Prewitt transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Prewitt edge map. + + Notes + ----- + We use the following kernel:: + + 1/3 1/3 1/3 + 0 0 0 + -1/3 -1/3 -1/3 + + """ + check_nD(image, 2) + return prewitt(image, mask=mask, axis=0) + + +def prewitt_v(image, mask=None): + """Find the vertical edges of an image using the Prewitt transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Prewitt edge map. + + Notes + ----- + We use the following kernel:: + + 1/3 0 -1/3 + 1/3 0 -1/3 + 1/3 0 -1/3 + + """ + check_nD(image, 2) + return prewitt(image, mask=mask, axis=1) + + +def roberts(image, mask=None): + """Find the edge magnitude using Roberts' cross operator. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Roberts' Cross edge map. + + See also + -------- + roberts_pos_diag, roberts_neg_diag : diagonal edge detection. + sobel, scharr, prewitt, skimage.feature.canny + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.roberts(camera) + + """ + check_nD(image, 2) + out = np.sqrt( + roberts_pos_diag(image, mask) ** 2 + roberts_neg_diag(image, mask) ** 2 + ) + out /= np.sqrt(2) + return out + + +def roberts_pos_diag(image, mask=None): + """Find the cross edges of an image using Roberts' cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Robert's edge map. + + Notes + ----- + We use the following kernel:: + + 1 0 + 0 -1 + + """ + check_nD(image, 2) + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + result = convolve(image, ROBERTS_PD_WEIGHTS) + return _mask_filter_result(result, mask) + + +def roberts_neg_diag(image, mask=None): + """Find the cross edges of an image using the Roberts' Cross operator. + + The kernel is applied to the input image to produce separate measurements + of the gradient component one orientation. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Robert's edge map. + + Notes + ----- + We use the following kernel:: + + 0 1 + -1 0 + + """ + check_nD(image, 2) + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + result = convolve(image, ROBERTS_ND_WEIGHTS) + return _mask_filter_result(result, mask) + + +def laplace(image, ksize=3, mask=None): + """Find the edges of an image using the Laplace operator. + + Parameters + ---------- + image : ndarray + Image to process. + ksize : int, optional + Define the size of the discrete Laplacian operator such that it + will have a size of (ksize,) * image.ndim. + mask : ndarray, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : ndarray + The Laplace edge map. + + Notes + ----- + The Laplacian operator is generated using the function + skimage.restoration.uft.laplacian(). + + """ + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + # Create the discrete Laplacian operator - We keep only the real part of + # the filter + _, laplace_op = laplacian(image.ndim, (ksize,) * image.ndim) + result = convolve(image, laplace_op) + return _mask_filter_result(result, mask) + + +def farid(image, mask=None, *, axis=None, mode='reflect', cval=0.0): + """Find the edge magnitude using the Farid transform. + + Parameters + ---------- + image : array + The input image. + mask : array of bool, optional + Clip the output image to this mask. (Values where mask=0 will be set + to 0.) + axis : int or sequence of int, optional + Compute the edge filter along this axis. If not provided, the edge + magnitude is computed. This is defined as:: + + farid_mag = np.sqrt(sum([farid(image, axis=i)**2 + for i in range(image.ndim)]) / image.ndim) + + The magnitude is also computed if axis is a sequence. + mode : str or sequence of str, optional + The boundary mode for the convolution. See `scipy.ndimage.convolve` + for a description of the modes. This can be either a single boundary + mode or one boundary mode per axis. + cval : float, optional + When `mode` is ``'constant'``, this is the constant used in values + outside the boundary of the image data. + + Returns + ------- + output : array of float + The Farid edge map. + + See also + -------- + farid_h, farid_v : horizontal and vertical edge detection. + scharr, sobel, prewitt, skimage.feature.canny + + Notes + ----- + Take the square root of the sum of the squares of the horizontal and + vertical derivatives to get a magnitude that is somewhat insensitive to + direction. Similar to the Scharr operator, this operator is designed with + a rotation invariance constraint. + + References + ---------- + .. [1] Farid, H. and Simoncelli, E. P., "Differentiation of discrete + multidimensional signals", IEEE Transactions on Image Processing + 13(4): 496-508, 2004. :DOI:`10.1109/TIP.2004.823819` + .. [2] Wikipedia, "Farid and Simoncelli Derivatives." Available at: + + + Examples + -------- + >>> from skimage import data + >>> camera = data.camera() + >>> from skimage import filters + >>> edges = filters.farid(camera) + """ + output = _generic_edge_filter( + image, + smooth_weights=farid_smooth, + edge_weights=farid_edge, + axis=axis, + mode=mode, + cval=cval, + ) + output = _mask_filter_result(output, mask) + return output + + +def farid_h(image, *, mask=None): + """Find the horizontal edges of an image using the Farid transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Farid edge map. + + Notes + ----- + The kernel was constructed using the 5-tap weights from [1]. + + References + ---------- + .. [1] Farid, H. and Simoncelli, E. P., "Differentiation of discrete + multidimensional signals", IEEE Transactions on Image Processing + 13(4): 496-508, 2004. :DOI:`10.1109/TIP.2004.823819` + .. [2] Farid, H. and Simoncelli, E. P. "Optimally rotation-equivariant + directional derivative kernels", In: 7th International Conference on + Computer Analysis of Images and Patterns, Kiel, Germany. Sep, 1997. + """ + check_nD(image, 2) + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + result = convolve(image, HFARID_WEIGHTS) + return _mask_filter_result(result, mask) + + +def farid_v(image, *, mask=None): + """Find the vertical edges of an image using the Farid transform. + + Parameters + ---------- + image : 2-D array + Image to process. + mask : 2-D array, optional + An optional mask to limit the application to a certain area. + Note that pixels surrounding masked regions are also masked to + prevent masked regions from affecting the result. + + Returns + ------- + output : 2-D array + The Farid edge map. + + Notes + ----- + The kernel was constructed using the 5-tap weights from [1]. + + References + ---------- + .. [1] Farid, H. and Simoncelli, E. P., "Differentiation of discrete + multidimensional signals", IEEE Transactions on Image Processing + 13(4): 496-508, 2004. :DOI:`10.1109/TIP.2004.823819` + """ + check_nD(image, 2) + if image.dtype.kind == 'f': + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + else: + image = img_as_float(image) + result = convolve(image, VFARID_WEIGHTS) + return _mask_filter_result(result, mask) diff --git a/lib/python3.10/site-packages/skimage/filters/lpi_filter.py b/lib/python3.10/site-packages/skimage/filters/lpi_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..6741eef815c8fdb438f846f8cbd9672026a60e9f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/lpi_filter.py @@ -0,0 +1,261 @@ +""" +:author: Stefan van der Walt, 2008 +:license: modified BSD +""" + +import numpy as np +import scipy.fft as fft + +from .._shared.utils import _supported_float_type, check_nD + + +def _min_limit(x, val=np.finfo(float).eps): + mask = np.abs(x) < val + x[mask] = np.sign(x[mask]) * val + + +def _center(x, oshape): + """Return an array of shape ``oshape`` from the center of array ``x``.""" + start = (np.array(x.shape) - np.array(oshape)) // 2 + out = x[tuple(slice(s, s + n) for s, n in zip(start, oshape))] + return out + + +def _pad(data, shape): + """Pad the data to the given shape with zeros. + + Parameters + ---------- + data : 2-d ndarray + Input data + shape : (2,) tuple + + """ + out = np.zeros(shape, dtype=data.dtype) + out[tuple(slice(0, n) for n in data.shape)] = data + return out + + +class LPIFilter2D: + """Linear Position-Invariant Filter (2-dimensional)""" + + def __init__(self, impulse_response, **filter_params): + """ + Parameters + ---------- + impulse_response : callable `f(r, c, **filter_params)` + Function that yields the impulse response. ``r`` and ``c`` are + 1-dimensional vectors that represent row and column positions, in + other words coordinates are (r[0],c[0]),(r[0],c[1]) etc. + `**filter_params` are passed through. + + In other words, ``impulse_response`` would be called like this: + + >>> def impulse_response(r, c, **filter_params): + ... pass + >>> + >>> r = [0,0,0,1,1,1,2,2,2] + >>> c = [0,1,2,0,1,2,0,1,2] + >>> filter_params = {'kw1': 1, 'kw2': 2, 'kw3': 3} + >>> impulse_response(r, c, **filter_params) + + + Examples + -------- + Gaussian filter without normalization of coefficients: + + >>> def filt_func(r, c, sigma=1): + ... return np.exp(-(r**2 + c**2)/(2 * sigma**2)) + >>> filter = LPIFilter2D(filt_func) + + """ + if not callable(impulse_response): + raise ValueError("Impulse response must be a callable.") + + self.impulse_response = impulse_response + self.filter_params = filter_params + self._cache = None + + def _prepare(self, data): + """Calculate filter and data FFT in preparation for filtering.""" + dshape = np.array(data.shape) + even_offset = (dshape % 2 == 0).astype(int) + dshape += even_offset # all filter dimensions must be uneven + oshape = np.array(data.shape) * 2 - 1 + + float_dtype = _supported_float_type(data.dtype) + data = data.astype(float_dtype, copy=False) + + if self._cache is None or np.any(self._cache.shape != oshape): + coords = np.mgrid[ + [ + slice(0 + offset, float(n + offset)) + for (n, offset) in zip(dshape, even_offset) + ] + ] + # this steps over two sets of coordinates, + # not over the coordinates individually + for k, coord in enumerate(coords): + coord -= (dshape[k] - 1) / 2.0 + coords = coords.reshape(2, -1).T # coordinate pairs (r,c) + coords = coords.astype(float_dtype, copy=False) + + f = self.impulse_response( + coords[:, 0], coords[:, 1], **self.filter_params + ).reshape(dshape) + + f = _pad(f, oshape) + F = fft.fftn(f) + self._cache = F + else: + F = self._cache + + data = _pad(data, oshape) + G = fft.fftn(data) + + return F, G + + def __call__(self, data): + """Apply the filter to the given data. + + Parameters + ---------- + data : (M, N) ndarray + + """ + check_nD(data, 2, 'data') + F, G = self._prepare(data) + out = fft.ifftn(F * G) + out = np.abs(_center(out, data.shape)) + return out + + +def filter_forward( + data, impulse_response=None, filter_params=None, predefined_filter=None +): + """Apply the given filter to data. + + Parameters + ---------- + data : (M, N) ndarray + Input data. + impulse_response : callable `f(r, c, **filter_params)` + Impulse response of the filter. See LPIFilter2D.__init__. + filter_params : dict, optional + Additional keyword parameters to the impulse_response function. + + Other Parameters + ---------------- + predefined_filter : LPIFilter2D + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. + + Examples + -------- + + Gaussian filter without normalization: + + >>> def filt_func(r, c, sigma=1): + ... return np.exp(-(r**2 + c**2)/(2 * sigma**2)) + >>> + >>> from skimage import data + >>> filtered = filter_forward(data.coins(), filt_func) + + """ + if filter_params is None: + filter_params = {} + check_nD(data, 2, 'data') + if predefined_filter is None: + predefined_filter = LPIFilter2D(impulse_response, **filter_params) + return predefined_filter(data) + + +def filter_inverse( + data, impulse_response=None, filter_params=None, max_gain=2, predefined_filter=None +): + """Apply the filter in reverse to the given data. + + Parameters + ---------- + data : (M, N) ndarray + Input data. + impulse_response : callable `f(r, c, **filter_params)` + Impulse response of the filter. See :class:`~.LPIFilter2D`. This is a required + argument unless a `predifined_filter` is provided. + filter_params : dict, optional + Additional keyword parameters to the impulse_response function. + max_gain : float, optional + Limit the filter gain. Often, the filter contains zeros, which would + cause the inverse filter to have infinite gain. High gain causes + amplification of artefacts, so a conservative limit is recommended. + + Other Parameters + ---------------- + predefined_filter : LPIFilter2D, optional + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. + + """ + if filter_params is None: + filter_params = {} + + check_nD(data, 2, 'data') + if predefined_filter is None: + filt = LPIFilter2D(impulse_response, **filter_params) + else: + filt = predefined_filter + + F, G = filt._prepare(data) + _min_limit(F, val=np.finfo(F.real.dtype).eps) + + F = 1 / F + mask = np.abs(F) > max_gain + F[mask] = np.sign(F[mask]) * max_gain + + return _center(np.abs(fft.ifftshift(fft.ifftn(G * F))), data.shape) + + +def wiener( + data, impulse_response=None, filter_params=None, K=0.25, predefined_filter=None +): + """Minimum Mean Square Error (Wiener) inverse filter. + + Parameters + ---------- + data : (M, N) ndarray + Input data. + K : float or (M, N) ndarray + Ratio between power spectrum of noise and undegraded + image. + impulse_response : callable `f(r, c, **filter_params)` + Impulse response of the filter. See LPIFilter2D.__init__. + filter_params : dict, optional + Additional keyword parameters to the impulse_response function. + + Other Parameters + ---------------- + predefined_filter : LPIFilter2D + If you need to apply the same filter multiple times over different + images, construct the LPIFilter2D and specify it here. + + """ + if filter_params is None: + filter_params = {} + + check_nD(data, 2, 'data') + + if not isinstance(K, float): + check_nD(K, 2, 'K') + + if predefined_filter is None: + filt = LPIFilter2D(impulse_response, **filter_params) + else: + filt = predefined_filter + + F, G = filt._prepare(data) + _min_limit(F, val=np.finfo(F.real.dtype).eps) + + H_mag_sqr = np.abs(F) ** 2 + F = 1 / F * H_mag_sqr / (H_mag_sqr + K) + + return _center(np.abs(fft.ifftshift(fft.ifftn(G * F))), data.shape) diff --git a/lib/python3.10/site-packages/skimage/filters/ridges.py b/lib/python3.10/site-packages/skimage/filters/ridges.py new file mode 100644 index 0000000000000000000000000000000000000000..b163e926670af1561fb052b9c351b60fd4b1b1a9 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/ridges.py @@ -0,0 +1,393 @@ +""" +Ridge filters. + +Ridge filters can be used to detect continuous edges, such as vessels, +neurites, wrinkles, rivers, and other tube-like structures. The present +class of ridge filters relies on the eigenvalues of the Hessian matrix of +image intensities to detect tube-like structures where the intensity changes +perpendicular but not along the structure. +""" + +from warnings import warn + +import numpy as np +from scipy import linalg + +from .._shared.utils import _supported_float_type, check_nD +from ..feature.corner import hessian_matrix, hessian_matrix_eigvals + + +def meijering( + image, sigmas=range(1, 10, 2), alpha=None, black_ridges=True, mode='reflect', cval=0 +): + """ + Filter an image with the Meijering neuriteness filter. + + This filter can be used to detect continuous ridges, e.g. neurites, + wrinkles, rivers. It can be used to calculate the fraction of the + whole image containing such objects. + + Calculates the eigenvalues of the Hessian to compute the similarity of + an image region to neurites, according to the method described in [1]_. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Array with input image data. + sigmas : iterable of floats, optional + Sigmas used as scales of filter + alpha : float, optional + Shaping filter constant, that selects maximally flat elongated + features. The default, None, selects the optimal value -1/(ndim+1). + black_ridges : boolean, optional + When True (the default), the filter detects black ridges; when + False, it detects white ridges. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + out : (M, N[, ...]) ndarray + Filtered image (maximum of pixels across all scales). + + See also + -------- + sato + frangi + hessian + + References + ---------- + .. [1] Meijering, E., Jacob, M., Sarria, J. C., Steiner, P., Hirling, H., + Unser, M. (2004). Design and validation of a tool for neurite tracing + and analysis in fluorescence microscopy images. Cytometry Part A, + 58(2), 167-176. + :DOI:`10.1002/cyto.a.20022` + """ + + image = image.astype(_supported_float_type(image.dtype), copy=False) + if not black_ridges: # Normalize to black ridges. + image = -image + + if alpha is None: + alpha = 1 / (image.ndim + 1) + mtx = linalg.circulant([1, *[alpha] * (image.ndim - 1)]).astype(image.dtype) + + # Generate empty array for storing maximum value + # from different (sigma) scales + filtered_max = np.zeros_like(image) + for sigma in sigmas: # Filter for all sigmas. + eigvals = hessian_matrix_eigvals( + hessian_matrix( + image, sigma, mode=mode, cval=cval, use_gaussian_derivatives=True + ) + ) + # Compute normalized eigenvalues l_i = e_i + sum_{j!=i} alpha * e_j. + vals = np.tensordot(mtx, eigvals, 1) + # Get largest normalized eigenvalue (by magnitude) at each pixel. + vals = np.take_along_axis(vals, abs(vals).argmax(0)[None], 0).squeeze(0) + # Remove negative values. + vals = np.maximum(vals, 0) + # Normalize to max = 1 (unless everything is already zero). + max_val = vals.max() + if max_val > 0: + vals /= max_val + filtered_max = np.maximum(filtered_max, vals) + + return filtered_max # Return pixel-wise max over all sigmas. + + +def sato(image, sigmas=range(1, 10, 2), black_ridges=True, mode='reflect', cval=0): + """ + Filter an image with the Sato tubeness filter. + + This filter can be used to detect continuous ridges, e.g. tubes, + wrinkles, rivers. It can be used to calculate the fraction of the + whole image containing such objects. + + Defined only for 2-D and 3-D images. Calculates the eigenvalues of the + Hessian to compute the similarity of an image region to tubes, according to + the method described in [1]_. + + Parameters + ---------- + image : (M, N[, P]) ndarray + Array with input image data. + sigmas : iterable of floats, optional + Sigmas used as scales of filter. + black_ridges : boolean, optional + When True (the default), the filter detects black ridges; when + False, it detects white ridges. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + out : (M, N[, P]) ndarray + Filtered image (maximum of pixels across all scales). + + See also + -------- + meijering + frangi + hessian + + References + ---------- + .. [1] Sato, Y., Nakajima, S., Shiraga, N., Atsumi, H., Yoshida, S., + Koller, T., ..., Kikinis, R. (1998). Three-dimensional multi-scale line + filter for segmentation and visualization of curvilinear structures in + medical images. Medical image analysis, 2(2), 143-168. + :DOI:`10.1016/S1361-8415(98)80009-1` + """ + + check_nD(image, [2, 3]) # Check image dimensions. + image = image.astype(_supported_float_type(image.dtype), copy=False) + if not black_ridges: # Normalize to black ridges. + image = -image + + # Generate empty array for storing maximum value + # from different (sigma) scales + filtered_max = np.zeros_like(image) + for sigma in sigmas: # Filter for all sigmas. + eigvals = hessian_matrix_eigvals( + hessian_matrix( + image, sigma, mode=mode, cval=cval, use_gaussian_derivatives=True + ) + ) + # Compute normalized tubeness (eqs. (9) and (22), ref. [1]_) as the + # geometric mean of eigvals other than the lowest one + # (hessian_matrix_eigvals returns eigvals in decreasing order), clipped + # to 0, multiplied by sigma^2. + eigvals = eigvals[:-1] + vals = sigma**2 * np.prod(np.maximum(eigvals, 0), 0) ** (1 / len(eigvals)) + filtered_max = np.maximum(filtered_max, vals) + return filtered_max # Return pixel-wise max over all sigmas. + + +def frangi( + image, + sigmas=range(1, 10, 2), + scale_range=None, + scale_step=None, + alpha=0.5, + beta=0.5, + gamma=None, + black_ridges=True, + mode='reflect', + cval=0, +): + """ + Filter an image with the Frangi vesselness filter. + + This filter can be used to detect continuous ridges, e.g. vessels, + wrinkles, rivers. It can be used to calculate the fraction of the + whole image containing such objects. + + Defined only for 2-D and 3-D images. Calculates the eigenvalues of the + Hessian to compute the similarity of an image region to vessels, according + to the method described in [1]_. + + Parameters + ---------- + image : (M, N[, P]) ndarray + Array with input image data. + sigmas : iterable of floats, optional + Sigmas used as scales of filter, i.e., + np.arange(scale_range[0], scale_range[1], scale_step) + scale_range : 2-tuple of floats, optional + The range of sigmas used. + scale_step : float, optional + Step size between sigmas. + alpha : float, optional + Frangi correction constant that adjusts the filter's + sensitivity to deviation from a plate-like structure. + beta : float, optional + Frangi correction constant that adjusts the filter's + sensitivity to deviation from a blob-like structure. + gamma : float, optional + Frangi correction constant that adjusts the filter's + sensitivity to areas of high variance/texture/structure. + The default, None, uses half of the maximum Hessian norm. + black_ridges : boolean, optional + When True (the default), the filter detects black ridges; when + False, it detects white ridges. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + out : (M, N[, P]) ndarray + Filtered image (maximum of pixels across all scales). + + Notes + ----- + Earlier versions of this filter were implemented by Marc Schrijver, + (November 2001), D. J. Kroon, University of Twente (May 2009) [2]_, and + D. G. Ellis (January 2017) [3]_. + + See also + -------- + meijering + sato + hessian + + References + ---------- + .. [1] Frangi, A. F., Niessen, W. J., Vincken, K. L., & Viergever, M. A. + (1998,). Multiscale vessel enhancement filtering. In International + Conference on Medical Image Computing and Computer-Assisted + Intervention (pp. 130-137). Springer Berlin Heidelberg. + :DOI:`10.1007/BFb0056195` + .. [2] Kroon, D. J.: Hessian based Frangi vesselness filter. + .. [3] Ellis, D. G.: https://github.com/ellisdg/frangi3d/tree/master/frangi + """ + if scale_range is not None and scale_step is not None: + warn( + 'Use keyword parameter `sigmas` instead of `scale_range` and ' + '`scale_range` which will be removed in version 0.17.', + stacklevel=2, + ) + sigmas = np.arange(scale_range[0], scale_range[1], scale_step) + + check_nD(image, [2, 3]) # Check image dimensions. + image = image.astype(_supported_float_type(image.dtype), copy=False) + if not black_ridges: # Normalize to black ridges. + image = -image + + # Generate empty array for storing maximum value + # from different (sigma) scales + filtered_max = np.zeros_like(image) + for sigma in sigmas: # Filter for all sigmas. + eigvals = hessian_matrix_eigvals( + hessian_matrix( + image, sigma, mode=mode, cval=cval, use_gaussian_derivatives=True + ) + ) + # Sort eigenvalues by magnitude. + eigvals = np.take_along_axis(eigvals, abs(eigvals).argsort(0), 0) + lambda1 = eigvals[0] + if image.ndim == 2: + (lambda2,) = np.maximum(eigvals[1:], 1e-10) + r_a = np.inf # implied by eq. (15). + r_b = abs(lambda1) / lambda2 # eq. (15). + else: # ndim == 3 + lambda2, lambda3 = np.maximum(eigvals[1:], 1e-10) + r_a = lambda2 / lambda3 # eq. (11). + r_b = abs(lambda1) / np.sqrt(lambda2 * lambda3) # eq. (10). + s = np.sqrt((eigvals**2).sum(0)) # eq. (12). + if gamma is None: + gamma = s.max() / 2 + if gamma == 0: + gamma = 1 # If s == 0 everywhere, gamma doesn't matter. + # Filtered image, eq. (13) and (15). Our implementation relies on the + # blobness exponential factor underflowing to zero whenever the second + # or third eigenvalues are negative (we clip them to 1e-10, to make r_b + # very large). + vals = 1.0 - np.exp( + -(r_a**2) / (2 * alpha**2), dtype=image.dtype + ) # plate sensitivity + vals *= np.exp(-(r_b**2) / (2 * beta**2), dtype=image.dtype) # blobness + vals *= 1.0 - np.exp( + -(s**2) / (2 * gamma**2), dtype=image.dtype + ) # structuredness + filtered_max = np.maximum(filtered_max, vals) + return filtered_max # Return pixel-wise max over all sigmas. + + +def hessian( + image, + sigmas=range(1, 10, 2), + scale_range=None, + scale_step=None, + alpha=0.5, + beta=0.5, + gamma=15, + black_ridges=True, + mode='reflect', + cval=0, +): + """Filter an image with the Hybrid Hessian filter. + + This filter can be used to detect continuous edges, e.g. vessels, + wrinkles, rivers. It can be used to calculate the fraction of the whole + image containing such objects. + + Defined only for 2-D and 3-D images. Almost equal to Frangi filter, but + uses alternative method of smoothing. Refer to [1]_ to find the differences + between Frangi and Hessian filters. + + Parameters + ---------- + image : (M, N[, P]) ndarray + Array with input image data. + sigmas : iterable of floats, optional + Sigmas used as scales of filter, i.e., + np.arange(scale_range[0], scale_range[1], scale_step) + scale_range : 2-tuple of floats, optional + The range of sigmas used. + scale_step : float, optional + Step size between sigmas. + beta : float, optional + Frangi correction constant that adjusts the filter's + sensitivity to deviation from a blob-like structure. + gamma : float, optional + Frangi correction constant that adjusts the filter's + sensitivity to areas of high variance/texture/structure. + black_ridges : boolean, optional + When True (the default), the filter detects black ridges; when + False, it detects white ridges. + mode : {'constant', 'reflect', 'wrap', 'nearest', 'mirror'}, optional + How to handle values outside the image borders. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + + Returns + ------- + out : (M, N[, P]) ndarray + Filtered image (maximum of pixels across all scales). + + Notes + ----- + Written by Marc Schrijver (November 2001) + Re-Written by D. J. Kroon University of Twente (May 2009) [2]_ + + See also + -------- + meijering + sato + frangi + + References + ---------- + .. [1] Ng, C. C., Yap, M. H., Costen, N., & Li, B. (2014,). Automatic + wrinkle detection using hybrid Hessian filter. In Asian Conference on + Computer Vision (pp. 609-622). Springer International Publishing. + :DOI:`10.1007/978-3-319-16811-1_40` + .. [2] Kroon, D. J.: Hessian based Frangi vesselness filter. + """ + filtered = frangi( + image, + sigmas=sigmas, + scale_range=scale_range, + scale_step=scale_step, + alpha=alpha, + beta=beta, + gamma=gamma, + black_ridges=black_ridges, + mode=mode, + cval=cval, + ) + + filtered[filtered <= 0] = 1 + return filtered diff --git a/lib/python3.10/site-packages/skimage/filters/thresholding.py b/lib/python3.10/site-packages/skimage/filters/thresholding.py new file mode 100644 index 0000000000000000000000000000000000000000..a9914af851c07f16d2387ad398d738f0a6a5291f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/filters/thresholding.py @@ -0,0 +1,1339 @@ +import inspect +import itertools +import math +from collections import OrderedDict +from collections.abc import Iterable + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type, warn +from .._shared.version_requirements import require +from ..exposure import histogram +from ..filters._multiotsu import ( + _get_multiotsu_thresh_indices, + _get_multiotsu_thresh_indices_lut, +) +from ..transform import integral_image +from ..util import dtype_limits +from ._sparse import _correlate_sparse, _validate_window_size + +__all__ = [ + 'try_all_threshold', + 'threshold_otsu', + 'threshold_yen', + 'threshold_isodata', + 'threshold_li', + 'threshold_local', + 'threshold_minimum', + 'threshold_mean', + 'threshold_niblack', + 'threshold_sauvola', + 'threshold_triangle', + 'apply_hysteresis_threshold', + 'threshold_multiotsu', +] + + +def _try_all(image, methods=None, figsize=None, num_cols=2, verbose=True): + """Returns a figure comparing the outputs of different methods. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + methods : dict, optional + Names and associated functions. + Functions must take and return an image. + figsize : tuple, optional + Figure size (in inches). + num_cols : int, optional + Number of columns. + verbose : bool, optional + Print function name for each method. + + Returns + ------- + fig, ax : tuple + Matplotlib figure and axes. + """ + from matplotlib import pyplot as plt + + # Compute the image histogram for better performances + nbins = 256 # Default in threshold functions + hist = histogram(image.reshape(-1), nbins, source_range='image') + + # Handle default value + methods = methods or {} + + num_rows = math.ceil((len(methods) + 1.0) / num_cols) + fig, ax = plt.subplots( + num_rows, num_cols, figsize=figsize, sharex=True, sharey=True + ) + ax = ax.reshape(-1) + + ax[0].imshow(image, cmap=plt.cm.gray) + ax[0].set_title('Original') + + i = 1 + for name, func in methods.items(): + # Use precomputed histogram for supporting functions + sig = inspect.signature(func) + _kwargs = dict(hist=hist) if 'hist' in sig.parameters else {} + + ax[i].set_title(name) + try: + ax[i].imshow(func(image, **_kwargs), cmap=plt.cm.gray) + except Exception as e: + ax[i].text( + 0.5, + 0.5, + f"{type(e).__name__}", + ha="center", + va="center", + transform=ax[i].transAxes, + ) + i += 1 + if verbose: + print(func.__orifunc__) + + for a in ax: + a.axis('off') + + fig.tight_layout() + return fig, ax + + +@require("matplotlib", ">=3.3") +def try_all_threshold(image, figsize=(8, 5), verbose=True): + """Returns a figure comparing the outputs of different thresholding methods. + + Parameters + ---------- + image : (M, N) ndarray + Input image. + figsize : tuple, optional + Figure size (in inches). + verbose : bool, optional + Print function name for each method. + + Returns + ------- + fig, ax : tuple + Matplotlib figure and axes. + + Notes + ----- + The following algorithms are used: + + * isodata + * li + * mean + * minimum + * otsu + * triangle + * yen + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('matplotlib') + + >>> from skimage.data import text + >>> fig, ax = try_all_threshold(text(), figsize=(10, 6), verbose=False) + """ + + def thresh(func): + """ + A wrapper function to return a thresholded image. + """ + + def wrapper(im): + return im > func(im) + + try: + wrapper.__orifunc__ = func.__orifunc__ + except AttributeError: + wrapper.__orifunc__ = func.__module__ + '.' + func.__name__ + return wrapper + + # Global algorithms. + methods = OrderedDict( + { + 'Isodata': thresh(threshold_isodata), + 'Li': thresh(threshold_li), + 'Mean': thresh(threshold_mean), + 'Minimum': thresh(threshold_minimum), + 'Otsu': thresh(threshold_otsu), + 'Triangle': thresh(threshold_triangle), + 'Yen': thresh(threshold_yen), + } + ) + + return _try_all(image, figsize=figsize, methods=methods, verbose=verbose) + + +def threshold_local( + image, block_size=3, method='gaussian', offset=0, mode='reflect', param=None, cval=0 +): + """Compute a threshold mask image based on local pixel neighborhood. + + Also known as adaptive or dynamic thresholding. The threshold value is + the weighted mean for the local neighborhood of a pixel subtracted by a + constant. Alternatively the threshold can be determined dynamically by a + given function, using the 'generic' method. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + block_size : int or sequence of int + Odd size of pixel neighborhood which is used to calculate the + threshold value (e.g. 3, 5, 7, ..., 21, ...). + method : {'generic', 'gaussian', 'mean', 'median'}, optional + Method used to determine adaptive threshold for local neighborhood in + weighted mean image. + + * 'generic': use custom function (see ``param`` parameter) + * 'gaussian': apply gaussian filter (see ``param`` parameter for custom\ + sigma value) + * 'mean': apply arithmetic mean filter + * 'median': apply median rank filter + + By default, the 'gaussian' method is used. + offset : float, optional + Constant subtracted from weighted mean of neighborhood to calculate + the local threshold value. Default offset is 0. + mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + Default is 'reflect'. + param : {int, function}, optional + Either specify sigma for 'gaussian' method or function object for + 'generic' method. This functions takes the flat array of local + neighborhood as a single argument and returns the calculated + threshold for the centre pixel. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + + Returns + ------- + threshold : (M, N[, ...]) ndarray + Threshold image. All pixels in the input image higher than the + corresponding pixel in the threshold image are considered foreground. + + References + ---------- + .. [1] Gonzalez, R. C. and Wood, R. E. "Digital Image Processing + (2nd Edition)." Prentice-Hall Inc., 2002: 600--612. + ISBN: 0-201-18075-8 + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera()[:50, :50] + >>> binary_image1 = image > threshold_local(image, 15, 'mean') + >>> func = lambda arr: arr.mean() + >>> binary_image2 = image > threshold_local(image, 15, 'generic', + ... param=func) + + """ + + if np.isscalar(block_size): + block_size = (block_size,) * image.ndim + elif len(block_size) != image.ndim: + raise ValueError("len(block_size) must equal image.ndim.") + block_size = tuple(block_size) + if any(b % 2 == 0 for b in block_size): + raise ValueError( + f'block_size must be odd! Given block_size ' + f'{block_size} contains even values.' + ) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + thresh_image = np.zeros(image.shape, dtype=float_dtype) + if method == 'generic': + ndi.generic_filter( + image, param, block_size, output=thresh_image, mode=mode, cval=cval + ) + elif method == 'gaussian': + if param is None: + # automatically determine sigma which covers > 99% of distribution + sigma = tuple([(b - 1) / 6.0 for b in block_size]) + else: + sigma = param + gaussian(image, sigma=sigma, out=thresh_image, mode=mode, cval=cval) + elif method == 'mean': + ndi.uniform_filter(image, block_size, output=thresh_image, mode=mode, cval=cval) + elif method == 'median': + ndi.median_filter(image, block_size, output=thresh_image, mode=mode, cval=cval) + else: + raise ValueError( + "Invalid method specified. Please use `generic`, " + "`gaussian`, `mean`, or `median`." + ) + + return thresh_image - offset + + +def _validate_image_histogram(image, hist, nbins=None, normalize=False): + """Ensure that either image or hist were given, return valid histogram. + + If hist is given, image is ignored. + + Parameters + ---------- + image : array or None + Grayscale image. + hist : array, 2-tuple of array, or None + Histogram, either a 1D counts array, or an array of counts together + with an array of bin centers. + nbins : int, optional + The number of bins with which to compute the histogram, if `hist` is + None. + normalize : bool + If hist is not given, it will be computed by this function. This + parameter determines whether the computed histogram is normalized + (i.e. entries sum up to 1) or not. + + Returns + ------- + counts : 1D array of float + Each element is the number of pixels falling in each intensity bin. + bin_centers : 1D array + Each element is the value corresponding to the center of each intensity + bin. + + Raises + ------ + ValueError : if image and hist are both None + """ + if image is None and hist is None: + raise Exception("Either image or hist must be provided.") + + if hist is not None: + if isinstance(hist, (tuple, list)): + counts, bin_centers = hist + else: + counts = hist + bin_centers = np.arange(counts.size) + + if counts[0] == 0 or counts[-1] == 0: + # Trim histogram from both ends by removing starting and + # ending zeroes as in histogram(..., source_range="image") + cond = counts > 0 + start = np.argmax(cond) + end = cond.size - np.argmax(cond[::-1]) + counts, bin_centers = counts[start:end], bin_centers[start:end] + else: + counts, bin_centers = histogram( + image.reshape(-1), nbins, source_range='image', normalize=normalize + ) + return counts.astype('float32', copy=False), bin_centers + + +def threshold_otsu(image=None, nbins=256, *, hist=None): + """Return threshold value based on Otsu's method. + + Either image or hist must be provided. If hist is provided, the actual + histogram of the image is ignored. + + Parameters + ---------- + image : (M, N[, ...]) ndarray, optional + Grayscale input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + hist : array, or 2-tuple of arrays, optional + Histogram from which to determine the threshold, and optionally a + corresponding array of bin center intensities. If no hist provided, + this function will compute it from the image. + + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + References + ---------- + .. [1] Wikipedia, https://en.wikipedia.org/wiki/Otsu's_Method + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_otsu(image) + >>> binary = image <= thresh + + Notes + ----- + The input image must be grayscale. + """ + if image is not None and image.ndim > 2 and image.shape[-1] in (3, 4): + warn( + f'threshold_otsu is expected to work correctly only for ' + f'grayscale images; image shape {image.shape} looks like ' + f'that of an RGB image.' + ) + + # Check if the image has more than one intensity value; if not, return that + # value + if image is not None: + first_pixel = image.reshape(-1)[0] + if np.all(image == first_pixel): + return first_pixel + + counts, bin_centers = _validate_image_histogram(image, hist, nbins) + + # class probabilities for all possible thresholds + weight1 = np.cumsum(counts) + weight2 = np.cumsum(counts[::-1])[::-1] + # class means for all possible thresholds + mean1 = np.cumsum(counts * bin_centers) / weight1 + mean2 = (np.cumsum((counts * bin_centers)[::-1]) / weight2[::-1])[::-1] + + # Clip ends to align class 1 and class 2 variables: + # The last value of ``weight1``/``mean1`` should pair with zero values in + # ``weight2``/``mean2``, which do not exist. + variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 + + idx = np.argmax(variance12) + threshold = bin_centers[idx] + + return threshold + + +def threshold_yen(image=None, nbins=256, *, hist=None): + """Return threshold value based on Yen's method. + Either image or hist must be provided. In case hist is given, the actual + histogram of the image is ignored. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + hist : array, or 2-tuple of arrays, optional + Histogram from which to determine the threshold, and optionally a + corresponding array of bin center intensities. + An alternative use of this function is to pass it only hist. + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + References + ---------- + .. [1] Yen J.C., Chang F.J., and Chang S. (1995) "A New Criterion + for Automatic Multilevel Thresholding" IEEE Trans. on Image + Processing, 4(3): 370-378. :DOI:`10.1109/83.366472` + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, :DOI:`10.1117/1.1631315` + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + .. [3] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_yen(image) + >>> binary = image <= thresh + """ + counts, bin_centers = _validate_image_histogram(image, hist, nbins) + + # On blank images (e.g. filled with 0) with int dtype, `histogram()` + # returns ``bin_centers`` containing only one value. Speed up with it. + if bin_centers.size == 1: + return bin_centers[0] + + # Calculate probability mass function + pmf = counts.astype('float32', copy=False) / counts.sum() + P1 = np.cumsum(pmf) # Cumulative normalized histogram + P1_sq = np.cumsum(pmf**2) + # Get cumsum calculated from end of squared array: + P2_sq = np.cumsum(pmf[::-1] ** 2)[::-1] + # P2_sq indexes is shifted +1. I assume, with P1[:-1] it's help avoid + # '-inf' in crit. ImageJ Yen implementation replaces those values by zero. + crit = np.log(((P1_sq[:-1] * P2_sq[1:]) ** -1) * (P1[:-1] * (1.0 - P1[:-1])) ** 2) + return bin_centers[crit.argmax()] + + +def threshold_isodata(image=None, nbins=256, return_all=False, *, hist=None): + """Return threshold value(s) based on ISODATA method. + + Histogram-based threshold, known as Ridler-Calvard method or inter-means. + Threshold values returned satisfy the following equality:: + + threshold = (image[image <= threshold].mean() + + image[image > threshold].mean()) / 2.0 + + That is, returned thresholds are intensities that separate the image into + two groups of pixels, where the threshold intensity is midway between the + mean intensities of these groups. + + For integer images, the above equality holds to within one; for floating- + point images, the equality holds to within the histogram bin-width. + + Either image or hist must be provided. In case hist is given, the actual + histogram of the image is ignored. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + return_all : bool, optional + If False (default), return only the lowest threshold that satisfies + the above equality. If True, return all valid thresholds. + hist : array, or 2-tuple of arrays, optional + Histogram to determine the threshold from and a corresponding array + of bin center intensities. Alternatively, only the histogram can be + passed. + + Returns + ------- + threshold : float or int or array + Threshold value(s). + + References + ---------- + .. [1] Ridler, TW & Calvard, S (1978), "Picture thresholding using an + iterative selection method" + IEEE Transactions on Systems, Man and Cybernetics 8: 630-632, + :DOI:`10.1109/TSMC.1978.4310039` + .. [2] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165, + http://www.busim.ee.boun.edu.tr/~sankur/SankurFolder/Threshold_survey.pdf + :DOI:`10.1117/1.1631315` + .. [3] ImageJ AutoThresholder code, + http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import coins + >>> image = coins() + >>> thresh = threshold_isodata(image) + >>> binary = image > thresh + """ + counts, bin_centers = _validate_image_histogram(image, hist, nbins) + + # image only contains one unique value + if len(bin_centers) == 1: + if return_all: + return bin_centers + else: + return bin_centers[0] + + counts = counts.astype('float32', copy=False) + + # csuml and csumh contain the count of pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively + csuml = np.cumsum(counts) + csumh = csuml[-1] - csuml + + # intensity_sum contains the total pixel intensity from each bin + intensity_sum = counts * bin_centers + + # l and h contain average value of all pixels in that bin or lower, and + # in all bins strictly higher than that bin, respectively. + # Note that since exp.histogram does not include empty bins at the low or + # high end of the range, csuml and csumh are strictly > 0, except in the + # last bin of csumh, which is zero by construction. + # So no worries about division by zero in the following lines, except + # for the last bin, but we can ignore that because no valid threshold + # can be in the top bin. + # To avoid the division by zero, we simply skip over the last element in + # all future computation. + csum_intensity = np.cumsum(intensity_sum) + lower = csum_intensity[:-1] / csuml[:-1] + higher = (csum_intensity[-1] - csum_intensity[:-1]) / csumh[:-1] + + # isodata finds threshold values that meet the criterion t = (l + m)/2 + # where l is the mean of all pixels <= t and h is the mean of all pixels + # > t, as calculated above. So we are looking for places where + # (l + m) / 2 equals the intensity value for which those l and m figures + # were calculated -- which is, of course, the histogram bin centers. + # We only require this equality to be within the precision of the bin + # width, of course. + all_mean = (lower + higher) / 2.0 + bin_width = bin_centers[1] - bin_centers[0] + + # Look only at thresholds that are below the actual all_mean value, + # for consistency with the threshold being included in the lower pixel + # group. Otherwise, can get thresholds that are not actually fixed-points + # of the isodata algorithm. For float images, this matters less, since + # there really can't be any guarantees anymore anyway. + distances = all_mean - bin_centers[:-1] + thresholds = bin_centers[:-1][(distances >= 0) & (distances < bin_width)] + + if return_all: + return thresholds + else: + return thresholds[0] + + +# Computing a histogram using np.histogram on a uint8 image with bins=256 +# doesn't work and results in aliasing problems. We use a fully specified set +# of bins to ensure that each uint8 value false into its own bin. +_DEFAULT_ENTROPY_BINS = tuple(np.arange(-0.5, 255.51, 1)) + + +def _cross_entropy(image, threshold, bins=_DEFAULT_ENTROPY_BINS): + """Compute cross-entropy between distributions above and below a threshold. + + Parameters + ---------- + image : array + The input array of values. + threshold : float + The value dividing the foreground and background in ``image``. + bins : int or array of float, optional + The number of bins or the bin edges. (Any valid value to the ``bins`` + argument of ``np.histogram`` will work here.) For an exact calculation, + each unique value should have its own bin. The default value for bins + ensures exact handling of uint8 images: ``bins=256`` results in + aliasing problems due to bin width not being equal to 1. + + Returns + ------- + nu : float + The cross-entropy target value as defined in [1]_. + + Notes + ----- + See Li and Lee, 1993 [1]_; this is the objective function ``threshold_li`` + minimizes. This function can be improved but this implementation most + closely matches equation 8 in [1]_ and equations 1-3 in [2]_. + + References + ---------- + .. [1] Li C.H. and Lee C.K. (1993) "Minimum Cross Entropy Thresholding" + Pattern Recognition, 26(4): 617-625 + :DOI:`10.1016/0031-3203(93)90115-D` + .. [2] Li C.H. and Tam P.K.S. (1998) "An Iterative Algorithm for Minimum + Cross Entropy Thresholding" Pattern Recognition Letters, 18(8): 771-776 + :DOI:`10.1016/S0167-8655(98)00057-9` + """ + histogram, bin_edges = np.histogram(image, bins=bins, density=True) + bin_centers = np.convolve(bin_edges, [0.5, 0.5], mode='valid') + t = np.flatnonzero(bin_centers > threshold)[0] + m0a = np.sum(histogram[:t]) # 0th moment, background + m0b = np.sum(histogram[t:]) + m1a = np.sum(histogram[:t] * bin_centers[:t]) # 1st moment, background + m1b = np.sum(histogram[t:] * bin_centers[t:]) + mua = m1a / m0a # mean value, background + mub = m1b / m0b + nu = -m1a * np.log(mua) - m1b * np.log(mub) + return nu + + +def threshold_li(image, *, tolerance=None, initial_guess=None, iter_callback=None): + """Compute threshold value by Li's iterative Minimum Cross Entropy method. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + tolerance : float, optional + Finish the computation when the change in the threshold in an iteration + is less than this value. By default, this is half the smallest + difference between intensity values in ``image``. + initial_guess : float or Callable[[array[float]], float], optional + Li's iterative method uses gradient descent to find the optimal + threshold. If the image intensity histogram contains more than two + modes (peaks), the gradient descent could get stuck in a local optimum. + An initial guess for the iteration can help the algorithm find the + globally-optimal threshold. A float value defines a specific start + point, while a callable should take in an array of image intensities + and return a float value. Example valid callables include + ``numpy.mean`` (default), ``lambda arr: numpy.quantile(arr, 0.95)``, + or even :func:`skimage.filters.threshold_otsu`. + iter_callback : Callable[[float], Any], optional + A function that will be called on the threshold at every iteration of + the algorithm. + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + References + ---------- + .. [1] Li C.H. and Lee C.K. (1993) "Minimum Cross Entropy Thresholding" + Pattern Recognition, 26(4): 617-625 + :DOI:`10.1016/0031-3203(93)90115-D` + .. [2] Li C.H. and Tam P.K.S. (1998) "An Iterative Algorithm for Minimum + Cross Entropy Thresholding" Pattern Recognition Letters, 18(8): 771-776 + :DOI:`10.1016/S0167-8655(98)00057-9` + .. [3] Sezgin M. and Sankur B. (2004) "Survey over Image Thresholding + Techniques and Quantitative Performance Evaluation" Journal of + Electronic Imaging, 13(1): 146-165 + :DOI:`10.1117/1.1631315` + .. [4] ImageJ AutoThresholder code, http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_li(image) + >>> binary = image > thresh + """ + # Remove nan: + image = image[~np.isnan(image)] + if image.size == 0: + return np.nan + + # Make sure image has more than one value; otherwise, return that value + # This works even for np.inf + if np.all(image == image.flat[0]): + return image.flat[0] + + # At this point, the image only contains np.inf, -np.inf, or valid numbers + image = image[np.isfinite(image)] + # if there are no finite values in the image, return 0. This is because + # at this point we *know* that there are *both* inf and -inf values, + # because inf == inf evaluates to True. We might as well separate them. + if image.size == 0: + return 0.0 + + # Li's algorithm requires positive image (because of log(mean)) + image_min = np.min(image) + image -= image_min + if image.dtype.kind in 'iu': + tolerance = tolerance or 0.5 + else: + tolerance = tolerance or np.min(np.diff(np.unique(image))) / 2 + + # Initial estimate for iteration. See "initial_guess" in the parameter list + if initial_guess is None: + t_next = np.mean(image) + elif callable(initial_guess): + t_next = initial_guess(image) + elif np.isscalar(initial_guess): # convert to new, positive image range + t_next = initial_guess - float(image_min) + image_max = np.max(image) + image_min + if not 0 < t_next < np.max(image): + msg = ( + f'The initial guess for threshold_li must be within the ' + f'range of the image. Got {initial_guess} for image min ' + f'{image_min} and max {image_max}.' + ) + raise ValueError(msg) + t_next = image.dtype.type(t_next) + else: + raise TypeError( + 'Incorrect type for `initial_guess`; should be ' + 'a floating point value, or a function mapping an ' + 'array to a floating point value.' + ) + + # initial value for t_curr must be different from t_next by at + # least the tolerance. Since the image is positive, we ensure this + # by setting to a large-enough negative number + t_curr = -2 * tolerance + + # Callback on initial iterations + if iter_callback is not None: + iter_callback(t_next + image_min) + + # Stop the iterations when the difference between the + # new and old threshold values is less than the tolerance + # or if the background mode has only one value left, + # since log(0) is not defined. + + if image.dtype.kind in 'iu': + hist, bin_centers = histogram(image.reshape(-1), source_range='image') + hist = hist.astype('float32', copy=False) + while abs(t_next - t_curr) > tolerance: + t_curr = t_next + foreground = bin_centers > t_curr + background = ~foreground + + mean_fore = np.average(bin_centers[foreground], weights=hist[foreground]) + mean_back = np.average(bin_centers[background], weights=hist[background]) + + if mean_back == 0: + break + + t_next = (mean_back - mean_fore) / (np.log(mean_back) - np.log(mean_fore)) + + if iter_callback is not None: + iter_callback(t_next + image_min) + + else: + while abs(t_next - t_curr) > tolerance: + t_curr = t_next + foreground = image > t_curr + mean_fore = np.mean(image[foreground]) + mean_back = np.mean(image[~foreground]) + + if mean_back == 0.0: + break + + t_next = (mean_back - mean_fore) / (np.log(mean_back) - np.log(mean_fore)) + + if iter_callback is not None: + iter_callback(t_next + image_min) + + threshold = t_next + image_min + return threshold + + +def threshold_minimum(image=None, nbins=256, max_num_iter=10000, *, hist=None): + """Return threshold value based on minimum method. + + The histogram of the input ``image`` is computed if not provided and + smoothed until there are only two maxima. Then the minimum in between is + the threshold value. + + Either image or hist must be provided. In case hist is given, the actual + histogram of the image is ignored. + + Parameters + ---------- + image : (M, N[, ...]) ndarray, optional + Grayscale input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + max_num_iter : int, optional + Maximum number of iterations to smooth the histogram. + hist : array, or 2-tuple of arrays, optional + Histogram to determine the threshold from and a corresponding array + of bin center intensities. Alternatively, only the histogram can be + passed. + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + Raises + ------ + RuntimeError + If unable to find two local maxima in the histogram or if the + smoothing takes more than 1e4 iterations. + + References + ---------- + .. [1] C. A. Glasbey, "An analysis of histogram-based thresholding + algorithms," CVGIP: Graphical Models and Image Processing, + vol. 55, pp. 532-537, 1993. + .. [2] Prewitt, JMS & Mendelsohn, ML (1966), "The analysis of cell + images", Annals of the New York Academy of Sciences 128: 1035-1053 + :DOI:`10.1111/j.1749-6632.1965.tb11715.x` + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_minimum(image) + >>> binary = image > thresh + """ + + def find_local_maxima_idx(hist): + # We can't use scipy.signal.argrelmax + # as it fails on plateaus + maximum_idxs = list() + direction = 1 + + for i in range(hist.shape[0] - 1): + if direction > 0: + if hist[i + 1] < hist[i]: + direction = -1 + maximum_idxs.append(i) + else: + if hist[i + 1] > hist[i]: + direction = 1 + + return maximum_idxs + + counts, bin_centers = _validate_image_histogram(image, hist, nbins) + + smooth_hist = counts.astype('float32', copy=False) + + for counter in range(max_num_iter): + smooth_hist = ndi.uniform_filter1d(smooth_hist, 3) + maximum_idxs = find_local_maxima_idx(smooth_hist) + if len(maximum_idxs) < 3: + break + + if len(maximum_idxs) != 2: + raise RuntimeError('Unable to find two maxima in histogram') + elif counter == max_num_iter - 1: + raise RuntimeError('Maximum iteration reached for histogram' 'smoothing') + + # Find the lowest point between the maxima + threshold_idx = np.argmin(smooth_hist[maximum_idxs[0] : maximum_idxs[1] + 1]) + + return bin_centers[maximum_idxs[0] + threshold_idx] + + +def threshold_mean(image): + """Return threshold value based on the mean of grayscale values. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + References + ---------- + .. [1] C. A. Glasbey, "An analysis of histogram-based thresholding + algorithms," CVGIP: Graphical Models and Image Processing, + vol. 55, pp. 532-537, 1993. + :DOI:`10.1006/cgip.1993.1040` + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_mean(image) + >>> binary = image > thresh + """ + return np.mean(image) + + +def threshold_triangle(image, nbins=256): + """Return threshold value based on the triangle algorithm. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + nbins : int, optional + Number of bins used to calculate histogram. This value is ignored for + integer arrays. + + Returns + ------- + threshold : float + Upper threshold value. All pixels with an intensity higher than + this value are assumed to be foreground. + + References + ---------- + .. [1] Zack, G. W., Rogers, W. E. and Latt, S. A., 1977, + Automatic Measurement of Sister Chromatid Exchange Frequency, + Journal of Histochemistry and Cytochemistry 25 (7), pp. 741-753 + :DOI:`10.1177/25.7.70454` + .. [2] ImageJ AutoThresholder code, + http://fiji.sc/wiki/index.php/Auto_Threshold + + Examples + -------- + >>> from skimage.data import camera + >>> image = camera() + >>> thresh = threshold_triangle(image) + >>> binary = image > thresh + """ + # nbins is ignored for integer arrays + # so, we recalculate the effective nbins. + hist, bin_centers = histogram(image.reshape(-1), nbins, source_range='image') + nbins = len(hist) + + # Find peak, lowest and highest gray levels. + arg_peak_height = np.argmax(hist) + peak_height = hist[arg_peak_height] + arg_low_level, arg_high_level = np.flatnonzero(hist)[[0, -1]] + + if arg_low_level == arg_high_level: + # Image has constant intensity. + return image.ravel()[0] + + # Flip is True if left tail is shorter. + flip = arg_peak_height - arg_low_level < arg_high_level - arg_peak_height + if flip: + hist = hist[::-1] + arg_low_level = nbins - arg_high_level - 1 + arg_peak_height = nbins - arg_peak_height - 1 + + # If flip == True, arg_high_level becomes incorrect + # but we don't need it anymore. + del arg_high_level + + # Set up the coordinate system. + width = arg_peak_height - arg_low_level + x1 = np.arange(width) + y1 = hist[x1 + arg_low_level] + + # Normalize. + norm = np.sqrt(peak_height**2 + width**2) + peak_height /= norm + width /= norm + + # Maximize the length. + # The ImageJ implementation includes an additional constant when calculating + # the length, but here we omit it as it does not affect the location of the + # minimum. + length = peak_height * x1 - width * y1 + arg_level = np.argmax(length) + arg_low_level + + if flip: + arg_level = nbins - arg_level - 1 + + return bin_centers[arg_level] + + +def _mean_std(image, w): + """Return local mean and standard deviation of each pixel using a + neighborhood defined by a rectangular window size ``w``. + The algorithm uses integral images to speedup computation. This is + used by :func:`threshold_niblack` and :func:`threshold_sauvola`. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + w : int, or iterable of int + Window size specified as a single odd integer (3, 5, 7, …), + or an iterable of length ``image.ndim`` containing only odd + integers (e.g. ``(1, 5, 5)``). + + Returns + ------- + m : ndarray of float, same shape as ``image`` + Local mean of the image. + s : ndarray of float, same shape as ``image`` + Local standard deviation of the image. + + References + ---------- + .. [1] F. Shafait, D. Keysers, and T. M. Breuel, "Efficient + implementation of local adaptive thresholding techniques + using integral images." in Document Recognition and + Retrieval XV, (San Jose, USA), Jan. 2008. + :DOI:`10.1117/12.767755` + """ + + if not isinstance(w, Iterable): + w = (w,) * image.ndim + _validate_window_size(w) + + float_dtype = _supported_float_type(image.dtype) + pad_width = tuple((k // 2 + 1, k // 2) for k in w) + padded = np.pad(image.astype(float_dtype, copy=False), pad_width, mode='reflect') + + # Note: keep float64 integral images for accuracy. Outputs of + # _correlate_sparse can later be safely cast to float_dtype + integral = integral_image(padded, dtype=np.float64) + padded *= padded + integral_sq = integral_image(padded, dtype=np.float64) + + # Create lists of non-zero kernel indices and values + kernel_indices = list(itertools.product(*tuple([(0, _w) for _w in w]))) + kernel_values = [ + (-1) ** (image.ndim % 2 != np.sum(indices) % 2) for indices in kernel_indices + ] + + total_window_size = math.prod(w) + kernel_shape = tuple(_w + 1 for _w in w) + m = _correlate_sparse(integral, kernel_shape, kernel_indices, kernel_values) + m = m.astype(float_dtype, copy=False) + m /= total_window_size + g2 = _correlate_sparse(integral_sq, kernel_shape, kernel_indices, kernel_values) + g2 = g2.astype(float_dtype, copy=False) + g2 /= total_window_size + # Note: we use np.clip because g2 is not guaranteed to be greater than + # m*m when floating point error is considered + s = np.sqrt(np.clip(g2 - m * m, 0, None)) + return m, s + + +def threshold_niblack(image, window_size=15, k=0.2): + """Applies Niblack local threshold to an array. + + A threshold T is calculated for every pixel in the image using the + following formula:: + + T = m(x,y) - k * s(x,y) + + where m(x,y) and s(x,y) are the mean and standard deviation of + pixel (x,y) neighborhood defined by a rectangular window with size w + times w centered around the pixel. k is a configurable parameter + that weights the effect of standard deviation. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + window_size : int, or iterable of int, optional + Window size specified as a single odd integer (3, 5, 7, …), + or an iterable of length ``image.ndim`` containing only odd + integers (e.g. ``(1, 5, 5)``). + k : float, optional + Value of parameter k in threshold formula. + + Returns + ------- + threshold : (M, N[, ...]) ndarray + Threshold mask. All pixels with an intensity higher than + this value are assumed to be foreground. + + Notes + ----- + This algorithm is originally designed for text recognition. + + The Bradley threshold is a particular case of the Niblack + one, being equivalent to + + >>> from skimage import data + >>> image = data.page() + >>> q = 1 + >>> threshold_image = threshold_niblack(image, k=0) * q + + for some value ``q``. By default, Bradley and Roth use ``q=1``. + + + References + ---------- + .. [1] W. Niblack, An introduction to Digital Image Processing, + Prentice-Hall, 1986. + .. [2] D. Bradley and G. Roth, "Adaptive thresholding using Integral + Image", Journal of Graphics Tools 12(2), pp. 13-21, 2007. + :DOI:`10.1080/2151237X.2007.10129236` + + Examples + -------- + >>> from skimage import data + >>> image = data.page() + >>> threshold_image = threshold_niblack(image, window_size=7, k=0.1) + """ + m, s = _mean_std(image, window_size) + return m - k * s + + +def threshold_sauvola(image, window_size=15, k=0.2, r=None): + """Applies Sauvola local threshold to an array. Sauvola is a + modification of Niblack technique. + + In the original method a threshold T is calculated for every pixel + in the image using the following formula:: + + T = m(x,y) * (1 + k * ((s(x,y) / R) - 1)) + + where m(x,y) and s(x,y) are the mean and standard deviation of + pixel (x,y) neighborhood defined by a rectangular window with size w + times w centered around the pixel. k is a configurable parameter + that weights the effect of standard deviation. + R is the maximum standard deviation of a grayscale image. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Grayscale input image. + window_size : int, or iterable of int, optional + Window size specified as a single odd integer (3, 5, 7, …), + or an iterable of length ``image.ndim`` containing only odd + integers (e.g. ``(1, 5, 5)``). + k : float, optional + Value of the positive parameter k. + r : float, optional + Value of R, the dynamic range of standard deviation. + If None, set to the half of the image dtype range. + + Returns + ------- + threshold : (M, N[, ...]) ndarray + Threshold mask. All pixels with an intensity higher than + this value are assumed to be foreground. + + Notes + ----- + This algorithm is originally designed for text recognition. + + References + ---------- + .. [1] J. Sauvola and M. Pietikainen, "Adaptive document image + binarization," Pattern Recognition 33(2), + pp. 225-236, 2000. + :DOI:`10.1016/S0031-3203(99)00055-2` + + Examples + -------- + >>> from skimage import data + >>> image = data.page() + >>> t_sauvola = threshold_sauvola(image, window_size=15, k=0.2) + >>> binary_image = image > t_sauvola + """ + if r is None: + imin, imax = dtype_limits(image, clip_negative=False) + r = 0.5 * (imax - imin) + m, s = _mean_std(image, window_size) + return m * (1 + k * ((s / r) - 1)) + + +def apply_hysteresis_threshold(image, low, high): + """Apply hysteresis thresholding to ``image``. + + This algorithm finds regions where ``image`` is greater than ``high`` + OR ``image`` is greater than ``low`` *and* that region is connected to + a region greater than ``high``. + + Parameters + ---------- + image : (M[, ...]) ndarray + Grayscale input image. + low : float, or array of same shape as ``image`` + Lower threshold. + high : float, or array of same shape as ``image`` + Higher threshold. + + Returns + ------- + thresholded : (M[, ...]) array of bool + Array in which ``True`` indicates the locations where ``image`` + was above the hysteresis threshold. + + Examples + -------- + >>> image = np.array([1, 2, 3, 2, 1, 2, 1, 3, 2]) + >>> apply_hysteresis_threshold(image, 1.5, 2.5).astype(int) + array([0, 1, 1, 1, 0, 0, 0, 1, 1]) + + References + ---------- + .. [1] J. Canny. A computational approach to edge detection. + IEEE Transactions on Pattern Analysis and Machine Intelligence. + 1986; vol. 8, pp.679-698. + :DOI:`10.1109/TPAMI.1986.4767851` + """ + low = np.clip(low, a_min=None, a_max=high) # ensure low always below high + mask_low = image > low + mask_high = image > high + # Connected components of mask_low + labels_low, num_labels = ndi.label(mask_low) + # Check which connected components contain pixels from mask_high + sums = ndi.sum(mask_high, labels_low, np.arange(num_labels + 1)) + connected_to_high = sums > 0 + thresholded = connected_to_high[labels_low] + return thresholded + + +def threshold_multiotsu(image=None, classes=3, nbins=256, *, hist=None): + r"""Generate `classes`-1 threshold values to divide gray levels in `image`, + following Otsu's method for multiple classes. + + The threshold values are chosen to maximize the total sum of pairwise + variances between the thresholded graylevel classes. See Notes and [1]_ + for more details. + + Either image or hist must be provided. If hist is provided, the actual + histogram of the image is ignored. + + Parameters + ---------- + image : (M, N[, ...]) ndarray, optional + Grayscale input image. + classes : int, optional + Number of classes to be thresholded, i.e. the number of resulting + regions. + nbins : int, optional + Number of bins used to calculate the histogram. This value is ignored + for integer arrays. + hist : array, or 2-tuple of arrays, optional + Histogram from which to determine the threshold, and optionally a + corresponding array of bin center intensities. If no hist provided, + this function will compute it from the image (see notes). + + Returns + ------- + thresh : array + Array containing the threshold values for the desired classes. + + Raises + ------ + ValueError + If ``image`` contains less grayscale value then the desired + number of classes. + + Notes + ----- + This implementation relies on a Cython function whose complexity + is :math:`O\left(\frac{Ch^{C-1}}{(C-1)!}\right)`, where :math:`h` + is the number of histogram bins and :math:`C` is the number of + classes desired. + + If no hist is given, this function will make use of + `skimage.exposure.histogram`, which behaves differently than + `np.histogram`. While both allowed, use the former for consistent + behaviour. + + The input image must be grayscale. + + References + ---------- + .. [1] Liao, P-S., Chen, T-S. and Chung, P-C., "A fast algorithm for + multilevel thresholding", Journal of Information Science and + Engineering 17 (5): 713-727, 2001. Available at: + + :DOI:`10.6688/JISE.2001.17.5.1` + .. [2] Tosa, Y., "Multi-Otsu Threshold", a java plugin for ImageJ. + Available at: + + + Examples + -------- + >>> from skimage.color import label2rgb + >>> from skimage import data + >>> image = data.camera() + >>> thresholds = threshold_multiotsu(image) + >>> regions = np.digitize(image, bins=thresholds) + >>> regions_colorized = label2rgb(regions) + """ + if image is not None and image.ndim > 2 and image.shape[-1] in (3, 4): + warn( + f'threshold_multiotsu is expected to work correctly only for ' + f'grayscale images; image shape {image.shape} looks like ' + f'that of an RGB image.' + ) + + # calculating the histogram and the probability of each gray level. + prob, bin_centers = _validate_image_histogram(image, hist, nbins, normalize=True) + prob = prob.astype('float32', copy=False) + + nvalues = np.count_nonzero(prob) + if nvalues < classes: + msg = ( + f'After discretization into bins, the input image has ' + f'only {nvalues} different values. It cannot be thresholded ' + f'in {classes} classes. If there are more unique values ' + f'before discretization, try increasing the number of bins ' + f'(`nbins`).' + ) + raise ValueError(msg) + elif nvalues == classes: + thresh_idx = np.flatnonzero(prob)[:-1] + else: + # Get threshold indices + try: + thresh_idx = _get_multiotsu_thresh_indices_lut(prob, classes - 1) + except MemoryError: + # Don't use LUT if the number of bins is too large (if the + # image is uint16 for example): in this case, the + # allocated memory is too large. + thresh_idx = _get_multiotsu_thresh_indices(prob, classes - 1) + + thresh = bin_centers[thresh_idx] + + return thresh diff --git a/lib/python3.10/site-packages/skimage/future/__init__.py b/lib/python3.10/site-packages/skimage/future/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..de3ab448b046e1d1c656687633c095c18e5dd9a8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/future/__init__.py @@ -0,0 +1,13 @@ +"""Functionality with an experimental API. + +.. warning:: + Although you can count on the functions in this package being + around in the future, the API may change with any version update + **and will not follow the skimage two-version deprecation path**. + Therefore, use the functions herein with care, and do not use them + in production code that will depend on updated skimage versions. +""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/future/__init__.pyi b/lib/python3.10/site-packages/skimage/future/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..001d5a3929099d48949fdbfff765093431520ba7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/future/__init__.pyi @@ -0,0 +1,14 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + "manual_lasso_segmentation", + "manual_polygon_segmentation", + "fit_segmenter", + "predict_segmenter", + "TrainableSegmenter", +] + +from .manual_segmentation import manual_lasso_segmentation, manual_polygon_segmentation +from .trainable_segmentation import TrainableSegmenter, fit_segmenter, predict_segmenter diff --git a/lib/python3.10/site-packages/skimage/future/manual_segmentation.py b/lib/python3.10/site-packages/skimage/future/manual_segmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..8223393cef6322e40f815d6dde54ef7a0686ddc5 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/future/manual_segmentation.py @@ -0,0 +1,235 @@ +from functools import reduce +import numpy as np +from ..draw import polygon +from .._shared.version_requirements import require + + +LEFT_CLICK = 1 +RIGHT_CLICK = 3 + + +def _mask_from_vertices(vertices, shape, label): + mask = np.zeros(shape, dtype=int) + pr = [y for x, y in vertices] + pc = [x for x, y in vertices] + rr, cc = polygon(pr, pc, shape) + mask[rr, cc] = label + return mask + + +@require("matplotlib", ">=3.3") +def _draw_polygon(ax, vertices, alpha=0.4): + from matplotlib.patches import Polygon + from matplotlib.collections import PatchCollection + import matplotlib.pyplot as plt + + polygon = Polygon(vertices, closed=True) + p = PatchCollection([polygon], match_original=True, alpha=alpha) + polygon_object = ax.add_collection(p) + plt.draw() + return polygon_object + + +@require("matplotlib", ">=3.3") +def manual_polygon_segmentation(image, alpha=0.4, return_all=False): + """Return a label image based on polygon selections made with the mouse. + + Parameters + ---------- + image : (M, N[, 3]) array + Grayscale or RGB image. + + alpha : float, optional + Transparency value for polygons drawn over the image. + + return_all : bool, optional + If True, an array containing each separate polygon drawn is returned. + (The polygons may overlap.) If False (default), latter polygons + "overwrite" earlier ones where they overlap. + + Returns + ------- + labels : array of int, shape ([Q, ]M, N) + The segmented regions. If mode is `'separate'`, the leading dimension + of the array corresponds to the number of regions that the user drew. + + Notes + ----- + Use left click to select the vertices of the polygon + and right click to confirm the selection once all vertices are selected. + + Examples + -------- + >>> from skimage import data, future + >>> import matplotlib.pyplot as plt # doctest: +SKIP + >>> camera = data.camera() + >>> mask = future.manual_polygon_segmentation(camera) # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(mask) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + """ + import matplotlib + import matplotlib.pyplot as plt + + list_of_vertex_lists = [] + polygons_drawn = [] + + temp_list = [] + preview_polygon_drawn = [] + + if image.ndim not in (2, 3): + raise ValueError('Only 2D grayscale or RGB images are supported.') + + fig, ax = plt.subplots() + fig.subplots_adjust(bottom=0.2) + ax.imshow(image, cmap="gray") + ax.set_axis_off() + + def _undo(*args, **kwargs): + if list_of_vertex_lists: + list_of_vertex_lists.pop() + # Remove last polygon from list of polygons... + last_poly = polygons_drawn.pop() + # ... then from the plot + last_poly.remove() + fig.canvas.draw_idle() + + undo_pos = fig.add_axes([0.85, 0.05, 0.075, 0.075]) + undo_button = matplotlib.widgets.Button(undo_pos, '\u27f2') + undo_button.on_clicked(_undo) + + def _extend_polygon(event): + # Do not record click events outside axis or in undo button + if event.inaxes is None or event.inaxes is undo_pos: + return + # Do not record click events when toolbar is active + if ax.get_navigate_mode(): + return + + if event.button == LEFT_CLICK: # Select vertex + temp_list.append([event.xdata, event.ydata]) + # Remove previously drawn preview polygon if any. + if preview_polygon_drawn: + poly = preview_polygon_drawn.pop() + poly.remove() + + # Preview polygon with selected vertices. + polygon = _draw_polygon(ax, temp_list, alpha=(alpha / 1.4)) + preview_polygon_drawn.append(polygon) + + elif event.button == RIGHT_CLICK: # Confirm the selection + if not temp_list: + return + + # Store the vertices of the polygon as shown in preview. + # Redraw polygon and store it in polygons_drawn so that + # `_undo` works correctly. + list_of_vertex_lists.append(temp_list[:]) + polygon_object = _draw_polygon(ax, temp_list, alpha=alpha) + polygons_drawn.append(polygon_object) + + # Empty the temporary variables. + preview_poly = preview_polygon_drawn.pop() + preview_poly.remove() + del temp_list[:] + + plt.draw() + + fig.canvas.mpl_connect('button_press_event', _extend_polygon) + + plt.show(block=True) + + labels = ( + _mask_from_vertices(vertices, image.shape[:2], i) + for i, vertices in enumerate(list_of_vertex_lists, start=1) + ) + if return_all: + return np.stack(labels) + else: + return reduce(np.maximum, labels, np.broadcast_to(0, image.shape[:2])) + + +@require("matplotlib", ">=3.3") +def manual_lasso_segmentation(image, alpha=0.4, return_all=False): + """Return a label image based on freeform selections made with the mouse. + + Parameters + ---------- + image : (M, N[, 3]) array + Grayscale or RGB image. + + alpha : float, optional + Transparency value for polygons drawn over the image. + + return_all : bool, optional + If True, an array containing each separate polygon drawn is returned. + (The polygons may overlap.) If False (default), latter polygons + "overwrite" earlier ones where they overlap. + + Returns + ------- + labels : array of int, shape ([Q, ]M, N) + The segmented regions. If mode is `'separate'`, the leading dimension + of the array corresponds to the number of regions that the user drew. + + Notes + ----- + Press and hold the left mouse button to draw around each object. + + Examples + -------- + >>> from skimage import data, future + >>> import matplotlib.pyplot as plt # doctest: +SKIP + >>> camera = data.camera() + >>> mask = future.manual_lasso_segmentation(camera) # doctest: +SKIP + >>> fig, ax = plt.subplots() # doctest: +SKIP + >>> ax.imshow(mask) # doctest: +SKIP + >>> plt.show() # doctest: +SKIP + """ + import matplotlib + import matplotlib.pyplot as plt + + list_of_vertex_lists = [] + polygons_drawn = [] + + if image.ndim not in (2, 3): + raise ValueError('Only 2D grayscale or RGB images are supported.') + + fig, ax = plt.subplots() + fig.subplots_adjust(bottom=0.2) + ax.imshow(image, cmap="gray") + ax.set_axis_off() + + def _undo(*args, **kwargs): + if list_of_vertex_lists: + list_of_vertex_lists.pop() + # Remove last polygon from list of polygons... + last_poly = polygons_drawn.pop() + # ... then from the plot + last_poly.remove() + fig.canvas.draw_idle() + + undo_pos = fig.add_axes([0.85, 0.05, 0.075, 0.075]) + undo_button = matplotlib.widgets.Button(undo_pos, '\u27f2') + undo_button.on_clicked(_undo) + + def _on_lasso_selection(vertices): + if len(vertices) < 3: + return + list_of_vertex_lists.append(vertices) + polygon_object = _draw_polygon(ax, vertices, alpha=alpha) + polygons_drawn.append(polygon_object) + plt.draw() + + matplotlib.widgets.LassoSelector(ax, _on_lasso_selection) + + plt.show(block=True) + + labels = ( + _mask_from_vertices(vertices, image.shape[:2], i) + for i, vertices in enumerate(list_of_vertex_lists, start=1) + ) + if return_all: + return np.stack(labels) + else: + return reduce(np.maximum, labels, np.broadcast_to(0, image.shape[:2])) diff --git a/lib/python3.10/site-packages/skimage/future/trainable_segmentation.py b/lib/python3.10/site-packages/skimage/future/trainable_segmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..7b5d7df6e16409a4b65f69e1090c71f2165cf0e9 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/future/trainable_segmentation.py @@ -0,0 +1,164 @@ +from skimage.feature import multiscale_basic_features + +try: + from sklearn.exceptions import NotFittedError + from sklearn.ensemble import RandomForestClassifier + + has_sklearn = True +except ImportError: + has_sklearn = False + + class NotFittedError(Exception): + pass + + +class TrainableSegmenter: + """Estimator for classifying pixels. + + Parameters + ---------- + clf : classifier object, optional + classifier object, exposing a ``fit`` and a ``predict`` method as in + scikit-learn's API, for example an instance of + ``RandomForestClassifier`` or ``LogisticRegression`` classifier. + features_func : function, optional + function computing features on all pixels of the image, to be passed + to the classifier. The output should be of shape + ``(m_features, *labels.shape)``. If None, + :func:`skimage.feature.multiscale_basic_features` is used. + + Methods + ------- + compute_features + fit + predict + """ + + def __init__(self, clf=None, features_func=None): + if clf is None: + if has_sklearn: + self.clf = RandomForestClassifier(n_estimators=100, n_jobs=-1) + else: + raise ImportError( + "Please install scikit-learn or pass a classifier instance" + "to TrainableSegmenter." + ) + else: + self.clf = clf + self.features_func = features_func + + def compute_features(self, image): + if self.features_func is None: + self.features_func = multiscale_basic_features + self.features = self.features_func(image) + + def fit(self, image, labels): + """Train classifier using partially labeled (annotated) image. + + Parameters + ---------- + image : ndarray + Input image, which can be grayscale or multichannel, and must have a + number of dimensions compatible with ``self.features_func``. + labels : ndarray of ints + Labeled array of shape compatible with ``image`` (same shape for a + single-channel image). Labels >= 1 correspond to the training set and + label 0 to unlabeled pixels to be segmented. + """ + self.compute_features(image) + fit_segmenter(labels, self.features, self.clf) + + def predict(self, image): + """Segment new image using trained internal classifier. + + Parameters + ---------- + image : ndarray + Input image, which can be grayscale or multichannel, and must have a + number of dimensions compatible with ``self.features_func``. + + Raises + ------ + NotFittedError if ``self.clf`` has not been fitted yet (use ``self.fit``). + """ + if self.features_func is None: + self.features_func = multiscale_basic_features + features = self.features_func(image) + return predict_segmenter(features, self.clf) + + +def fit_segmenter(labels, features, clf): + """Segmentation using labeled parts of the image and a classifier. + + Parameters + ---------- + labels : ndarray of ints + Image of labels. Labels >= 1 correspond to the training set and + label 0 to unlabeled pixels to be segmented. + features : ndarray + Array of features, with the first dimension corresponding to the number + of features, and the other dimensions correspond to ``labels.shape``. + clf : classifier object + classifier object, exposing a ``fit`` and a ``predict`` method as in + scikit-learn's API, for example an instance of + ``RandomForestClassifier`` or ``LogisticRegression`` classifier. + + Returns + ------- + clf : classifier object + classifier trained on ``labels`` + + Raises + ------ + NotFittedError if ``self.clf`` has not been fitted yet (use ``self.fit``). + """ + mask = labels > 0 + training_data = features[mask] + training_labels = labels[mask].ravel() + clf.fit(training_data, training_labels) + return clf + + +def predict_segmenter(features, clf): + """Segmentation of images using a pretrained classifier. + + Parameters + ---------- + features : ndarray + Array of features, with the last dimension corresponding to the number + of features, and the other dimensions are compatible with the shape of + the image to segment, or a flattened image. + clf : classifier object + trained classifier object, exposing a ``predict`` method as in + scikit-learn's API, for example an instance of + ``RandomForestClassifier`` or ``LogisticRegression`` classifier. The + classifier must be already trained, for example with + :func:`skimage.future.fit_segmenter`. + + Returns + ------- + output : ndarray + Labeled array, built from the prediction of the classifier. + """ + sh = features.shape + if features.ndim > 2: + features = features.reshape((-1, sh[-1])) + + try: + predicted_labels = clf.predict(features) + except NotFittedError: + raise NotFittedError( + "You must train the classifier `clf` first" + "for example with the `fit_segmenter` function." + ) + except ValueError as err: + if err.args and 'x must consist of vectors of length' in err.args[0]: + raise ValueError( + err.args[0] + + '\n' + + "Maybe you did not use the same type of features for training the classifier." + ) + else: + raise err + output = predicted_labels.reshape(sh[:-1]) + return output diff --git a/lib/python3.10/site-packages/skimage/graph/__init__.py b/lib/python3.10/site-packages/skimage/graph/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9e366208a84906e3e7ca12e352845299bd2555e1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/__init__.py @@ -0,0 +1,12 @@ +""" +Graph-based operations, e.g., shortest paths. + +This includes creating adjacency graphs of pixels in an image, finding the +central pixel in an image, finding (minimum-cost) paths across pixels, merging +and cutting of graphs, etc. + +""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/graph/__init__.pyi b/lib/python3.10/site-packages/skimage/graph/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..5582d999e90ecd56805004550b19f0de04a33012 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/__init__.pyi @@ -0,0 +1,27 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'pixel_graph', + 'central_pixel', + 'shortest_path', + 'MCP', + 'MCP_Geometric', + 'MCP_Connect', + 'MCP_Flexible', + 'route_through_array', + 'rag_mean_color', + 'rag_boundary', + 'cut_threshold', + 'cut_normalized', + 'merge_hierarchical', + 'RAG', +] + +from ._graph import pixel_graph, central_pixel +from ._graph_cut import cut_threshold, cut_normalized +from ._graph_merge import merge_hierarchical +from ._rag import rag_mean_color, RAG, show_rag, rag_boundary +from .spath import shortest_path +from .mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible, route_through_array diff --git a/lib/python3.10/site-packages/skimage/graph/_graph.py b/lib/python3.10/site-packages/skimage/graph/_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..228c4dfddc2a3acfde981c864a262a727483b123 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/_graph.py @@ -0,0 +1,220 @@ +import numpy as np +from scipy import sparse +from scipy.sparse import csgraph +from ..morphology._util import _raveled_offsets_and_distances +from ..util._map_array import map_array +from ..segmentation.random_walker_segmentation import _safe_downcast_indices + + +def _weighted_abs_diff(values0, values1, distances): + """A default edge function for complete image graphs. + + A pixel graph on an image with no edge values and no mask is a very + boring regular lattice, so we define a default edge weight to be the + absolute difference between values *weighted* by the distance + between them. + + Parameters + ---------- + values0 : array + The pixel values for each node. + values1 : array + The pixel values for each neighbor. + distances : array + The distance between each node and its neighbor. + + Returns + ------- + edge_values : array of float + The computed values: abs(values0 - values1) * distances. + """ + return np.abs(values0 - values1) * distances + + +def pixel_graph( + image, + *, + mask=None, + edge_function=None, + connectivity=1, + spacing=None, + sparse_type="matrix", +): + """Create an adjacency graph of pixels in an image. + + Pixels where the mask is True are nodes in the returned graph, and they are + connected by edges to their neighbors according to the connectivity + parameter. By default, the *value* of an edge when a mask is given, or when + the image is itself the mask, is the Euclidean distance between the pixels. + + However, if an int- or float-valued image is given with no mask, the value + of the edges is the absolute difference in intensity between adjacent + pixels, weighted by the Euclidean distance. + + Parameters + ---------- + image : array + The input image. If the image is of type bool, it will be used as the + mask as well. + mask : array of bool + Which pixels to use. If None, the graph for the whole image is used. + edge_function : callable + A function taking an array of pixel values, and an array of neighbor + pixel values, and an array of distances, and returning a value for the + edge. If no function is given, the value of an edge is just the + distance. + connectivity : int + The square connectivity of the pixel neighborhood: the number of + orthogonal steps allowed to consider a pixel a neighbor. See + `scipy.ndimage.generate_binary_structure` for details. + spacing : tuple of float + The spacing between pixels along each axis. + sparse_type : {"matrix", "array"}, optional + The return type of `graph`, either `scipy.sparse.csr_array` or + `scipy.sparse.csr_matrix` (default). + + Returns + ------- + graph : scipy.sparse.csr_matrix or scipy.sparse.csr_array + A sparse adjacency matrix in which entry (i, j) is 1 if nodes i and j + are neighbors, 0 otherwise. Depending on `sparse_type`, this can be + returned as a `scipy.sparse.csr_array`. + nodes : array of int + The nodes of the graph. These correspond to the raveled indices of the + nonzero pixels in the mask. + """ + if mask is None: + if image.dtype == bool: + mask = image + else: + mask = np.ones_like(image, dtype=bool) + + if edge_function is None: + if image.dtype == bool: + + def edge_function(x, y, distances): + return distances + + else: + edge_function = _weighted_abs_diff + + # Strategy: we are going to build the (i, j, data) arrays of a scipy + # sparse CSR matrix. + # - grab the raveled IDs of the foreground (mask == True) parts of the + # image **in the padded space**. + # - broadcast them together with the raveled offsets to their neighbors. + # This gives us for each foreground pixel a list of neighbors (that + # may or may not be selected by the mask). (We also track the *distance* + # to each neighbor.) + # - select "valid" entries in the neighbors and distance arrays by indexing + # into the mask, which we can do since these are raveled indices. + # - use np.repeat() to repeat each source index according to the number + # of neighbors selected by the mask it has. Each of these repeated + # indices will be lined up with its neighbor, i.e. **this is the row_ind + # array** of the CSR format matrix. + # - use the mask as a boolean index to get a 1D view of the selected + # neighbors. **This is the col_ind array.** + # - by default, the same boolean indexing can be applied to the distances + # to each neighbor, to give the **data array.** Optionally, a + # provided edge function can be computed on the pixel values and the + # distances to give a different value for the edges. + # Note, we use map_array to map the raveled coordinates in the padded + # image to the ones in the original image, and those are the returned + # nodes. + padded = np.pad(mask, 1, mode='constant', constant_values=False) + nodes_padded = np.flatnonzero(padded) + neighbor_offsets_padded, distances_padded = _raveled_offsets_and_distances( + padded.shape, connectivity=connectivity, spacing=spacing + ) + neighbors_padded = nodes_padded[:, np.newaxis] + neighbor_offsets_padded + neighbor_distances_full = np.broadcast_to(distances_padded, neighbors_padded.shape) + nodes = np.flatnonzero(mask) + nodes_sequential = np.arange(nodes.size) + # neighbors outside the mask get mapped to 0, which is a valid index, + # BUT, they will be masked out in the next step. + neighbors = map_array(neighbors_padded, nodes_padded, nodes) + neighbors_mask = padded.reshape(-1)[neighbors_padded] + num_neighbors = np.sum(neighbors_mask, axis=1) + indices = np.repeat(nodes, num_neighbors) + indices_sequential = np.repeat(nodes_sequential, num_neighbors) + neighbor_indices = neighbors[neighbors_mask] + neighbor_distances = neighbor_distances_full[neighbors_mask] + neighbor_indices_sequential = map_array(neighbor_indices, nodes, nodes_sequential) + + image_r = image.reshape(-1) + data = edge_function( + image_r[indices], image_r[neighbor_indices], neighbor_distances + ) + + m = nodes_sequential.size + graph = sparse.csr_array( + (data, (indices_sequential, neighbor_indices_sequential)), shape=(m, m) + ) + + if sparse_type == "matrix": + graph = sparse.csr_matrix(graph) + elif sparse_type != "array": + msg = f"`sparse_type` must be 'array' or 'matrix', got {sparse_type}" + raise ValueError(msg) + + return graph, nodes + + +def central_pixel(graph, nodes=None, shape=None, partition_size=100): + """Find the pixel with the highest closeness centrality. + + Closeness centrality is the inverse of the total sum of shortest distances + from a node to every other node. + + Parameters + ---------- + graph : scipy.sparse.csr_array or scipy.sparse.csr_matrix + The sparse representation of the graph. + nodes : array of int + The raveled index of each node in graph in the image. If not provided, + the returned value will be the index in the input graph. + shape : tuple of int + The shape of the image in which the nodes are embedded. If provided, + the returned coordinates are a NumPy multi-index of the same + dimensionality as the input shape. Otherwise, the returned coordinate + is the raveled index provided in `nodes`. + partition_size : int + This function computes the shortest path distance between every pair + of nodes in the graph. This can result in a very large (N*N) matrix. + As a simple performance tweak, the distance values are computed in + lots of `partition_size`, resulting in a memory requirement of only + partition_size*N. + + Returns + ------- + position : int or tuple of int + If shape is given, the coordinate of the central pixel in the image. + Otherwise, the raveled index of that pixel. + distances : array of float + The total sum of distances from each node to each other reachable + node. + """ + if nodes is None: + nodes = np.arange(graph.shape[0]) + if partition_size is None: + num_splits = 1 + else: + num_splits = max(2, graph.shape[0] // partition_size) + graph.indices, graph.indptr = _safe_downcast_indices( + graph, np.int32, 'index values too large for csgraph' + ) + idxs = np.arange(graph.shape[0]) + total_shortest_path_len_list = [] + for partition in np.array_split(idxs, num_splits): + shortest_paths = csgraph.shortest_path(graph, directed=False, indices=partition) + shortest_paths_no_inf = np.nan_to_num(shortest_paths) + total_shortest_path_len_list.append(np.sum(shortest_paths_no_inf, axis=1)) + total_shortest_path_len = np.concatenate(total_shortest_path_len_list) + nonzero = np.flatnonzero(total_shortest_path_len) + min_sp = np.argmin(total_shortest_path_len[nonzero]) + raveled_index = nodes[nonzero[min_sp]] + if shape is not None: + central = np.unravel_index(raveled_index, shape) + else: + central = raveled_index + return central, total_shortest_path_len diff --git a/lib/python3.10/site-packages/skimage/graph/_graph_cut.py b/lib/python3.10/site-packages/skimage/graph/_graph_cut.py new file mode 100644 index 0000000000000000000000000000000000000000..9fe059e1e2a1b7e0b62c13c3b5325d6391dafe36 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/_graph_cut.py @@ -0,0 +1,314 @@ +import networkx as nx +import numpy as np +from scipy.sparse import linalg + +from . import _ncut, _ncut_cy + + +def cut_threshold(labels, rag, thresh, in_place=True): + """Combine regions separated by weight less than threshold. + + Given an image's labels and its RAG, output new labels by + combining regions whose nodes are separated by a weight less + than the given threshold. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. Regions connected by edges with smaller weights are + combined. + in_place : bool + If set, modifies `rag` in place. The function will remove the edges + with weights less that `thresh`. If set to `False` the function + makes a copy of `rag` before proceeding. + + Returns + ------- + out : ndarray + The new labelled array. + + Examples + -------- + >>> from skimage import data, segmentation, graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_mean_color(img, labels) + >>> new_labels = graph.cut_threshold(labels, rag, 10) + + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + :DOI:`10.1109/83.841950` + + """ + if not in_place: + rag = rag.copy() + + # Because deleting edges while iterating through them produces an error. + to_remove = [(x, y) for x, y, d in rag.edges(data=True) if d['weight'] >= thresh] + rag.remove_edges_from(to_remove) + + comps = nx.connected_components(rag) + + # We construct an array which can map old labels to the new ones. + # All the labels within a connected component are assigned to a single + # label in the output. + map_array = np.arange(labels.max() + 1, dtype=labels.dtype) + for i, nodes in enumerate(comps): + for node in nodes: + for label in rag.nodes[node]['labels']: + map_array[label] = i + + return map_array[labels] + + +def cut_normalized( + labels, + rag, + thresh=0.001, + num_cuts=10, + in_place=True, + max_edge=1.0, + *, + rng=None, +): + """Perform Normalized Graph cut on the Region Adjacency Graph. + + Given an image's labels and its similarity RAG, recursively perform + a 2-way normalized cut on it. All nodes belonging to a subgraph + that cannot be cut further are assigned a unique label in the + output. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. + in_place : bool + If set, modifies `rag` in place. For each node `n` the function will + set a new attribute ``rag.nodes[n]['ncut label']``. + max_edge : float, optional + The maximum possible value of an edge in the RAG. This corresponds to + an edge between identical regions. This is used to put self + edges in the RAG. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + The `rng` is used to determine the starting point + of `scipy.sparse.linalg.eigsh`. + + Returns + ------- + out : ndarray + The new labeled array. + + Examples + -------- + >>> from skimage import data, segmentation, graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_mean_color(img, labels, mode='similarity') + >>> new_labels = graph.cut_normalized(labels, rag) + + References + ---------- + .. [1] Shi, J.; Malik, J., "Normalized cuts and image segmentation", + Pattern Analysis and Machine Intelligence, + IEEE Transactions on, vol. 22, no. 8, pp. 888-905, August 2000. + + """ + rng = np.random.default_rng(rng) + if not in_place: + rag = rag.copy() + + for node in rag.nodes(): + rag.add_edge(node, node, weight=max_edge) + + _ncut_relabel(rag, thresh, num_cuts, rng) + + map_array = np.zeros(labels.max() + 1, dtype=labels.dtype) + # Mapping from old labels to new + for n, d in rag.nodes(data=True): + map_array[d['labels']] = d['ncut label'] + + return map_array[labels] + + +def partition_by_cut(cut, rag): + """Compute resulting subgraphs from given bi-partition. + + Parameters + ---------- + cut : array + A array of booleans. Elements set to `True` belong to one + set. + rag : RAG + The Region Adjacency Graph. + + Returns + ------- + sub1, sub2 : RAG + The two resulting subgraphs from the bi-partition. + """ + # `cut` is derived from `D` and `W` matrices, which also follow the + # ordering returned by `rag.nodes()` because we use + # nx.to_scipy_sparse_array. + + # Example + # rag.nodes() = [3, 7, 9, 13] + # cut = [True, False, True, False] + # nodes1 = [3, 9] + # nodes2 = [7, 10] + + nodes1 = [n for i, n in enumerate(rag.nodes()) if cut[i]] + nodes2 = [n for i, n in enumerate(rag.nodes()) if not cut[i]] + + sub1 = rag.subgraph(nodes1) + sub2 = rag.subgraph(nodes2) + + return sub1, sub2 + + +def get_min_ncut(ev, d, w, num_cuts): + """Threshold an eigenvector evenly, to determine minimum ncut. + + Parameters + ---------- + ev : array + The eigenvector to threshold. + d : ndarray + The diagonal matrix of the graph. + w : ndarray + The weight matrix of the graph. + num_cuts : int + The number of evenly spaced thresholds to check for. + + Returns + ------- + mask : array + The array of booleans which denotes the bi-partition. + mcut : float + The value of the minimum ncut. + """ + mcut = np.inf + mn = ev.min() + mx = ev.max() + + # If all values in `ev` are equal, it implies that the graph can't be + # further sub-divided. In this case the bi-partition is the the graph + # itself and an empty set. + min_mask = np.zeros_like(ev, dtype=bool) + if np.allclose(mn, mx): + return min_mask, mcut + + # Refer Shi & Malik 2001, Section 3.1.3, Page 892 + # Perform evenly spaced n-cuts and determine the optimal one. + for t in np.linspace(mn, mx, num_cuts, endpoint=False): + mask = ev > t + cost = _ncut.ncut_cost(mask, d, w) + if cost < mcut: + min_mask = mask + mcut = cost + + return min_mask, mcut + + +def _label_all(rag, attr_name): + """Assign a unique integer to the given attribute in the RAG. + + This function assumes that all labels in `rag` are unique. It + picks up a random label from them and assigns it to the `attr_name` + attribute of all the nodes. + + rag : RAG + The Region Adjacency Graph. + attr_name : string + The attribute to which a unique integer is assigned. + """ + node = min(rag.nodes()) + new_label = rag.nodes[node]['labels'][0] + for n, d in rag.nodes(data=True): + d[attr_name] = new_label + + +def _ncut_relabel(rag, thresh, num_cuts, random_generator): + """Perform Normalized Graph cut on the Region Adjacency Graph. + + Recursively partition the graph into 2, until further subdivision + yields a cut greater than `thresh` or such a cut cannot be computed. + For such a subgraph, indices to labels of all its nodes map to a single + unique value. + + Parameters + ---------- + rag : RAG + The region adjacency graph. + thresh : float + The threshold. A subgraph won't be further subdivided if the + value of the N-cut exceeds `thresh`. + num_cuts : int + The number or N-cuts to perform before determining the optimal one. + random_generator : `numpy.random.Generator` + Provides initial values for eigenvalue solver. + """ + d, w = _ncut.DW_matrices(rag) + m = w.shape[0] + + if (m > 2) and (d != w).nnz > 0: + # This avoids further segmenting a graph that is too small, + # and the degenerate case (d == w), which typically occurs + # when only three single pixels remain. + # + # We're not sure exactly why this latter case arises. For + # SciPy <= 0.14, SciPy continued to compute an eigenvector, + # but newer versions (correctly) won't. We refuse to guess, + # and stop further segmentation. + # + # It may make sense to a warning here; on the other hand segmentations + # are not a ground truth, so this level of "noise" should be acceptable. + + d2 = d.copy() + # Since d is diagonal, we can directly operate on its data + # the inverse of the square root + d2.data = np.reciprocal(np.sqrt(d2.data, out=d2.data), out=d2.data) + + # Refer Shi & Malik 2001, Equation 7, Page 891 + A = d2 @ (d - w) @ d2 + # Initialize the vector to ensure reproducibility. + v0 = random_generator.random(A.shape[0]) + vals, vectors = linalg.eigsh(A, which='SM', v0=v0, k=min(100, m - 2)) + + # Pick second smallest eigenvector. + # Refer Shi & Malik 2001, Section 3.2.3, Page 893 + vals, vectors = np.real(vals), np.real(vectors) + index2 = _ncut_cy.argmin2(vals) + ev = vectors[:, index2] + + cut_mask, mcut = get_min_ncut(ev, d, w, num_cuts) + if mcut < thresh: + # Sub divide and perform N-cut again + # Refer Shi & Malik 2001, Section 3.2.5, Page 893 + sub1, sub2 = partition_by_cut(cut_mask, rag) + + _ncut_relabel(sub1, thresh, num_cuts, random_generator) + _ncut_relabel(sub2, thresh, num_cuts, random_generator) + return + + # The N-cut wasn't small enough, or could not be computed. + # The remaining graph is a region. + # Assign `ncut label` by picking any label from the existing nodes, since + # `labels` are unique, `new_label` is also unique. + _label_all(rag, 'ncut label') diff --git a/lib/python3.10/site-packages/skimage/graph/_graph_merge.py b/lib/python3.10/site-packages/skimage/graph/_graph_merge.py new file mode 100644 index 0000000000000000000000000000000000000000..ba7a20c4bff0f63d00d0e3a760b93e6b2d543bd4 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/_graph_merge.py @@ -0,0 +1,138 @@ +import numpy as np +import heapq + + +def _revalidate_node_edges(rag, node, heap_list): + """Handles validation and invalidation of edges incident to a node. + + This function invalidates all existing edges incident on `node` and inserts + new items in `heap_list` updated with the valid weights. + + rag : RAG + The Region Adjacency Graph. + node : int + The id of the node whose incident edges are to be validated/invalidated + . + heap_list : list + The list containing the existing heap of edges. + """ + # networkx updates data dictionary if edge exists + # this would mean we have to reposition these edges in + # heap if their weight is updated. + # instead we invalidate them + + for nbr in rag.neighbors(node): + data = rag[node][nbr] + try: + # invalidate edges incident on `dst`, they have new weights + data['heap item'][3] = False + _invalidate_edge(rag, node, nbr) + except KeyError: + # will handle the case where the edge did not exist in the existing + # graph + pass + + wt = data['weight'] + heap_item = [wt, node, nbr, True] + data['heap item'] = heap_item + heapq.heappush(heap_list, heap_item) + + +def _rename_node(graph, node_id, copy_id): + """Rename `node_id` in `graph` to `copy_id`.""" + + graph._add_node_silent(copy_id) + graph.nodes[copy_id].update(graph.nodes[node_id]) + + for nbr in graph.neighbors(node_id): + wt = graph[node_id][nbr]['weight'] + graph.add_edge(nbr, copy_id, {'weight': wt}) + + graph.remove_node(node_id) + + +def _invalidate_edge(graph, n1, n2): + """Invalidates the edge (n1, n2) in the heap.""" + graph[n1][n2]['heap item'][3] = False + + +def merge_hierarchical( + labels, rag, thresh, rag_copy, in_place_merge, merge_func, weight_func +): + """Perform hierarchical merging of a RAG. + + Greedily merges the most similar pair of nodes until no edges lower than + `thresh` remain. + + Parameters + ---------- + labels : ndarray + The array of labels. + rag : RAG + The Region Adjacency Graph. + thresh : float + Regions connected by an edge with weight smaller than `thresh` are + merged. + rag_copy : bool + If set, the RAG copied before modifying. + in_place_merge : bool + If set, the nodes are merged in place. Otherwise, a new node is + created for each merge.. + merge_func : callable + This function is called before merging two nodes. For the RAG `graph` + while merging `src` and `dst`, it is called as follows + ``merge_func(graph, src, dst)``. + weight_func : callable + The function to compute the new weights of the nodes adjacent to the + merged node. This is directly supplied as the argument `weight_func` + to `merge_nodes`. + + Returns + ------- + out : ndarray + The new labeled array. + + """ + if rag_copy: + rag = rag.copy() + + edge_heap = [] + for n1, n2, data in rag.edges(data=True): + # Push a valid edge in the heap + wt = data['weight'] + heap_item = [wt, n1, n2, True] + heapq.heappush(edge_heap, heap_item) + + # Reference to the heap item in the graph + data['heap item'] = heap_item + + while len(edge_heap) > 0 and edge_heap[0][0] < thresh: + _, n1, n2, valid = heapq.heappop(edge_heap) + + # Ensure popped edge is valid, if not, the edge is discarded + if valid: + # Invalidate all neighbors of `src` before its deleted + + for nbr in rag.neighbors(n1): + _invalidate_edge(rag, n1, nbr) + + for nbr in rag.neighbors(n2): + _invalidate_edge(rag, n2, nbr) + + if not in_place_merge: + next_id = rag.next_id() + _rename_node(rag, n2, next_id) + src, dst = n1, next_id + else: + src, dst = n1, n2 + + merge_func(rag, src, dst) + new_id = rag.merge_nodes(src, dst, weight_func) + _revalidate_node_edges(rag, new_id, edge_heap) + + label_map = np.arange(labels.max() + 1) + for ix, (n, d) in enumerate(rag.nodes(data=True)): + for label in d['labels']: + label_map[label] = ix + + return label_map[labels] diff --git a/lib/python3.10/site-packages/skimage/graph/_ncut.py b/lib/python3.10/site-packages/skimage/graph/_ncut.py new file mode 100644 index 0000000000000000000000000000000000000000..504bd6f69bab25672a05b908d419825a51158ee8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/_ncut.py @@ -0,0 +1,64 @@ +import networkx as nx +import numpy as np +from scipy import sparse +from . import _ncut_cy + + +def DW_matrices(graph): + """Returns the diagonal and weight matrices of a graph. + + Parameters + ---------- + graph : RAG + A Region Adjacency Graph. + + Returns + ------- + D : csc_array + The diagonal matrix of the graph. ``D[i, i]`` is the sum of weights of + all edges incident on `i`. All other entries are `0`. + W : csc_array + The weight matrix of the graph. ``W[i, j]`` is the weight of the edge + joining `i` to `j`. + """ + # sparse.eighsh is most efficient with CSC-formatted input + W = nx.to_scipy_sparse_array(graph, format='csc') + entries = W.sum(axis=0) + D = sparse.dia_array((entries, 0), shape=W.shape).tocsc() + + return D, W + + +def ncut_cost(cut, D, W): + """Returns the N-cut cost of a bi-partition of a graph. + + Parameters + ---------- + cut : ndarray + The mask for the nodes in the graph. Nodes corresponding to a `True` + value are in one set. + D : csc_array + The diagonal matrix of the graph. + W : csc_array + The weight matrix of the graph. + + Returns + ------- + cost : float + The cost of performing the N-cut. + + References + ---------- + .. [1] Normalized Cuts and Image Segmentation, Jianbo Shi and + Jitendra Malik, IEEE Transactions on Pattern Analysis and Machine + Intelligence, Page 889, Equation 2. + """ + cut = np.array(cut) + cut_cost = _ncut_cy.cut_cost(cut, W.data, W.indices, W.indptr, num_cols=W.shape[0]) + + # D has elements only along the diagonal, one per node, so we can directly + # index the data attribute with cut. + assoc_a = D.data[cut].sum() + assoc_b = D.data[~cut].sum() + + return (cut_cost / assoc_a) + (cut_cost / assoc_b) diff --git a/lib/python3.10/site-packages/skimage/graph/_rag.py b/lib/python3.10/site-packages/skimage/graph/_rag.py new file mode 100644 index 0000000000000000000000000000000000000000..d663df9c8ebfa822d6c3dd89b118d0b215a4f2ea --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/_rag.py @@ -0,0 +1,581 @@ +import networkx as nx +import numpy as np +from scipy import ndimage as ndi +from scipy import sparse +import math + +from .. import measure, segmentation, util, color +from .._shared.version_requirements import require + + +def _edge_generator_from_csr(csr_array): + """Yield weighted edge triples for use by NetworkX from a CSR matrix. + + This function is a straight rewrite of + `networkx.convert_matrix._csr_gen_triples`. Since that is a private + function, it is safer to include our own here. + + Parameters + ---------- + csr_array : scipy.sparse.csr_array + The input matrix. An edge (i, j, w) will be yielded if there is a + data value for coordinates (i, j) in the matrix, even if that value + is 0. + + Yields + ------ + i, j, w : (int, int, float) tuples + Each value `w` in the matrix along with its coordinates (i, j). + + Examples + -------- + + >>> dense = np.eye(2, dtype=float) + >>> csr = sparse.csr_array(dense) + >>> edges = _edge_generator_from_csr(csr) + >>> list(edges) + [(0, 0, 1.0), (1, 1, 1.0)] + """ + nrows = csr_array.shape[0] + values = csr_array.data + indptr = csr_array.indptr + col_indices = csr_array.indices + for i in range(nrows): + for j in range(indptr[i], indptr[i + 1]): + yield i, col_indices[j], values[j] + + +def min_weight(graph, src, dst, n): + """Callback to handle merging nodes by choosing minimum weight. + + Returns a dictionary with `"weight"` set as either the weight between + (`src`, `n`) or (`dst`, `n`) in `graph` or the minimum of the two when + both exist. + + Parameters + ---------- + graph : RAG + The graph under consideration. + src, dst : int + The verices in `graph` to be merged. + n : int + A neighbor of `src` or `dst` or both. + + Returns + ------- + data : dict + A dict with the `"weight"` attribute set the weight between + (`src`, `n`) or (`dst`, `n`) in `graph` or the minimum of the two when + both exist. + + """ + + # cover the cases where n only has edge to either `src` or `dst` + default = {'weight': np.inf} + w1 = graph[n].get(src, default)['weight'] + w2 = graph[n].get(dst, default)['weight'] + return {'weight': min(w1, w2)} + + +def _add_edge_filter(values, graph): + """Create edge in `graph` between central element of `values` and the rest. + + Add an edge between the middle element in `values` and + all other elements of `values` into `graph`. ``values[len(values) // 2]`` + is expected to be the central value of the footprint used. + + Parameters + ---------- + values : array + The array to process. + graph : RAG + The graph to add edges in. + + Returns + ------- + 0 : float + Always returns 0. The return value is required so that `generic_filter` + can put it in the output array, but it is ignored by this filter. + """ + values = values.astype(int) + center = values[len(values) // 2] + for value in values: + if value != center and not graph.has_edge(center, value): + graph.add_edge(center, value) + return 0.0 + + +class RAG(nx.Graph): + """The Region Adjacency Graph (RAG) of an image, subclasses :obj:`networkx.Graph`. + + Parameters + ---------- + label_image : array of int + An initial segmentation, with each region labeled as a different + integer. Every unique value in ``label_image`` will correspond to + a node in the graph. + connectivity : int in {1, ..., ``label_image.ndim``}, optional + The connectivity between pixels in ``label_image``. For a 2D image, + a connectivity of 1 corresponds to immediate neighbors up, down, + left, and right, while a connectivity of 2 also includes diagonal + neighbors. See :func:`scipy.ndimage.generate_binary_structure`. + data : :obj:`networkx.Graph` specification, optional + Initial or additional edges to pass to :obj:`networkx.Graph` + constructor. Valid edge specifications include edge list (list of tuples), + NumPy arrays, and SciPy sparse matrices. + **attr : keyword arguments, optional + Additional attributes to add to the graph. + """ + + def __init__(self, label_image=None, connectivity=1, data=None, **attr): + super().__init__(data, **attr) + if self.number_of_nodes() == 0: + self.max_id = 0 + else: + self.max_id = max(self.nodes()) + + if label_image is not None: + fp = ndi.generate_binary_structure(label_image.ndim, connectivity) + # In the next ``ndi.generic_filter`` function, the kwarg + # ``output`` is used to provide a strided array with a single + # 64-bit floating point number, to which the function repeatedly + # writes. This is done because even if we don't care about the + # output, without this, a float array of the same shape as the + # input image will be created and that could be expensive in + # memory consumption. + output = np.broadcast_to(1.0, label_image.shape) + output.setflags(write=True) + ndi.generic_filter( + label_image, + function=_add_edge_filter, + footprint=fp, + mode='nearest', + output=output, + extra_arguments=(self,), + ) + + def merge_nodes( + self, + src, + dst, + weight_func=min_weight, + in_place=True, + extra_arguments=None, + extra_keywords=None, + ): + """Merge node `src` and `dst`. + + The new combined node is adjacent to all the neighbors of `src` + and `dst`. `weight_func` is called to decide the weight of edges + incident on the new node. + + Parameters + ---------- + src, dst : int + Nodes to be merged. + weight_func : callable, optional + Function to decide the attributes of edges incident on the new + node. For each neighbor `n` for `src` and `dst`, `weight_func` will + be called as follows: `weight_func(src, dst, n, *extra_arguments, + **extra_keywords)`. `src`, `dst` and `n` are IDs of vertices in the + RAG object which is in turn a subclass of :obj:`networkx.Graph`. It is + expected to return a dict of attributes of the resulting edge. + in_place : bool, optional + If set to `True`, the merged node has the id `dst`, else merged + node has a new id which is returned. + extra_arguments : sequence, optional + The sequence of extra positional arguments passed to + `weight_func`. + extra_keywords : dictionary, optional + The dict of keyword arguments passed to the `weight_func`. + + Returns + ------- + id : int + The id of the new node. + + Notes + ----- + If `in_place` is `False` the resulting node has a new id, rather than + `dst`. + """ + if extra_arguments is None: + extra_arguments = [] + if extra_keywords is None: + extra_keywords = {} + + src_nbrs = set(self.neighbors(src)) + dst_nbrs = set(self.neighbors(dst)) + neighbors = (src_nbrs | dst_nbrs) - {src, dst} + + if in_place: + new = dst + else: + new = self.next_id() + self.add_node(new) + + for neighbor in neighbors: + data = weight_func( + self, src, dst, neighbor, *extra_arguments, **extra_keywords + ) + self.add_edge(neighbor, new, attr_dict=data) + + self.nodes[new]['labels'] = ( + self.nodes[src]['labels'] + self.nodes[dst]['labels'] + ) + self.remove_node(src) + + if not in_place: + self.remove_node(dst) + + return new + + def add_node(self, n, attr_dict=None, **attr): + """Add node `n` while updating the maximum node id. + + .. seealso:: :obj:`networkx.Graph.add_node`.""" + if attr_dict is None: # compatibility with old networkx + attr_dict = attr + else: + attr_dict.update(attr) + super().add_node(n, **attr_dict) + self.max_id = max(n, self.max_id) + + def add_edge(self, u, v, attr_dict=None, **attr): + """Add an edge between `u` and `v` while updating max node id. + + .. seealso:: :obj:`networkx.Graph.add_edge`.""" + if attr_dict is None: # compatibility with old networkx + attr_dict = attr + else: + attr_dict.update(attr) + super().add_edge(u, v, **attr_dict) + self.max_id = max(u, v, self.max_id) + + def copy(self): + """Copy the graph with its max node id. + + .. seealso:: :obj:`networkx.Graph.copy`.""" + g = super().copy() + g.max_id = self.max_id + return g + + def fresh_copy(self): + """Return a fresh copy graph with the same data structure. + + A fresh copy has no nodes, edges or graph attributes. It is + the same data structure as the current graph. This method is + typically used to create an empty version of the graph. + + This is required when subclassing Graph with networkx v2 and + does not cause problems for v1. Here is more detail from + the network migrating from 1.x to 2.x document:: + + With the new GraphViews (SubGraph, ReversedGraph, etc) + you can't assume that ``G.__class__()`` will create a new + instance of the same graph type as ``G``. In fact, the + call signature for ``__class__`` differs depending on + whether ``G`` is a view or a base class. For v2.x you + should use ``G.fresh_copy()`` to create a null graph of + the correct type---ready to fill with nodes and edges. + + """ + return RAG() + + def next_id(self): + """Returns the `id` for the new node to be inserted. + + The current implementation returns one more than the maximum `id`. + + Returns + ------- + id : int + The `id` of the new node to be inserted. + """ + return self.max_id + 1 + + def _add_node_silent(self, n): + """Add node `n` without updating the maximum node id. + + This is a convenience method used internally. + + .. seealso:: :obj:`networkx.Graph.add_node`.""" + super().add_node(n) + + +def rag_mean_color(image, labels, connectivity=2, mode='distance', sigma=255.0): + """Compute the Region Adjacency Graph using mean colors. + + Given an image and its initial segmentation, this method constructs the + corresponding Region Adjacency Graph (RAG). Each node in the RAG + represents a set of pixels within `image` with the same label in `labels`. + The weight between two adjacent regions represents how similar or + dissimilar two regions are depending on the `mode` parameter. + + Parameters + ---------- + image : ndarray, shape(M, N[, ..., P], 3) + Input image. + labels : ndarray, shape(M, N[, ..., P]) + The labelled image. This should have one dimension less than + `image`. If `image` has dimensions `(M, N, 3)` `labels` should have + dimensions `(M, N)`. + connectivity : int, optional + Pixels with a squared distance less than `connectivity` from each other + are considered adjacent. It can range from 1 to `labels.ndim`. Its + behavior is the same as `connectivity` parameter in + ``scipy.ndimage.generate_binary_structure``. + mode : {'distance', 'similarity'}, optional + The strategy to assign edge weights. + + 'distance' : The weight between two adjacent regions is the + :math:`|c_1 - c_2|`, where :math:`c_1` and :math:`c_2` are the mean + colors of the two regions. It represents the Euclidean distance in + their average color. + + 'similarity' : The weight between two adjacent is + :math:`e^{-d^2/sigma}` where :math:`d=|c_1 - c_2|`, where + :math:`c_1` and :math:`c_2` are the mean colors of the two regions. + It represents how similar two regions are. + sigma : float, optional + Used for computation when `mode` is "similarity". It governs how + close to each other two colors should be, for their corresponding edge + weight to be significant. A very large value of `sigma` could make + any two colors behave as though they were similar. + + Returns + ------- + out : RAG + The region adjacency graph. + + Examples + -------- + >>> from skimage import data, segmentation, graph + >>> img = data.astronaut() + >>> labels = segmentation.slic(img) + >>> rag = graph.rag_mean_color(img, labels) + + References + ---------- + .. [1] Alain Tremeau and Philippe Colantoni + "Regions Adjacency Graph Applied To Color Image Segmentation" + :DOI:`10.1109/83.841950` + """ + graph = RAG(labels, connectivity=connectivity) + + for n in graph: + graph.nodes[n].update( + { + 'labels': [n], + 'pixel count': 0, + 'total color': np.array([0, 0, 0], dtype=np.float64), + } + ) + + for index in np.ndindex(labels.shape): + current = labels[index] + graph.nodes[current]['pixel count'] += 1 + graph.nodes[current]['total color'] += image[index] + + for n in graph: + graph.nodes[n]['mean color'] = ( + graph.nodes[n]['total color'] / graph.nodes[n]['pixel count'] + ) + + for x, y, d in graph.edges(data=True): + diff = graph.nodes[x]['mean color'] - graph.nodes[y]['mean color'] + diff = np.linalg.norm(diff) + if mode == 'similarity': + d['weight'] = math.e ** (-(diff**2) / sigma) + elif mode == 'distance': + d['weight'] = diff + else: + raise ValueError(f"The mode '{mode}' is not recognised") + + return graph + + +def rag_boundary(labels, edge_map, connectivity=2): + """Comouter RAG based on region boundaries + + Given an image's initial segmentation and its edge map this method + constructs the corresponding Region Adjacency Graph (RAG). Each node in the + RAG represents a set of pixels within the image with the same label in + `labels`. The weight between two adjacent regions is the average value + in `edge_map` along their boundary. + + labels : ndarray + The labelled image. + edge_map : ndarray + This should have the same shape as that of `labels`. For all pixels + along the boundary between 2 adjacent regions, the average value of the + corresponding pixels in `edge_map` is the edge weight between them. + connectivity : int, optional + Pixels with a squared distance less than `connectivity` from each other + are considered adjacent. It can range from 1 to `labels.ndim`. Its + behavior is the same as `connectivity` parameter in + `scipy.ndimage.generate_binary_structure`. + + Examples + -------- + >>> from skimage import data, segmentation, filters, color, graph + >>> img = data.chelsea() + >>> labels = segmentation.slic(img) + >>> edge_map = filters.sobel(color.rgb2gray(img)) + >>> rag = graph.rag_boundary(labels, edge_map) + + """ + + conn = ndi.generate_binary_structure(labels.ndim, connectivity) + eroded = ndi.grey_erosion(labels, footprint=conn) + dilated = ndi.grey_dilation(labels, footprint=conn) + boundaries0 = eroded != labels + boundaries1 = dilated != labels + labels_small = np.concatenate((eroded[boundaries0], labels[boundaries1])) + labels_large = np.concatenate((labels[boundaries0], dilated[boundaries1])) + n = np.max(labels_large) + 1 + + # use a dummy broadcast array as data for RAG + ones = np.broadcast_to(1.0, labels_small.shape) + count_matrix = sparse.csr_array( + (ones, (labels_small, labels_large)), dtype=int, shape=(n, n) + ) + data = np.concatenate((edge_map[boundaries0], edge_map[boundaries1])) + + graph_matrix = sparse.csr_array((data, (labels_small, labels_large))) + graph_matrix.data /= count_matrix.data + + rag = RAG() + rag.add_weighted_edges_from(_edge_generator_from_csr(graph_matrix), weight='weight') + rag.add_weighted_edges_from(_edge_generator_from_csr(count_matrix), weight='count') + + for n in rag.nodes(): + rag.nodes[n].update({'labels': [n]}) + + return rag + + +@require("matplotlib", ">=3.3") +def show_rag( + labels, + rag, + image, + border_color='black', + edge_width=1.5, + edge_cmap='magma', + img_cmap='bone', + in_place=True, + ax=None, +): + """Show a Region Adjacency Graph on an image. + + Given a labelled image and its corresponding RAG, show the nodes and edges + of the RAG on the image with the specified colors. Edges are displayed between + the centroid of the 2 adjacent regions in the image. + + Parameters + ---------- + labels : ndarray, shape (M, N) + The labelled image. + rag : RAG + The Region Adjacency Graph. + image : ndarray, shape (M, N[, 3]) + Input image. If `colormap` is `None`, the image should be in RGB + format. + border_color : color spec, optional + Color with which the borders between regions are drawn. + edge_width : float, optional + The thickness with which the RAG edges are drawn. + edge_cmap : :py:class:`matplotlib.colors.Colormap`, optional + Any matplotlib colormap with which the edges are drawn. + img_cmap : :py:class:`matplotlib.colors.Colormap`, optional + Any matplotlib colormap with which the image is draw. If set to `None` + the image is drawn as it is. + in_place : bool, optional + If set, the RAG is modified in place. For each node `n` the function + will set a new attribute ``rag.nodes[n]['centroid']``. + ax : :py:class:`matplotlib.axes.Axes`, optional + The axes to draw on. If not specified, new axes are created and drawn + on. + + Returns + ------- + lc : :py:class:`matplotlib.collections.LineCollection` + A collection of lines that represent the edges of the graph. It can be + passed to the :meth:`matplotlib.figure.Figure.colorbar` function. + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('matplotlib') + + >>> from skimage import data, segmentation, graph + >>> import matplotlib.pyplot as plt + >>> + >>> img = data.coffee() + >>> labels = segmentation.slic(img) + >>> g = graph.rag_mean_color(img, labels) + >>> lc = graph.show_rag(labels, g, img) + >>> cbar = plt.colorbar(lc) + """ + from matplotlib import colors + from matplotlib import pyplot as plt + from matplotlib.collections import LineCollection + + if not in_place: + rag = rag.copy() + + if ax is None: + fig, ax = plt.subplots() + out = util.img_as_float(image, force_copy=True) + + if img_cmap is None: + if image.ndim < 3 or image.shape[2] not in [3, 4]: + msg = 'If colormap is `None`, an RGB or RGBA image should be given' + raise ValueError(msg) + # Ignore the alpha channel + out = image[:, :, :3] + else: + img_cmap = plt.get_cmap(img_cmap) + out = color.rgb2gray(image) + # Ignore the alpha channel + out = img_cmap(out)[:, :, :3] + + edge_cmap = plt.get_cmap(edge_cmap) + + # Handling the case where one node has multiple labels + # offset is 1 so that regionprops does not ignore 0 + offset = 1 + map_array = np.arange(labels.max() + 1) + for n, d in rag.nodes(data=True): + for label in d['labels']: + map_array[label] = offset + offset += 1 + + rag_labels = map_array[labels] + regions = measure.regionprops(rag_labels) + + for (n, data), region in zip(rag.nodes(data=True), regions): + data['centroid'] = tuple(map(int, region['centroid'])) + + cc = colors.ColorConverter() + if border_color is not None: + border_color = cc.to_rgb(border_color) + out = segmentation.mark_boundaries(out, rag_labels, color=border_color) + + ax.imshow(out) + + # Defining the end points of the edges + # The tuple[::-1] syntax reverses a tuple as matplotlib uses (x,y) + # convention while skimage uses (row, column) + lines = [ + [rag.nodes[n1]['centroid'][::-1], rag.nodes[n2]['centroid'][::-1]] + for (n1, n2) in rag.edges() + ] + + lc = LineCollection(lines, linewidths=edge_width, cmap=edge_cmap) + edge_weights = [d['weight'] for x, y, d in rag.edges(data=True)] + lc.set_array(np.array(edge_weights)) + ax.add_collection(lc) + + return lc diff --git a/lib/python3.10/site-packages/skimage/graph/mcp.py b/lib/python3.10/site-packages/skimage/graph/mcp.py new file mode 100644 index 0000000000000000000000000000000000000000..bf1669ebfe7b88c5b824747479df9771ed280b8b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/mcp.py @@ -0,0 +1,88 @@ +from ._mcp import MCP, MCP_Geometric, MCP_Connect, MCP_Flexible # noqa: F401 + + +def route_through_array(array, start, end, fully_connected=True, geometric=True): + """Simple example of how to use the MCP and MCP_Geometric classes. + + See the MCP and MCP_Geometric class documentation for explanation of the + path-finding algorithm. + + Parameters + ---------- + array : ndarray + Array of costs. + start : iterable + n-d index into `array` defining the starting point + end : iterable + n-d index into `array` defining the end point + fully_connected : bool (optional) + If True, diagonal moves are permitted, if False, only axial moves. + geometric : bool (optional) + If True, the MCP_Geometric class is used to calculate costs, if False, + the MCP base class is used. See the class documentation for + an explanation of the differences between MCP and MCP_Geometric. + + Returns + ------- + path : list + List of n-d index tuples defining the path from `start` to `end`. + cost : float + Cost of the path. If `geometric` is False, the cost of the path is + the sum of the values of `array` along the path. If `geometric` is + True, a finer computation is made (see the documentation of the + MCP_Geometric class). + + See Also + -------- + MCP, MCP_Geometric + + Examples + -------- + >>> import numpy as np + >>> from skimage.graph import route_through_array + >>> + >>> image = np.array([[1, 3], [10, 12]]) + >>> image + array([[ 1, 3], + [10, 12]]) + >>> # Forbid diagonal steps + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False) + ([(0, 0), (0, 1), (1, 1)], 9.5) + >>> # Now allow diagonal steps: the path goes directly from start to end + >>> route_through_array(image, [0, 0], [1, 1]) + ([(0, 0), (1, 1)], 9.19238815542512) + >>> # Cost is the sum of array values along the path (16 = 1 + 3 + 12) + >>> route_through_array(image, [0, 0], [1, 1], fully_connected=False, + ... geometric=False) + ([(0, 0), (0, 1), (1, 1)], 16.0) + >>> # Larger array where we display the path that is selected + >>> image = np.arange((36)).reshape((6, 6)) + >>> image + array([[ 0, 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29], + [30, 31, 32, 33, 34, 35]]) + >>> # Find the path with lowest cost + >>> indices, weight = route_through_array(image, (0, 0), (5, 5)) + >>> indices = np.stack(indices, axis=-1) + >>> path = np.zeros_like(image) + >>> path[indices[0], indices[1]] = 1 + >>> path + array([[1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]]) + + """ + start, end = tuple(start), tuple(end) + if geometric: + mcp_class = MCP_Geometric + else: + mcp_class = MCP + m = mcp_class(array, fully_connected=fully_connected) + costs, traceback_array = m.find_costs([start], [end]) + return m.traceback(end), costs[end] diff --git a/lib/python3.10/site-packages/skimage/graph/spath.py b/lib/python3.10/site-packages/skimage/graph/spath.py new file mode 100644 index 0000000000000000000000000000000000000000..4d0d8da405b4fabac90ba95873c28565dde25766 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/graph/spath.py @@ -0,0 +1,83 @@ +import numpy as np +from . import _spath + + +def shortest_path(arr, reach=1, axis=-1, output_indexlist=False): + """Find the shortest path through an n-d array from one side to another. + + Parameters + ---------- + arr : ndarray of float64 + reach : int, optional + By default (``reach = 1``), the shortest path can only move + one row up or down for every step it moves forward (i.e., + the path gradient is limited to 1). `reach` defines the + number of elements that can be skipped along each non-axis + dimension at each step. + axis : int, optional + The axis along which the path must always move forward (default -1) + output_indexlist : bool, optional + See return value `p` for explanation. + + Returns + ------- + p : iterable of int + For each step along `axis`, the coordinate of the shortest path. + If `output_indexlist` is True, then the path is returned as a list of + n-d tuples that index into `arr`. If False, then the path is returned + as an array listing the coordinates of the path along the non-axis + dimensions for each step along the axis dimension. That is, + `p.shape == (arr.shape[axis], arr.ndim-1)` except that p is squeezed + before returning so if `arr.ndim == 2`, then + `p.shape == (arr.shape[axis],)` + cost : float + Cost of path. This is the absolute sum of all the + differences along the path. + + """ + # First: calculate the valid moves from any given position. Basically, + # always move +1 along the given axis, and then can move anywhere within + # a grid defined by the reach. + if axis < 0: + axis += arr.ndim + offset_ind_shape = (2 * reach + 1,) * (arr.ndim - 1) + offset_indices = np.indices(offset_ind_shape) - reach + offset_indices = np.insert(offset_indices, axis, np.ones(offset_ind_shape), axis=0) + offset_size = np.multiply.reduce(offset_ind_shape) + offsets = np.reshape(offset_indices, (arr.ndim, offset_size), order='F').T + + # Valid starting positions are anywhere on the hyperplane defined by + # position 0 on the given axis. Ending positions are anywhere on the + # hyperplane at position -1 along the same. + non_axis_shape = arr.shape[:axis] + arr.shape[axis + 1 :] + non_axis_indices = np.indices(non_axis_shape) + non_axis_size = np.multiply.reduce(non_axis_shape) + start_indices = np.insert(non_axis_indices, axis, np.zeros(non_axis_shape), axis=0) + starts = np.reshape(start_indices, (arr.ndim, non_axis_size), order='F').T + end_indices = np.insert( + non_axis_indices, + axis, + np.full(non_axis_shape, -1, dtype=non_axis_indices.dtype), + axis=0, + ) + ends = np.reshape(end_indices, (arr.ndim, non_axis_size), order='F').T + + # Find the minimum-cost path to one of the end-points + m = _spath.MCP_Diff(arr, offsets=offsets) + costs, traceback = m.find_costs(starts, ends, find_all_ends=False) + + # Figure out which end-point was found + for end in ends: + cost = costs[tuple(end)] + if cost != np.inf: + break + traceback = m.traceback(end) + + if not output_indexlist: + traceback = np.array(traceback) + traceback = np.concatenate( + [traceback[:, :axis], traceback[:, axis + 1 :]], axis=1 + ) + traceback = np.squeeze(traceback) + + return traceback, cost diff --git a/lib/python3.10/site-packages/skimage/io/__init__.py b/lib/python3.10/site-packages/skimage/io/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..190c1e2f35f9b3090dd42d28e7ff8c3ccd709e8b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/__init__.py @@ -0,0 +1,43 @@ +"""Reading and saving of images and videos.""" + +import warnings + +from .manage_plugins import * +from .manage_plugins import _hide_plugin_deprecation_warnings +from .sift import * +from .collection import * + +from ._io import * +from ._image_stack import * + + +with _hide_plugin_deprecation_warnings(): + reset_plugins() + + +__all__ = [ + "concatenate_images", + "imread", + "imread_collection", + "imread_collection_wrapper", + "imsave", + "load_sift", + "load_surf", + "pop", + "push", + "ImageCollection", + "MultiImage", +] + + +def __getattr__(name): + if name == "available_plugins": + warnings.warn( + "`available_plugins` is deprecated since version 0.25 and will " + "be removed in version 0.27. Instead, use `imageio` or other " + "I/O packages directly.", + category=FutureWarning, + stacklevel=2, + ) + return globals()["_available_plugins"] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/lib/python3.10/site-packages/skimage/io/_image_stack.py b/lib/python3.10/site-packages/skimage/io/_image_stack.py new file mode 100644 index 0000000000000000000000000000000000000000..ca9896d5d866217b9b07ffc3010e8b6eb6b6842a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/_image_stack.py @@ -0,0 +1,35 @@ +import numpy as np + + +__all__ = ['image_stack', 'push', 'pop'] + + +# Shared image queue +image_stack = [] + + +def push(img): + """Push an image onto the shared image stack. + + Parameters + ---------- + img : ndarray + Image to push. + + """ + if not isinstance(img, np.ndarray): + raise ValueError("Can only push ndarrays to the image stack.") + + image_stack.append(img) + + +def pop(): + """Pop an image from the shared image stack. + + Returns + ------- + img : ndarray + Image popped from the stack. + + """ + return image_stack.pop() diff --git a/lib/python3.10/site-packages/skimage/io/_io.py b/lib/python3.10/site-packages/skimage/io/_io.py new file mode 100644 index 0000000000000000000000000000000000000000..96f4f80d50fdd33f52e17eb09b58ef7f440be57e --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/_io.py @@ -0,0 +1,289 @@ +import pathlib +import warnings + +import numpy as np + +from .._shared.utils import warn, deprecate_func, deprecate_parameter, DEPRECATED +from .._shared.version_requirements import require +from ..exposure import is_low_contrast +from ..color.colorconv import rgb2gray, rgba2rgb +from ..io.manage_plugins import call_plugin, _hide_plugin_deprecation_warnings +from .util import file_or_url_context + +__all__ = [ + 'imread', + 'imsave', + 'imshow', + 'show', + 'imread_collection', + 'imshow_collection', +] + + +_remove_plugin_param_template = ( + "The plugin infrastructure in `skimage.io` and the parameter " + "`{deprecated_name}` are deprecated since version {deprecated_version} and " + "will be removed in {changed_version} (or later). To avoid this warning, " + "please do not use the parameter `{deprecated_name}`. Instead, use `imageio` " + "or other I/O packages directly. See also `{func_name}`." +) + + +@deprecate_parameter( + "plugin", + start_version="0.25", + stop_version="0.27", + template=_remove_plugin_param_template, +) +def imread(fname, as_gray=False, plugin=DEPRECATED, **plugin_args): + """Load an image from file. + + Parameters + ---------- + fname : str or pathlib.Path + Image file name, e.g. ``test.jpg`` or URL. + as_gray : bool, optional + If True, convert color images to gray-scale (64-bit floats). + Images that are already in gray-scale format are not converted. + + Other Parameters + ---------------- + plugin_args : DEPRECATED + The plugin infrastructure is deprecated. + + Returns + ------- + img_array : ndarray + The different color bands/channels are stored in the + third dimension, such that a gray-image is MxN, an + RGB-image MxNx3 and an RGBA-image MxNx4. + + """ + if plugin is DEPRECATED: + plugin = None + if plugin_args: + msg = ( + "The plugin infrastructure in `skimage.io` is deprecated since " + "version 0.25 and will be removed in 0.27 (or later). To avoid " + "this warning, please do not pass additional keyword arguments " + "for plugins (`**plugin_args`). Instead, use `imageio` or other " + "I/O packages directly. See also `skimage.io.imread`." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=3) + + if isinstance(fname, pathlib.Path): + fname = str(fname.resolve()) + + if plugin is None and hasattr(fname, 'lower'): + if fname.lower().endswith(('.tiff', '.tif')): + plugin = 'tifffile' + + with file_or_url_context(fname) as fname, _hide_plugin_deprecation_warnings(): + img = call_plugin('imread', fname, plugin=plugin, **plugin_args) + + if not hasattr(img, 'ndim'): + return img + + if img.ndim > 2: + if img.shape[-1] not in (3, 4) and img.shape[-3] in (3, 4): + img = np.swapaxes(img, -1, -3) + img = np.swapaxes(img, -2, -3) + + if as_gray: + if img.shape[2] == 4: + img = rgba2rgb(img) + img = rgb2gray(img) + + return img + + +@deprecate_parameter( + "plugin", + start_version="0.25", + stop_version="0.27", + template=_remove_plugin_param_template, +) +def imread_collection( + load_pattern, conserve_memory=True, plugin=DEPRECATED, **plugin_args +): + """ + Load a collection of images. + + Parameters + ---------- + load_pattern : str or list + List of objects to load. These are usually filenames, but may + vary depending on the currently active plugin. See :class:`ImageCollection` + for the default behaviour of this parameter. + conserve_memory : bool, optional + If True, never keep more than one in memory at a specific + time. Otherwise, images will be cached once they are loaded. + + Returns + ------- + ic : :class:`ImageCollection` + Collection of images. + + Other Parameters + ---------------- + plugin_args : DEPRECATED + The plugin infrastructure is deprecated. + + """ + if plugin is DEPRECATED: + plugin = None + if plugin_args: + msg = ( + "The plugin infrastructure in `skimage.io` is deprecated since " + "version 0.25 and will be removed in 0.27 (or later). To avoid " + "this warning, please do not pass additional keyword arguments " + "for plugins (`**plugin_args`). Instead, use `imageio` or other " + "I/O packages directly. See also `skimage.io.imread_collection`." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=3) + with _hide_plugin_deprecation_warnings(): + return call_plugin( + 'imread_collection', + load_pattern, + conserve_memory, + plugin=plugin, + **plugin_args, + ) + + +@deprecate_parameter( + "plugin", + start_version="0.25", + stop_version="0.27", + template=_remove_plugin_param_template, +) +def imsave(fname, arr, plugin=DEPRECATED, *, check_contrast=True, **plugin_args): + """Save an image to file. + + Parameters + ---------- + fname : str or pathlib.Path + Target filename. + arr : ndarray of shape (M,N) or (M,N,3) or (M,N,4) + Image data. + check_contrast : bool, optional + Check for low contrast and print warning (default: True). + + Other Parameters + ---------------- + plugin_args : DEPRECATED + The plugin infrastructure is deprecated. + """ + if plugin is DEPRECATED: + plugin = None + if plugin_args: + msg = ( + "The plugin infrastructure in `skimage.io` is deprecated since " + "version 0.25 and will be removed in 0.27 (or later). To avoid " + "this warning, please do not pass additional keyword arguments " + "for plugins (`**plugin_args`). Instead, use `imageio` or other " + "I/O packages directly. See also `skimage.io.imsave`." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=3) + + if isinstance(fname, pathlib.Path): + fname = str(fname.resolve()) + if plugin is None and hasattr(fname, 'lower'): + if fname.lower().endswith(('.tiff', '.tif')): + plugin = 'tifffile' + if arr.dtype == bool: + warn( + f'{fname} is a boolean image: setting True to 255 and False to 0. ' + 'To silence this warning, please convert the image using ' + 'img_as_ubyte.', + stacklevel=3, + ) + arr = arr.astype('uint8') * 255 + if check_contrast and is_low_contrast(arr): + warn(f'{fname} is a low contrast image') + + with _hide_plugin_deprecation_warnings(): + return call_plugin('imsave', fname, arr, plugin=plugin, **plugin_args) + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Please use `matplotlib`, `napari`, etc. to visualize images.", +) +def imshow(arr, plugin=None, **plugin_args): + """Display an image. + + Parameters + ---------- + arr : ndarray or str + Image data or name of image file. + plugin : str + Name of plugin to use. By default, the different plugins are + tried (starting with imageio) until a suitable candidate is found. + + Other Parameters + ---------------- + plugin_args : keywords + Passed to the given plugin. + + """ + if isinstance(arr, str): + arr = call_plugin('imread', arr, plugin=plugin) + with _hide_plugin_deprecation_warnings(): + return call_plugin('imshow', arr, plugin=plugin, **plugin_args) + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Please use `matplotlib`, `napari`, etc. to visualize images.", +) +def imshow_collection(ic, plugin=None, **plugin_args): + """Display a collection of images. + + Parameters + ---------- + ic : :class:`ImageCollection` + Collection to display. + + Other Parameters + ---------------- + plugin_args : keywords + Passed to the given plugin. + + """ + with _hide_plugin_deprecation_warnings(): + return call_plugin('imshow_collection', ic, plugin=plugin, **plugin_args) + + +@require("matplotlib", ">=3.3") +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Please use `matplotlib`, `napari`, etc. to visualize images.", +) +def show(): + """Display pending images. + + Launch the event loop of the current GUI plugin, and display all + pending images, queued via `imshow`. This is required when using + `imshow` from non-interactive scripts. + + A call to `show` will block execution of code until all windows + have been closed. + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('matplotlib') + + >>> import skimage.io as io + >>> rng = np.random.default_rng() + >>> for i in range(4): + ... ax_im = io.imshow(rng.random((50, 50))) # doctest: +SKIP + >>> io.show() # doctest: +SKIP + + """ + with _hide_plugin_deprecation_warnings(): + return call_plugin('_app_show') diff --git a/lib/python3.10/site-packages/skimage/io/collection.py b/lib/python3.10/site-packages/skimage/io/collection.py new file mode 100644 index 0000000000000000000000000000000000000000..c87c5a552b01349a30abec308c0eb65d1e079272 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/collection.py @@ -0,0 +1,493 @@ +"""Data structures to hold collections of images, with optional caching.""" + +import os +from glob import glob +import re +from collections.abc import Sequence +from copy import copy + +import numpy as np +from PIL import Image + +from tifffile import TiffFile + + +__all__ = [ + 'MultiImage', + 'ImageCollection', + 'concatenate_images', + 'imread_collection_wrapper', +] + + +def concatenate_images(ic): + """Concatenate all images in the image collection into an array. + + Parameters + ---------- + ic : an iterable of images + The images to be concatenated. + + Returns + ------- + array_cat : ndarray + An array having one more dimension than the images in `ic`. + + See Also + -------- + ImageCollection.concatenate + MultiImage.concatenate + + Raises + ------ + ValueError + If images in `ic` don't have identical shapes. + + Notes + ----- + ``concatenate_images`` receives any iterable object containing images, + including ImageCollection and MultiImage, and returns a NumPy array. + """ + all_images = [image[np.newaxis, ...] for image in ic] + try: + array_cat = np.concatenate(all_images) + except ValueError: + raise ValueError('Image dimensions must agree.') + return array_cat + + +def alphanumeric_key(s): + """Convert string to list of strings and ints that gives intuitive sorting. + + Parameters + ---------- + s : string + + Returns + ------- + k : a list of strings and ints + + Examples + -------- + >>> alphanumeric_key('z23a') + ['z', 23, 'a'] + >>> filenames = ['f9.10.png', 'e10.png', 'f9.9.png', 'f10.10.png', + ... 'f10.9.png'] + >>> sorted(filenames) + ['e10.png', 'f10.10.png', 'f10.9.png', 'f9.10.png', 'f9.9.png'] + >>> sorted(filenames, key=alphanumeric_key) + ['e10.png', 'f9.9.png', 'f9.10.png', 'f10.9.png', 'f10.10.png'] + """ + k = [int(c) if c.isdigit() else c for c in re.split('([0-9]+)', s)] + return k + + +def _is_multipattern(input_pattern): + """Helping function. Returns True if pattern contains a tuple, list, or a + string separated with os.pathsep.""" + # Conditions to be accepted by ImageCollection: + has_str_ospathsep = isinstance(input_pattern, str) and os.pathsep in input_pattern + not_a_string = not isinstance(input_pattern, str) + has_iterable = isinstance(input_pattern, Sequence) + has_strings = all(isinstance(pat, str) for pat in input_pattern) + + is_multipattern = has_str_ospathsep or ( + not_a_string and has_iterable and has_strings + ) + return is_multipattern + + +class ImageCollection: + """Load and manage a collection of image files. + + Parameters + ---------- + load_pattern : str or list of str + Pattern string or list of strings to load. The filename path can be + absolute or relative. + conserve_memory : bool, optional + If True, :class:`skimage.io.ImageCollection` does not keep more than one in + memory at a specific time. Otherwise, images will be cached once they are loaded. + + Other parameters + ---------------- + load_func : callable + ``imread`` by default. See Notes below. + **load_func_kwargs : dict + Any other keyword arguments are passed to `load_func`. + + Attributes + ---------- + files : list of str + If a pattern string is given for `load_pattern`, this attribute + stores the expanded file list. Otherwise, this is equal to + `load_pattern`. + + Notes + ----- + Note that files are always returned in alphanumerical order. Also note that slicing + returns a new :class:`skimage.io.ImageCollection`, *not* a view into the data. + + ImageCollection image loading can be customized through + `load_func`. For an ImageCollection ``ic``, ``ic[5]`` calls + ``load_func(load_pattern[5])`` to load that image. + + For example, here is an ImageCollection that, for each video provided, + loads every second frame:: + + import imageio.v3 as iio3 + import itertools + + def vidread_step(f, step): + vid = iio3.imiter(f) + return list(itertools.islice(vid, None, None, step) + + video_file = 'no_time_for_that_tiny.gif' + ic = ImageCollection(video_file, load_func=vidread_step, step=2) + + ic # is an ImageCollection object of length 1 because 1 video is provided + + x = ic[0] + x[5] # the 10th frame of the first video + + Alternatively, if `load_func` is provided and `load_pattern` is a + sequence, an :class:`skimage.io.ImageCollection` of corresponding length will + be created, and the individual images will be loaded by calling `load_func` with the + matching element of the `load_pattern` as its first argument. In this + case, the elements of the sequence do not need to be names of existing + files (or strings at all). For example, to create an :class:`skimage.io.ImageCollection` + containing 500 images from a video:: + + class FrameReader: + def __init__ (self, f): + self.f = f + def __call__ (self, index): + return iio3.imread(self.f, index=index) + + ic = ImageCollection(range(500), load_func=FrameReader('movie.mp4')) + + ic # is an ImageCollection object of length 500 + + Another use of `load_func` would be to convert all images to ``uint8``:: + + def imread_convert(f): + return imread(f).astype(np.uint8) + + ic = ImageCollection('/tmp/*.png', load_func=imread_convert) + + Examples + -------- + >>> import imageio.v3 as iio3 + >>> import skimage.io as io + + # Where your images are located + >>> data_dir = os.path.join(os.path.dirname(__file__), '../data') + + >>> coll = io.ImageCollection(data_dir + '/chess*.png') + >>> len(coll) + 2 + >>> coll[0].shape + (200, 200) + + >>> image_col = io.ImageCollection([f'{data_dir}/*.png', '{data_dir}/*.jpg']) + + >>> class MultiReader: + ... def __init__ (self, f): + ... self.f = f + ... def __call__ (self, index): + ... return iio3.imread(self.f, index=index) + ... + >>> filename = data_dir + '/no_time_for_that_tiny.gif' + >>> ic = io.ImageCollection(range(24), load_func=MultiReader(filename)) + >>> len(image_col) + 23 + >>> isinstance(ic[0], np.ndarray) + True + """ + + def __init__( + self, load_pattern, conserve_memory=True, load_func=None, **load_func_kwargs + ): + """Load and manage a collection of images.""" + self._files = [] + if _is_multipattern(load_pattern): + if isinstance(load_pattern, str): + load_pattern = load_pattern.split(os.pathsep) + for pattern in load_pattern: + self._files.extend(glob(pattern)) + self._files = sorted(self._files, key=alphanumeric_key) + elif isinstance(load_pattern, str): + self._files.extend(glob(load_pattern)) + self._files = sorted(self._files, key=alphanumeric_key) + elif isinstance(load_pattern, Sequence) and load_func is not None: + self._files = list(load_pattern) + else: + raise TypeError('Invalid pattern as input.') + + if load_func is None: + from ._io import imread + + self.load_func = imread + self._numframes = self._find_images() + else: + self.load_func = load_func + self._numframes = len(self._files) + self._frame_index = None + + if conserve_memory: + memory_slots = 1 + else: + memory_slots = self._numframes + + self._conserve_memory = conserve_memory + self._cached = None + + self.load_func_kwargs = load_func_kwargs + self.data = np.empty(memory_slots, dtype=object) + + @property + def files(self): + return self._files + + @property + def conserve_memory(self): + return self._conserve_memory + + def _find_images(self): + index = [] + for fname in self._files: + if fname.lower().endswith(('.tiff', '.tif')): + with open(fname, 'rb') as f: + img = TiffFile(f) + index += [(fname, i) for i in range(len(img.pages))] + else: + try: + im = Image.open(fname) + im.seek(0) + except OSError: + continue + i = 0 + while True: + try: + im.seek(i) + except EOFError: + break + index.append((fname, i)) + i += 1 + if hasattr(im, 'fp') and im.fp: + im.fp.close() + self._frame_index = index + return len(index) + + def __getitem__(self, n): + """Return selected image(s) in the collection. + + Loading is done on demand. + + Parameters + ---------- + n : int or slice + The image number to be returned, or a slice selecting the images + and ordering to be returned in a new ImageCollection. + + Returns + ------- + img : ndarray or :class:`skimage.io.ImageCollection` + The `n`-th image in the collection, or a new ImageCollection with + the selected images. + """ + if hasattr(n, '__index__'): + n = n.__index__() + + if not isinstance(n, (int, slice)): + raise TypeError('slicing must be with an int or slice object') + + if isinstance(n, int): + n = self._check_imgnum(n) + idx = n % len(self.data) + + if (self.conserve_memory and n != self._cached) or (self.data[idx] is None): + kwargs = self.load_func_kwargs + if self._frame_index: + fname, img_num = self._frame_index[n] + if img_num is not None: + kwargs['img_num'] = img_num + try: + self.data[idx] = self.load_func(fname, **kwargs) + # Account for functions that do not accept an img_num kwarg + except TypeError as e: + if "unexpected keyword argument 'img_num'" in str(e): + del kwargs['img_num'] + self.data[idx] = self.load_func(fname, **kwargs) + else: + raise + else: + self.data[idx] = self.load_func(self.files[n], **kwargs) + self._cached = n + + return self.data[idx] + else: + # A slice object was provided, so create a new ImageCollection + # object. Any loaded image data in the original ImageCollection + # will be copied by reference to the new object. Image data + # loaded after this creation is not linked. + fidx = range(self._numframes)[n] + new_ic = copy(self) + + if self._frame_index: + new_ic._files = [self._frame_index[i][0] for i in fidx] + new_ic._frame_index = [self._frame_index[i] for i in fidx] + else: + new_ic._files = [self._files[i] for i in fidx] + + new_ic._numframes = len(fidx) + + if self.conserve_memory: + if self._cached in fidx: + new_ic._cached = fidx.index(self._cached) + new_ic.data = np.copy(self.data) + else: + new_ic.data = np.empty(1, dtype=object) + else: + new_ic.data = self.data[fidx] + return new_ic + + def _check_imgnum(self, n): + """Check that the given image number is valid.""" + num = self._numframes + if -num <= n < num: + n = n % num + else: + raise IndexError(f"There are only {num} images in the collection") + return n + + def __iter__(self): + """Iterate over the images.""" + for i in range(len(self)): + yield self[i] + + def __len__(self): + """Number of images in collection.""" + return self._numframes + + def __str__(self): + return str(self.files) + + def reload(self, n=None): + """Clear the image cache. + + Parameters + ---------- + n : None or int + Clear the cache for this image only. By default, the + entire cache is erased. + + """ + self.data = np.empty_like(self.data) + + def concatenate(self): + """Concatenate all images in the collection into an array. + + Returns + ------- + ar : np.ndarray + An array having one more dimension than the images in `self`. + + See Also + -------- + skimage.io.concatenate_images + + Raises + ------ + ValueError + If images in the :class:`skimage.io.ImageCollection` do not have identical + shapes. + """ + return concatenate_images(self) + + +def imread_collection_wrapper(imread): + def imread_collection(load_pattern, conserve_memory=True): + """Return an `ImageCollection` from files matching the given pattern. + + Note that files are always stored in alphabetical order. Also note that + slicing returns a new ImageCollection, *not* a view into the data. + + See `skimage.io.ImageCollection` for details. + + Parameters + ---------- + load_pattern : str or list + Pattern glob or filenames to load. The path can be absolute or + relative. Multiple patterns should be separated by a colon, + e.g. ``/tmp/work/*.png:/tmp/other/*.jpg``. Also see + implementation notes below. + conserve_memory : bool, optional + If True, never keep more than one in memory at a specific + time. Otherwise, images will be cached once they are loaded. + + """ + return ImageCollection( + load_pattern, conserve_memory=conserve_memory, load_func=imread + ) + + return imread_collection + + +class MultiImage(ImageCollection): + """A class containing all frames from multi-frame TIFF images. + + Parameters + ---------- + load_pattern : str or list of str + Pattern glob or filenames to load. The path can be absolute or + relative. + conserve_memory : bool, optional + Whether to conserve memory by only caching the frames of a single + image. Default is True. + + Notes + ----- + `MultiImage` returns a list of image-data arrays. In this + regard, it is very similar to `ImageCollection`, but the two differ in + their treatment of multi-frame images. + + For a TIFF image containing N frames of size WxH, `MultiImage` stores + all frames of that image as a single element of shape `(N, W, H)` in the + list. `ImageCollection` instead creates N elements of shape `(W, H)`. + + For an animated GIF image, `MultiImage` reads only the first frame, while + `ImageCollection` reads all frames by default. + + Examples + -------- + # Where your images are located + >>> data_dir = os.path.join(os.path.dirname(__file__), '../data') + + >>> multipage_tiff = data_dir + '/multipage.tif' + >>> multi_img = MultiImage(multipage_tiff) + >>> len(multi_img) # multi_img contains one element + 1 + >>> multi_img[0].shape # this element is a two-frame image of shape: + (2, 15, 10) + + >>> image_col = ImageCollection(multipage_tiff) + >>> len(image_col) # image_col contains two elements + 2 + >>> for frame in image_col: + ... print(frame.shape) # each element is a frame of shape (15, 10) + ... + (15, 10) + (15, 10) + """ + + def __init__(self, filename, conserve_memory=True, dtype=None, **imread_kwargs): + """Load a multi-img.""" + from ._io import imread + + self._filename = filename + super().__init__(filename, conserve_memory, load_func=imread, **imread_kwargs) + + @property + def filename(self): + return self._filename diff --git a/lib/python3.10/site-packages/skimage/io/manage_plugins.py b/lib/python3.10/site-packages/skimage/io/manage_plugins.py new file mode 100644 index 0000000000000000000000000000000000000000..3d0d3b25c77c8ba98d7f56bc8e66b569c59937af --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/manage_plugins.py @@ -0,0 +1,405 @@ +"""Handle image reading, writing and plotting plugins. + +To improve performance, plugins are only loaded as needed. As a result, there +can be multiple states for a given plugin: + + available: Defined in an *ini file located in ``skimage.io._plugins``. + See also :func:`skimage.io.available_plugins`. + partial definition: Specified in an *ini file, but not defined in the + corresponding plugin module. This will raise an error when loaded. + available but not on this system: Defined in ``skimage.io._plugins``, but + a dependent library (e.g. Qt, PIL) is not available on your system. + This will raise an error when loaded. + loaded: The real availability is determined when it's explicitly loaded, + either because it's one of the default plugins, or because it's + loaded explicitly by the user. + +""" + +import os.path +import warnings +from configparser import ConfigParser +from glob import glob +from contextlib import contextmanager + +from .._shared.utils import deprecate_func +from .collection import imread_collection_wrapper + +__all__ = [ + 'use_plugin', + 'call_plugin', + 'plugin_info', + 'plugin_order', + 'reset_plugins', + 'find_available_plugins', + '_available_plugins', +] + +# The plugin store will save a list of *loaded* io functions for each io type +# (e.g. 'imread', 'imsave', etc.). Plugins are loaded as requested. +plugin_store = None +# Dictionary mapping plugin names to a list of functions they provide. +plugin_provides = {} +# The module names for the plugins in `skimage.io._plugins`. +plugin_module_name = {} +# Meta-data about plugins provided by *.ini files. +plugin_meta_data = {} +# For each plugin type, default to the first available plugin as defined by +# the following preferences. +preferred_plugins = { + # Default plugins for all types (overridden by specific types below). + 'all': ['imageio', 'pil', 'matplotlib'], + 'imshow': ['matplotlib'], + 'imshow_collection': ['matplotlib'], +} + + +@contextmanager +def _hide_plugin_deprecation_warnings(): + """Ignore warnings related to plugin infrastructure deprecation.""" + with warnings.catch_warnings(): + warnings.filterwarnings( + action="ignore", + message=".*use `imageio` or other I/O packages directly.*", + category=FutureWarning, + module="skimage", + ) + yield + + +def _clear_plugins(): + """Clear the plugin state to the default, i.e., where no plugins are loaded""" + global plugin_store + plugin_store = { + 'imread': [], + 'imsave': [], + 'imshow': [], + 'imread_collection': [], + 'imshow_collection': [], + '_app_show': [], + } + + +with _hide_plugin_deprecation_warnings(): + _clear_plugins() + + +def _load_preferred_plugins(): + # Load preferred plugin for each io function. + io_types = ['imsave', 'imshow', 'imread_collection', 'imshow_collection', 'imread'] + for p_type in io_types: + _set_plugin(p_type, preferred_plugins['all']) + + plugin_types = (p for p in preferred_plugins.keys() if p != 'all') + for p_type in plugin_types: + _set_plugin(p_type, preferred_plugins[p_type]) + + +def _set_plugin(plugin_type, plugin_list): + for plugin in plugin_list: + if plugin not in _available_plugins: + continue + try: + use_plugin(plugin, kind=plugin_type) + break + except (ImportError, RuntimeError, OSError): + pass + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def reset_plugins(): + with _hide_plugin_deprecation_warnings(): + _clear_plugins() + _load_preferred_plugins() + + +def _parse_config_file(filename): + """Return plugin name and meta-data dict from plugin config file.""" + parser = ConfigParser() + parser.read(filename) + name = parser.sections()[0] + + meta_data = {} + for opt in parser.options(name): + meta_data[opt] = parser.get(name, opt) + + return name, meta_data + + +def _scan_plugins(): + """Scan the plugins directory for .ini files and parse them + to gather plugin meta-data. + """ + pd = os.path.dirname(__file__) + config_files = glob(os.path.join(pd, '_plugins', '*.ini')) + + for filename in config_files: + name, meta_data = _parse_config_file(filename) + if 'provides' not in meta_data: + warnings.warn( + f'file {filename} not recognized as a scikit-image io plugin, skipping.' + ) + continue + plugin_meta_data[name] = meta_data + provides = [s.strip() for s in meta_data['provides'].split(',')] + valid_provides = [p for p in provides if p in plugin_store] + + for p in provides: + if p not in plugin_store: + print(f"Plugin `{name}` wants to provide non-existent `{p}`. Ignoring.") + + # Add plugins that provide 'imread' as provider of 'imread_collection'. + need_to_add_collection = ( + 'imread_collection' not in valid_provides and 'imread' in valid_provides + ) + if need_to_add_collection: + valid_provides.append('imread_collection') + + plugin_provides[name] = valid_provides + + plugin_module_name[name] = os.path.basename(filename)[:-4] + + +with _hide_plugin_deprecation_warnings(): + _scan_plugins() + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def find_available_plugins(loaded=False): + """List available plugins. + + Parameters + ---------- + loaded : bool + If True, show only those plugins currently loaded. By default, + all plugins are shown. + + Returns + ------- + p : dict + Dictionary with plugin names as keys and exposed functions as + values. + + """ + active_plugins = set() + for plugin_func in plugin_store.values(): + for plugin, func in plugin_func: + active_plugins.add(plugin) + + d = {} + for plugin in plugin_provides: + if not loaded or plugin in active_plugins: + d[plugin] = [f for f in plugin_provides[plugin] if not f.startswith('_')] + + return d + + +with _hide_plugin_deprecation_warnings(): + _available_plugins = find_available_plugins() + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def call_plugin(kind, *args, **kwargs): + """Find the appropriate plugin of 'kind' and execute it. + + Parameters + ---------- + kind : {'imshow', 'imsave', 'imread', 'imread_collection'} + Function to look up. + plugin : str, optional + Plugin to load. Defaults to None, in which case the first + matching plugin is used. + *args, **kwargs : arguments and keyword arguments + Passed to the plugin function. + + """ + if kind not in plugin_store: + raise ValueError(f'Invalid function ({kind}) requested.') + + plugin_funcs = plugin_store[kind] + if len(plugin_funcs) == 0: + msg = ( + f"No suitable plugin registered for {kind}.\n\n" + "You may load I/O plugins with the `skimage.io.use_plugin` " + "command. A list of all available plugins are shown in the " + "`skimage.io` docstring." + ) + raise RuntimeError(msg) + + plugin = kwargs.pop('plugin', None) + if plugin is None: + _, func = plugin_funcs[0] + else: + _load(plugin) + try: + func = [f for (p, f) in plugin_funcs if p == plugin][0] + except IndexError: + raise RuntimeError(f'Could not find the plugin "{plugin}" for {kind}.') + + return func(*args, **kwargs) + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def use_plugin(name, kind=None): + """Set the default plugin for a specified operation. The plugin + will be loaded if it hasn't been already. + + Parameters + ---------- + name : str + Name of plugin. See ``skimage.io.available_plugins`` for a list of available + plugins. + kind : {'imsave', 'imread', 'imshow', 'imread_collection', 'imshow_collection'}, optional + Set the plugin for this function. By default, + the plugin is set for all functions. + + Examples + -------- + To use Matplotlib as the default image reader, you would write: + + >>> from skimage import io + >>> io.use_plugin('matplotlib', 'imread') + + To see a list of available plugins run ``skimage.io.available_plugins``. Note + that this lists plugins that are defined, but the full list may not be usable + if your system does not have the required libraries installed. + + """ + if kind is None: + kind = plugin_store.keys() + else: + if kind not in plugin_provides[name]: + raise RuntimeError(f"Plugin {name} does not support `{kind}`.") + + if kind == 'imshow': + kind = [kind, '_app_show'] + else: + kind = [kind] + + _load(name) + + for k in kind: + if k not in plugin_store: + raise RuntimeError(f"'{k}' is not a known plugin function.") + + funcs = plugin_store[k] + + # Shuffle the plugins so that the requested plugin stands first + # in line + funcs = [(n, f) for (n, f) in funcs if n == name] + [ + (n, f) for (n, f) in funcs if n != name + ] + + plugin_store[k] = funcs + + +def _inject_imread_collection_if_needed(module): + """Add `imread_collection` to module if not already present.""" + if not hasattr(module, 'imread_collection') and hasattr(module, 'imread'): + imread = getattr(module, 'imread') + func = imread_collection_wrapper(imread) + setattr(module, 'imread_collection', func) + + +@_hide_plugin_deprecation_warnings() +def _load(plugin): + """Load the given plugin. + + Parameters + ---------- + plugin : str + Name of plugin to load. + + See Also + -------- + plugins : List of available plugins + + """ + if plugin in find_available_plugins(loaded=True): + return + if plugin not in plugin_module_name: + raise ValueError(f"Plugin {plugin} not found.") + else: + modname = plugin_module_name[plugin] + plugin_module = __import__('skimage.io._plugins.' + modname, fromlist=[modname]) + + provides = plugin_provides[plugin] + for p in provides: + if p == 'imread_collection': + _inject_imread_collection_if_needed(plugin_module) + elif not hasattr(plugin_module, p): + print(f"Plugin {plugin} does not provide {p} as advertised. Ignoring.") + continue + + store = plugin_store[p] + func = getattr(plugin_module, p) + if (plugin, func) not in store: + store.append((plugin, func)) + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def plugin_info(plugin): + """Return plugin meta-data. + + Parameters + ---------- + plugin : str + Name of plugin. + + Returns + ------- + m : dict + Meta data as specified in plugin ``.ini``. + + """ + try: + return plugin_meta_data[plugin] + except KeyError: + raise ValueError(f'No information on plugin "{plugin}"') + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="The plugin infrastructure of `skimage.io` is deprecated. " + "Instead, use `imageio` or other I/O packages directly.", +) +def plugin_order(): + """Return the currently preferred plugin order. + + Returns + ------- + p : dict + Dictionary of preferred plugin order, with function name as key and + plugins (in order of preference) as value. + + """ + p = {} + for func in plugin_store: + p[func] = [plugin_name for (plugin_name, f) in plugin_store[func]] + return p diff --git a/lib/python3.10/site-packages/skimage/io/sift.py b/lib/python3.10/site-packages/skimage/io/sift.py new file mode 100644 index 0000000000000000000000000000000000000000..d3789678260160dc9d980701ed31b3e063e630f7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/sift.py @@ -0,0 +1,93 @@ +import numpy as np + +__all__ = ['load_sift', 'load_surf'] + + +def _sift_read(filelike, mode='SIFT'): + """Read SIFT or SURF features from externally generated file. + + This routine reads SIFT or SURF files generated by binary utilities from + http://people.cs.ubc.ca/~lowe/keypoints/ and + http://www.vision.ee.ethz.ch/~surf/. + + This routine *does not* generate SIFT/SURF features from an image. These + algorithms are patent encumbered. Please use :obj:`skimage.feature.CENSURE` + instead. + + Parameters + ---------- + filelike : string or open file + Input file generated by the feature detectors from + http://people.cs.ubc.ca/~lowe/keypoints/ or + http://www.vision.ee.ethz.ch/~surf/ . + mode : {'SIFT', 'SURF'}, optional + Kind of descriptor used to generate `filelike`. + + Returns + ------- + data : record array with fields + - row: int + row position of feature + - column: int + column position of feature + - scale: float + feature scale + - orientation: float + feature orientation + - data: array + feature values + + """ + if isinstance(filelike, str): + f = open(filelike) + filelike_is_str = True + else: + f = filelike + filelike_is_str = False + + if mode == 'SIFT': + nr_features, feature_len = map(int, f.readline().split()) + datatype = np.dtype( + [ + ('row', float), + ('column', float), + ('scale', float), + ('orientation', float), + ('data', (float, feature_len)), + ] + ) + else: + mode = 'SURF' + feature_len = int(f.readline()) - 1 + nr_features = int(f.readline()) + datatype = np.dtype( + [ + ('column', float), + ('row', float), + ('second_moment', (float, 3)), + ('sign', float), + ('data', (float, feature_len)), + ] + ) + + data = np.fromfile(f, sep=' ') + if data.size != nr_features * datatype.itemsize / np.dtype(float).itemsize: + raise OSError(f'Invalid {mode} feature file.') + + # If `filelike` is passed to the function as filename - close the file + if filelike_is_str: + f.close() + + return data.view(datatype) + + +def load_sift(f): + return _sift_read(f, mode='SIFT') + + +def load_surf(f): + return _sift_read(f, mode='SURF') + + +load_sift.__doc__ = _sift_read.__doc__ +load_surf.__doc__ = _sift_read.__doc__ diff --git a/lib/python3.10/site-packages/skimage/io/util.py b/lib/python3.10/site-packages/skimage/io/util.py new file mode 100644 index 0000000000000000000000000000000000000000..921e22a4e2cf01224ab78246e6bccac538807707 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/io/util.py @@ -0,0 +1,41 @@ +import urllib.parse +import urllib.request +from urllib.error import URLError, HTTPError + +import os +import re +import tempfile +from contextlib import contextmanager + + +URL_REGEX = re.compile(r'http://|https://|ftp://|file://|file:\\') + + +def is_url(filename): + """Return True if string is an http or ftp path.""" + return isinstance(filename, str) and URL_REGEX.match(filename) is not None + + +@contextmanager +def file_or_url_context(resource_name): + """Yield name of file from the given resource (i.e. file or url).""" + if is_url(resource_name): + url_components = urllib.parse.urlparse(resource_name) + _, ext = os.path.splitext(url_components.path) + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as f: + with urllib.request.urlopen(resource_name) as u: + f.write(u.read()) + # f must be closed before yielding + yield f.name + except (URLError, HTTPError): + # could not open URL + os.remove(f.name) + raise + except (FileNotFoundError, FileExistsError, PermissionError, BaseException): + # could not create temporary file + raise + else: + os.remove(f.name) + else: + yield resource_name diff --git a/lib/python3.10/site-packages/skimage/measure/__init__.py b/lib/python3.10/site-packages/skimage/measure/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..08699c314daf0df76637ee2573b621c91722aae7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/__init__.py @@ -0,0 +1,5 @@ +"""Measurement of image properties, e.g., region properties, contours.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/measure/__init__.pyi b/lib/python3.10/site-packages/skimage/measure/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..fb814feff8e290dbdae78eb7ff2ba58c31df528b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/__init__.pyi @@ -0,0 +1,75 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'find_contours', + 'regionprops', + 'regionprops_table', + 'perimeter', + 'perimeter_crofton', + 'euler_number', + 'approximate_polygon', + 'subdivide_polygon', + 'LineModelND', + 'CircleModel', + 'EllipseModel', + 'ransac', + 'block_reduce', + 'moments', + 'moments_central', + 'moments_coords', + 'moments_coords_central', + 'moments_normalized', + 'moments_hu', + 'inertia_tensor', + 'inertia_tensor_eigvals', + 'marching_cubes', + 'mesh_surface_area', + 'profile_line', + 'label', + 'points_in_poly', + 'grid_points_in_poly', + 'shannon_entropy', + 'blur_effect', + 'pearson_corr_coeff', + 'manders_coloc_coeff', + 'manders_overlap_coeff', + 'intersection_coeff', + 'centroid', +] + +from ._find_contours import find_contours +from ._marching_cubes_lewiner import marching_cubes, mesh_surface_area +from ._regionprops import ( + regionprops, + perimeter, + perimeter_crofton, + euler_number, + regionprops_table, +) +from ._polygon import approximate_polygon, subdivide_polygon +from .pnpoly import points_in_poly, grid_points_in_poly +from ._moments import ( + moments, + moments_central, + moments_coords, + moments_coords_central, + moments_normalized, + centroid, + moments_hu, + inertia_tensor, + inertia_tensor_eigvals, +) +from .profile import profile_line +from .fit import LineModelND, CircleModel, EllipseModel, ransac +from .block import block_reduce +from ._label import label +from .entropy import shannon_entropy +from ._blur_effect import blur_effect +from ._colocalization import ( + pearson_corr_coeff, + manders_coloc_coeff, + manders_overlap_coeff, + intersection_coeff, +) diff --git a/lib/python3.10/site-packages/skimage/measure/_blur_effect.py b/lib/python3.10/site-packages/skimage/measure/_blur_effect.py new file mode 100644 index 0000000000000000000000000000000000000000..f0cead0070a5841ca2fb256531cae1fe31f02333 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_blur_effect.py @@ -0,0 +1,98 @@ +import numpy as np +import scipy.ndimage as ndi + +from ..color import rgb2gray +from ..util import img_as_float + +# TODO: when minimum numpy dependency is 1.25 use: +# np..exceptions.AxisError instead of AxisError +# and remove this try-except +try: + from numpy import AxisError +except ImportError: + from numpy.exceptions import AxisError + + +__all__ = ['blur_effect'] + + +_EPSILON = np.spacing(np.float64(1)) + + +def blur_effect(image, h_size=11, channel_axis=None, reduce_func=np.max): + """Compute a metric that indicates the strength of blur in an image + (0 for no blur, 1 for maximal blur). + + Parameters + ---------- + image : ndarray + RGB or grayscale nD image. The input image is converted to grayscale + before computing the blur metric. + h_size : int, optional + Size of the re-blurring filter. + channel_axis : int or None, optional + If None, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array + corresponds to color channels. + reduce_func : callable, optional + Function used to calculate the aggregation of blur metrics along all + axes. If set to None, the entire list is returned, where the i-th + element is the blur metric along the i-th axis. + + Returns + ------- + blur : float (0 to 1) or list of floats + Blur metric: by default, the maximum of blur metrics along all axes. + + Notes + ----- + `h_size` must keep the same value in order to compare results between + images. Most of the time, the default size (11) is enough. This means that + the metric can clearly discriminate blur up to an average 11x11 filter; if + blur is higher, the metric still gives good results but its values tend + towards an asymptote. + + References + ---------- + .. [1] Frederique Crete, Thierry Dolmiere, Patricia Ladret, and Marina + Nicolas "The blur effect: perception and estimation with a new + no-reference perceptual blur metric" Proc. SPIE 6492, Human Vision and + Electronic Imaging XII, 64920I (2007) + https://hal.archives-ouvertes.fr/hal-00232709 + :DOI:`10.1117/12.702790` + """ + + if channel_axis is not None: + try: + # ensure color channels are in the final dimension + image = np.moveaxis(image, channel_axis, -1) + except AxisError: + print('channel_axis must be one of the image array dimensions') + raise + except TypeError: + print('channel_axis must be an integer') + raise + image = rgb2gray(image) + n_axes = image.ndim + image = img_as_float(image) + shape = image.shape + B = [] + + from ..filters import sobel + + slices = tuple([slice(2, s - 1) for s in shape]) + for ax in range(n_axes): + filt_im = ndi.uniform_filter1d(image, h_size, axis=ax) + im_sharp = np.abs(sobel(image, axis=ax)) + im_blur = np.abs(sobel(filt_im, axis=ax)) + + # avoid numerical instabilities + im_sharp = np.maximum(_EPSILON, im_sharp) + im_blur = np.maximum(_EPSILON, im_blur) + + T = np.maximum(0, im_sharp - im_blur) + M1 = np.sum(im_sharp[slices]) + M2 = np.sum(T[slices]) + B.append(np.abs(M1 - M2) / M1) + + return B if reduce_func is None else reduce_func(B) diff --git a/lib/python3.10/site-packages/skimage/measure/_colocalization.py b/lib/python3.10/site-packages/skimage/measure/_colocalization.py new file mode 100644 index 0000000000000000000000000000000000000000..196f1a1a81ba9c0d779293197bc6307912905501 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_colocalization.py @@ -0,0 +1,304 @@ +import numpy as np +from scipy.stats import pearsonr + +from .._shared.utils import check_shape_equality, as_binary_ndarray + +__all__ = [ + 'pearson_corr_coeff', + 'manders_coloc_coeff', + 'manders_overlap_coeff', + 'intersection_coeff', +] + + +def pearson_corr_coeff(image0, image1, mask=None): + r"""Calculate Pearson's Correlation Coefficient between pixel intensities + in channels. + + Parameters + ---------- + image0 : (M, N) ndarray + Image of channel A. + image1 : (M, N) ndarray + Image of channel 2 to be correlated with channel B. + Must have same dimensions as `image0`. + mask : (M, N) ndarray of dtype bool, optional + Only `image0` and `image1` pixels within this region of interest mask + are included in the calculation. Must have same dimensions as `image0`. + + Returns + ------- + pcc : float + Pearson's correlation coefficient of the pixel intensities between + the two images, within the mask if provided. + p-value : float + Two-tailed p-value. + + Notes + ----- + Pearson's Correlation Coefficient (PCC) measures the linear correlation + between the pixel intensities of the two images. Its value ranges from -1 + for perfect linear anti-correlation to +1 for perfect linear correlation. + The calculation of the p-value assumes that the intensities of pixels in + each input image are normally distributed. + + Scipy's implementation of Pearson's correlation coefficient is used. Please + refer to it for further information and caveats [1]_. + + .. math:: + r = \frac{\sum (A_i - m_A_i) (B_i - m_B_i)} + {\sqrt{\sum (A_i - m_A_i)^2 \sum (B_i - m_B_i)^2}} + + where + :math:`A_i` is the value of the :math:`i^{th}` pixel in `image0` + :math:`B_i` is the value of the :math:`i^{th}` pixel in `image1`, + :math:`m_A_i` is the mean of the pixel values in `image0` + :math:`m_B_i` is the mean of the pixel values in `image1` + + A low PCC value does not necessarily mean that there is no correlation + between the two channel intensities, just that there is no linear + correlation. You may wish to plot the pixel intensities of each of the two + channels in a 2D scatterplot and use Spearman's rank correlation if a + non-linear correlation is visually identified [2]_. Also consider if you + are interested in correlation or co-occurence, in which case a method + involving segmentation masks (e.g. MCC or intersection coefficient) may be + more suitable [3]_ [4]_. + + Providing the mask of only relevant sections of the image (e.g., cells, or + particular cellular compartments) and removing noise is important as the + PCC is sensitive to these measures [3]_ [4]_. + + References + ---------- + .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.pearsonr.html + .. [2] https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.spearmanr.html + .. [3] Dunn, K. W., Kamocka, M. M., & McDonald, J. H. (2011). A practical + guide to evaluating colocalization in biological microscopy. + American journal of physiology. Cell physiology, 300(4), C723–C742. + https://doi.org/10.1152/ajpcell.00462.2010 + .. [4] Bolte, S. and Cordelières, F.P. (2006), A guided tour into + subcellular colocalization analysis in light microscopy. Journal of + Microscopy, 224: 213-232. + https://doi.org/10.1111/j.1365-2818.2006.01706.x + """ + image0 = np.asarray(image0) + image1 = np.asarray(image1) + if mask is not None: + mask = as_binary_ndarray(mask, variable_name="mask") + check_shape_equality(image0, image1, mask) + image0 = image0[mask] + image1 = image1[mask] + else: + check_shape_equality(image0, image1) + # scipy pearsonr function only takes flattened arrays + image0 = image0.reshape(-1) + image1 = image1.reshape(-1) + + return tuple(float(v) for v in pearsonr(image0, image1)) + + +def manders_coloc_coeff(image0, image1_mask, mask=None): + r"""Manders' colocalization coefficient between two channels. + + Parameters + ---------- + image0 : (M, N) ndarray + Image of channel A. All pixel values should be non-negative. + image1_mask : (M, N) ndarray of dtype bool + Binary mask with segmented regions of interest in channel B. + Must have same dimensions as `image0`. + mask : (M, N) ndarray of dtype bool, optional + Only `image0` pixel values within this region of interest mask are + included in the calculation. + Must have same dimensions as `image0`. + + Returns + ------- + mcc : float + Manders' colocalization coefficient. + + Notes + ----- + Manders' Colocalization Coefficient (MCC) is the fraction of total + intensity of a certain channel (channel A) that is within the segmented + region of a second channel (channel B) [1]_. It ranges from 0 for no + colocalisation to 1 for complete colocalization. It is also referred to + as M1 and M2. + + MCC is commonly used to measure the colocalization of a particular protein + in a subceullar compartment. Typically a segmentation mask for channel B + is generated by setting a threshold that the pixel values must be above + to be included in the MCC calculation. In this implementation, + the channel B mask is provided as the argument `image1_mask`, allowing + the exact segmentation method to be decided by the user beforehand. + + The implemented equation is: + + .. math:: + r = \frac{\sum A_{i,coloc}}{\sum A_i} + + where + :math:`A_i` is the value of the :math:`i^{th}` pixel in `image0` + :math:`A_{i,coloc} = A_i` if :math:`Bmask_i > 0` + :math:`Bmask_i` is the value of the :math:`i^{th}` pixel in + `mask` + + MCC is sensitive to noise, with diffuse signal in the first channel + inflating its value. Images should be processed to remove out of focus and + background light before the MCC is calculated [2]_. + + References + ---------- + .. [1] Manders, E.M.M., Verbeek, F.J. and Aten, J.A. (1993), Measurement of + co-localization of objects in dual-colour confocal images. Journal + of Microscopy, 169: 375-382. + https://doi.org/10.1111/j.1365-2818.1993.tb03313.x + https://imagej.net/media/manders.pdf + .. [2] Dunn, K. W., Kamocka, M. M., & McDonald, J. H. (2011). A practical + guide to evaluating colocalization in biological microscopy. + American journal of physiology. Cell physiology, 300(4), C723–C742. + https://doi.org/10.1152/ajpcell.00462.2010 + + """ + image0 = np.asarray(image0) + image1_mask = as_binary_ndarray(image1_mask, variable_name="image1_mask") + if mask is not None: + mask = as_binary_ndarray(mask, variable_name="mask") + check_shape_equality(image0, image1_mask, mask) + image0 = image0[mask] + image1_mask = image1_mask[mask] + else: + check_shape_equality(image0, image1_mask) + # check non-negative image + if image0.min() < 0: + raise ValueError("image contains negative values") + + sum = np.sum(image0) + if sum == 0: + return 0 + return np.sum(image0 * image1_mask) / sum + + +def manders_overlap_coeff(image0, image1, mask=None): + r"""Manders' overlap coefficient + + Parameters + ---------- + image0 : (M, N) ndarray + Image of channel A. All pixel values should be non-negative. + image1 : (M, N) ndarray + Image of channel B. All pixel values should be non-negative. + Must have same dimensions as `image0` + mask : (M, N) ndarray of dtype bool, optional + Only `image0` and `image1` pixel values within this region of interest + mask are included in the calculation. + Must have ♣same dimensions as `image0`. + + Returns + ------- + moc: float + Manders' Overlap Coefficient of pixel intensities between the two + images. + + Notes + ----- + Manders' Overlap Coefficient (MOC) is given by the equation [1]_: + + .. math:: + r = \frac{\sum A_i B_i}{\sqrt{\sum A_i^2 \sum B_i^2}} + + where + :math:`A_i` is the value of the :math:`i^{th}` pixel in `image0` + :math:`B_i` is the value of the :math:`i^{th}` pixel in `image1` + + It ranges between 0 for no colocalization and 1 for complete colocalization + of all pixels. + + MOC does not take into account pixel intensities, just the fraction of + pixels that have positive values for both channels[2]_ [3]_. Its usefulness + has been criticized as it changes in response to differences in both + co-occurence and correlation and so a particular MOC value could indicate + a wide range of colocalization patterns [4]_ [5]_. + + References + ---------- + .. [1] Manders, E.M.M., Verbeek, F.J. and Aten, J.A. (1993), Measurement of + co-localization of objects in dual-colour confocal images. Journal + of Microscopy, 169: 375-382. + https://doi.org/10.1111/j.1365-2818.1993.tb03313.x + https://imagej.net/media/manders.pdf + .. [2] Dunn, K. W., Kamocka, M. M., & McDonald, J. H. (2011). A practical + guide to evaluating colocalization in biological microscopy. + American journal of physiology. Cell physiology, 300(4), C723–C742. + https://doi.org/10.1152/ajpcell.00462.2010 + .. [3] Bolte, S. and Cordelières, F.P. (2006), A guided tour into + subcellular colocalization analysis in light microscopy. Journal of + Microscopy, 224: 213-232. + https://doi.org/10.1111/j.1365-2818.2006.01 + .. [4] Adler J, Parmryd I. (2010), Quantifying colocalization by + correlation: the Pearson correlation coefficient is + superior to the Mander's overlap coefficient. Cytometry A. + Aug;77(8):733-42.https://doi.org/10.1002/cyto.a.20896 + .. [5] Adler, J, Parmryd, I. Quantifying colocalization: The case for + discarding the Manders overlap coefficient. Cytometry. 2021; 99: + 910– 920. https://doi.org/10.1002/cyto.a.24336 + + """ + image0 = np.asarray(image0) + image1 = np.asarray(image1) + if mask is not None: + mask = as_binary_ndarray(mask, variable_name="mask") + check_shape_equality(image0, image1, mask) + image0 = image0[mask] + image1 = image1[mask] + else: + check_shape_equality(image0, image1) + + # check non-negative image + if image0.min() < 0: + raise ValueError("image0 contains negative values") + if image1.min() < 0: + raise ValueError("image1 contains negative values") + + denom = (np.sum(np.square(image0)) * (np.sum(np.square(image1)))) ** 0.5 + return np.sum(np.multiply(image0, image1)) / denom + + +def intersection_coeff(image0_mask, image1_mask, mask=None): + r"""Fraction of a channel's segmented binary mask that overlaps with a + second channel's segmented binary mask. + + Parameters + ---------- + image0_mask : (M, N) ndarray of dtype bool + Image mask of channel A. + image1_mask : (M, N) ndarray of dtype bool + Image mask of channel B. + Must have same dimensions as `image0_mask`. + mask : (M, N) ndarray of dtype bool, optional + Only `image0_mask` and `image1_mask` pixels within this region of + interest + mask are included in the calculation. + Must have same dimensions as `image0_mask`. + + Returns + ------- + Intersection coefficient, float + Fraction of `image0_mask` that overlaps with `image1_mask`. + + """ + image0_mask = as_binary_ndarray(image0_mask, variable_name="image0_mask") + image1_mask = as_binary_ndarray(image1_mask, variable_name="image1_mask") + if mask is not None: + mask = as_binary_ndarray(mask, variable_name="mask") + check_shape_equality(image0_mask, image1_mask, mask) + image0_mask = image0_mask[mask] + image1_mask = image1_mask[mask] + else: + check_shape_equality(image0_mask, image1_mask) + + nonzero_image0 = np.count_nonzero(image0_mask) + if nonzero_image0 == 0: + return 0 + nonzero_joint = np.count_nonzero(np.logical_and(image0_mask, image1_mask)) + return nonzero_joint / nonzero_image0 diff --git a/lib/python3.10/site-packages/skimage/measure/_find_contours.py b/lib/python3.10/site-packages/skimage/measure/_find_contours.py new file mode 100644 index 0000000000000000000000000000000000000000..c32fa743b237f20a0a513c1addff50a3e0fdb1cf --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_find_contours.py @@ -0,0 +1,218 @@ +import numpy as np + +from ._find_contours_cy import _get_contour_segments + +from collections import deque + +_param_options = ('high', 'low') + + +def find_contours( + image, level=None, fully_connected='low', positive_orientation='low', *, mask=None +): + """Find iso-valued contours in a 2D array for a given level value. + + Uses the "marching squares" method to compute the iso-valued contours of + the input 2D array for a particular level value. Array values are linearly + interpolated to provide better precision for the output contours. + + Parameters + ---------- + image : (M, N) ndarray of double + Input image in which to find contours. + level : float, optional + Value along which to find contours in the array. By default, the level + is set to (max(image) + min(image)) / 2 + + .. versionchanged:: 0.18 + This parameter is now optional. + fully_connected : str, {'low', 'high'} + Indicates whether array elements below the given level value are to be + considered fully-connected (and hence elements above the value will + only be face connected), or vice-versa. (See notes below for details.) + positive_orientation : str, {'low', 'high'} + Indicates whether the output contours will produce positively-oriented + polygons around islands of low- or high-valued elements. If 'low' then + contours will wind counter-clockwise around elements below the + iso-value. Alternately, this means that low-valued elements are always + on the left of the contour. (See below for details.) + mask : (M, N) ndarray of bool or None + A boolean mask, True where we want to draw contours. + Note that NaN values are always excluded from the considered region + (``mask`` is set to ``False`` wherever ``array`` is ``NaN``). + + Returns + ------- + contours : list of (K, 2) ndarrays + Each contour is a ndarray of ``(row, column)`` coordinates along the contour. + + See Also + -------- + skimage.measure.marching_cubes + + Notes + ----- + The marching squares algorithm is a special case of the marching cubes + algorithm [1]_. A simple explanation is available here: + + https://users.polytech.unice.fr/~lingrand/MarchingCubes/algo.html + + There is a single ambiguous case in the marching squares algorithm: when + a given ``2 x 2``-element square has two high-valued and two low-valued + elements, each pair diagonally adjacent. (Where high- and low-valued is + with respect to the contour value sought.) In this case, either the + high-valued elements can be 'connected together' via a thin isthmus that + separates the low-valued elements, or vice-versa. When elements are + connected together across a diagonal, they are considered 'fully + connected' (also known as 'face+vertex-connected' or '8-connected'). Only + high-valued or low-valued elements can be fully-connected, the other set + will be considered as 'face-connected' or '4-connected'. By default, + low-valued elements are considered fully-connected; this can be altered + with the 'fully_connected' parameter. + + Output contours are not guaranteed to be closed: contours which intersect + the array edge or a masked-off region (either where mask is False or where + array is NaN) will be left open. All other contours will be closed. (The + closed-ness of a contours can be tested by checking whether the beginning + point is the same as the end point.) + + Contours are oriented. By default, array values lower than the contour + value are to the left of the contour and values greater than the contour + value are to the right. This means that contours will wind + counter-clockwise (i.e. in 'positive orientation') around islands of + low-valued pixels. This behavior can be altered with the + 'positive_orientation' parameter. + + The order of the contours in the output list is determined by the position + of the smallest ``x,y`` (in lexicographical order) coordinate in the + contour. This is a side effect of how the input array is traversed, but + can be relied upon. + + .. warning:: + + Array coordinates/values are assumed to refer to the *center* of the + array element. Take a simple example input: ``[0, 1]``. The interpolated + position of 0.5 in this array is midway between the 0-element (at + ``x=0``) and the 1-element (at ``x=1``), and thus would fall at + ``x=0.5``. + + This means that to find reasonable contours, it is best to find contours + midway between the expected "light" and "dark" values. In particular, + given a binarized array, *do not* choose to find contours at the low or + high value of the array. This will often yield degenerate contours, + especially around structures that are a single array element wide. Instead, + choose a middle value, as above. + + References + ---------- + .. [1] Lorensen, William and Harvey E. Cline. Marching Cubes: A High + Resolution 3D Surface Construction Algorithm. Computer Graphics + (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + :DOI:`10.1145/37401.37422` + + Examples + -------- + >>> a = np.zeros((3, 3)) + >>> a[0, 0] = 1 + >>> a + array([[1., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) + >>> find_contours(a, 0.5) + [array([[0. , 0.5], + [0.5, 0. ]])] + """ + if fully_connected not in _param_options: + raise ValueError( + 'Parameters "fully_connected" must be either ' '"high" or "low".' + ) + if positive_orientation not in _param_options: + raise ValueError( + 'Parameters "positive_orientation" must be either ' '"high" or "low".' + ) + if image.shape[0] < 2 or image.shape[1] < 2: + raise ValueError("Input array must be at least 2x2.") + if image.ndim != 2: + raise ValueError('Only 2D arrays are supported.') + if mask is not None: + if mask.shape != image.shape: + raise ValueError('Parameters "array" and "mask"' ' must have same shape.') + if not np.can_cast(mask.dtype, bool, casting='safe'): + raise TypeError('Parameter "mask" must be a binary array.') + mask = mask.astype(np.uint8, copy=False) + if level is None: + level = (np.nanmin(image) + np.nanmax(image)) / 2.0 + + segments = _get_contour_segments( + image.astype(np.float64), float(level), fully_connected == 'high', mask=mask + ) + contours = _assemble_contours(segments) + if positive_orientation == 'high': + contours = [c[::-1] for c in contours] + return contours + + +def _assemble_contours(segments): + current_index = 0 + contours = {} + starts = {} + ends = {} + for from_point, to_point in segments: + # Ignore degenerate segments. + # This happens when (and only when) one vertex of the square is + # exactly the contour level, and the rest are above or below. + # This degenerate vertex will be picked up later by neighboring + # squares. + if from_point == to_point: + continue + + tail, tail_num = starts.pop(to_point, (None, None)) + head, head_num = ends.pop(from_point, (None, None)) + + if tail is not None and head is not None: + # We need to connect these two contours. + if tail is head: + # We need to closed a contour: add the end point + head.append(to_point) + else: # tail is not head + # We need to join two distinct contours. + # We want to keep the first contour segment created, so that + # the final contours are ordered left->right, top->bottom. + if tail_num > head_num: + # tail was created second. Append tail to head. + head.extend(tail) + # Remove tail from the detected contours + contours.pop(tail_num, None) + # Update starts and ends + starts[head[0]] = (head, head_num) + ends[head[-1]] = (head, head_num) + else: # tail_num <= head_num + # head was created second. Prepend head to tail. + tail.extendleft(reversed(head)) + # Remove head from the detected contours + starts.pop(head[0], None) # head[0] can be == to_point! + contours.pop(head_num, None) + # Update starts and ends + starts[tail[0]] = (tail, tail_num) + ends[tail[-1]] = (tail, tail_num) + elif tail is None and head is None: + # We need to add a new contour + new_contour = deque((from_point, to_point)) + contours[current_index] = new_contour + starts[from_point] = (new_contour, current_index) + ends[to_point] = (new_contour, current_index) + current_index += 1 + elif head is None: # tail is not None + # tail first element is to_point: the new segment should be + # prepended. + tail.appendleft(from_point) + # Update starts + starts[from_point] = (tail, tail_num) + else: # tail is None and head is not None: + # head last element is from_point: the new segment should be + # appended + head.append(to_point) + # Update ends + ends[to_point] = (head, head_num) + + return [np.array(contour) for _, contour in sorted(contours.items())] diff --git a/lib/python3.10/site-packages/skimage/measure/_label.py b/lib/python3.10/site-packages/skimage/measure/_label.py new file mode 100644 index 0000000000000000000000000000000000000000..786b99a24f130c2c06d3533dfcb497602d980441 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_label.py @@ -0,0 +1,125 @@ +from scipy import ndimage +from ._ccomp import label_cython as clabel + + +def _label_bool(image, background=None, return_num=False, connectivity=None): + """Faster implementation of clabel for boolean input. + + See context: https://github.com/scikit-image/scikit-image/issues/4833 + """ + from ..morphology._util import _resolve_neighborhood + + if background == 1: + image = ~image + + if connectivity is None: + connectivity = image.ndim + + if not 1 <= connectivity <= image.ndim: + raise ValueError( + f'Connectivity for {image.ndim}D image should ' + f'be in [1, ..., {image.ndim}]. Got {connectivity}.' + ) + + footprint = _resolve_neighborhood(None, connectivity, image.ndim) + result = ndimage.label(image, structure=footprint) + + if return_num: + return result + else: + return result[0] + + +def label(label_image, background=None, return_num=False, connectivity=None): + r"""Label connected regions of an integer array. + + Two pixels are connected when they are neighbors and have the same value. + In 2D, they can be neighbors either in a 1- or 2-connected sense. + The value refers to the maximum number of orthogonal hops to consider a + pixel/voxel a neighbor:: + + 1-connectivity 2-connectivity diagonal connection close-up + + [ ] [ ] [ ] [ ] [ ] + | \ | / | <- hop 2 + [ ]--[x]--[ ] [ ]--[x]--[ ] [x]--[ ] + | / | \ hop 1 + [ ] [ ] [ ] [ ] + + Parameters + ---------- + label_image : ndarray of dtype int + Image to label. + background : int, optional + Consider all pixels with this value as background pixels, and label + them as 0. By default, 0-valued pixels are considered as background + pixels. + return_num : bool, optional + Whether to return the number of assigned labels. + connectivity : int, optional + Maximum number of orthogonal hops to consider a pixel/voxel + as a neighbor. + Accepted values are ranging from 1 to input.ndim. If ``None``, a full + connectivity of ``input.ndim`` is used. + + Returns + ------- + labels : ndarray of dtype int + Labeled array, where all connected regions are assigned the + same integer value. + num : int, optional + Number of labels, which equals the maximum label index and is only + returned if return_num is `True`. + + See Also + -------- + skimage.measure.regionprops + skimage.measure.regionprops_table + + References + ---------- + .. [1] Christophe Fiorio and Jens Gustedt, "Two linear time Union-Find + strategies for image processing", Theoretical Computer Science + 154 (1996), pp. 165-181. + .. [2] Kensheng Wu, Ekow Otoo and Arie Shoshani, "Optimizing connected + component labeling algorithms", Paper LBNL-56864, 2005, + Lawrence Berkeley National Laboratory (University of California), + http://repositories.cdlib.org/lbnl/LBNL-56864 + + Examples + -------- + >>> import numpy as np + >>> x = np.eye(3).astype(int) + >>> print(x) + [[1 0 0] + [0 1 0] + [0 0 1]] + >>> print(label(x, connectivity=1)) + [[1 0 0] + [0 2 0] + [0 0 3]] + >>> print(label(x, connectivity=2)) + [[1 0 0] + [0 1 0] + [0 0 1]] + >>> print(label(x, background=-1)) + [[1 2 2] + [2 1 2] + [2 2 1]] + >>> x = np.array([[1, 0, 0], + ... [1, 1, 5], + ... [0, 0, 0]]) + >>> print(label(x)) + [[1 0 0] + [1 1 2] + [0 0 0]] + """ + if label_image.dtype == bool: + return _label_bool( + label_image, + background=background, + return_num=return_num, + connectivity=connectivity, + ) + else: + return clabel(label_image, background, return_num, connectivity) diff --git a/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner.py b/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner.py new file mode 100644 index 0000000000000000000000000000000000000000..8ffb981a1ea83f317e0f11a75167ef642f270827 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner.py @@ -0,0 +1,352 @@ +import base64 + +import numpy as np + +from . import _marching_cubes_lewiner_luts as mcluts +from . import _marching_cubes_lewiner_cy + + +def marching_cubes( + volume, + level=None, + *, + spacing=(1.0, 1.0, 1.0), + gradient_direction='descent', + step_size=1, + allow_degenerate=True, + method='lewiner', + mask=None, +): + """Marching cubes algorithm to find surfaces in 3d volumetric data. + + In contrast with Lorensen et al. approach [2]_, Lewiner et + al. algorithm is faster, resolves ambiguities, and guarantees + topologically correct results. Therefore, this algorithm generally + a better choice. + + Parameters + ---------- + volume : (M, N, P) ndarray + Input data volume to find isosurfaces. Will internally be + converted to float32 if necessary. + level : float, optional + Contour value to search for isosurfaces in `volume`. If not + given or None, the average of the min and max of vol is used. + spacing : length-3 tuple of floats, optional + Voxel spacing in spatial dimensions corresponding to numpy array + indexing dimensions (M, N, P) as in `volume`. + gradient_direction : string, optional + Controls if the mesh was generated from an isosurface with gradient + descent toward objects of interest (the default), or the opposite, + considering the *left-hand* rule. + The two options are: + * descent : Object was greater than exterior + * ascent : Exterior was greater than object + step_size : int, optional + Step size in voxels. Default 1. Larger steps yield faster but + coarser results. The result will always be topologically correct + though. + allow_degenerate : bool, optional + Whether to allow degenerate (i.e. zero-area) triangles in the + end-result. Default True. If False, degenerate triangles are + removed, at the cost of making the algorithm slower. + method: {'lewiner', 'lorensen'}, optional + Whether the method of Lewiner et al. or Lorensen et al. will be used. + mask : (M, N, P) array, optional + Boolean array. The marching cube algorithm will be computed only on + True elements. This will save computational time when interfaces + are located within certain region of the volume M, N, P-e.g. the top + half of the cube-and also allow to compute finite surfaces-i.e. open + surfaces that do not end at the border of the cube. + + Returns + ------- + verts : (V, 3) array + Spatial coordinates for V unique mesh vertices. Coordinate order + matches input `volume` (M, N, P). If ``allow_degenerate`` is set to + True, then the presence of degenerate triangles in the mesh can make + this array have duplicate vertices. + faces : (F, 3) array + Define triangular faces via referencing vertex indices from ``verts``. + This algorithm specifically outputs triangles, so each face has + exactly three indices. + normals : (V, 3) array + The normal direction at each vertex, as calculated from the + data. + values : (V,) array + Gives a measure for the maximum value of the data in the local region + near each vertex. This can be used by visualization tools to apply + a colormap to the mesh. + + See Also + -------- + skimage.measure.mesh_surface_area + skimage.measure.find_contours + + Notes + ----- + The algorithm [1]_ is an improved version of Chernyaev's Marching + Cubes 33 algorithm. It is an efficient algorithm that relies on + heavy use of lookup tables to handle the many different cases, + keeping the algorithm relatively easy. This implementation is + written in Cython, ported from Lewiner's C++ implementation. + + To quantify the area of an isosurface generated by this algorithm, pass + verts and faces to `skimage.measure.mesh_surface_area`. + + Regarding visualization of algorithm output, to contour a volume + named `myvolume` about the level 0.0, using the ``mayavi`` package:: + + >>> + >> from mayavi import mlab + >> verts, faces, _, _ = marching_cubes(myvolume, 0.0) + >> mlab.triangular_mesh([vert[0] for vert in verts], + [vert[1] for vert in verts], + [vert[2] for vert in verts], + faces) + >> mlab.show() + + Similarly using the ``visvis`` package:: + + >>> + >> import visvis as vv + >> verts, faces, normals, values = marching_cubes(myvolume, 0.0) + >> vv.mesh(np.fliplr(verts), faces, normals, values) + >> vv.use().Run() + + To reduce the number of triangles in the mesh for better performance, + see this `example + `_ + using the ``mayavi`` package. + + References + ---------- + .. [1] Thomas Lewiner, Helio Lopes, Antonio Wilson Vieira and Geovan + Tavares. Efficient implementation of Marching Cubes' cases with + topological guarantees. Journal of Graphics Tools 8(2) + pp. 1-15 (december 2003). + :DOI:`10.1080/10867651.2003.10487582` + .. [2] Lorensen, William and Harvey E. Cline. Marching Cubes: A High + Resolution 3D Surface Construction Algorithm. Computer Graphics + (SIGGRAPH 87 Proceedings) 21(4) July 1987, p. 163-170). + :DOI:`10.1145/37401.37422` + """ + use_classic = False + if method == 'lorensen': + use_classic = True + elif method != 'lewiner': + raise ValueError("method should be either 'lewiner' or 'lorensen'") + return _marching_cubes_lewiner( + volume, + level, + spacing, + gradient_direction, + step_size, + allow_degenerate, + use_classic=use_classic, + mask=mask, + ) + + +def _marching_cubes_lewiner( + volume, + level, + spacing, + gradient_direction, + step_size, + allow_degenerate, + use_classic, + mask, +): + """Lewiner et al. algorithm for marching cubes. See + marching_cubes_lewiner for documentation. + + """ + + # Check volume and ensure its in the format that the alg needs + if not isinstance(volume, np.ndarray) or (volume.ndim != 3): + raise ValueError('Input volume should be a 3D numpy array.') + if volume.shape[0] < 2 or volume.shape[1] < 2 or volume.shape[2] < 2: + raise ValueError("Input array must be at least 2x2x2.") + volume = np.ascontiguousarray(volume, np.float32) # no copy if not necessary + + # Check/convert other inputs: + # level + if level is None: + level = 0.5 * (volume.min() + volume.max()) + else: + level = float(level) + if level < volume.min() or level > volume.max(): + raise ValueError("Surface level must be within volume data range.") + # spacing + if len(spacing) != 3: + raise ValueError("`spacing` must consist of three floats.") + # step_size + step_size = int(step_size) + if step_size < 1: + raise ValueError('step_size must be at least one.') + # use_classic + use_classic = bool(use_classic) + + # Get LutProvider class (reuse if possible) + L = _get_mc_luts() + + # Check if a mask array is passed + if mask is not None: + if not mask.shape == volume.shape: + raise ValueError('volume and mask must have the same shape.') + + # Apply algorithm + func = _marching_cubes_lewiner_cy.marching_cubes + vertices, faces, normals, values = func( + volume, level, L, step_size, use_classic, mask + ) + + if not len(vertices): + raise RuntimeError('No surface found at the given iso value.') + + # Output in z-y-x order, as is common in skimage + vertices = np.fliplr(vertices) + normals = np.fliplr(normals) + + # Finishing touches to output + faces.shape = -1, 3 + if gradient_direction == 'descent': + # MC implementation is right-handed, but gradient_direction is + # left-handed + faces = np.fliplr(faces) + elif not gradient_direction == 'ascent': + raise ValueError( + f"Incorrect input {gradient_direction} in `gradient_direction`, " + "see docstring." + ) + if not np.array_equal(spacing, (1, 1, 1)): + vertices = vertices * np.r_[spacing] + + if allow_degenerate: + return vertices, faces, normals, values + else: + fun = _marching_cubes_lewiner_cy.remove_degenerate_faces + return fun(vertices.astype(np.float32), faces, normals, values) + + +def _to_array(args): + shape, text = args + byts = base64.decodebytes(text.encode('utf-8')) + ar = np.frombuffer(byts, dtype='int8') + ar.shape = shape + return ar + + +# Map an edge-index to two relative pixel positions. The edge index +# represents a point that lies somewhere in between these pixels. +# Linear interpolation should be used to determine where it is exactly. +# 0 +# 3 1 -> 0x +# 2 xx + +# fmt: off +EDGETORELATIVEPOSX = np.array([ [0,1],[1,1],[1,0],[0,0], [0,1],[1,1],[1,0],[0,0], [0,0],[1,1],[1,1],[0,0] ], 'int8') +EDGETORELATIVEPOSY = np.array([ [0,0],[0,1],[1,1],[1,0], [0,0],[0,1],[1,1],[1,0], [0,0],[0,0],[1,1],[1,1] ], 'int8') +EDGETORELATIVEPOSZ = np.array([ [0,0],[0,0],[0,0],[0,0], [1,1],[1,1],[1,1],[1,1], [0,1],[0,1],[0,1],[0,1] ], 'int8') +# fmt: on + + +def _get_mc_luts(): + """Kind of lazy obtaining of the luts.""" + if not hasattr(mcluts, 'THE_LUTS'): + mcluts.THE_LUTS = _marching_cubes_lewiner_cy.LutProvider( + EDGETORELATIVEPOSX, + EDGETORELATIVEPOSY, + EDGETORELATIVEPOSZ, + _to_array(mcluts.CASESCLASSIC), + _to_array(mcluts.CASES), + _to_array(mcluts.TILING1), + _to_array(mcluts.TILING2), + _to_array(mcluts.TILING3_1), + _to_array(mcluts.TILING3_2), + _to_array(mcluts.TILING4_1), + _to_array(mcluts.TILING4_2), + _to_array(mcluts.TILING5), + _to_array(mcluts.TILING6_1_1), + _to_array(mcluts.TILING6_1_2), + _to_array(mcluts.TILING6_2), + _to_array(mcluts.TILING7_1), + _to_array(mcluts.TILING7_2), + _to_array(mcluts.TILING7_3), + _to_array(mcluts.TILING7_4_1), + _to_array(mcluts.TILING7_4_2), + _to_array(mcluts.TILING8), + _to_array(mcluts.TILING9), + _to_array(mcluts.TILING10_1_1), + _to_array(mcluts.TILING10_1_1_), + _to_array(mcluts.TILING10_1_2), + _to_array(mcluts.TILING10_2), + _to_array(mcluts.TILING10_2_), + _to_array(mcluts.TILING11), + _to_array(mcluts.TILING12_1_1), + _to_array(mcluts.TILING12_1_1_), + _to_array(mcluts.TILING12_1_2), + _to_array(mcluts.TILING12_2), + _to_array(mcluts.TILING12_2_), + _to_array(mcluts.TILING13_1), + _to_array(mcluts.TILING13_1_), + _to_array(mcluts.TILING13_2), + _to_array(mcluts.TILING13_2_), + _to_array(mcluts.TILING13_3), + _to_array(mcluts.TILING13_3_), + _to_array(mcluts.TILING13_4), + _to_array(mcluts.TILING13_5_1), + _to_array(mcluts.TILING13_5_2), + _to_array(mcluts.TILING14), + _to_array(mcluts.TEST3), + _to_array(mcluts.TEST4), + _to_array(mcluts.TEST6), + _to_array(mcluts.TEST7), + _to_array(mcluts.TEST10), + _to_array(mcluts.TEST12), + _to_array(mcluts.TEST13), + _to_array(mcluts.SUBCONFIG13), + ) + + return mcluts.THE_LUTS + + +def mesh_surface_area(verts, faces): + """Compute surface area, given vertices and triangular faces. + + Parameters + ---------- + verts : (V, 3) array of floats + Array containing coordinates for V unique mesh vertices. + faces : (F, 3) array of ints + List of length-3 lists of integers, referencing vertex coordinates as + provided in `verts`. + + Returns + ------- + area : float + Surface area of mesh. Units now [coordinate units] ** 2. + + Notes + ----- + The arguments expected by this function are the first two outputs from + `skimage.measure.marching_cubes`. For unit correct output, ensure correct + `spacing` was passed to `skimage.measure.marching_cubes`. + + This algorithm works properly only if the ``faces`` provided are all + triangles. + + See Also + -------- + skimage.measure.marching_cubes + + """ + # Fancy indexing to define two vector arrays from triangle vertices + actual_verts = verts[faces] + a = actual_verts[:, 0, :] - actual_verts[:, 1, :] + b = actual_verts[:, 0, :] - actual_verts[:, 2, :] + del actual_verts + + # Area of triangle in 3D = 1/2 * Euclidean norm of cross product + return ((np.cross(a, b) ** 2).sum(axis=1) ** 0.5).sum() / 2.0 diff --git a/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner_luts.py b/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner_luts.py new file mode 100644 index 0000000000000000000000000000000000000000..afe5f7c9058b2040d897362411a6a47a63d2b29e --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_marching_cubes_lewiner_luts.py @@ -0,0 +1,672 @@ +# This file was auto-generated from `mc_meta/LookUpTable.h` by +# `mc_meta/createluts.py`. The `mc_meta` scripts are not +# distributed with scikit-image, but are available in the +# repository under tools/precompute/mc_meta. + +# static const char casesClassic[256][16] +CASESCLASSIC = ( + (256, 16), + """ +/////////////////////wAIA/////////////////8AAQn/////////////////AQgDCQgB//// +/////////wECCv////////////////8ACAMBAgr/////////////CQIKAAIJ/////////////wII +AwIKCAoJCP////////8DCwL/////////////////AAsCCAsA/////////////wEJAAIDC/////// +//////8BCwIBCQsJCAv/////////AwoBCwoD/////////////wAKAQAICggLCv////////8DCQAD +CwkLCgn/////////CQgKCggL/////////////wQHCP////////////////8EAwAHAwT///////// +////AAEJCAQH/////////////wQBCQQHAQcDAf////////8BAgoIBAf/////////////AwQHAwAE +AQIK/////////wkCCgkAAggEB/////////8CCgkCCQcCBwMHCQT/////CAQHAwsC//////////// +/wsEBwsCBAIABP////////8JAAEIBAcCAwv/////////BAcLCQQLCQsCCQIB/////wMKAQMLCgcI +BP////////8BCwoBBAsBAAQHCwT/////BAcICQALCQsKCwAD/////wQHCwQLCQkLCv////////8J +BQT/////////////////CQUEAAgD/////////////wAFBAEFAP////////////8IBQQIAwUDAQX/ +////////AQIKCQUE/////////////wMACAECCgQJBf////////8FAgoFBAIEAAL/////////AgoF +AwIFAwUEAwQI/////wkFBAIDC/////////////8ACwIACAsECQX/////////AAUEAAEFAgML//// +/////wIBBQIFCAIICwQIBf////8KAwsKAQMJBQT/////////BAkFAAgBCAoBCAsK/////wUEAAUA +CwULCgsAA/////8FBAgFCAoKCAv/////////CQcIBQcJ/////////////wkDAAkFAwUHA/////// +//8ABwgAAQcBBQf/////////AQUDAwUH/////////////wkHCAkFBwoBAv////////8KAQIJBQAF +AwAFBwP/////CAACCAIFCAUHCgUC/////wIKBQIFAwMFB/////////8HCQUHCAkDCwL///////// +CQUHCQcCCQIAAgcL/////wIDCwABCAEHCAEFB/////8LAgELAQcHAQX/////////CQUICAUHCgED +CgML/////wUHAAUACQcLAAEACgsKAP8LCgALAAMKBQAIAAcFBwD/CwoFBwsF/////////////woG +Bf////////////////8ACAMFCgb/////////////CQABBQoG/////////////wEIAwEJCAUKBv// +//////8BBgUCBgH/////////////AQYFAQIGAwAI/////////wkGBQkABgACBv////////8FCQgF +CAIFAgYDAgj/////AgMLCgYF/////////////wsACAsCAAoGBf////////8AAQkCAwsFCgb///// +////BQoGAQkCCQsCCQgL/////wYDCwYFAwUBA/////////8ACAsACwUABQEFCwb/////AwsGAAMG +AAYFAAUJ/////wYFCQYJCwsJCP////////8FCgYEBwj/////////////BAMABAcDBgUK//////// +/wEJAAUKBggEB/////////8KBgUBCQcBBwMHCQT/////BgECBgUBBAcI/////////wECBQUCBgMA +BAMEB/////8IBAcJAAUABgUAAgb/////BwMJBwkEAwIJBQkGAgYJ/wMLAgcIBAoGBf////////8F +CgYEBwIEAgACBwv/////AAEJBAcIAgMLBQoG/////wkCAQkLAgkECwcLBAUKBv8IBAcDCwUDBQEF +Cwb/////BQELBQsGAQALBwsEAAQL/wAFCQAGBQADBgsGAwgEB/8GBQkGCQsEBwkHCwn/////CgQJ +BgQK/////////////wQKBgQJCgAIA/////////8KAAEKBgAGBAD/////////CAMBCAEGCAYEBgEK +/////wEECQECBAIGBP////////8DAAgBAgkCBAkCBgT/////AAIEBAIG/////////////wgDAggC +BAQCBv////////8KBAkKBgQLAgP/////////AAgCAggLBAkKBAoG/////wMLAgABBgAGBAYBCv// +//8GBAEGAQoECAECAQsICwH/CQYECQMGCQEDCwYD/////wgLAQgBAAsGAQkBBAYEAf8DCwYDBgAA +BgT/////////BgQICwYI/////////////wcKBgcICggJCv////////8ABwMACgcACQoGBwr///// +CgYHAQoHAQcIAQgA/////woGBwoHAQEHA/////////8BAgYBBggBCAkIBgf/////AgYJAgkBBgcJ +AAkDBwMJ/wcIAAcABgYAAv////////8HAwIGBwL/////////////AgMLCgYICggJCAYH/////wIA +BwIHCwAJBwYHCgkKB/8BCAABBwgBCgcGBwoCAwv/CwIBCwEHCgYBBgcB/////wgJBggGBwkBBgsG +AwEDBv8ACQELBgf/////////////BwgABwAGAwsACwYA/////wcLBv////////////////8HBgv/ +////////////////AwAICwcG/////////////wABCQsHBv////////////8IAQkIAwELBwb///// +////CgECBgsH/////////////wECCgMACAYLB/////////8CCQACCgkGCwf/////////BgsHAgoD +CggDCgkI/////wcCAwYCB/////////////8HAAgHBgAGAgD/////////AgcGAgMHAAEJ//////// +/wEGAgEIBgEJCAgHBv////8KBwYKAQcBAwf/////////CgcGAQcKAQgHAQAI/////wADBwAHCgAK +CQYKB/////8HBgoHCggICgn/////////BggECwgG/////////////wMGCwMABgAEBv////////8I +BgsIBAYJAAH/////////CQQGCQYDCQMBCwMG/////wYIBAYLCAIKAf////////8BAgoDAAsABgsA +BAb/////BAsIBAYLAAIJAgoJ/////woJAwoDAgkEAwsDBgQGA/8IAgMIBAIEBgL/////////AAQC +BAYC/////////////wEJAAIDBAIEBgQDCP////8BCQQBBAICBAb/////////CAEDCAYBCAQGBgoB +/////woBAAoABgYABP////////8EBgMEAwgGCgMAAwkKCQP/CgkEBgoE/////////////wQJBQcG +C/////////////8ACAMECQULBwb/////////BQABBQQABwYL/////////wsHBggDBAMFBAMBBf// +//8JBQQKAQIHBgv/////////BgsHAQIKAAgDBAkF/////wcGCwUECgQCCgQAAv////8DBAgDBQQD +AgUKBQILBwb/BwIDBwYCBQQJ/////////wkFBAAIBgAGAgYIB/////8DBgIDBwYBBQAFBAD///// +BgIIBggHAgEIBAgFAQUI/wkFBAoBBgEHBgEDB/////8BBgoBBwYBAAcIBwAJBQT/BAAKBAoFAAMK +BgoHAwcK/wcGCgcKCAUECgQICv////8GCQUGCwkLCAn/////////AwYLAAYDAAUGAAkF/////wAL +CAAFCwABBQUGC/////8GCwMGAwUFAwH/////////AQIKCQULCQsICwUG/////wALAwAGCwAJBgUG +CQECCv8LCAULBQYIAAUKBQIAAgX/BgsDBgMFAgoDCgUD/////wUICQUCCAUGAgMIAv////8JBQYJ +BgAABgL/////////AQUIAQgABQYIAwgCBgII/wEFBgIBBv////////////8BAwYBBgoDCAYFBgkI +CQb/CgEACgAGCQUABQYA/////wADCAUGCv////////////8KBQb/////////////////CwUKBwUL +/////////////wsFCgsHBQgDAP////////8FCwcFCgsBCQD/////////CgcFCgsHCQgBCAMB//// +/wsBAgsHAQcFAf////////8ACAMBAgcBBwUHAgv/////CQcFCQIHCQACAgsH/////wcFAgcCCwUJ +AgMCCAkIAv8CBQoCAwUDBwX/////////CAIACAUCCAcFCgIF/////wkAAQUKAwUDBwMKAv////8J +CAIJAgEIBwIKAgUHBQL/AQMFAwcF/////////////wAIBwAHAQEHBf////////8JAAMJAwUFAwf/ +////////CQgHBQkH/////////////wUIBAUKCAoLCP////////8FAAQFCwAFCgsLAwD/////AAEJ +CAQKCAoLCgQF/////woLBAoEBQsDBAkEAQMBBP8CBQECCAUCCwgEBQj/////AAQLAAsDBAULAgsB +BQEL/wACBQAFCQILBQQFCAsIBf8JBAUCCwP/////////////AgUKAwUCAwQFAwgE/////wUKAgUC +BAQCAP////////8DCgIDBQoDCAUEBQgAAQn/BQoCBQIEAQkCCQQC/////wgEBQgFAwMFAf////// +//8ABAUBAAX/////////////CAQFCAUDCQAFAAMF/////wkEBf////////////////8ECwcECQsJ +Cgv/////////AAgDBAkHCQsHCQoL/////wEKCwELBAEEAAcEC/////8DAQQDBAgBCgQHBAsKCwT/ +BAsHCQsECQILCQEC/////wkHBAkLBwkBCwILAQAIA/8LBwQLBAICBAD/////////CwcECwQCCAME +AwIE/////wIJCgIHCQIDBwcECf////8JCgcJBwQKAgcIBwACAAf/AwcKAwoCBwQKAQoABAAK/wEK +AggHBP////////////8ECQEEAQcHAQP/////////BAkBBAEHAAgBCAcB/////wQAAwcEA/////// +//////8ECAf/////////////////CQoICgsI/////////////wMACQMJCwsJCv////////8AAQoA +CggICgv/////////AwEKCwMK/////////////wECCwELCQkLCP////////8DAAkDCQsBAgkCCwn/ +////AAILCAAL/////////////wMCC/////////////////8CAwgCCAoKCAn/////////CQoCAAkC +/////////////wIDCAIICgABCAEKCP////8BCgL/////////////////AQMICQEI//////////// +/wAJAf////////////////8AAwj//////////////////////////////////////w== +""", +) + +# static const char cases[256][2] +CASES = ( + (256, 2), + """ +AP8BAAEBAgABAgMAAgMFAAEDAgEDAwUBAgUFBAUJCAABBAICAwQFAgQCBgIGCQsAAwgFBQcDCQEG +EA4DDAwFGAEFAwECBAUDAwYHAAUKCQAEAwYEBgsOAQYRDAQLBgUZAggFBwUMCAEGEgwFDgcFHAYV +CwQMDwUeCgUGIAYnAgwBBgQAAwUGAAIGBgMFCw4AAwkGBQcEDAEFDgsDCQQFGgMKBgYHBQwCBhMK +AQwNBhgHBwwJDQEHCQwUBiEHDQMMAgoGBwUNCwIFEAwHCAMFHQYWCgIMEQYbDgkGIgUnAg4FFA4F +CQUFIAsKBiMFKQIQDBcGJQcOAxAGLgQGAxUBCAEHAwIEAQYBAwcHAQYKDAACBwUGBgwLAQUPCQIO +BgUbAgkFCAYNDgIGFAwGCgMGGQUSCAIMEAUfCwkFIgYoAg0DCwcCBg4MAwcGDQAMDgcIBhcMCgoE +BhwMFQcKBikDDQUVCQMLCAUhDBYHCwYqAw4OCwUkBiwCEQYvAxIEBwEJAgsGCAYPCgAFEQwICwcG +GgUTDgQMEgYdCAQFIwUoAg8FFgsFDBMGHg4KBiQGKwQECQcFJQcPAxEFLAITAxYBCgUXDAsOCAYf +CQYHDAUqAw8LCwYmBi0EBQUtAxMCFQELCAUFJgUrAhIFLgMUAhYBDAUvAhQDFwENAhcBDgEPAP8= +""", +) + +# static const char tiling1[16][3] +TILING1 = ( + (16, 3), + """ +AAgDAAEJAQIKAwsCBAcICQUECgYFBwYLBwsGCgUGCQQFBAgHAwILAQoCAAkBAAMI +""", +) + +# static const char tiling2[24][6] +TILING2 = ( + (24, 6), + """ +AQgDCQgBAAsCCAsABAMABwMECQIKAAIJAAUEAQUAAwoBCwoDAQYFAgYBBwIDBgIHCQcIBQcJBggE +CwgGCgQJBgQKCwUKBwULCwoFBwsFCgkEBgoEBgQICwYICQgHBQkHBwMCBgcCAQUGAgEGAwEKCwMK +AAQFAQAFCQoCAAkCBAADBwQDAAILCAALAQMICQEI +""", +) + +# static const char tiling3_1[24][6] +TILING3_1 = ( + (24, 6), + """ +AAgDAQIKCQUEAAgDAwAICwcGAQkAAgMLAAEJCAQHCQABBQoGAQIKCQUECgECBgsHCAQHAwsCAgML +CgYFBQoGBAcIBAkFBwYLBQkECwYHBgoFCAcECwMCBQYKBwQIAgsDAgEKBwsGCgIBBAUJAQAJBgoF +CQEABwQIAAkBCwMCCAADBgcLBAUJAwgAAwgACgIB +""", +) + +# static const char tiling3_2[24][12] +TILING3_2 = ( + (24, 12), + """ +CgMCCggDCgEACAoAAwQIAwUEAwAJBQMJBggHBgAIBgsDAAYDCwADCwkACwIBCQsBBwkEBwEJBwgA +AQcABgEKBgABCQAGCQYFBAoFBAIKBAkBAgQBBwILBwECBwYKAQcKAgcLAgQHAgMIBAIIBQsGBQML +BQoCAwUCCAYHCAoGCAQFCggFCwUGCwkFCwcECQsEBgULBQkLBAcLBAsJBwYIBgoIBQQIBQgKBgsF +CwMFAgoFAgUDCwcCBwQCCAMCCAIECwIHAgEHCgYHCgcBBQoECgIEAQkEAQQCCgEGAQAGBgAJBQYJ +BAkHCQEHAAgHAAcBAwALAAkLAQILAQsJBwgGCAAGAwsGAwYACAQDBAUDCQADCQMFAgMKAwgKAAEK +AAoI +""", +) + +# static const char tiling4_1[8][6] +TILING4_1 = ( + (8, 6), + """ +AAgDBQoGAAEJCwcGAQIKCAQHCQUEAgMLBAUJCwMCCgIBBwQICQEABgcLAwgABgoF +""", +) + +# static const char tiling4_2[8][18] +TILING4_2 = ( + (8, 18), + """ +CAUABQgGAwYIBgMKAAoDCgAFCQYBBgkHAAcJBwALAQsACwEGCgcCBwoEAQQKBAEIAggBCAIHCwQD +BAsFAgULBQIJAwkCCQMEAwQLBQsECwUCCQIFAgkDBAMJAgcKBAoHCgQBCAEEAQgCBwIIAQYJBwkG +CQcACwAHAAsBBgELAAUIBggFCAYDCgMGAwoABQAK +""", +) + +# static const char tiling5[48][9] +TILING5 = ( + (48, 9), + """ +AggDAgoICgkIAQsCAQkLCQgLBAEJBAcBBwMBCAUECAMFAwEFAAoBAAgKCAsKCwQHCwIEAgAEBwAI +BwYABgIACQMACQUDBQcDAwYLAwAGAAQGAwkAAwsJCwoJBQIKBQQCBAACCQYFCQAGAAIGAAcIAAEH +AQUHCgABCgYABgQABgMLBgUDBQEDCgcGCgEHAQMHAQQJAQIEAgYECwECCwcBBwUBCAIDCAQCBAYC +AgUKAgMFAwcFBwoGBwgKCAkKBgkFBgsJCwgJBQgEBQoICgsIBAsHBAkLCQoLBAcLBAsJCQsKBQQI +BQgKCggLBgUJBgkLCwkIBwYKBwoICAoJAgoFAgUDAwUHCAMCCAIEBAIGCwIBCwEHBwEFAQkEAQQC +AgQGCgYHCgcBAQcDBgsDBgMFBQMBCgEACgAGBgAEAAgHAAcBAQcFCQUGCQYAAAYCBQoCBQIEBAIA +AwAJAwkLCwkKAwsGAwYAAAYECQADCQMFBQMHBwgABwAGBgACCwcECwQCAgQAAAEKAAoICAoLCAQF +CAUDAwUBBAkBBAEHBwEDAQILAQsJCQsIAgMIAggKCggJ +""", +) + +# static const char tiling6_1_1[48][9] +TILING6_1_1 = ( + (48, 9), + """ +BgUKAwEICQgBCwcGCQMBAwkIAQIKBwAEAAcDAwAIBQIGAgUBBQQJAgALCAsACgYFCAIAAggLCgYF +AAQDBwMEAwAIBgQKCQoECAMACgcFBwoLCAQHCgACAAoJBwYLAAIJCgkCAgMLBAEFAQQAAAEJBgMH +AwYCCQABCwQGBAsICwcGAQUABAAFAAEJBwULCgsFBAcIAQMKCwoDCQUECwEDAQsKCgECCAUHBQgJ +CAQHAgYBBQEGAQIKBAYICwgGAgMLBQcJCAkHCwIDCQYEBgkKCQUEAwcCBgIHBAUJAgcDBwIGAwIL +BAYJCgkGCwMCCQcFBwkICgIBCAYEBggLBwQIAQYCBgEFAgEKBwUICQgFBAUJAwELCgsBCAcECgMB +AwoLCQEACwUHBQsKBgcLAAUBBQAEAQAJBgQLCAsECQEABwMGAgYDCwMCBQEEAAQBCwYHCQIAAgkK +BwQIAgAKCQoAAAMIBQcKCwoHCAADCgQGBAoJBQYKAwQABAMHBQYKAAIICwgCCQQFCwACAAsICAAD +BgIFAQUCCgIBBAAHAwcABgcLAQMJCAkDCgUGCAEDAQgJ +""", +) + +# static const char tiling6_1_2[48][27] +TILING6_1_2 = ( + (48, 27), + """ +AQwDDAoDBgMKAwYIBQgGCAUMDAkIAQkMDAUKAQwDAQsMCwEGCQYBBgkHDAcJCQgMDAgDCwcMBAwA +BAEMAQQKBwoECgcCDAIHBwMMDAMAAQIMBgwCBgMMAwYIBQgGCAUADAAFBQEMDAECAwAMAAwCDAkC +BQIJAgULBAsFCwQMDAgLAAgMDAQJAAwCAAoMCgAFCAUABQgGDAYICAsMDAsCCgYMBAwADAUACgAF +AAoDBgMKAwYMDAcDBAcMDAYFBAwGDAgGAwYIBgMKAAoDCgAMDAkKBAkMDAAIBQwHBQgMCAUACgAF +AAoDDAMKCgsMDAsHCAMMAgwAAggMCAIHCgcCBwoEDAQKCgkMDAkACAQMAgwADAsABwALAAcJBgkH +CQYMDAoJAgoMDAYLBQwBBQIMAgULBAsFCwQDDAMEBAAMDAABAgMMBwwDBwAMAAcJBgkHCQYBDAEG +BgIMDAIDAAEMBgwEBgkMCQYBCwEGAQsADAALCwgMDAgECQAMBQwBDAYBCwEGAQsABwALAAcMDAQA +BQQMDAcGBQwHDAkHAAcJBwALAQsACwEMDAoLBQoMDAEJAwwBDAgBBAEIAQQKBwoECgcMDAsKAwsM +DAcIAwwBAwkMCQMECwQDBAsFDAULCwoMDAoBCQUMBwwFBwoMCgcCCAIHAggBDAEICAkMDAkFCgEM +BgwCDAcCCAIHAggBBAEIAQQMDAUBBgUMDAQHBgwEDAoEAQQKBAEIAggBCAIMDAsIBgsMDAIKBwwF +DAsFAgULBQIJAwkCCQMMDAgJBwgMDAMLBAwGBAsMCwQDCQMEAwkCDAIJCQoMDAoGCwIMBwwDDAQD +CQMEAwkCBQIJAgUMDAYCBwYMDAUEAwwHAwQMBAMJAgkDCQIFDAUCAgYMDAYHBAUMBgwEDAsEAwQL +BAMJAgkDCQIMDAoJBgoMDAILBQwHBQsMCwUCCQIFAgkDDAMJCQgMDAgHCwMMBAwGBAoMCgQBCAEE +AQgCDAIICAsMDAsGCgIMAgwGAgcMBwIIAQgCCAEEDAQBAQUMDAUGBwQMBQwHDAoHAgcKBwIIAQgC +CAEMDAkIBQkMDAEKAQwDDAkDBAMJAwQLBQsECwUMDAoLAQoMDAUJAQwDAQgMCAEECgQBBAoHDAcK +CgsMDAsDCAcMBwwFBwkMCQcACwAHAAsBDAELCwoMDAoFCQEMAQwFAQYMBgELAAsBCwAHDAcAAAQM +DAQFBgcMBAwGDAkGAQYJBgELAAsBCwAMDAgLBAgMDAAJAwwHDAAHCQcABwkGAQYJBgEMDAIGAwIM +DAEAAQwFDAIFCwUCBQsEAwQLBAMMDAAEAQAMDAMCAAwCAAsMCwAHCQcABwkGDAYJCQoMDAoCCwYM +AAwCDAgCBwIIAgcKBAoHCgQMDAkKAAkMDAQIBwwFDAgFAAUIBQAKAwoACgMMDAsKBwsMDAMIBgwE +BggMCAYDCgMGAwoADAAKCgkMDAkECAAMAAwEAAUMBQAKAwoACgMGDAYDAwcMDAcEBQYMAgwADAoA +BQAKAAUIBggFCAYMDAsIAgsMDAYKAgwAAgkMCQIFCwUCBQsEDAQLCwgMDAgACQQMAgwGDAMGCAYD +BggFAAUIBQAMDAEFAgEMDAADAAwEDAEECgQBBAoHAgcKBwIMDAMHAAMMDAIBAwwBDAsBBgELAQYJ +BwkGCQcMDAgJAwgMDAcLAwwBAwoMCgMGCAYDBggFDAUICAkMDAkBCgUM +""", +) + +# static const char tiling6_2[48][15] +TILING6_2 = ( + (48, 15), + """ +AQoDBgMKAwYIBQgGCAUJAQsDCwEGCQYBBgkHCAcJBAEAAQQKBwoECgcCAwIHBgMCAwYIBQgGCAUA +AQAFAAkCBQIJAgULBAsFCwQIAAoCCgAFCAUABQgGCwYIBAUACgAFAAoDBgMKAwYHBAgGAwYIBgMK +AAoDCgAJBQgHCAUACgAFAAoDCwMKAggACAIHCgcCBwoECQQKAgsABwALAAcJBgkHCQYKBQIBAgUL +BAsFCwQDAAMEBwADAAcJBgkHCQYBAgEGBgkECQYBCwEGAQsACAALBQYBCwEGAQsABwALAAcEBQkH +AAcJBwALAQsACwEKAwgBBAEIAQQKBwoECgcLAwkBCQMECwQDBAsFCgULBwoFCgcCCAIHAggBCQEI +BgcCCAIHAggBBAEIAQQFBgoEAQQKBAEIAggBCAILBwsFAgULBQIJAwkCCQMIBAsGCwQDCQMEAwkC +CgIJBwQDCQMEAwkCBQIJAgUGAwQHBAMJAgkDCQIFBgUCBgsEAwQLBAMJAgkDCQIKBQsHCwUCCQIF +AgkDCAMJBAoGCgQBCAEEAQgCCwIIAgcGBwIIAQgCCAEEBQQBBQoHAgcKBwIIAQgCCAEJAQkDBAMJ +AwQLBQsECwUKAQgDCAEECgQBBAoHCwcKBwkFCQcACwAHAAsBCgELAQYFBgELAAsBCwAHBAcABAkG +AQYJBgELAAsBCwAIAwAHCQcABwkGAQYJBgECAQIFCwUCBQsEAwQLBAMAAAsCCwAHCQcABwkGCgYJ +AAgCBwIIAgcKBAoHCgQJBwgFAAUIBQAKAwoACgMLBggECAYDCgMGAwoACQAKAAUEBQAKAwoACgMG +BwYDAgoABQAKAAUIBggFCAYLAgkACQIFCwUCBQsECAQLAgMGCAYDBggFAAUIBQABAAEECgQBBAoH +AgcKBwIDAwsBBgELAQYJBwkGCQcIAwoBCgMGCAYDBggFCQUI +""", +) + +# static const char tiling7_1[16][9] +TILING7_1 = ( + (16, 9), + """ +CQUECgECCAMACwcGCAMACgECAwAIBQQJBwYLCAQHCQABCwIDCgYFCwIDCQABAAEJBgUKBAcIAQIK +BwYLBQQJAgMLBAcIBgUKCwMCCAcECgUGCgIBCwYHCQQFCQEACgUGCAcEBQYKAwILAQAJBwQIAQAJ +AwILCAADCQQFCwYHBgcLAAMIAgEKBAUJAgEKAAMI +""", +) + +# static const char tiling7_2[16][3][15] +TILING7_2 = ( + (16, 3, 15), + """ +AQIKAwQIBAMFAAUDBQAJAwAICQEEAgQBBAIFCgUCCQUEAAoBCgAICggCAwIIAwAIAQYKBgEHAgcB +BwILAQIKCwMGAAYDBgAHCAcACwcGAggDCAIKCAoAAQAKCQUECwMGAAYDBgAHCAcACwcGAwQIBAMF +AAUDBQAJAwAIBAkHCwcJBQsJCwUGAAEJAgcLBwIEAwQCBAMIAgMLCAAHAQcABwEECQQBCAQHAwkA +CQMLCQsBAgELAgMLAAUJBQAGAQYABgEKAAEJCgIFAwUCBQMGCwYDBgUKAQsCCwEJCwkDAAMJBgUK +CAAHAQcABwEECQQBCAQHAAUJBQAGAQYABgEKAAEJBQoECAQKBggKCAYHCwcGCQEEAgQBBAIFCgUC +CQUEAQYKBgEHAgcBBwILAQIKBgsFCQULBwkLCQcECAQHCgIFAwUCBQMGCwYDBgUKAgcLBwIEAwQC +BAMIAgMLBwgGCgYIBAoICgQFBwQIBQIKAgUDBgMFAwYLCgUGCwcCBAIHAgQDCAMECwMCBggHCAYK +CAoEBQQKBgcLBAEJAQQCBQIEAgUKBAUJCgYBBwEGAQcCCwIHCgIBBQsGCwUJCwkHBAcJCgUGBwAI +AAcBBAEHAQQJBwQICQUABgAFAAYBCgEGCQEABAoFCgQICggGBwYICwMCCQUABgAFAAYBCgEGCQEA +BQIKAgUDBgMFAwYLCgUGAgsBCQELAwkLCQMACQEACwcCBAIHAgQDCAMECwMCBwAIAAcBBAEHAQQJ +BwQIAAkDCwMJAQsJCwECBAUJBgMLAwYABwAGAAcIBgcLCAQDBQMEAwUACQAFCAADBwkECQcLCQsF +BgULCAADCgYBBwEGAQcCCwIHCgIBBgMLAwYABwAGAAcIBgcLAwgCCgIIAAoICgABCgIBCAQDBQME +AwUACQAFCAADBAEJAQQCBQIEAgUKBAUJAQoACAAKAggKCAID +""", +) + +# static const char tiling7_3[16][3][27] +TILING7_3 = ( + (16, 3, 27), + """ +DAIKDAoFDAUEDAQIDAgDDAMADAAJDAkBDAECDAUEDAQIDAgDDAMCDAIKDAoBDAEADAAJDAkFBQQM +CgUMAgoMAwIMCAMMAAgMAQAMCQEMBAkMDAAIDAgHDAcGDAYKDAoBDAECDAILDAsDDAMADAcGDAYK +DAoBDAEADAAIDAgDDAMCDAILDAsHBwYMCAcMAAgMAQAMCgEMAgoMAwIMCwMMBgsMCQUMAAkMAwAM +CwMMBgsMBwYMCAcMBAgMBQQMAwAMCwMMBgsMBQYMCQUMBAkMBwQMCAcMAAgMDAMADAAJDAkFDAUG +DAYLDAsHDAcEDAQIDAgDDAEJDAkEDAQHDAcLDAsCDAIDDAMIDAgADAABDAQHDAcLDAsCDAIBDAEJ +DAkADAADDAMIDAgEBAcMCQQMAQkMAgEMCwIMAwsMAAMMCAAMBwgMDAMLDAsGDAYFDAUJDAkADAAB +DAEKDAoCDAIDDAYFDAUJDAkADAADDAMLDAsCDAIBDAEKDAoGBgUMCwYMAwsMAAMMCQAMAQkMAgEM +CgIMBQoMCgYMAQoMAAEMCAAMBwgMBAcMCQQMBQkMBgUMAAEMCAAMBwgMBgcMCgYMBQoMBAUMCQQM +AQkMDAABDAEKDAoGDAYHDAcIDAgEDAQFDAUJDAkACwcMAgsMAQIMCQEMBAkMBQQMCgUMBgoMBwYM +AQIMCQEMBAkMBwQMCwcMBgsMBQYMCgUMAgoMDAECDAILDAsHDAcEDAQJDAkFDAUGDAYKDAoBCAQM +AwgMAgMMCgIMBQoMBgUMCwYMBwsMBAcMAgMMCgIMBQoMBAUMCAQMBwgMBgcMCwYMAwsMDAIDDAMI +DAgEDAQFDAUKDAoGDAYHDAcLDAsCDAQIDAgDDAMCDAIKDAoFDAUGDAYLDAsHDAcEDAMCDAIKDAoF +DAUEDAQIDAgHDAcGDAYLDAsDAwIMCAMMBAgMBQQMCgUMBgoMBwYMCwcMAgsMDAcLDAsCDAIBDAEJ +DAkEDAQFDAUKDAoGDAYHDAIBDAEJDAkEDAQHDAcLDAsGDAYFDAUKDAoCAgEMCwIMBwsMBAcMCQQM +BQkMBgUMCgYMAQoMDAYKDAoBDAEADAAIDAgHDAcEDAQJDAkFDAUGDAEADAAIDAgHDAcGDAYKDAoF +DAUEDAQJDAkBAQAMCgEMBgoMBwYMCAcMBAgMBQQMCQUMAAkMCwMMBgsMBQYMCQUMAAkMAQAMCgEM +AgoMAwIMBQYMCQUMAAkMAwAMCwMMAgsMAQIMCgEMBgoMDAUGDAYLDAsDDAMADAAJDAkBDAECDAIK +DAoFCQEMBAkMBwQMCwcMAgsMAwIMCAMMAAgMAQAMBwQMCwcMAgsMAQIMCQEMAAkMAwAMCAMMBAgM +DAcEDAQJDAkBDAECDAILDAsDDAMADAAIDAgHDAUJDAkADAADDAMLDAsGDAYHDAcIDAgEDAQFDAAD +DAMLDAsGDAYFDAUJDAkEDAQHDAcIDAgAAAMMCQAMBQkMBgUMCwYMBwsMBAcMCAQMAwgMCAAMBwgM +BgcMCgYMAQoMAgEMCwIMAwsMAAMMBgcMCgYMAQoMAAEMCAAMAwgMAgMMCwIMBwsMDAYHDAcIDAgA +DAABDAEKDAoCDAIDDAMLDAsGCgIMBQoMBAUMCAQMAwgMAAMMCQAMAQkMAgEMBAUMCAQMAwgMAgMM +CgIMAQoMAAEMCQAMBQkMDAQFDAUKDAoCDAIDDAMIDAgADAABDAEJDAkE +""", +) + +# static const char tiling7_4_1[16][15] +TILING7_4_1 = ( + (16, 15), + """ +AwQIBAMKAgoDBAoFCQEAAQYKBgEIAAgBBggHCwMCCwMGCQYDBgkFAAkDBwQIAgcLBwIJAQkCBwkE +CAADAAUJBQALAwsABQsGCgIBCAAHCgcABwoGAQoABAUJCQEECwQBBAsHAgsBBQYKCgIFCAUCBQgE +AwgCBgcLBQIKAgUIBAgFAggDCwcGBAEJAQQLBwsEAQsCCgYFBwAIAAcKBgoHAAoBCQUECQUACwAF +AAsDBgsFAQIKCwcCCQIHAgkBBAkHAwAIBgMLAwYJBQkGAwkACAQHCgYBCAEGAQgABwgGAgMLCAQD +CgMEAwoCBQoEAAEJ +""", +) + +# static const char tiling7_4_2[16][27] +TILING7_4_2 = ( + (16, 27), + """ +CQQIBAkFCgUJAQoJCgECAAIBAgADCAMACQgACwYKBgsHCAcLAwgLCAMAAgADAAIBCgECCwoCCwMI +AAgDCAAJCAkEBQQJBAUHBgcFBwYLBwsICAcLBwgECQQIAAkICQABAwEAAQMCCwIDCAsDCgUJBQoG +CwYKAgsKCwIDAQMCAwEACQABCgkBCAAJAQkACQEKCQoFBgUKBQYEBwQGBAcIBAgJCQEKAgoBCgIL +CgsGBwYLBgcFBAUHBQQJBQkKCgILAwsCCwMICwgHBAcIBwQGBQYEBgUKBgoLCwIKAgsDCAMLBwgL +CAcEBgQHBAYFCgUGCwoGCgEJAQoCCwIKBgsKCwYHBQcGBwUECQQFCgkFCQAIAAkBCgEJBQoJCgUG +BAYFBgQHCAcECQgECQUKBgoFCgYLCgsCAwILAgMBAAEDAQAJAQkKCwcIBAgHCAQJCAkAAQAJAAED +AgMBAwILAwsICAMLAwgACQAIBAkICQQFBwUEBQcGCwYHCAsHCgYLBwsGCwcICwgDAAMIAwACAQIA +AgEKAgoLCAQJBQkECQUKCQoBAgEKAQIAAwACAAMIAAgJ +""", +) + +# static const char tiling8[6][6] +TILING8 = ( + (6, 6), + """ +CQgKCggLAQUDAwUHAAQCBAYCAAIEBAIGAQMFAwcFCQoICgsI +""", +) + +# static const char tiling9[8][12] +TILING9 = ( + (8, 12), + """ +AgoFAwIFAwUEAwQIBAcLCQQLCQsCCQIBCgcGAQcKAQgHAQAIAwYLAAYDAAUGAAkFAwsGAAMGAAYF +AAUJCgYHAQoHAQcIAQgABAsHCQsECQILCQECAgUKAwUCAwQFAwgE +""", +) + +# static const char tiling10_1_1[6][12] +TILING10_1_1 = ( + (6, 12), + """ +BQoHCwcKCAEJAQgDAQIFBgUCBAMAAwQHCwAIAAsCBAkGCgYJCQAKAgoABggECAYLBwIDAgcGAAEE +BQQBBwkFCQcICgELAwsB +""", +) + +# static const char tiling10_1_1_[6][12] +TILING10_1_1_ = ( + (6, 12), + """ +BQkHCAcJCwEKAQsDAwIHBgcCBAEAAQQFCgAJAAoCBAgGCwYICAALAgsABgkECQYKBQIBAgUGAAME +BwQDBwoFCgcLCQEIAwgB +""", +) + +# static const char tiling10_1_2[6][24] +TILING10_1_2 = ( + (6, 24), + """ +AwsHAwcICQgHBQkHCQUKCQoBAwEKCwMKBwYFBwUEAAQFAQAFAAECAAIDBwMCBgcCCwIKBgsKCwYE +CwQIAAgECQAEAAkKAAoCCwIKCwoGBAYKCQQKBAkABAAICwgAAgsABwYFBAcFBwQABwADAgMAAQIA +AgEFAgUGBwgDCwcDBwsKBwoFCQUKAQkKCQEDCQMI +""", +) + +# static const char tiling10_2[6][24] +TILING10_2 = ( + (6, 24), + """ +DAUJDAkIDAgDDAMBDAEKDAoLDAsHDAcFDAEADAAEDAQHDAcDDAMCDAIGDAYFDAUBBAgMBgQMCgYM +CQoMAAkMAgAMCwIMCAsMDAkEDAQGDAYLDAsIDAgADAACDAIKDAoJAAMMBAAMBQQMAQUMAgEMBgIM +BwYMAwcMCgUMCwoMAwsMAQMMCQEMCAkMBwgMBQcM +""", +) + +# static const char tiling10_2_[6][24] +TILING10_2_ = ( + (6, 24), + """ +CAcMCQgMAQkMAwEMCwMMCgsMBQoMBwUMBAUMAAQMAwAMBwMMBgcMAgYMAQIMBQEMDAsGDAYEDAQJ +DAkKDAoCDAIADAAIDAgLBgoMBAYMCAQMCwgMAgsMAAIMCQAMCgkMDAcEDAQADAABDAEFDAUGDAYC +DAIDDAMHDAcLDAsKDAoBDAEDDAMIDAgJDAkFDAUH +""", +) + +# static const char tiling11[12][12] +TILING11 = ( + (12, 12), + """ +AgoJAgkHAgcDBwkEAQYCAQgGAQkICAcGCAMBCAEGCAYEBgEKAAgLAAsFAAUBBQsGCQUHCQcCCQIA +AgcLBQAEBQsABQoLCwMABQQABQALBQsKCwADCQcFCQIHCQACAgsHAAsIAAULAAEFBQYLCAEDCAYB +CAQGBgoBAQIGAQYIAQgJCAYHAgkKAgcJAgMHBwQJ +""", +) + +# static const char tiling12_1_1[24][12] +TILING12_1_1 = ( + (24, 12), + """ +BwYLCgMCAwoICQgKBgUKCQIBAgkLCAsJCgYFBwkECQcBAwEHBwYLBAgFAwUIBQMBBQQJCAEAAQgK +CwoIAQIKAAkDBQMJAwUHCgECAAsDCwAGBAYACAMAAgkBCQIEBgQCAwAIAgsBBwELAQcFBgUKBwsE +AgQLBAIACQUEBggHCAYAAgAGCAMABwQLCQsECwkKBAcICwADAAsJCgkLBAcIBQkGAAYJBgACCwcG +BAoFCgQCAAIECwIDAQgACAEHBQcBAAEJAwgCBAIIAgQGAgMLAQoABgAKAAYECQABAwoCCgMFBwUD +CQABBAUICggFCAoLCAQHBQsGCwUDAQMFBQQJBgoHAQcKBwEDCgECBQYJCwkGCQsICwIDBgcKCAoH +CggJ +""", +) + +# static const char tiling12_1_1_[24][12] +TILING12_1_1_ = ( + (24, 12), + """ +AwILCgcGBwoICQgKAgEKCQYFBgkLCAsJCQQFBwoGCgcBAwEHBwQIBgsFAwULBQMBAQAJCAUEBQgK +CwoIAQAJAgoDBQMKAwUHCwMCAAoBCgAGBAYACQEAAggDCAIEBgQCAwILAAgBBwEIAQcFBgcLBQoE +AgQKBAIACAcEBgkFCQYAAgAGCAcEAwALCQsACwkKAAMICwQHBAsJCgkLBAUJBwgGAAYIBgACCgUG +BAsHCwQCAAIECAADAQsCCwEHBQcBAAMIAQkCBAIJAgQGAgEKAwsABgALAAYECgIBAwkACQMFBwUD +CQQFAAEICggBCAoLCwYHBQgECAUDAQMFBQYKBAkHAQcJBwEDCgUGAQIJCwkCCQsICwYHAgMKCAoD +CggJ +""", +) + +# static const char tiling12_1_2[24][24] +TILING12_1_2 = ( + (24, 24), + """ +BwMLAwcICQgHBgkHCQYKAgoGCwIGAgsDBgIKAgYLCAsGBQgGCAUJAQkFCgEFAQoCCgkFCQoBAwEK +BgMKAwYHBAcGBQQGBAUJBwgLAwsICwMBCwEGBQYBBgUEBgQHCAcEBQEJAQUKCwoFBAsFCwQIAAgE +CQAEAAkBAQkKBQoJCgUHCgcCAwIHAgMAAgABCQEACgsCCwoGBAYKAQQKBAEAAwABAgMBAwILCAkA +CQgEBgQIAwYIBgMCAQIDAAEDAQAJAwsIBwgLCAcFCAUAAQAFAAECAAIDCwMCBgsKAgoLCgIACgAF +BAUABQQHBQcGCwYHCQgECAkAAgAJBQIJAgUGBwYFBAcFBwQICAQACQAEAAkKAAoDCwMKAwsHAwcI +BAgHBAAIAAQJCgkEBwoECgcLAwsHCAMHAwgABAkIAAgJCAACCAIHBgcCBwYFBwUECQQFCwoGCgsC +AAILBwALAAcEBQQHBgUHBQYKCwgDCAsHBQcLAgULBQIBAAECAwACAAMIAAgJBAkICQQGCQYBAgEG +AQIDAQMACAADAgoLBgsKCwYECwQDAAMEAwABAwECCgIBCQoBCgkFBwUJAAcJBwADAgMAAQIAAgEK +CQUBCgEFAQoLAQsACAALAAgEAAQJBQkECAsHCwgDAQMIBAEIAQQFBgUEBwYEBgcLBQoJAQkKCQED +CQMEBwQDBAcGBAYFCgUGCgYCCwIGAgsIAggBCQEIAQkFAQUKBgoFCwcDCAMHAwgJAwkCCgIJAgoG +AgYLBwsG +""", +) + +# static const char tiling12_2[24][24] +TILING12_2 = ( + (24, 24), + """ +CQgMCgkMAgoMAwIMCwMMBgsMBwYMCAcMCAsMCQgMAQkMAgEMCgIMBQoMBgUMCwYMAwEMBwMMBAcM +CQQMBQkMBgUMCgYMAQoMDAMBDAEFDAUGDAYLDAsHDAcEDAQIDAgDCwoMCAsMAAgMAQAMCQEMBAkM +BQQMCgUMDAUHDAcDDAMCDAIKDAoBDAEADAAJDAkFBAYMAAQMAQAMCgEMAgoMAwIMCwMMBgsMBgQM +AgYMAwIMCAMMAAgMAQAMCQEMBAkMDAcFDAUBDAEADAAIDAgDDAMCDAILDAsHDAIADAAEDAQFDAUK +DAoGDAYHDAcLDAsCAgAMBgIMBwYMCAcMBAgMBQQMCQUMAAkMDAkKDAoLDAsHDAcEDAQIDAgDDAMA +DAAJCgkMCwoMBwsMBAcMCAQMAwgMAAMMCQAMDAACDAIGDAYHDAcIDAgEDAQFDAUJDAkAAAIMBAAM +BQQMCgUMBgoMBwYMCwcMAgsMBQcMAQUMAAEMCAAMAwgMAgMMCwIMBwsMDAQGDAYCDAIDDAMIDAgA +DAABDAEJDAkEDAYEDAQADAABDAEKDAoCDAIDDAMLDAsGBwUMAwcMAgMMCgIMAQoMAAEMCQAMBQkM +DAoLDAsIDAgADAABDAEJDAkEDAQFDAUKAQMMBQEMBgUMCwYMBwsMBAcMCAQMAwgMDAEDDAMHDAcE +DAQJDAkFDAUGDAYKDAoBDAsIDAgJDAkBDAECDAIKDAoFDAUGDAYLDAgJDAkKDAoCDAIDDAMLDAsG +DAYHDAcI +""", +) + +# static const char tiling12_2_[24][24] +TILING12_2_ = ( + (24, 24), + """ +DAILDAsHDAcGDAYKDAoJDAkIDAgDDAMCDAEKDAoGDAYFDAUJDAkIDAgLDAsCDAIBDAQFDAUKDAoG +DAYHDAcDDAMBDAEJDAkEBwYMCAcMBAgMBQQMAQUMAwEMCwMMBgsMDAAJDAkFDAUEDAQIDAgLDAsK +DAoBDAEAAQIMCQEMAAkMAwAMBwMMBQcMCgUMAgoMDAECDAILDAsDDAMADAAEDAQGDAYKDAoBDAMA +DAAJDAkBDAECDAIGDAYEDAQIDAgDAwAMCwMMAgsMAQIMBQEMBwUMCAcMAAgMBgUMCwYMBwsMBAcM +AAQMAgAMCgIMBQoMDAcEDAQJDAkFDAUGDAYCDAIADAAIDAgHCAcMAAgMAwAMCwMMCgsMCQoMBAkM +BwQMDAcIDAgADAADDAMLDAsKDAoJDAkEDAQHBAcMCQQMBQkMBgUMAgYMAAIMCAAMBwgMDAUGDAYL +DAsHDAcEDAQADAACDAIKDAoFDAADDAMLDAsCDAIBDAEFDAUHDAcIDAgAAAMMCQAMAQkMAgEMBgIM +BAYMCAQMAwgMAgEMCwIMAwsMAAMMBAAMBgQMCgYMAQoMDAIBDAEJDAkADAADDAMHDAcFDAUKDAoC +CQAMBQkMBAUMCAQMCwgMCgsMAQoMAAEMDAYHDAcIDAgEDAQFDAUBDAEDDAMLDAsGBQQMCgUMBgoM +BwYMAwcMAQMMCQEMBAkMCgEMBgoMBQYMCQUMCAkMCwgMAgsMAQIMCwIMBwsMBgcMCgYMCQoMCAkM +AwgMAgMM +""", +) + +# static const char tiling13_1[2][12] +TILING13_1 = ( + (2, 12), + """ +CwcGAQIKCAMACQUECAQHAgMLCQABCgYF +""", +) + +# static const char tiling13_1_[2][12] +TILING13_1_ = ( + (2, 12), + """ +BwQICwMCAQAJBQYKBgcLCgIBAAMIBAUJ +""", +) + +# static const char tiling13_2[2][6][18] +TILING13_2 = ( + (2, 6, 18), + """ +AQIKCwcGAwQIBAMFAAUDBQAJCAMACwcGCQEEAgQBBAIFCgUCCQUECAMAAQYKBgEHAgcBBwILCQUE +AQIKCwMGAAYDBgAHCAcACQUECwcGAAoBCgAICggCAwIIAQIKAwAIBAkHCwcJBQsJCwUGAgMLCAQH +AAUJBQAGAQYABgEKCQABCAQHCgIFAwUCBQMGCwYDBgUKCQABAgcLBwIEAwQCBAMIBgUKAgMLCAAH +AQcABwEECQQBBgUKCAQHAQsCCwEJCwkDAAMJAgMLAAEJBQoECAQKBggKCAYH +""", +) + +# static const char tiling13_2_[2][6][18] +TILING13_2_ = ( + (2, 6, 18), + """ +CgUGCwMCBwAIAAcBBAEHAQQJCwMCBwQICQUABgAFAAYBCgEGAQAJBwQIBQIKAgUDBgMFAwYLCgUG +AQAJCwcCBAIHAgQDCAMECgUGBwQIAgsBCQELAwkLCQMACwMCCQEABAoFCgQICggGBwYIBgcLCAAD +BAEJAQQCBQIEAgUKCAADBAUJCgYBBwEGAQcCCwIHAgEKBAUJBgMLAwYABwAGAAcIBgcLAgEKCAQD +BQMEAwUACQAFBgcLBAUJAwgCCgIIAAoICgABCAADCgIBBQsGCwUJCwkHBAcJ +""", +) + +# static const char tiling13_3[2][12][30] +TILING13_3 = ( + (2, 12, 30), + """ +CwcGDAIKDAoFDAUEDAQIDAgDDAMADAAJDAkBDAECAQIKCQUMAAkMAwAMCwMMBgsMBwYMCAcMBAgM +BQQMCwcGDAUEDAQIDAgDDAMCDAIKDAoBDAEADAAJDAkFAQIKDAMADAAJDAkFDAUGDAYLDAsHDAcE +DAQIDAgDCAMACwcMAgsMAQIMCQEMBAkMBQQMCgUMBgoMBwYMCwcGBQQMCgUMAgoMAwIMCAMMAAgM +AQAMCQEMBAkMCAMAAQIMCQEMBAkMBwQMCwcMBgsMBQYMCgUMAgoMCQUEDAAIDAgHDAcGDAYKDAoB +DAECDAILDAsDDAMACQUEDAcGDAYKDAoBDAEADAAIDAgDDAMCDAILDAsHCAMADAECDAILDAsHDAcE +DAQJDAkFDAUGDAYKDAoBCQUEBwYMCAcMAAgMAQAMCgEMAgoMAwIMCwMMBgsMAQIKAwAMCwMMBgsM +BQYMCQUMBAkMBwQMCAcMAAgMCAQHDAMLDAsGDAYFDAUJDAkADAABDAEKDAoCDAIDAgMLCgYMAQoM +AAEMCAAMBwgMBAcMCQQMBQkMBgUMCAQHDAYFDAUJDAkADAADDAMLDAsCDAIBDAEKDAoGAgMLDAAB +DAEKDAoGDAYHDAcIDAgEDAQFDAUJDAkAAAEJCAQMAwgMAgMMCgIMBQoMBgUMCwYMBwsMBAcMCAQH +BgUMCwYMAwsMAAMMCQAMAQkMAgEMCgIMBQoMCQABAgMMCgIMBQoMBAUMCAQMBwgMBgcMCwYMAwsM +BgUKDAEJDAkEDAQHDAcLDAsCDAIDDAMIDAgADAABBgUKDAQHDAcLDAsCDAIBDAEJDAkADAADDAMI +DAgECQABDAIDDAMIDAgEDAQFDAUKDAoGDAYHDAcLDAsCBgUKBAcMCQQMAQkMAgEMCwIMAwsMAAMM +CAAMBwgMAgMLAAEMCAAMBwgMBgcMCgYMBQoMBAUMCQQMAQkM +""", +) + +# static const char tiling13_3_[2][12][30] +TILING13_3_ = ( + (2, 12, 30), + """ +AwILCAcMAAgMAQAMCgEMBgoMBQYMCQUMBAkMBwQMBQYKDAILDAsHDAcEDAQJDAkBDAEADAAIDAgD +DAMCCgUGDAcEDAQJDAkBDAECDAILDAsDDAMADAAIDAgHCwMCDAEADAAIDAgHDAcGDAYKDAoFDAUE +DAQJDAkBBwQICwMMBgsMBQYMCQUMAAkMAQAMCgEMAgoMAwIMBwQIBQYMCQUMAAkMAwAMCwMMAgsM +AQIMCgEMBgoMCwMCAQAMCgEMBgoMBwYMCAcMBAgMBQQMCQUMAAkMAQAJDAQIDAgDDAMCDAIKDAoF +DAUGDAYLDAsHDAcEBwQIDAUGDAYLDAsDDAMADAAJDAkBDAECDAIKDAoFAQAJDAMCDAIKDAoFDAUE +DAQIDAgHDAcGDAYLDAsDCgUGBwQMCwcMAgsMAQIMCQEMAAkMAwAMCAMMBAgMCQEAAwIMCAMMBAgM +BQQMCgUMBgoMBwYMCwcMAgsMAAMICQQMAQkMAgEMCwIMBwsMBgcMCgYMBQoMBAUMCwYHDAMIDAgE +DAQFDAUKDAoCDAIBDAEJDAkADAADBgcLDAQFDAUKDAoCDAIDDAMIDAgADAABDAEJDAkECAADDAIB +DAEJDAkEDAQHDAcLDAsGDAYFDAUKDAoCBAUJCAAMBwgMBgcMCgYMAQoMAgEMCwIMAwsMAAMMBAUJ +BgcMCgYMAQoMAAEMCAAMAwgMAgMMCwIMBwsMCAADAgEMCwIMBwsMBAcMCQQMBQkMBgUMCgYMAQoM +AgEKDAUJDAkADAADDAMLDAsGDAYHDAcIDAgEDAQFBAUJDAYHDAcIDAgADAABDAEKDAoCDAIDDAML +DAsGAgEKDAADDAMLDAsGDAYFDAUJDAkEDAQHDAcIDAgABgcLBAUMCAQMAwgMAgMMCgIMAQoMAAEM +CQAMBQkMCgIBAAMMCQAMBQkMBgUMCwYMBwsMBAcMCAQMAwgM +""", +) + +# static const char tiling13_4[2][4][36] +TILING13_4 = ( + (2, 4, 36), + """ +DAIKDAoFDAUGDAYLDAsHDAcEDAQIDAgDDAMADAAJDAkBDAECCwMMBgsMBwYMCAcMBAgMBQQMCQUM +AAkMAQAMCgEMAgoMAwIMCQEMBAkMBQQMCgUMBgoMBwYMCwcMAgsMAwIMCAMMAAgMAQAMDAAIDAgH +DAcEDAQJDAkFDAUGDAYKDAoBDAECDAILDAsDDAMADAMLDAsGDAYHDAcIDAgEDAQFDAUJDAkADAAB +DAEKDAoCDAIDCAAMBwgMBAcMCQQMBQkMBgUMCgYMAQoMAgEMCwIMAwsMAAMMCgIMBQoMBgUMCwYM +BwsMBAcMCAQMAwgMAAMMCQAMAQkMAgEMDAEJDAkEDAQFDAUKDAoGDAYHDAcLDAsCDAIDDAMIDAgA +DAAB +""", +) + +# static const char tiling13_5_1[2][4][18] +TILING13_5_1 = ( + (2, 4, 18), + """ +BwYLAQAJCgMCAwoFAwUIBAgFAQIKBwQIAwALBgsACQYABgkFAwAIBQYKAQIJBAkCCwQCBAsHBQQJ +AwILCAEAAQgHAQcKBgoHBAcIAgEKCwADAAsGAAYJBQkGAgMLBAUJAAEIBwgBCgcBBwoGAAEJBgcL +AgMKBQoDCAUDBQgEBgUKAAMICQIBAgkEAgQLBwsE +""", +) + +# static const char tiling13_5_2[2][4][30] +TILING13_5_2 = ( + (2, 4, 30), + """ +AQAJBwQIBwgDBwMLAgsDCwIKCwoGBQYKBgUHBAcFBwQICwMCBgsCCgYCBgoFCQUKAQkKCQEAAgAB +AAIDBQYKCQEABAkACAQABAgHCwcIAwsICwMCAAIDAgABAwILBQYKBQoBBQEJAAkBCQAICQgEBAgH +BAcFBgUHAgEKBAUJBAkABAAIAwgACAMLCAsHBgcLBwYEBQQGBAUJCAADBwgDCwcDBwsGCgYLAgoL +CgIBAwECAQMABgcLCgIBBQoBCQUBBQkECAQJAAgJCAADAQMAAwECAAMIBgcLBgsCBgIKAQoCCgEJ +CgkFBQkEBQQGBwYE +""", +) + +# static const char tiling14[12][12] +TILING14 = ( + (12, 12), + """ +BQkIBQgCBQIGAwIIAgEFAgUIAggLBAgFCQQGCQYDCQMBCwMGAQsKAQQLAQAEBwsECAIACAUCCAcF +CgIFAAcDAAoHAAkKBgcKAAMHAAcKAAoJBgoHCAACCAIFCAUHCgUCAQoLAQsEAQQABwQLCQYECQMG +CQEDCwYDAgUBAggFAgsIBAUIBQgJBQIIBQYCAwgC +""", +) + +# static const char test3[24] +TEST3 = ( + (24,), + """ +BQEEBQECAgMEAwYG+vr9/P3+/v/7/P/7 +""", +) + +# static const char test4[8] +TEST4 = ( + (8,), + """ +BwcHB/n5+fk= +""", +) + +# static const char test6[48][3] +TEST6 = ( + (48, 3), + """ +AgcKBAcLBQcBBQcDAQcJAwcKBgcFAQcIBAcIAQcIAwcLBQcCBQcAAQcJBgcGAgcJBAcIAgcJAgcK +BgcHAwcKBAcLAwcLBgcE+vkE/fkL/PkL/fkK+vkH/vkK/vkJ/PkI/vkJ+vkG//kJ+/kA+/kC/fkL +//kI/PkI//kI+vkF/fkK//kJ+/kD+/kB/PkL/vkK +""", +) + +# static const char test7[16][5] +TEST7 = ( + (16, 5), + """ +AQIFBwEDBAUHAwQBBgcEBAEFBwACAwUHAgECBgcFAgMGBwYDBAYHB/38+vkH/v36+Qb//vr5Bf79 ++/kC/P/7+QD8//r5BP38+/kD//77+QE= +""", +) + +# static const char test10[6][3] +TEST10 = ( + (6, 3), + """ +AgQHBQYHAQMHAQMHBQYHAgQH +""", +) + +# static const char test12[24][4] +TEST12 = ( + (24, 4), + """ +BAMHCwMCBwoCBgcFBgQHBwIBBwkFAgcBBQMHAgUBBwAFBAcDBgMHBgEGBwQBBAcIBAEHCAYBBwQD +BgcGBAUHAwEFBwADBQcCAgUHAQECBwkEBgcHBgIHBQIDBwoDBAcL +""", +) + +# static const char test13[2][7] +TEST13 = ( + (2, 7), + """ +AQIDBAUGBwIDBAEFBgc= +""", +) + +# static const char subconfig13[64] +SUBCONFIG13 = ( + (64,), + """ +AAECBwP/C/8ECP//Dv///wUJDBcP/xUmERT/JBohHiwGCg0TEP8ZJRIY/yMWIB0r////Iv//HCr/ +H/8pGygnLQ== +""", +) diff --git a/lib/python3.10/site-packages/skimage/measure/_moments.py b/lib/python3.10/site-packages/skimage/measure/_moments.py new file mode 100644 index 0000000000000000000000000000000000000000..fa9dcbc1e6aabd8c1eaa1b6fb412b7d76f5e3145 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_moments.py @@ -0,0 +1,504 @@ +import itertools + +import numpy as np + +from .._shared.utils import _supported_float_type, check_nD +from . import _moments_cy +from ._moments_analytical import moments_raw_to_central + + +def moments_coords(coords, order=3): + """Calculate all raw image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as: ``M[0, 0]``. + * Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}. + + Note that raw moments are neither translation, scale, nor rotation + invariant. + + Parameters + ---------- + coords : (N, D) double or uint8 array + Array of N points that describe an image of D dimensionality in + Cartesian space. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + M : (``order + 1``, ``order + 1``, ...) array + Raw image moments. (D dimensions) + + References + ---------- + .. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham + University, version 0.2, Durham, 2001. + + Examples + -------- + >>> coords = np.array([[row, col] + ... for row in range(13, 17) + ... for col in range(14, 18)], dtype=np.float64) + >>> M = moments_coords(coords) + >>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0]) + >>> centroid + (14.5, 15.5) + """ + return moments_coords_central(coords, 0, order=order) + + +def moments_coords_central(coords, center=None, order=3): + """Calculate all central image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as: ``M[0, 0]``. + * Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}. + + Note that raw moments are neither translation, scale nor rotation + invariant. + + Parameters + ---------- + coords : (N, D) double or uint8 array + Array of N points that describe an image of D dimensionality in + Cartesian space. A tuple of coordinates as returned by + ``np.nonzero`` is also accepted as input. + center : tuple of float, optional + Coordinates of the image centroid. This will be computed if it + is not provided. + order : int, optional + Maximum order of moments. Default is 3. + + Returns + ------- + Mc : (``order + 1``, ``order + 1``, ...) array + Central image moments. (D dimensions) + + References + ---------- + .. [1] Johannes Kilian. Simple Image Analysis By Moments. Durham + University, version 0.2, Durham, 2001. + + Examples + -------- + >>> coords = np.array([[row, col] + ... for row in range(13, 17) + ... for col in range(14, 18)]) + >>> moments_coords_central(coords) + array([[16., 0., 20., 0.], + [ 0., 0., 0., 0.], + [20., 0., 25., 0.], + [ 0., 0., 0., 0.]]) + + As seen above, for symmetric objects, odd-order moments (columns 1 and 3, + rows 1 and 3) are zero when centered on the centroid, or center of mass, + of the object (the default). If we break the symmetry by adding a new + point, this no longer holds: + + >>> coords2 = np.concatenate((coords, [[17, 17]]), axis=0) + >>> np.round(moments_coords_central(coords2), + ... decimals=2) # doctest: +NORMALIZE_WHITESPACE + array([[17. , 0. , 22.12, -2.49], + [ 0. , 3.53, 1.73, 7.4 ], + [25.88, 6.02, 36.63, 8.83], + [ 4.15, 19.17, 14.8 , 39.6 ]]) + + Image moments and central image moments are equivalent (by definition) + when the center is (0, 0): + + >>> np.allclose(moments_coords(coords), + ... moments_coords_central(coords, (0, 0))) + True + """ + if isinstance(coords, tuple): + # This format corresponds to coordinate tuples as returned by + # e.g. np.nonzero: (row_coords, column_coords). + # We represent them as an npoints x ndim array. + coords = np.stack(coords, axis=-1) + check_nD(coords, 2) + ndim = coords.shape[1] + + float_type = _supported_float_type(coords.dtype) + if center is None: + center = np.mean(coords, axis=0, dtype=float) + + # center the coordinates + coords = coords.astype(float_type, copy=False) - center + + # generate all possible exponents for each axis in the given set of points + # produces a matrix of shape (N, D, order + 1) + coords = np.stack([coords**c for c in range(order + 1)], axis=-1) + + # add extra dimensions for proper broadcasting + coords = coords.reshape(coords.shape + (1,) * (ndim - 1)) + + calc = 1 + + for axis in range(ndim): + # isolate each point's axis + isolated_axis = coords[:, axis] + + # rotate orientation of matrix for proper broadcasting + isolated_axis = np.moveaxis(isolated_axis, 1, 1 + axis) + + # calculate the moments for each point, one axis at a time + calc = calc * isolated_axis + + # sum all individual point moments to get our final answer + Mc = np.sum(calc, axis=0) + + return Mc + + +def moments(image, order=3, *, spacing=None): + """Calculate all raw image moments up to a certain order. + + The following properties can be calculated from raw image moments: + * Area as: ``M[0, 0]``. + * Centroid as: {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}. + + Note that raw moments are neither translation, scale nor rotation + invariant. + + Parameters + ---------- + image : (N[, ...]) double or uint8 array + Rasterized shape as image. + order : int, optional + Maximum order of moments. Default is 3. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + m : (``order + 1``, ``order + 1``) array + Raw image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] https://en.wikipedia.org/wiki/Image_moment + + Examples + -------- + >>> image = np.zeros((20, 20), dtype=np.float64) + >>> image[13:17, 13:17] = 1 + >>> M = moments(image) + >>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0]) + >>> centroid + (14.5, 14.5) + """ + return moments_central(image, (0,) * image.ndim, order=order, spacing=spacing) + + +def moments_central(image, center=None, order=3, *, spacing=None, **kwargs): + """Calculate all central image moments up to a certain order. + + The center coordinates (cr, cc) can be calculated from the raw moments as: + {``M[1, 0] / M[0, 0]``, ``M[0, 1] / M[0, 0]``}. + + Note that central moments are translation invariant but not scale and + rotation invariant. + + Parameters + ---------- + image : (N[, ...]) double or uint8 array + Rasterized shape as image. + center : tuple of float, optional + Coordinates of the image centroid. This will be computed if it + is not provided. + order : int, optional + The maximum order of moments computed. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + mu : (``order + 1``, ``order + 1``) array + Central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] https://en.wikipedia.org/wiki/Image_moment + + Examples + -------- + >>> image = np.zeros((20, 20), dtype=np.float64) + >>> image[13:17, 13:17] = 1 + >>> M = moments(image) + >>> centroid = (M[1, 0] / M[0, 0], M[0, 1] / M[0, 0]) + >>> moments_central(image, centroid) + array([[16., 0., 20., 0.], + [ 0., 0., 0., 0.], + [20., 0., 25., 0.], + [ 0., 0., 0., 0.]]) + """ + if center is None: + # Note: No need for an explicit call to centroid. + # The centroid will be obtained from the raw moments. + moments_raw = moments(image, order=order, spacing=spacing) + return moments_raw_to_central(moments_raw) + float_dtype = _supported_float_type(image.dtype) + if spacing is None: + spacing = np.ones(image.ndim, dtype=float_dtype) + calc = image.astype(float_dtype, copy=False) + for dim, dim_length in enumerate(image.shape): + delta = np.arange(dim_length, dtype=float_dtype) * spacing[dim] - center[dim] + powers_of_delta = delta[:, np.newaxis] ** np.arange( + order + 1, dtype=float_dtype + ) + calc = np.rollaxis(calc, dim, image.ndim) + calc = np.dot(calc, powers_of_delta) + calc = np.rollaxis(calc, -1, dim) + return calc + + +def moments_normalized(mu, order=3, spacing=None): + """Calculate all normalized central image moments up to a certain order. + + Note that normalized central moments are translation and scale invariant + but not rotation invariant. + + Parameters + ---------- + mu : (M[, ...], M) array + Central image moments, where M must be greater than or equal + to ``order``. + order : int, optional + Maximum order of moments. Default is 3. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + nu : (``order + 1``[, ...], ``order + 1``) array + Normalized central image moments. + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] https://en.wikipedia.org/wiki/Image_moment + + Examples + -------- + >>> image = np.zeros((20, 20), dtype=np.float64) + >>> image[13:17, 13:17] = 1 + >>> m = moments(image) + >>> centroid = (m[0, 1] / m[0, 0], m[1, 0] / m[0, 0]) + >>> mu = moments_central(image, centroid) + >>> moments_normalized(mu) + array([[ nan, nan, 0.078125 , 0. ], + [ nan, 0. , 0. , 0. ], + [0.078125 , 0. , 0.00610352, 0. ], + [0. , 0. , 0. , 0. ]]) + """ + if np.any(np.array(mu.shape) <= order): + raise ValueError("Shape of image moments must be >= `order`") + if spacing is None: + spacing = np.ones(mu.ndim) + nu = np.zeros_like(mu) + mu0 = mu.ravel()[0] + scale = min(spacing) + for powers in itertools.product(range(order + 1), repeat=mu.ndim): + if sum(powers) < 2: + nu[powers] = np.nan + else: + nu[powers] = (mu[powers] / scale ** sum(powers)) / ( + mu0 ** (sum(powers) / nu.ndim + 1) + ) + return nu + + +def moments_hu(nu): + """Calculate Hu's set of image moments (2D-only). + + Note that this set of moments is proved to be translation, scale and + rotation invariant. + + Parameters + ---------- + nu : (M, M) array + Normalized central image moments, where M must be >= 4. + + Returns + ------- + nu : (7,) array + Hu's set of image moments. + + References + ---------- + .. [1] M. K. Hu, "Visual Pattern Recognition by Moment Invariants", + IRE Trans. Info. Theory, vol. IT-8, pp. 179-187, 1962 + .. [2] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [3] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [4] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [5] https://en.wikipedia.org/wiki/Image_moment + + Examples + -------- + >>> image = np.zeros((20, 20), dtype=np.float64) + >>> image[13:17, 13:17] = 0.5 + >>> image[10:12, 10:12] = 1 + >>> mu = moments_central(image) + >>> nu = moments_normalized(mu) + >>> moments_hu(nu) + array([0.74537037, 0.35116598, 0.10404918, 0.04064421, 0.00264312, + 0.02408546, 0. ]) + """ + dtype = np.float32 if nu.dtype == 'float32' else np.float64 + return _moments_cy.moments_hu(nu.astype(dtype, copy=False)) + + +def centroid(image, *, spacing=None): + """Return the (weighted) centroid of an image. + + Parameters + ---------- + image : array + The input image. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + center : tuple of float, length ``image.ndim`` + The centroid of the (nonzero) pixels in ``image``. + + Examples + -------- + >>> image = np.zeros((20, 20), dtype=np.float64) + >>> image[13:17, 13:17] = 0.5 + >>> image[10:12, 10:12] = 1 + >>> centroid(image) + array([13.16666667, 13.16666667]) + """ + M = moments_central(image, center=(0,) * image.ndim, order=1, spacing=spacing) + center = ( + M[tuple(np.eye(image.ndim, dtype=int))] # array of weighted sums + # for each axis + / M[(0,) * image.ndim] + ) # weighted sum of all points + return center + + +def inertia_tensor(image, mu=None, *, spacing=None): + """Compute the inertia tensor of the input image. + + Parameters + ---------- + image : array + The input image. + mu : array, optional + The pre-computed central moments of ``image``. The inertia tensor + computation requires the central moments of the image. If an + application requires both the central moments and the inertia tensor + (for example, `skimage.measure.regionprops`), then it is more + efficient to pre-compute them and pass them to the inertia tensor + call. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + T : array, shape ``(image.ndim, image.ndim)`` + The inertia tensor of the input image. :math:`T_{i, j}` contains + the covariance of image intensity along axes :math:`i` and :math:`j`. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor + .. [2] Bernd Jähne. Spatio-Temporal Image Processing: Theory and + Scientific Applications. (Chapter 8: Tensor Methods) Springer, 1993. + """ + if mu is None: + mu = moments_central( + image, order=2, spacing=spacing + ) # don't need higher-order moments + mu0 = mu[(0,) * image.ndim] + result = np.zeros((image.ndim, image.ndim), dtype=mu.dtype) + + # nD expression to get coordinates ([2, 0], [0, 2]) (2D), + # ([2, 0, 0], [0, 2, 0], [0, 0, 2]) (3D), etc. + corners2 = tuple(2 * np.eye(image.ndim, dtype=int)) + d = np.diag(result) + d.flags.writeable = True + # See https://ocw.mit.edu/courses/aeronautics-and-astronautics/ + # 16-07-dynamics-fall-2009/lecture-notes/MIT16_07F09_Lec26.pdf + # Iii is the sum of second-order moments of every axis *except* i, not the + # second order moment of axis i. + # See also https://github.com/scikit-image/scikit-image/issues/3229 + d[:] = (np.sum(mu[corners2]) - mu[corners2]) / mu0 + + for dims in itertools.combinations(range(image.ndim), 2): + mu_index = np.zeros(image.ndim, dtype=int) + mu_index[list(dims)] = 1 + result[dims] = -mu[tuple(mu_index)] / mu0 + result.T[dims] = -mu[tuple(mu_index)] / mu0 + return result + + +def inertia_tensor_eigvals(image, mu=None, T=None, *, spacing=None): + """Compute the eigenvalues of the inertia tensor of the image. + + The inertia tensor measures covariance of the image intensity along + the image axes. (See `inertia_tensor`.) The relative magnitude of the + eigenvalues of the tensor is thus a measure of the elongation of a + (bright) object in the image. + + Parameters + ---------- + image : array + The input image. + mu : array, optional + The pre-computed central moments of ``image``. + T : array, shape ``(image.ndim, image.ndim)`` + The pre-computed inertia tensor. If ``T`` is given, ``mu`` and + ``image`` are ignored. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + eigvals : list of float, length ``image.ndim`` + The eigenvalues of the inertia tensor of ``image``, in descending + order. + + Notes + ----- + Computing the eigenvalues requires the inertia tensor of the input image. + This is much faster if the central moments (``mu``) are provided, or, + alternatively, one can provide the inertia tensor (``T``) directly. + """ + if T is None: + T = inertia_tensor(image, mu, spacing=spacing) + eigvals = np.linalg.eigvalsh(T) + # Floating point precision problems could make a positive + # semidefinite matrix have an eigenvalue that is very slightly + # negative. This can cause problems down the line, so set values + # very near zero to zero. + eigvals = np.clip(eigvals, 0, None, out=eigvals) + return sorted(eigvals, reverse=True) diff --git a/lib/python3.10/site-packages/skimage/measure/_moments_analytical.py b/lib/python3.10/site-packages/skimage/measure/_moments_analytical.py new file mode 100644 index 0000000000000000000000000000000000000000..4f6ff130f033e23cbe1fffe96e45ca0dafcfb0f0 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_moments_analytical.py @@ -0,0 +1,171 @@ +"""Analytical transformations from raw image moments to central moments. + +The expressions for the 2D central moments of order <=2 are often given in +textbooks. Expressions for higher orders and dimensions were generated in SymPy +using ``tools/precompute/moments_sympy.py`` in the GitHub repository. + +""" + +import itertools +import math + +import numpy as np + + +def _moments_raw_to_central_fast(moments_raw): + """Analytical formulae for 2D and 3D central moments of order < 4. + + `moments_raw_to_central` will automatically call this function when + ndim < 4 and order < 4. + + Parameters + ---------- + moments_raw : ndarray + The raw moments. + + Returns + ------- + moments_central : ndarray + The central moments. + """ + ndim = moments_raw.ndim + order = moments_raw.shape[0] - 1 + float_dtype = moments_raw.dtype + # convert to float64 during the computation for better accuracy + moments_raw = moments_raw.astype(np.float64, copy=False) + moments_central = np.zeros_like(moments_raw) + if order >= 4 or ndim not in [2, 3]: + raise ValueError("This function only supports 2D or 3D moments of order < 4.") + m = moments_raw + if ndim == 2: + cx = m[1, 0] / m[0, 0] + cy = m[0, 1] / m[0, 0] + moments_central[0, 0] = m[0, 0] + # Note: 1st order moments are both 0 + if order > 1: + # 2nd order moments + moments_central[1, 1] = m[1, 1] - cx * m[0, 1] + moments_central[2, 0] = m[2, 0] - cx * m[1, 0] + moments_central[0, 2] = m[0, 2] - cy * m[0, 1] + if order > 2: + # 3rd order moments + moments_central[2, 1] = ( + m[2, 1] + - 2 * cx * m[1, 1] + - cy * m[2, 0] + + cx**2 * m[0, 1] + + cy * cx * m[1, 0] + ) + moments_central[1, 2] = ( + m[1, 2] - 2 * cy * m[1, 1] - cx * m[0, 2] + 2 * cy * cx * m[0, 1] + ) + moments_central[3, 0] = m[3, 0] - 3 * cx * m[2, 0] + 2 * cx**2 * m[1, 0] + moments_central[0, 3] = m[0, 3] - 3 * cy * m[0, 2] + 2 * cy**2 * m[0, 1] + else: + # 3D case + cx = m[1, 0, 0] / m[0, 0, 0] + cy = m[0, 1, 0] / m[0, 0, 0] + cz = m[0, 0, 1] / m[0, 0, 0] + moments_central[0, 0, 0] = m[0, 0, 0] + # Note: all first order moments are 0 + if order > 1: + # 2nd order moments + moments_central[0, 0, 2] = -cz * m[0, 0, 1] + m[0, 0, 2] + moments_central[0, 1, 1] = -cy * m[0, 0, 1] + m[0, 1, 1] + moments_central[0, 2, 0] = -cy * m[0, 1, 0] + m[0, 2, 0] + moments_central[1, 0, 1] = -cx * m[0, 0, 1] + m[1, 0, 1] + moments_central[1, 1, 0] = -cx * m[0, 1, 0] + m[1, 1, 0] + moments_central[2, 0, 0] = -cx * m[1, 0, 0] + m[2, 0, 0] + if order > 2: + # 3rd order moments + moments_central[0, 0, 3] = ( + 2 * cz**2 * m[0, 0, 1] - 3 * cz * m[0, 0, 2] + m[0, 0, 3] + ) + moments_central[0, 1, 2] = ( + -cy * m[0, 0, 2] + 2 * cz * (cy * m[0, 0, 1] - m[0, 1, 1]) + m[0, 1, 2] + ) + moments_central[0, 2, 1] = ( + cy**2 * m[0, 0, 1] + - 2 * cy * m[0, 1, 1] + + cz * (cy * m[0, 1, 0] - m[0, 2, 0]) + + m[0, 2, 1] + ) + moments_central[0, 3, 0] = ( + 2 * cy**2 * m[0, 1, 0] - 3 * cy * m[0, 2, 0] + m[0, 3, 0] + ) + moments_central[1, 0, 2] = ( + -cx * m[0, 0, 2] + 2 * cz * (cx * m[0, 0, 1] - m[1, 0, 1]) + m[1, 0, 2] + ) + moments_central[1, 1, 1] = ( + -cx * m[0, 1, 1] + + cy * (cx * m[0, 0, 1] - m[1, 0, 1]) + + cz * (cx * m[0, 1, 0] - m[1, 1, 0]) + + m[1, 1, 1] + ) + moments_central[1, 2, 0] = ( + -cx * m[0, 2, 0] - 2 * cy * (-cx * m[0, 1, 0] + m[1, 1, 0]) + m[1, 2, 0] + ) + moments_central[2, 0, 1] = ( + cx**2 * m[0, 0, 1] + - 2 * cx * m[1, 0, 1] + + cz * (cx * m[1, 0, 0] - m[2, 0, 0]) + + m[2, 0, 1] + ) + moments_central[2, 1, 0] = ( + cx**2 * m[0, 1, 0] + - 2 * cx * m[1, 1, 0] + + cy * (cx * m[1, 0, 0] - m[2, 0, 0]) + + m[2, 1, 0] + ) + moments_central[3, 0, 0] = ( + 2 * cx**2 * m[1, 0, 0] - 3 * cx * m[2, 0, 0] + m[3, 0, 0] + ) + + return moments_central.astype(float_dtype, copy=False) + + +def moments_raw_to_central(moments_raw): + ndim = moments_raw.ndim + order = moments_raw.shape[0] - 1 + if ndim in [2, 3] and order < 4: + return _moments_raw_to_central_fast(moments_raw) + + moments_central = np.zeros_like(moments_raw) + m = moments_raw + # centers as computed in centroid above + centers = tuple(m[tuple(np.eye(ndim, dtype=int))] / m[(0,) * ndim]) + + if ndim == 2: + # This is the general 2D formula from + # https://en.wikipedia.org/wiki/Image_moment#Central_moments + for p in range(order + 1): + for q in range(order + 1): + if p + q > order: + continue + for i in range(p + 1): + term1 = math.comb(p, i) + term1 *= (-centers[0]) ** (p - i) + for j in range(q + 1): + term2 = math.comb(q, j) + term2 *= (-centers[1]) ** (q - j) + moments_central[p, q] += term1 * term2 * m[i, j] + return moments_central + + # The nested loops below are an n-dimensional extension of the 2D formula + # given at https://en.wikipedia.org/wiki/Image_moment#Central_moments + + # iterate over all [0, order] (inclusive) on each axis + for orders in itertools.product(*((range(order + 1),) * ndim)): + # `orders` here is the index into the `moments_central` output array + if sum(orders) > order: + # skip any moment that is higher than the requested order + continue + # loop over terms from `m` contributing to `moments_central[orders]` + for idxs in itertools.product(*[range(o + 1) for o in orders]): + val = m[idxs] + for i_order, c, idx in zip(orders, centers, idxs): + val *= math.comb(i_order, idx) + val *= (-c) ** (i_order - idx) + moments_central[orders] += val + + return moments_central diff --git a/lib/python3.10/site-packages/skimage/measure/_polygon.py b/lib/python3.10/site-packages/skimage/measure/_polygon.py new file mode 100644 index 0000000000000000000000000000000000000000..23aa1cea463226d06ab91f9ef602032bbcf8eead --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_polygon.py @@ -0,0 +1,168 @@ +import numpy as np +from scipy import signal + + +def approximate_polygon(coords, tolerance): + """Approximate a polygonal chain with the specified tolerance. + + It is based on the Douglas-Peucker algorithm. + + Note that the approximated polygon is always within the convex hull of the + original polygon. + + Parameters + ---------- + coords : (K, 2) array + Coordinate array. + tolerance : float + Maximum distance from original points of polygon to approximated + polygonal chain. If tolerance is 0, the original coordinate array + is returned. + + Returns + ------- + coords : (L, 2) array + Approximated polygonal chain where L <= K. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm + """ + if tolerance <= 0: + return coords + + chain = np.zeros(coords.shape[0], 'bool') + # pre-allocate distance array for all points + dists = np.zeros(coords.shape[0]) + chain[0] = True + chain[-1] = True + pos_stack = [(0, chain.shape[0] - 1)] + end_of_chain = False + + while not end_of_chain: + start, end = pos_stack.pop() + # determine properties of current line segment + r0, c0 = coords[start, :] + r1, c1 = coords[end, :] + dr = r1 - r0 + dc = c1 - c0 + segment_angle = -np.arctan2(dr, dc) + segment_dist = c0 * np.sin(segment_angle) + r0 * np.cos(segment_angle) + + # select points in-between line segment + segment_coords = coords[start + 1 : end, :] + segment_dists = dists[start + 1 : end] + + # check whether to take perpendicular or euclidean distance with + # inner product of vectors + + # vectors from points -> start and end + dr0 = segment_coords[:, 0] - r0 + dc0 = segment_coords[:, 1] - c0 + dr1 = segment_coords[:, 0] - r1 + dc1 = segment_coords[:, 1] - c1 + # vectors points -> start and end projected on start -> end vector + projected_lengths0 = dr0 * dr + dc0 * dc + projected_lengths1 = -dr1 * dr - dc1 * dc + perp = np.logical_and(projected_lengths0 > 0, projected_lengths1 > 0) + eucl = np.logical_not(perp) + segment_dists[perp] = np.abs( + segment_coords[perp, 0] * np.cos(segment_angle) + + segment_coords[perp, 1] * np.sin(segment_angle) + - segment_dist + ) + segment_dists[eucl] = np.minimum( + # distance to start point + np.sqrt(dc0[eucl] ** 2 + dr0[eucl] ** 2), + # distance to end point + np.sqrt(dc1[eucl] ** 2 + dr1[eucl] ** 2), + ) + + if np.any(segment_dists > tolerance): + # select point with maximum distance to line + new_end = start + np.argmax(segment_dists) + 1 + pos_stack.append((new_end, end)) + pos_stack.append((start, new_end)) + chain[new_end] = True + + if len(pos_stack) == 0: + end_of_chain = True + + return coords[chain, :] + + +# B-Spline subdivision +_SUBDIVISION_MASKS = { + # degree: (mask_even, mask_odd) + # extracted from (degree + 2)th row of Pascal's triangle + 1: ([1, 1], [1, 1]), + 2: ([3, 1], [1, 3]), + 3: ([1, 6, 1], [0, 4, 4]), + 4: ([5, 10, 1], [1, 10, 5]), + 5: ([1, 15, 15, 1], [0, 6, 20, 6]), + 6: ([7, 35, 21, 1], [1, 21, 35, 7]), + 7: ([1, 28, 70, 28, 1], [0, 8, 56, 56, 8]), +} + + +def subdivide_polygon(coords, degree=2, preserve_ends=False): + """Subdivision of polygonal curves using B-Splines. + + Note that the resulting curve is always within the convex hull of the + original polygon. Circular polygons stay closed after subdivision. + + Parameters + ---------- + coords : (K, 2) array + Coordinate array. + degree : {1, 2, 3, 4, 5, 6, 7}, optional + Degree of B-Spline. Default is 2. + preserve_ends : bool, optional + Preserve first and last coordinate of non-circular polygon. Default is + False. + + Returns + ------- + coords : (L, 2) array + Subdivided coordinate array. + + References + ---------- + .. [1] http://mrl.nyu.edu/publications/subdiv-course2000/coursenotes00.pdf + """ + if degree not in _SUBDIVISION_MASKS: + raise ValueError("Invalid B-Spline degree. Only degree 1 - 7 is " "supported.") + + circular = np.all(coords[0, :] == coords[-1, :]) + + method = 'valid' + if circular: + # remove last coordinate because of wrapping + coords = coords[:-1, :] + # circular convolution by wrapping boundaries + method = 'same' + + mask_even, mask_odd = _SUBDIVISION_MASKS[degree] + # divide by total weight + mask_even = np.array(mask_even, float) / (2**degree) + mask_odd = np.array(mask_odd, float) / (2**degree) + + even = signal.convolve2d( + coords.T, np.atleast_2d(mask_even), mode=method, boundary='wrap' + ) + odd = signal.convolve2d( + coords.T, np.atleast_2d(mask_odd), mode=method, boundary='wrap' + ) + + out = np.zeros((even.shape[1] + odd.shape[1], 2)) + out[1::2] = even.T + out[::2] = odd.T + + if circular: + # close polygon + out = np.vstack([out, out[0, :]]) + + if preserve_ends and not circular: + out = np.vstack([coords[0, :], out, coords[-1, :]]) + + return out diff --git a/lib/python3.10/site-packages/skimage/measure/_regionprops.py b/lib/python3.10/site-packages/skimage/measure/_regionprops.py new file mode 100644 index 0000000000000000000000000000000000000000..ec5762ffa4dd625dc5ad1cf06a2bcf56d5f6233b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_regionprops.py @@ -0,0 +1,1437 @@ +import inspect +import sys +from functools import wraps +from math import atan2 +from math import pi as PI +from math import sqrt +from warnings import warn + +import numpy as np +from scipy import ndimage as ndi +from scipy.spatial.distance import pdist + +from . import _moments +from ._find_contours import find_contours +from ._marching_cubes_lewiner import marching_cubes +from ._regionprops_utils import ( + euler_number, + perimeter, + perimeter_crofton, + _normalize_spacing, +) + +__all__ = ['regionprops', 'euler_number', 'perimeter', 'perimeter_crofton'] + + +# All values in this PROPS dict correspond to current scikit-image property +# names. The keys in this PROPS dict correspond to older names used in prior +# releases. For backwards compatibility, these older names will continue to +# work, but will not be documented. +PROPS = { + 'Area': 'area', + 'BoundingBox': 'bbox', + 'BoundingBoxArea': 'area_bbox', + 'bbox_area': 'area_bbox', + 'CentralMoments': 'moments_central', + 'Centroid': 'centroid', + 'ConvexArea': 'area_convex', + 'convex_area': 'area_convex', + # 'ConvexHull', + 'ConvexImage': 'image_convex', + 'convex_image': 'image_convex', + 'Coordinates': 'coords', + 'Eccentricity': 'eccentricity', + 'EquivDiameter': 'equivalent_diameter_area', + 'equivalent_diameter': 'equivalent_diameter_area', + 'EulerNumber': 'euler_number', + 'Extent': 'extent', + # 'Extrema', + 'FeretDiameter': 'feret_diameter_max', + 'FeretDiameterMax': 'feret_diameter_max', + 'FilledArea': 'area_filled', + 'filled_area': 'area_filled', + 'FilledImage': 'image_filled', + 'filled_image': 'image_filled', + 'HuMoments': 'moments_hu', + 'Image': 'image', + 'InertiaTensor': 'inertia_tensor', + 'InertiaTensorEigvals': 'inertia_tensor_eigvals', + 'IntensityImage': 'image_intensity', + 'intensity_image': 'image_intensity', + 'Label': 'label', + 'LocalCentroid': 'centroid_local', + 'local_centroid': 'centroid_local', + 'MajorAxisLength': 'axis_major_length', + 'major_axis_length': 'axis_major_length', + 'MaxIntensity': 'intensity_max', + 'max_intensity': 'intensity_max', + 'MeanIntensity': 'intensity_mean', + 'mean_intensity': 'intensity_mean', + 'MinIntensity': 'intensity_min', + 'min_intensity': 'intensity_min', + 'std_intensity': 'intensity_std', + 'MinorAxisLength': 'axis_minor_length', + 'minor_axis_length': 'axis_minor_length', + 'Moments': 'moments', + 'NormalizedMoments': 'moments_normalized', + 'Orientation': 'orientation', + 'Perimeter': 'perimeter', + 'CroftonPerimeter': 'perimeter_crofton', + # 'PixelIdxList', + # 'PixelList', + 'Slice': 'slice', + 'Solidity': 'solidity', + # 'SubarrayIdx' + 'WeightedCentralMoments': 'moments_weighted_central', + 'weighted_moments_central': 'moments_weighted_central', + 'WeightedCentroid': 'centroid_weighted', + 'weighted_centroid': 'centroid_weighted', + 'WeightedHuMoments': 'moments_weighted_hu', + 'weighted_moments_hu': 'moments_weighted_hu', + 'WeightedLocalCentroid': 'centroid_weighted_local', + 'weighted_local_centroid': 'centroid_weighted_local', + 'WeightedMoments': 'moments_weighted', + 'weighted_moments': 'moments_weighted', + 'WeightedNormalizedMoments': 'moments_weighted_normalized', + 'weighted_moments_normalized': 'moments_weighted_normalized', +} + +COL_DTYPES = { + 'area': float, + 'area_bbox': float, + 'area_convex': float, + 'area_filled': float, + 'axis_major_length': float, + 'axis_minor_length': float, + 'bbox': int, + 'centroid': float, + 'centroid_local': float, + 'centroid_weighted': float, + 'centroid_weighted_local': float, + 'coords': object, + 'coords_scaled': object, + 'eccentricity': float, + 'equivalent_diameter_area': float, + 'euler_number': int, + 'extent': float, + 'feret_diameter_max': float, + 'image': object, + 'image_convex': object, + 'image_filled': object, + 'image_intensity': object, + 'inertia_tensor': float, + 'inertia_tensor_eigvals': float, + 'intensity_max': float, + 'intensity_mean': float, + 'intensity_min': float, + 'intensity_std': float, + 'label': int, + 'moments': float, + 'moments_central': float, + 'moments_hu': float, + 'moments_normalized': float, + 'moments_weighted': float, + 'moments_weighted_central': float, + 'moments_weighted_hu': float, + 'moments_weighted_normalized': float, + 'num_pixels': int, + 'orientation': float, + 'perimeter': float, + 'perimeter_crofton': float, + 'slice': object, + 'solidity': float, +} + +OBJECT_COLUMNS = [col for col, dtype in COL_DTYPES.items() if dtype == object] + +PROP_VALS = set(PROPS.values()) + +_require_intensity_image = ( + 'image_intensity', + 'intensity_max', + 'intensity_mean', + 'intensity_min', + 'intensity_std', + 'moments_weighted', + 'moments_weighted_central', + 'centroid_weighted', + 'centroid_weighted_local', + 'moments_weighted_hu', + 'moments_weighted_normalized', +) + + +def _infer_number_of_required_args(func): + """Infer the number of required arguments for a function + + Parameters + ---------- + func : callable + The function that is being inspected. + + Returns + ------- + n_args : int + The number of required arguments of func. + """ + argspec = inspect.getfullargspec(func) + n_args = len(argspec.args) + if argspec.defaults is not None: + n_args -= len(argspec.defaults) + return n_args + + +def _infer_regionprop_dtype(func, *, intensity, ndim): + """Infer the dtype of a region property calculated by func. + + If a region property function always returns the same shape and type of + output regardless of input size, then the dtype is the dtype of the + returned array. Otherwise, the property has object dtype. + + Parameters + ---------- + func : callable + Function to be tested. The signature should be array[bool] -> Any if + intensity is False, or *(array[bool], array[float]) -> Any otherwise. + intensity : bool + Whether the regionprop is calculated on an intensity image. + ndim : int + The number of dimensions for which to check func. + + Returns + ------- + dtype : NumPy data type + The data type of the returned property. + """ + mask_1 = np.ones((1,) * ndim, dtype=bool) + mask_1 = np.pad(mask_1, (0, 1), constant_values=False) + mask_2 = np.ones((2,) * ndim, dtype=bool) + mask_2 = np.pad(mask_2, (1, 0), constant_values=False) + propmasks = [mask_1, mask_2] + + rng = np.random.default_rng() + + if intensity and _infer_number_of_required_args(func) == 2: + + def _func(mask): + return func(mask, rng.random(mask.shape)) + + else: + _func = func + props1, props2 = map(_func, propmasks) + if ( + np.isscalar(props1) + and np.isscalar(props2) + or np.array(props1).shape == np.array(props2).shape + ): + dtype = np.array(props1).dtype.type + else: + dtype = np.object_ + return dtype + + +def _cached(f): + @wraps(f) + def wrapper(obj): + cache = obj._cache + prop = f.__name__ + + if not obj._cache_active: + return f(obj) + + if prop not in cache: + cache[prop] = f(obj) + + return cache[prop] + + return wrapper + + +def only2d(method): + @wraps(method) + def func2d(self, *args, **kwargs): + if self._ndim > 2: + raise NotImplementedError( + f"Property {method.__name__} is not implemented for 3D images" + ) + return method(self, *args, **kwargs) + + return func2d + + +def _inertia_eigvals_to_axes_lengths_3D(inertia_tensor_eigvals): + """Compute ellipsoid axis lengths from inertia tensor eigenvalues. + + Parameters + --------- + inertia_tensor_eigvals : sequence of float + A sequence of 3 floating point eigenvalues, sorted in descending order. + + Returns + ------- + axis_lengths : list of float + The ellipsoid axis lengths sorted in descending order. + + Notes + ----- + Let a >= b >= c be the ellipsoid semi-axes and s1 >= s2 >= s3 be the + inertia tensor eigenvalues. + + The inertia tensor eigenvalues are given for a solid ellipsoid in [1]_. + s1 = 1 / 5 * (a**2 + b**2) + s2 = 1 / 5 * (a**2 + c**2) + s3 = 1 / 5 * (b**2 + c**2) + + Rearranging to solve for a, b, c in terms of s1, s2, s3 gives + a = math.sqrt(5 / 2 * ( s1 + s2 - s3)) + b = math.sqrt(5 / 2 * ( s1 - s2 + s3)) + c = math.sqrt(5 / 2 * (-s1 + s2 + s3)) + + We can then simply replace sqrt(5/2) by sqrt(10) to get the full axes + lengths rather than the semi-axes lengths. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/List_of_moments_of_inertia#List_of_3D_inertia_tensors + """ + axis_lengths = [] + for ax in range(2, -1, -1): + w = sum(v * -1 if i == ax else v for i, v in enumerate(inertia_tensor_eigvals)) + axis_lengths.append(sqrt(10 * w)) + return axis_lengths + + +class RegionProperties: + """Please refer to `skimage.measure.regionprops` for more information + on the available region properties. + """ + + def __init__( + self, + slice, + label, + label_image, + intensity_image, + cache_active, + *, + extra_properties=None, + spacing=None, + offset=None, + ): + if intensity_image is not None: + ndim = label_image.ndim + if not ( + intensity_image.shape[:ndim] == label_image.shape + and intensity_image.ndim in [ndim, ndim + 1] + ): + raise ValueError( + 'Label and intensity image shapes must match,' + ' except for channel (last) axis.' + ) + multichannel = label_image.shape < intensity_image.shape + else: + multichannel = False + + self.label = label + if offset is None: + offset = np.zeros((label_image.ndim,), dtype=int) + self._offset = np.array(offset) + + self._slice = slice + self.slice = slice + self._label_image = label_image + self._intensity_image = intensity_image + + self._cache_active = cache_active + self._cache = {} + self._ndim = label_image.ndim + self._multichannel = multichannel + self._spatial_axes = tuple(range(self._ndim)) + if spacing is None: + spacing = np.full(self._ndim, 1.0) + self._spacing = _normalize_spacing(spacing, self._ndim) + self._pixel_area = np.prod(self._spacing) + + self._extra_properties = {} + if extra_properties is not None: + for func in extra_properties: + name = func.__name__ + if hasattr(self, name): + msg = ( + f"Extra property '{name}' is shadowed by existing " + f"property and will be inaccessible. Consider " + f"renaming it." + ) + warn(msg) + self._extra_properties = {func.__name__: func for func in extra_properties} + + def __getattr__(self, attr): + if attr == "__setstate__": + # When deserializing this object with pickle, `__setstate__` + # is accessed before any other attributes like `self._intensity_image` + # are available which leads to a RecursionError when trying to + # access them later on in this function. So guard against this by + # provoking the default AttributeError (gh-6465). + return self.__getattribute__(attr) + + if self._intensity_image is None and attr in _require_intensity_image: + raise AttributeError( + f"Attribute '{attr}' unavailable when `intensity_image` " + f"has not been specified." + ) + if attr in self._extra_properties: + func = self._extra_properties[attr] + n_args = _infer_number_of_required_args(func) + # determine whether func requires intensity image + if n_args == 2: + if self._intensity_image is not None: + if self._multichannel: + multichannel_list = [ + func(self.image, self.image_intensity[..., i]) + for i in range(self.image_intensity.shape[-1]) + ] + return np.stack(multichannel_list, axis=-1) + else: + return func(self.image, self.image_intensity) + else: + raise AttributeError( + f'intensity image required to calculate {attr}' + ) + elif n_args == 1: + return func(self.image) + else: + raise AttributeError( + f'Custom regionprop function\'s number of arguments must ' + f'be 1 or 2, but {attr} takes {n_args} arguments.' + ) + elif attr in PROPS and attr.lower() == attr: + if ( + self._intensity_image is None + and PROPS[attr] in _require_intensity_image + ): + raise AttributeError( + f"Attribute '{attr}' unavailable when `intensity_image` " + f"has not been specified." + ) + # retrieve deprecated property (excluding old CamelCase ones) + return getattr(self, PROPS[attr]) + else: + raise AttributeError(f"'{type(self)}' object has no attribute '{attr}'") + + def __setattr__(self, name, value): + if name in PROPS: + super().__setattr__(PROPS[name], value) + else: + super().__setattr__(name, value) + + @property + @_cached + def num_pixels(self): + return np.sum(self.image) + + @property + @_cached + def area(self): + return np.sum(self.image) * self._pixel_area + + @property + def bbox(self): + """ + Returns + ------- + A tuple of the bounding box's start coordinates for each dimension, + followed by the end coordinates for each dimension + """ + return tuple( + [self.slice[i].start for i in range(self._ndim)] + + [self.slice[i].stop for i in range(self._ndim)] + ) + + @property + def area_bbox(self): + return self.image.size * self._pixel_area + + @property + def centroid(self): + return tuple(self.coords_scaled.mean(axis=0)) + + @property + @_cached + def area_convex(self): + return np.sum(self.image_convex) * self._pixel_area + + @property + @_cached + def image_convex(self): + from ..morphology.convex_hull import convex_hull_image + + return convex_hull_image(self.image) + + @property + def coords_scaled(self): + indices = np.argwhere(self.image) + object_offset = np.array([self.slice[i].start for i in range(self._ndim)]) + return (object_offset + indices) * self._spacing + self._offset + + @property + def coords(self): + indices = np.argwhere(self.image) + object_offset = np.array([self.slice[i].start for i in range(self._ndim)]) + return object_offset + indices + self._offset + + @property + @only2d + def eccentricity(self): + l1, l2 = self.inertia_tensor_eigvals + if l1 == 0: + return 0 + return sqrt(1 - l2 / l1) + + @property + def equivalent_diameter_area(self): + return (2 * self._ndim * self.area / PI) ** (1 / self._ndim) + + @property + def euler_number(self): + if self._ndim not in [2, 3]: + raise NotImplementedError( + 'Euler number is implemented for ' '2D or 3D images only' + ) + return euler_number(self.image, self._ndim) + + @property + def extent(self): + return self.area / self.area_bbox + + @property + def feret_diameter_max(self): + identity_convex_hull = np.pad( + self.image_convex, 2, mode='constant', constant_values=0 + ) + if self._ndim == 2: + coordinates = np.vstack( + find_contours(identity_convex_hull, 0.5, fully_connected='high') + ) + elif self._ndim == 3: + coordinates, _, _, _ = marching_cubes(identity_convex_hull, level=0.5) + distances = pdist(coordinates * self._spacing, 'sqeuclidean') + return sqrt(np.max(distances)) + + @property + def area_filled(self): + return np.sum(self.image_filled) * self._pixel_area + + @property + @_cached + def image_filled(self): + structure = np.ones((3,) * self._ndim) + return ndi.binary_fill_holes(self.image, structure) + + @property + @_cached + def image(self): + return self._label_image[self.slice] == self.label + + @property + @_cached + def inertia_tensor(self): + mu = self.moments_central + return _moments.inertia_tensor(self.image, mu, spacing=self._spacing) + + @property + @_cached + def inertia_tensor_eigvals(self): + return _moments.inertia_tensor_eigvals(self.image, T=self.inertia_tensor) + + @property + @_cached + def image_intensity(self): + if self._intensity_image is None: + raise AttributeError('No intensity image specified.') + image = ( + self.image + if not self._multichannel + else np.expand_dims(self.image, self._ndim) + ) + return self._intensity_image[self.slice] * image + + def _image_intensity_double(self): + return self.image_intensity.astype(np.float64, copy=False) + + @property + def centroid_local(self): + M = self.moments + M0 = M[(0,) * self._ndim] + + def _get_element(axis): + return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis) + + return np.asarray( + tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim)) + ) + + @property + def intensity_max(self): + vals = self.image_intensity[self.image] + return np.max(vals, axis=0).astype(np.float64, copy=False) + + @property + def intensity_mean(self): + return np.mean(self.image_intensity[self.image], axis=0) + + @property + def intensity_min(self): + vals = self.image_intensity[self.image] + return np.min(vals, axis=0).astype(np.float64, copy=False) + + @property + def intensity_std(self): + vals = self.image_intensity[self.image] + return np.std(vals, axis=0) + + @property + def axis_major_length(self): + if self._ndim == 2: + l1 = self.inertia_tensor_eigvals[0] + return 4 * sqrt(l1) + elif self._ndim == 3: + # equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[0] + ev = self.inertia_tensor_eigvals + return sqrt(10 * (ev[0] + ev[1] - ev[2])) + else: + raise ValueError("axis_major_length only available in 2D and 3D") + + @property + def axis_minor_length(self): + if self._ndim == 2: + l2 = self.inertia_tensor_eigvals[-1] + return 4 * sqrt(l2) + elif self._ndim == 3: + # equivalent to _inertia_eigvals_to_axes_lengths_3D(ev)[-1] + ev = self.inertia_tensor_eigvals + return sqrt(10 * (-ev[0] + ev[1] + ev[2])) + else: + raise ValueError("axis_minor_length only available in 2D and 3D") + + @property + @_cached + def moments(self): + M = _moments.moments(self.image.astype(np.uint8), 3, spacing=self._spacing) + return M + + @property + @_cached + def moments_central(self): + mu = _moments.moments_central( + self.image.astype(np.uint8), + self.centroid_local, + order=3, + spacing=self._spacing, + ) + return mu + + @property + @only2d + def moments_hu(self): + if any(s != 1.0 for s in self._spacing): + raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only') + return _moments.moments_hu(self.moments_normalized) + + @property + @_cached + def moments_normalized(self): + return _moments.moments_normalized( + self.moments_central, 3, spacing=self._spacing + ) + + @property + @only2d + def orientation(self): + a, b, b, c = self.inertia_tensor.flat + if a - c == 0: + if b < 0: + return PI / 4.0 + else: + return -PI / 4.0 + else: + return 0.5 * atan2(-2 * b, c - a) + + @property + @only2d + def perimeter(self): + if len(np.unique(self._spacing)) != 1: + raise NotImplementedError('`perimeter` supports isotropic spacings only') + return perimeter(self.image, 4) * self._spacing[0] + + @property + @only2d + def perimeter_crofton(self): + if len(np.unique(self._spacing)) != 1: + raise NotImplementedError('`perimeter` supports isotropic spacings only') + return perimeter_crofton(self.image, 4) * self._spacing[0] + + @property + def solidity(self): + return self.area / self.area_convex + + @property + def centroid_weighted(self): + ctr = self.centroid_weighted_local + return tuple( + idx + slc.start * spc + for idx, slc, spc in zip(ctr, self.slice, self._spacing) + ) + + @property + def centroid_weighted_local(self): + M = self.moments_weighted + M0 = M[(0,) * self._ndim] + + def _get_element(axis): + return (0,) * axis + (1,) + (0,) * (self._ndim - 1 - axis) + + return np.asarray( + tuple(M[_get_element(axis)] / M0 for axis in range(self._ndim)) + ) + + @property + @_cached + def moments_weighted(self): + image = self._image_intensity_double() + if self._multichannel: + moments = np.stack( + [ + _moments.moments(image[..., i], order=3, spacing=self._spacing) + for i in range(image.shape[-1]) + ], + axis=-1, + ) + else: + moments = _moments.moments(image, order=3, spacing=self._spacing) + return moments + + @property + @_cached + def moments_weighted_central(self): + ctr = self.centroid_weighted_local + image = self._image_intensity_double() + if self._multichannel: + moments_list = [ + _moments.moments_central( + image[..., i], center=ctr[..., i], order=3, spacing=self._spacing + ) + for i in range(image.shape[-1]) + ] + moments = np.stack(moments_list, axis=-1) + else: + moments = _moments.moments_central( + image, ctr, order=3, spacing=self._spacing + ) + return moments + + @property + @only2d + def moments_weighted_hu(self): + if not (np.array(self._spacing) == np.array([1, 1])).all(): + raise NotImplementedError('`moments_hu` supports spacing = (1, 1) only') + nu = self.moments_weighted_normalized + if self._multichannel: + nchannels = self._intensity_image.shape[-1] + return np.stack( + [_moments.moments_hu(nu[..., i]) for i in range(nchannels)], + axis=-1, + ) + else: + return _moments.moments_hu(nu) + + @property + @_cached + def moments_weighted_normalized(self): + mu = self.moments_weighted_central + if self._multichannel: + nchannels = self._intensity_image.shape[-1] + return np.stack( + [ + _moments.moments_normalized( + mu[..., i], order=3, spacing=self._spacing + ) + for i in range(nchannels) + ], + axis=-1, + ) + else: + return _moments.moments_normalized(mu, order=3, spacing=self._spacing) + + def __iter__(self): + props = PROP_VALS + + if self._intensity_image is None: + unavailable_props = _require_intensity_image + props = props.difference(unavailable_props) + + return iter(sorted(props)) + + def __getitem__(self, key): + value = getattr(self, key, None) + if value is not None: + return value + else: # backwards compatibility + return getattr(self, PROPS[key]) + + def __eq__(self, other): + if not isinstance(other, RegionProperties): + return False + + for key in PROP_VALS: + try: + # so that NaNs are equal + np.testing.assert_equal( + getattr(self, key, None), getattr(other, key, None) + ) + except AssertionError: + return False + + return True + + +# For compatibility with code written prior to 0.16 +_RegionProperties = RegionProperties + + +def _props_to_dict(regions, properties=('label', 'bbox'), separator='-'): + """Convert image region properties list into a column dictionary. + + Parameters + ---------- + regions : (K,) list + List of RegionProperties objects as returned by :func:`regionprops`. + properties : tuple or list of str, optional + Properties that will be included in the resulting dictionary + For a list of available properties, please see :func:`regionprops`. + Users should remember to add "label" to keep track of region + identities. + separator : str, optional + For non-scalar properties not listed in OBJECT_COLUMNS, each element + will appear in its own column, with the index of that element separated + from the property name by this separator. For example, the inertia + tensor of a 2D region will appear in four columns: + ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``, + and ``inertia_tensor-1-1`` (where the separator is ``-``). + + Object columns are those that cannot be split in this way because the + number of columns would change depending on the object. For example, + ``image`` and ``coords``. + + Returns + ------- + out_dict : dict + Dictionary mapping property names to an array of values of that + property, one value per region. This dictionary can be used as input to + pandas ``DataFrame`` to map property names to columns in the frame and + regions to rows. + + Notes + ----- + Each column contains either a scalar property, an object property, or an + element in a multidimensional array. + + Properties with scalar values for each region, such as "eccentricity", will + appear as a float or int array with that property name as key. + + Multidimensional properties *of fixed size* for a given image dimension, + such as "centroid" (every centroid will have three elements in a 3D image, + no matter the region size), will be split into that many columns, with the + name {property_name}{separator}{element_num} (for 1D properties), + {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D + properties), and so on. + + For multidimensional properties that don't have a fixed size, such as + "image" (the image of a region varies in size depending on the region + size), an object array will be used, with the corresponding property name + as the key. + + Examples + -------- + >>> from skimage import data, util, measure + >>> image = data.coins() + >>> label_image = measure.label(image > 110, connectivity=image.ndim) + >>> proplist = regionprops(label_image, image) + >>> props = _props_to_dict(proplist, properties=['label', 'inertia_tensor', + ... 'inertia_tensor_eigvals']) + >>> props # doctest: +ELLIPSIS +SKIP + {'label': array([ 1, 2, ...]), ... + 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ... + ..., + 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])} + + The resulting dictionary can be directly passed to pandas, if installed, to + obtain a clean DataFrame: + + >>> import pandas as pd # doctest: +SKIP + >>> data = pd.DataFrame(props) # doctest: +SKIP + >>> data.head() # doctest: +SKIP + label inertia_tensor-0-0 ... inertia_tensor_eigvals-1 + 0 1 4012.909888 ... 267.065503 + 1 2 8.514739 ... 2.834806 + 2 3 0.666667 ... 0.000000 + 3 4 0.000000 ... 0.000000 + 4 5 0.222222 ... 0.111111 + + """ + + out = {} + n = len(regions) + for prop in properties: + r = regions[0] + # Copy the original property name so the output will have the + # user-provided property name in the case of deprecated names. + orig_prop = prop + # determine the current property name for any deprecated property. + prop = PROPS.get(prop, prop) + rp = getattr(r, prop) + if prop in COL_DTYPES: + dtype = COL_DTYPES[prop] + else: + func = r._extra_properties[prop] + dtype = _infer_regionprop_dtype( + func, + intensity=r._intensity_image is not None, + ndim=r.image.ndim, + ) + + # scalars and objects are dedicated one column per prop + # array properties are raveled into multiple columns + # for more info, refer to notes 1 + if np.isscalar(rp) or prop in OBJECT_COLUMNS or dtype is np.object_: + column_buffer = np.empty(n, dtype=dtype) + for i in range(n): + column_buffer[i] = regions[i][prop] + out[orig_prop] = np.copy(column_buffer) + else: + # precompute property column names and locations + modified_props = [] + locs = [] + for ind in np.ndindex(np.shape(rp)): + modified_props.append(separator.join(map(str, (orig_prop,) + ind))) + locs.append(ind if len(ind) > 1 else ind[0]) + + # fill temporary column data_array + n_columns = len(locs) + column_data = np.empty((n, n_columns), dtype=dtype) + for k in range(n): + # we coerce to a numpy array to ensure structures like + # tuple-of-arrays expand correctly into columns + rp = np.asarray(regions[k][prop]) + for i, loc in enumerate(locs): + column_data[k, i] = rp[loc] + + # add the columns to the output dictionary + for i, modified_prop in enumerate(modified_props): + out[modified_prop] = column_data[:, i] + return out + + +def regionprops_table( + label_image, + intensity_image=None, + properties=('label', 'bbox'), + *, + cache=True, + separator='-', + extra_properties=None, + spacing=None, +): + """Compute image properties and return them as a pandas-compatible table. + + The table is a dictionary mapping column names to value arrays. See Notes + section below for details. + + .. versionadded:: 0.16 + + Parameters + ---------- + label_image : (M, N[, P]) ndarray + Labeled input image. Labels with value 0 are ignored. + intensity_image : (M, N[, P][, C]) ndarray, optional + Intensity (i.e., input) image with same size as labeled image, plus + optionally an extra dimension for multichannel data. The channel dimension, + if present, must be the last axis. Default is None. + + .. versionchanged:: 0.18.0 + The ability to provide an extra dimension for channels was added. + properties : tuple or list of str, optional + Properties that will be included in the resulting dictionary + For a list of available properties, please see :func:`regionprops`. + Users should remember to add "label" to keep track of region + identities. + cache : bool, optional + Determine whether to cache calculated properties. The computation is + much faster for cached properties, whereas the memory consumption + increases. + separator : str, optional + For non-scalar properties not listed in OBJECT_COLUMNS, each element + will appear in its own column, with the index of that element separated + from the property name by this separator. For example, the inertia + tensor of a 2D region will appear in four columns: + ``inertia_tensor-0-0``, ``inertia_tensor-0-1``, ``inertia_tensor-1-0``, + and ``inertia_tensor-1-1`` (where the separator is ``-``). + + Object columns are those that cannot be split in this way because the + number of columns would change depending on the object. For example, + ``image`` and ``coords``. + extra_properties : Iterable of callables + Add extra property computation functions that are not included with + skimage. The name of the property is derived from the function name, + the dtype is inferred by calling the function on a small sample. + If the name of an extra property clashes with the name of an existing + property the extra property will not be visible and a UserWarning is + issued. A property computation function must take a region mask as its + first argument. If the property requires an intensity image, it must + accept the intensity image as the second argument. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + + Returns + ------- + out_dict : dict + Dictionary mapping property names to an array of values of that + property, one value per region. This dictionary can be used as input to + pandas ``DataFrame`` to map property names to columns in the frame and + regions to rows. If the image has no regions, + the arrays will have length 0, but the correct type. + + Notes + ----- + Each column contains either a scalar property, an object property, or an + element in a multidimensional array. + + Properties with scalar values for each region, such as "eccentricity", will + appear as a float or int array with that property name as key. + + Multidimensional properties *of fixed size* for a given image dimension, + such as "centroid" (every centroid will have three elements in a 3D image, + no matter the region size), will be split into that many columns, with the + name {property_name}{separator}{element_num} (for 1D properties), + {property_name}{separator}{elem_num0}{separator}{elem_num1} (for 2D + properties), and so on. + + For multidimensional properties that don't have a fixed size, such as + "image" (the image of a region varies in size depending on the region + size), an object array will be used, with the corresponding property name + as the key. + + Examples + -------- + >>> from skimage import data, util, measure + >>> image = data.coins() + >>> label_image = measure.label(image > 110, connectivity=image.ndim) + >>> props = measure.regionprops_table(label_image, image, + ... properties=['label', 'inertia_tensor', + ... 'inertia_tensor_eigvals']) + >>> props # doctest: +ELLIPSIS +SKIP + {'label': array([ 1, 2, ...]), ... + 'inertia_tensor-0-0': array([ 4.012...e+03, 8.51..., ...]), ... + ..., + 'inertia_tensor_eigvals-1': array([ 2.67...e+02, 2.83..., ...])} + + The resulting dictionary can be directly passed to pandas, if installed, to + obtain a clean DataFrame: + + >>> import pandas as pd # doctest: +SKIP + >>> data = pd.DataFrame(props) # doctest: +SKIP + >>> data.head() # doctest: +SKIP + label inertia_tensor-0-0 ... inertia_tensor_eigvals-1 + 0 1 4012.909888 ... 267.065503 + 1 2 8.514739 ... 2.834806 + 2 3 0.666667 ... 0.000000 + 3 4 0.000000 ... 0.000000 + 4 5 0.222222 ... 0.111111 + + [5 rows x 7 columns] + + If we want to measure a feature that does not come as a built-in + property, we can define custom functions and pass them as + ``extra_properties``. For example, we can create a custom function + that measures the intensity quartiles in a region: + + >>> from skimage import data, util, measure + >>> import numpy as np + >>> def quartiles(regionmask, intensity): + ... return np.percentile(intensity[regionmask], q=(25, 50, 75)) + >>> + >>> image = data.coins() + >>> label_image = measure.label(image > 110, connectivity=image.ndim) + >>> props = measure.regionprops_table(label_image, intensity_image=image, + ... properties=('label',), + ... extra_properties=(quartiles,)) + >>> import pandas as pd # doctest: +SKIP + >>> pd.DataFrame(props).head() # doctest: +SKIP + label quartiles-0 quartiles-1 quartiles-2 + 0 1 117.00 123.0 130.0 + 1 2 111.25 112.0 114.0 + 2 3 111.00 111.0 111.0 + 3 4 111.00 111.5 112.5 + 4 5 112.50 113.0 114.0 + + """ + regions = regionprops( + label_image, + intensity_image=intensity_image, + cache=cache, + extra_properties=extra_properties, + spacing=spacing, + ) + if extra_properties is not None: + properties = list(properties) + [prop.__name__ for prop in extra_properties] + if len(regions) == 0: + ndim = label_image.ndim + label_image = np.zeros((3,) * ndim, dtype=int) + label_image[(1,) * ndim] = 1 + if intensity_image is not None: + intensity_image = np.zeros( + label_image.shape + intensity_image.shape[ndim:], + dtype=intensity_image.dtype, + ) + regions = regionprops( + label_image, + intensity_image=intensity_image, + cache=cache, + extra_properties=extra_properties, + spacing=spacing, + ) + + out_d = _props_to_dict(regions, properties=properties, separator=separator) + return {k: v[:0] for k, v in out_d.items()} + + return _props_to_dict(regions, properties=properties, separator=separator) + + +def regionprops( + label_image, + intensity_image=None, + cache=True, + *, + extra_properties=None, + spacing=None, + offset=None, +): + r"""Measure properties of labeled image regions. + + Parameters + ---------- + label_image : (M, N[, P]) ndarray + Labeled input image. Labels with value 0 are ignored. + + .. versionchanged:: 0.14.1 + Previously, ``label_image`` was processed by ``numpy.squeeze`` and + so any number of singleton dimensions was allowed. This resulted in + inconsistent handling of images with singleton dimensions. To + recover the old behaviour, use + ``regionprops(np.squeeze(label_image), ...)``. + intensity_image : (M, N[, P][, C]) ndarray, optional + Intensity (i.e., input) image with same size as labeled image, plus + optionally an extra dimension for multichannel data. Currently, + this extra channel dimension, if present, must be the last axis. + Default is None. + + .. versionchanged:: 0.18.0 + The ability to provide an extra dimension for channels was added. + cache : bool, optional + Determine whether to cache calculated properties. The computation is + much faster for cached properties, whereas the memory consumption + increases. + extra_properties : Iterable of callables + Add extra property computation functions that are not included with + skimage. The name of the property is derived from the function name, + the dtype is inferred by calling the function on a small sample. + If the name of an extra property clashes with the name of an existing + property the extra property will not be visible and a UserWarning is + issued. A property computation function must take a region mask as its + first argument. If the property requires an intensity image, it must + accept the intensity image as the second argument. + spacing: tuple of float, shape (ndim,) + The pixel spacing along each axis of the image. + offset : array-like of int, shape `(label_image.ndim,)`, optional + Coordinates of the origin ("top-left" corner) of the label image. + Normally this is ([0, ]0, 0), but it might be different if one wants + to obtain regionprops of subvolumes within a larger volume. + + Returns + ------- + properties : list of RegionProperties + Each item describes one labeled region, and can be accessed using the + attributes listed below. + + Notes + ----- + The following properties can be accessed as attributes or keys: + + **area** : float + Area of the region i.e. number of pixels of the region scaled by pixel-area. + **area_bbox** : float + Area of the bounding box i.e. number of pixels of bounding box scaled by pixel-area. + **area_convex** : float + Area of the convex hull image, which is the smallest convex + polygon that encloses the region. + **area_filled** : float + Area of the region with all the holes filled in. + **axis_major_length** : float + The length of the major axis of the ellipse that has the same + normalized second central moments as the region. + **axis_minor_length** : float + The length of the minor axis of the ellipse that has the same + normalized second central moments as the region. + **bbox** : tuple + Bounding box ``(min_row, min_col, max_row, max_col)``. + Pixels belonging to the bounding box are in the half-open interval + ``[min_row; max_row)`` and ``[min_col; max_col)``. + **centroid** : array + Centroid coordinate tuple ``(row, col)``. + **centroid_local** : array + Centroid coordinate tuple ``(row, col)``, relative to region bounding + box. + **centroid_weighted** : array + Centroid coordinate tuple ``(row, col)`` weighted with intensity + image. + **centroid_weighted_local** : array + Centroid coordinate tuple ``(row, col)``, relative to region bounding + box, weighted with intensity image. + **coords_scaled** : (K, 2) ndarray + Coordinate list ``(row, col)`` of the region scaled by ``spacing``. + **coords** : (K, 2) ndarray + Coordinate list ``(row, col)`` of the region. + **eccentricity** : float + Eccentricity of the ellipse that has the same second-moments as the + region. The eccentricity is the ratio of the focal distance + (distance between focal points) over the major axis length. + The value is in the interval [0, 1). + When it is 0, the ellipse becomes a circle. + **equivalent_diameter_area** : float + The diameter of a circle with the same area as the region. + **euler_number** : int + Euler characteristic of the set of non-zero pixels. + Computed as number of connected components subtracted by number of + holes (input.ndim connectivity). In 3D, number of connected + components plus number of holes subtracted by number of tunnels. + **extent** : float + Ratio of pixels in the region to pixels in the total bounding box. + Computed as ``area / (rows * cols)`` + **feret_diameter_max** : float + Maximum Feret's diameter computed as the longest distance between + points around a region's convex hull contour as determined by + ``find_contours``. [5]_ + **image** : (H, J) ndarray + Sliced binary region image which has the same size as bounding box. + **image_convex** : (H, J) ndarray + Binary convex hull image which has the same size as bounding box. + **image_filled** : (H, J) ndarray + Binary region image with filled holes which has the same size as + bounding box. + **image_intensity** : ndarray + Image inside region bounding box. + **inertia_tensor** : ndarray + Inertia tensor of the region for the rotation around its mass. + **inertia_tensor_eigvals** : tuple + The eigenvalues of the inertia tensor in decreasing order. + **intensity_max** : float + Value with the greatest intensity in the region. + **intensity_mean** : float + Value with the mean intensity in the region. + **intensity_min** : float + Value with the least intensity in the region. + **intensity_std** : float + Standard deviation of the intensity in the region. + **label** : int + The label in the labeled input image. + **moments** : (3, 3) ndarray + Spatial moments up to 3rd order:: + + m_ij = sum{ array(row, col) * row^i * col^j } + + where the sum is over the `row`, `col` coordinates of the region. + **moments_central** : (3, 3) ndarray + Central moments (translation invariant) up to 3rd order:: + + mu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j } + + where the sum is over the `row`, `col` coordinates of the region, + and `row_c` and `col_c` are the coordinates of the region's centroid. + **moments_hu** : tuple + Hu moments (translation, scale and rotation invariant). + **moments_normalized** : (3, 3) ndarray + Normalized moments (translation and scale invariant) up to 3rd order:: + + nu_ij = mu_ij / m_00^[(i+j)/2 + 1] + + where `m_00` is the zeroth spatial moment. + **moments_weighted** : (3, 3) ndarray + Spatial moments of intensity image up to 3rd order:: + + wm_ij = sum{ array(row, col) * row^i * col^j } + + where the sum is over the `row`, `col` coordinates of the region. + **moments_weighted_central** : (3, 3) ndarray + Central moments (translation invariant) of intensity image up to + 3rd order:: + + wmu_ij = sum{ array(row, col) * (row - row_c)^i * (col - col_c)^j } + + where the sum is over the `row`, `col` coordinates of the region, + and `row_c` and `col_c` are the coordinates of the region's weighted + centroid. + **moments_weighted_hu** : tuple + Hu moments (translation, scale and rotation invariant) of intensity + image. + **moments_weighted_normalized** : (3, 3) ndarray + Normalized moments (translation and scale invariant) of intensity + image up to 3rd order:: + + wnu_ij = wmu_ij / wm_00^[(i+j)/2 + 1] + + where ``wm_00`` is the zeroth spatial moment (intensity-weighted area). + **num_pixels** : int + Number of foreground pixels. + **orientation** : float + Angle between the 0th axis (rows) and the major + axis of the ellipse that has the same second moments as the region, + ranging from `-pi/2` to `pi/2` counter-clockwise. + **perimeter** : float + Perimeter of object which approximates the contour as a line + through the centers of border pixels using a 4-connectivity. + **perimeter_crofton** : float + Perimeter of object approximated by the Crofton formula in 4 + directions. + **slice** : tuple of slices + A slice to extract the object from the source image. + **solidity** : float + Ratio of pixels in the region to pixels of the convex hull image. + + Each region also supports iteration, so that you can do:: + + for prop in region: + print(prop, region[prop]) + + See Also + -------- + label + + References + ---------- + .. [1] Wilhelm Burger, Mark Burge. Principles of Digital Image Processing: + Core Algorithms. Springer-Verlag, London, 2009. + .. [2] B. Jähne. Digital Image Processing. Springer-Verlag, + Berlin-Heidelberg, 6. edition, 2005. + .. [3] T. H. Reiss. Recognizing Planar Objects Using Invariant Image + Features, from Lecture notes in computer science, p. 676. Springer, + Berlin, 1993. + .. [4] https://en.wikipedia.org/wiki/Image_moment + .. [5] W. Pabst, E. Gregorová. Characterization of particles and particle + systems, pp. 27-28. ICT Prague, 2007. + https://old.vscht.cz/sil/keramika/Characterization_of_particles/CPPS%20_English%20version_.pdf + + Examples + -------- + >>> from skimage import data, util + >>> from skimage.measure import label, regionprops + >>> img = util.img_as_ubyte(data.coins()) > 110 + >>> label_img = label(img, connectivity=img.ndim) + >>> props = regionprops(label_img) + >>> # centroid of first labeled object + >>> props[0].centroid + (22.72987986048314, 81.91228523446583) + >>> # centroid of first labeled object + >>> props[0]['centroid'] + (22.72987986048314, 81.91228523446583) + + Add custom measurements by passing functions as ``extra_properties`` + + >>> from skimage import data, util + >>> from skimage.measure import label, regionprops + >>> import numpy as np + >>> img = util.img_as_ubyte(data.coins()) > 110 + >>> label_img = label(img, connectivity=img.ndim) + >>> def pixelcount(regionmask): + ... return np.sum(regionmask) + >>> props = regionprops(label_img, extra_properties=(pixelcount,)) + >>> props[0].pixelcount + 7741 + >>> props[1]['pixelcount'] + 42 + + """ + + if label_image.ndim not in (2, 3): + raise TypeError('Only 2-D and 3-D images supported.') + + if not np.issubdtype(label_image.dtype, np.integer): + if np.issubdtype(label_image.dtype, bool): + raise TypeError( + 'Non-integer image types are ambiguous: ' + 'use skimage.measure.label to label the connected ' + 'components of label_image, ' + 'or label_image.astype(np.uint8) to interpret ' + 'the True values as a single label.' + ) + else: + raise TypeError('Non-integer label_image types are ambiguous') + + if offset is None: + offset_arr = np.zeros((label_image.ndim,), dtype=int) + else: + offset_arr = np.asarray(offset) + if offset_arr.ndim != 1 or offset_arr.size != label_image.ndim: + raise ValueError( + 'Offset should be an array-like of integers ' + 'of shape (label_image.ndim,); ' + f'{offset} was provided.' + ) + + regions = [] + + objects = ndi.find_objects(label_image) + for i, sl in enumerate(objects): + if sl is None: + continue + + label = i + 1 + + props = RegionProperties( + sl, + label, + label_image, + intensity_image, + cache, + spacing=spacing, + extra_properties=extra_properties, + offset=offset_arr, + ) + regions.append(props) + + return regions + + +def _parse_docs(): + import re + import textwrap + + doc = regionprops.__doc__ or '' + arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n [\*\S]+)' + if sys.version_info >= (3, 13): + arg_regex = r'\*\*(\w+)\*\* \:.*?\n(.*?)(?=\n[\*\S]+)' + + matches = re.finditer(arg_regex, doc, flags=re.DOTALL) + prop_doc = {m.group(1): textwrap.dedent(m.group(2)) for m in matches} + + return prop_doc + + +def _install_properties_docs(): + prop_doc = _parse_docs() + + for p in [member for member in dir(RegionProperties) if not member.startswith('_')]: + getattr(RegionProperties, p).__doc__ = prop_doc[p] + + +if __debug__: + # don't install docstrings when in optimized/non-debug mode + _install_properties_docs() diff --git a/lib/python3.10/site-packages/skimage/measure/_regionprops_utils.py b/lib/python3.10/site-packages/skimage/measure/_regionprops_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f3307d159a9d3b172d41bab82a1cc433495c3ae6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/_regionprops_utils.py @@ -0,0 +1,630 @@ +from math import sqrt +from numbers import Real +import numpy as np +from scipy import ndimage as ndi + + +STREL_4 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8) +STREL_8 = np.ones((3, 3), dtype=np.uint8) + + +# Coefficients from +# Ohser J., Nagel W., Schladitz K. (2002) The Euler Number of Discretized Sets +# - On the Choice of Adjacency in Homogeneous Lattices. +# In: Mecke K., Stoyan D. (eds) Morphology of Condensed Matter. Lecture Notes +# in Physics, vol 600. Springer, Berlin, Heidelberg. +# The value of coefficients correspond to the contributions to the Euler number +# of specific voxel configurations, which are themselves encoded thanks to a +# LUT. Computing the Euler number from the addition of the contributions of +# local configurations is possible thanks to an integral geometry formula +# (see the paper by Ohser et al. for more details). +EULER_COEFS2D_4 = [0, 1, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0] +EULER_COEFS2D_8 = [0, 0, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, -1, 0] +EULER_COEFS3D_26 = np.array( + [ + 0, + 1, + 1, + 0, + 1, + 0, + -2, + -1, + 1, + -2, + 0, + -1, + 0, + -1, + -1, + 0, + 1, + 0, + -2, + -1, + -2, + -1, + -1, + -2, + -6, + -3, + -3, + -2, + -3, + -2, + 0, + -1, + 1, + -2, + 0, + -1, + -6, + -3, + -3, + -2, + -2, + -1, + -1, + -2, + -3, + 0, + -2, + -1, + 0, + -1, + -1, + 0, + -3, + -2, + 0, + -1, + -3, + 0, + -2, + -1, + 0, + 1, + 1, + 0, + 1, + -2, + -6, + -3, + 0, + -1, + -3, + -2, + -2, + -1, + -3, + 0, + -1, + -2, + -2, + -1, + 0, + -1, + -3, + -2, + -1, + 0, + 0, + -1, + -3, + 0, + 0, + 1, + -2, + -1, + 1, + 0, + -2, + -1, + -3, + 0, + -3, + 0, + 0, + 1, + -1, + 4, + 0, + 3, + 0, + 3, + 1, + 2, + -1, + -2, + -2, + -1, + -2, + -1, + 1, + 0, + 0, + 3, + 1, + 2, + 1, + 2, + 2, + 1, + 1, + -6, + -2, + -3, + -2, + -3, + -1, + 0, + 0, + -3, + -1, + -2, + -1, + -2, + -2, + -1, + -2, + -3, + -1, + 0, + -1, + 0, + 4, + 3, + -3, + 0, + 0, + 1, + 0, + 1, + 3, + 2, + 0, + -3, + -1, + -2, + -3, + 0, + 0, + 1, + -1, + 0, + 0, + -1, + -2, + 1, + -1, + 0, + -1, + -2, + -2, + -1, + 0, + 1, + 3, + 2, + -2, + 1, + -1, + 0, + 1, + 2, + 2, + 1, + 0, + -3, + -3, + 0, + -1, + -2, + 0, + 1, + -1, + 0, + -2, + 1, + 0, + -1, + -1, + 0, + -1, + -2, + 0, + 1, + -2, + -1, + 3, + 2, + -2, + 1, + 1, + 2, + -1, + 0, + 2, + 1, + -1, + 0, + -2, + 1, + -2, + 1, + 1, + 2, + -2, + 3, + -1, + 2, + -1, + 2, + 0, + 1, + 0, + -1, + -1, + 0, + -1, + 0, + 2, + 1, + -1, + 2, + 0, + 1, + 0, + 1, + 1, + 0, + ] +) + + +def euler_number(image, connectivity=None): + """Calculate the Euler characteristic in binary image. + + For 2D objects, the Euler number is the number of objects minus the number + of holes. For 3D objects, the Euler number is obtained as the number of + objects plus the number of holes, minus the number of tunnels, or loops. + + Parameters + ---------- + image: (M, N[, P]) ndarray + Input image. If image is not binary, all values greater than zero + are considered as the object. + connectivity : int, optional + Maximum number of orthogonal hops to consider a pixel/voxel + as a neighbor. + Accepted values are ranging from 1 to input.ndim. If ``None``, a full + connectivity of ``input.ndim`` is used. + 4 or 8 neighborhoods are defined for 2D images (connectivity 1 and 2, + respectively). + 6 or 26 neighborhoods are defined for 3D images, (connectivity 1 and 3, + respectively). Connectivity 2 is not defined. + + Returns + ------- + euler_number : int + Euler characteristic of the set of all objects in the image. + + Notes + ----- + The Euler characteristic is an integer number that describes the + topology of the set of all objects in the input image. If object is + 4-connected, then background is 8-connected, and conversely. + + The computation of the Euler characteristic is based on an integral + geometry formula in discretized space. In practice, a neighborhood + configuration is constructed, and a LUT is applied for each + configuration. The coefficients used are the ones of Ohser et al. + + It can be useful to compute the Euler characteristic for several + connectivities. A large relative difference between results + for different connectivities suggests that the image resolution + (with respect to the size of objects and holes) is too low. + + References + ---------- + .. [1] S. Rivollier. Analyse d’image geometrique et morphometrique par + diagrammes de forme et voisinages adaptatifs generaux. PhD thesis, + 2010. Ecole Nationale Superieure des Mines de Saint-Etienne. + https://tel.archives-ouvertes.fr/tel-00560838 + .. [2] Ohser J., Nagel W., Schladitz K. (2002) The Euler Number of + Discretized Sets - On the Choice of Adjacency in Homogeneous + Lattices. In: Mecke K., Stoyan D. (eds) Morphology of Condensed + Matter. Lecture Notes in Physics, vol 600. Springer, Berlin, + Heidelberg. + + Examples + -------- + >>> import numpy as np + >>> SAMPLE = np.zeros((100,100,100)); + >>> SAMPLE[40:60, 40:60, 40:60]=1 + >>> euler_number(SAMPLE) # doctest: +ELLIPSIS + 1... + >>> SAMPLE[45:55,45:55,45:55] = 0; + >>> euler_number(SAMPLE) # doctest: +ELLIPSIS + 2... + >>> SAMPLE = np.array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + ... [1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0], + ... [0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1], + ... [0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1]]) + >>> euler_number(SAMPLE) # doctest: + 0 + >>> euler_number(SAMPLE, connectivity=1) # doctest: + 2 + """ + + # as image can be a label image, transform it to binary + image = (image > 0).astype(int) + image = np.pad(image, pad_width=1, mode='constant') + + # check connectivity + if connectivity is None: + connectivity = image.ndim + + # config variable is an adjacency configuration. A coefficient given by + # variable coefs is attributed to each configuration in order to get + # the Euler characteristic. + if image.ndim == 2: + config = np.array([[0, 0, 0], [0, 1, 4], [0, 2, 8]]) + if connectivity == 1: + coefs = EULER_COEFS2D_4 + else: + coefs = EULER_COEFS2D_8 + bins = 16 + else: # 3D images + if connectivity == 2: + raise NotImplementedError( + 'For 3D images, Euler number is implemented ' + 'for connectivities 1 and 3 only' + ) + + config = np.array( + [ + [[0, 0, 0], [0, 0, 0], [0, 0, 0]], + [[0, 0, 0], [0, 1, 4], [0, 2, 8]], + [[0, 0, 0], [0, 16, 64], [0, 32, 128]], + ] + ) + if connectivity == 1: + coefs = EULER_COEFS3D_26[::-1] + else: + coefs = EULER_COEFS3D_26 + bins = 256 + + # XF has values in the 0-255 range in 3D, and in the 0-15 range in 2D, + # with one unique value for each binary configuration of the + # 27-voxel cube in 3D / 8-pixel square in 2D, up to symmetries + XF = ndi.convolve(image, config, mode='constant', cval=0) + h = np.bincount(XF.ravel(), minlength=bins) + + if image.ndim == 2: + return coefs @ h + else: + return int(0.125 * coefs @ h) + + +def perimeter(image, neighborhood=4): + """Calculate total perimeter of all objects in binary image. + + Parameters + ---------- + image : (M, N) ndarray + Binary input image. + neighborhood : 4 or 8, optional + Neighborhood connectivity for border pixel determination. It is used to + compute the contour. A higher neighborhood widens the border on which + the perimeter is computed. + + Returns + ------- + perimeter : float + Total perimeter of all objects in binary image. + + References + ---------- + .. [1] K. Benkrid, D. Crookes. Design and FPGA Implementation of + a Perimeter Estimator. The Queen's University of Belfast. + http://www.cs.qub.ac.uk/~d.crookes/webpubs/papers/perimeter.doc + + Examples + -------- + >>> from skimage import data, util + >>> from skimage.measure import label + >>> # coins image (binary) + >>> img_coins = data.coins() > 110 + >>> # total perimeter of all objects in the image + >>> perimeter(img_coins, neighborhood=4) # doctest: +ELLIPSIS + 7796.867... + >>> perimeter(img_coins, neighborhood=8) # doctest: +ELLIPSIS + 8806.268... + + """ + if image.ndim != 2: + raise NotImplementedError('`perimeter` supports 2D images only') + + if neighborhood == 4: + strel = STREL_4 + else: + strel = STREL_8 + image = image.astype(np.uint8) + eroded_image = ndi.binary_erosion(image, strel, border_value=0) + border_image = image - eroded_image + + perimeter_weights = np.zeros(50, dtype=np.float64) + perimeter_weights[[5, 7, 15, 17, 25, 27]] = 1 + perimeter_weights[[21, 33]] = sqrt(2) + perimeter_weights[[13, 23]] = (1 + sqrt(2)) / 2 + + perimeter_image = ndi.convolve( + border_image, + np.array([[10, 2, 10], [2, 1, 2], [10, 2, 10]]), + mode='constant', + cval=0, + ) + + # You can also write + # return perimeter_weights[perimeter_image].sum() + # but that was measured as taking much longer than bincount + np.dot (5x + # as much time) + perimeter_histogram = np.bincount(perimeter_image.ravel(), minlength=50) + total_perimeter = perimeter_histogram @ perimeter_weights + return total_perimeter + + +def perimeter_crofton(image, directions=4): + """Calculate total Crofton perimeter of all objects in binary image. + + Parameters + ---------- + image : (M, N) ndarray + Input image. If image is not binary, all values greater than zero + are considered as the object. + directions : 2 or 4, optional + Number of directions used to approximate the Crofton perimeter. By + default, 4 is used: it should be more accurate than 2. + Computation time is the same in both cases. + + Returns + ------- + perimeter : float + Total perimeter of all objects in binary image. + + Notes + ----- + This measure is based on Crofton formula [1], which is a measure from + integral geometry. It is defined for general curve length evaluation via + a double integral along all directions. In a discrete + space, 2 or 4 directions give a quite good approximation, 4 being more + accurate than 2 for more complex shapes. + + Similar to :func:`~.measure.perimeter`, this function returns an + approximation of the perimeter in continuous space. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Crofton_formula + .. [2] S. Rivollier. Analyse d’image geometrique et morphometrique par + diagrammes de forme et voisinages adaptatifs generaux. PhD thesis, + 2010. + Ecole Nationale Superieure des Mines de Saint-Etienne. + https://tel.archives-ouvertes.fr/tel-00560838 + + Examples + -------- + >>> from skimage import data, util + >>> from skimage.measure import label + >>> # coins image (binary) + >>> img_coins = data.coins() > 110 + >>> # total perimeter of all objects in the image + >>> perimeter_crofton(img_coins, directions=2) # doctest: +ELLIPSIS + 8144.578... + >>> perimeter_crofton(img_coins, directions=4) # doctest: +ELLIPSIS + 7837.077... + """ + if image.ndim != 2: + raise NotImplementedError('`perimeter_crofton` supports 2D images only') + + # as image could be a label image, transform it to binary image + image = (image > 0).astype(np.uint8) + image = np.pad(image, pad_width=1, mode='constant') + XF = ndi.convolve( + image, np.array([[0, 0, 0], [0, 1, 4], [0, 2, 8]]), mode='constant', cval=0 + ) + + h = np.bincount(XF.ravel(), minlength=16) + + # definition of the LUT + if directions == 2: + coefs = [ + 0, + np.pi / 2, + 0, + 0, + 0, + np.pi / 2, + 0, + 0, + np.pi / 2, + np.pi, + 0, + 0, + np.pi / 2, + np.pi, + 0, + 0, + ] + else: + coefs = [ + 0, + np.pi / 4 * (1 + 1 / (np.sqrt(2))), + np.pi / (4 * np.sqrt(2)), + np.pi / (2 * np.sqrt(2)), + 0, + np.pi / 4 * (1 + 1 / (np.sqrt(2))), + 0, + np.pi / (4 * np.sqrt(2)), + np.pi / 4, + np.pi / 2, + np.pi / (4 * np.sqrt(2)), + np.pi / (4 * np.sqrt(2)), + np.pi / 4, + np.pi / 2, + 0, + 0, + ] + + total_perimeter = coefs @ h + return total_perimeter + + +def _normalize_spacing(spacing, ndims): + """Normalize spacing parameter. + + The `spacing` parameter should be a sequence of numbers matching + the image dimensions. If `spacing` is a scalar, assume equal + spacing along all dimensions. + + Parameters + --------- + spacing : Any + User-provided `spacing` keyword. + ndims : int + Number of image dimensions. + + Returns + ------- + spacing : array + Corrected spacing. + + Raises + ------ + ValueError + If `spacing` is invalid. + + """ + spacing = np.array(spacing) + if spacing.shape == (): + spacing = np.broadcast_to(spacing, shape=(ndims,)) + elif spacing.shape != (ndims,): + raise ValueError( + f"spacing isn't a scalar nor a sequence of shape {(ndims,)}, got {spacing}." + ) + if not all(isinstance(s, Real) for s in spacing): + raise TypeError( + f"Element of spacing isn't float or integer type, got {spacing}." + ) + if not all(np.isfinite(spacing)): + raise ValueError( + f"Invalid spacing parameter. All elements must be finite, got {spacing}." + ) + return spacing diff --git a/lib/python3.10/site-packages/skimage/measure/block.py b/lib/python3.10/site-packages/skimage/measure/block.py new file mode 100644 index 0000000000000000000000000000000000000000..a256c077b26518609b2652ff0ff973515a191331 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/block.py @@ -0,0 +1,94 @@ +import numpy as np +from ..util import view_as_blocks + + +def block_reduce(image, block_size=2, func=np.sum, cval=0, func_kwargs=None): + """Downsample image by applying function `func` to local blocks. + + This function is useful for max and mean pooling, for example. + + Parameters + ---------- + image : (M[, ...]) ndarray + N-dimensional input image. + block_size : array_like or int + Array containing down-sampling integer factor along each axis. + Default block_size is 2. + func : callable + Function object which is used to calculate the return value for each + local block. This function must implement an ``axis`` parameter. + Primary functions are ``numpy.sum``, ``numpy.min``, ``numpy.max``, + ``numpy.mean`` and ``numpy.median``. See also `func_kwargs`. + cval : float + Constant padding value if image is not perfectly divisible by the + block size. + func_kwargs : dict + Keyword arguments passed to `func`. Notably useful for passing dtype + argument to ``np.mean``. Takes dictionary of inputs, e.g.: + ``func_kwargs={'dtype': np.float16})``. + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + + Examples + -------- + >>> from skimage.measure import block_reduce + >>> image = np.arange(3*3*4).reshape(3, 3, 4) + >>> image # doctest: +NORMALIZE_WHITESPACE + array([[[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11]], + [[12, 13, 14, 15], + [16, 17, 18, 19], + [20, 21, 22, 23]], + [[24, 25, 26, 27], + [28, 29, 30, 31], + [32, 33, 34, 35]]]) + >>> block_reduce(image, block_size=(3, 3, 1), func=np.mean) + array([[[16., 17., 18., 19.]]]) + >>> image_max1 = block_reduce(image, block_size=(1, 3, 4), func=np.max) + >>> image_max1 # doctest: +NORMALIZE_WHITESPACE + array([[[11]], + [[23]], + [[35]]]) + >>> image_max2 = block_reduce(image, block_size=(3, 1, 4), func=np.max) + >>> image_max2 # doctest: +NORMALIZE_WHITESPACE + array([[[27], + [31], + [35]]]) + """ + + if np.isscalar(block_size): + block_size = (block_size,) * image.ndim + elif len(block_size) != image.ndim: + raise ValueError( + "`block_size` must be a scalar or have " "the same length as `image.shape`" + ) + + if func_kwargs is None: + func_kwargs = {} + + pad_width = [] + for i in range(len(block_size)): + if block_size[i] < 1: + raise ValueError( + "Down-sampling factors must be >= 1. Use " + "`skimage.transform.resize` to up-sample an " + "image." + ) + if image.shape[i] % block_size[i] != 0: + after_width = block_size[i] - (image.shape[i] % block_size[i]) + else: + after_width = 0 + pad_width.append((0, after_width)) + + if np.any(np.asarray(pad_width)): + image = np.pad( + image, pad_width=pad_width, mode='constant', constant_values=cval + ) + + blocked = view_as_blocks(image, block_size) + + return func(blocked, axis=tuple(range(image.ndim, blocked.ndim)), **func_kwargs) diff --git a/lib/python3.10/site-packages/skimage/measure/entropy.py b/lib/python3.10/site-packages/skimage/measure/entropy.py new file mode 100644 index 0000000000000000000000000000000000000000..51c6bcc0706e25b710cdb15e4027a4061f1700d3 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/entropy.py @@ -0,0 +1,41 @@ +from numpy import unique +from scipy.stats import entropy as scipy_entropy + + +def shannon_entropy(image, base=2): + """Calculate the Shannon entropy of an image. + + The Shannon entropy is defined as S = -sum(pk * log(pk)), + where pk are frequency/probability of pixels of value k. + + Parameters + ---------- + image : (M, N) ndarray + Grayscale input image. + base : float, optional + The logarithmic base to use. + + Returns + ------- + entropy : float + + Notes + ----- + The returned value is measured in bits or shannon (Sh) for base=2, natural + unit (nat) for base=np.e and hartley (Hart) for base=10. + + References + ---------- + .. [1] `https://en.wikipedia.org/wiki/Entropy_(information_theory) `_ + .. [2] https://en.wiktionary.org/wiki/Shannon_entropy + + Examples + -------- + >>> from skimage import data + >>> from skimage.measure import shannon_entropy + >>> shannon_entropy(data.camera()) + 7.231695011055706 + """ + + _, counts = unique(image, return_counts=True) + return scipy_entropy(counts, base=base) diff --git a/lib/python3.10/site-packages/skimage/measure/fit.py b/lib/python3.10/site-packages/skimage/measure/fit.py new file mode 100644 index 0000000000000000000000000000000000000000..80bb1c561f5e608b829c65d7abf3a6f589c8d062 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/fit.py @@ -0,0 +1,977 @@ +import math +from warnings import warn + +import numpy as np +from numpy.linalg import inv +from scipy import optimize, spatial + + +_EPSILON = np.spacing(1) + + +def _check_data_dim(data, dim): + if data.ndim != 2 or data.shape[1] != dim: + raise ValueError(f"Input data must have shape (N, {dim}).") + + +def _check_data_atleast_2D(data): + if data.ndim < 2 or data.shape[1] < 2: + raise ValueError('Input data must be at least 2D.') + + +class BaseModel: + def __init__(self): + self.params = None + + +class LineModelND(BaseModel): + """Total least squares estimator for N-dimensional lines. + + In contrast to ordinary least squares line estimation, this estimator + minimizes the orthogonal distances of points to the estimated line. + + Lines are defined by a point (origin) and a unit vector (direction) + according to the following vector equation:: + + X = origin + lambda * direction + + Attributes + ---------- + params : tuple + Line model parameters in the following order `origin`, `direction`. + + Examples + -------- + >>> x = np.linspace(1, 2, 25) + >>> y = 1.5 * x + 3 + >>> lm = LineModelND() + >>> lm.estimate(np.stack([x, y], axis=-1)) + True + >>> tuple(np.round(lm.params, 5)) + (array([1.5 , 5.25]), array([0.5547 , 0.83205])) + >>> res = lm.residuals(np.stack([x, y], axis=-1)) + >>> np.abs(np.round(res, 9)) + array([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.]) + >>> np.round(lm.predict_y(x[:5]), 3) + array([4.5 , 4.562, 4.625, 4.688, 4.75 ]) + >>> np.round(lm.predict_x(y[:5]), 3) + array([1. , 1.042, 1.083, 1.125, 1.167]) + + """ + + def estimate(self, data): + """Estimate line model from data. + + This minimizes the sum of shortest (orthogonal) distances + from the given data points to the estimated line. + + Parameters + ---------- + data : (N, dim) array + N points in a space of dimensionality dim >= 2. + + Returns + ------- + success : bool + True, if model estimation succeeds. + """ + _check_data_atleast_2D(data) + + origin = data.mean(axis=0) + data = data - origin + + if data.shape[0] == 2: # well determined + direction = data[1] - data[0] + norm = np.linalg.norm(direction) + if norm != 0: # this should not happen to be norm 0 + direction /= norm + elif data.shape[0] > 2: # over-determined + # Note: with full_matrices=1 Python dies with joblib parallel_for. + _, _, v = np.linalg.svd(data, full_matrices=False) + direction = v[0] + else: # under-determined + return False + + self.params = (origin, direction) + + return True + + def residuals(self, data, params=None): + """Determine residuals of data to model. + + For each point, the shortest (orthogonal) distance to the line is + returned. It is obtained by projecting the data onto the line. + + Parameters + ---------- + data : (N, dim) array + N points in a space of dimension dim. + params : (2,) array, optional + Optional custom parameter set in the form (`origin`, `direction`). + + Returns + ------- + residuals : (N,) array + Residual for each data point. + """ + _check_data_atleast_2D(data) + if params is None: + if self.params is None: + raise ValueError('Parameters cannot be None') + params = self.params + if len(params) != 2: + raise ValueError('Parameters are defined by 2 sets.') + + origin, direction = params + res = (data - origin) - ((data - origin) @ direction)[ + ..., np.newaxis + ] * direction + return np.linalg.norm(res, axis=1) + + def predict(self, x, axis=0, params=None): + """Predict intersection of the estimated line model with a hyperplane + orthogonal to a given axis. + + Parameters + ---------- + x : (n, 1) array + Coordinates along an axis. + axis : int + Axis orthogonal to the hyperplane intersecting the line. + params : (2,) array, optional + Optional custom parameter set in the form (`origin`, `direction`). + + Returns + ------- + data : (n, m) array + Predicted coordinates. + + Raises + ------ + ValueError + If the line is parallel to the given axis. + """ + if params is None: + if self.params is None: + raise ValueError('Parameters cannot be None') + params = self.params + if len(params) != 2: + raise ValueError('Parameters are defined by 2 sets.') + + origin, direction = params + + if direction[axis] == 0: + # line parallel to axis + raise ValueError(f'Line parallel to axis {axis}') + + l = (x - origin[axis]) / direction[axis] + data = origin + l[..., np.newaxis] * direction + return data + + def predict_x(self, y, params=None): + """Predict x-coordinates for 2D lines using the estimated model. + + Alias for:: + + predict(y, axis=1)[:, 0] + + Parameters + ---------- + y : array + y-coordinates. + params : (2,) array, optional + Optional custom parameter set in the form (`origin`, `direction`). + + Returns + ------- + x : array + Predicted x-coordinates. + + """ + x = self.predict(y, axis=1, params=params)[:, 0] + return x + + def predict_y(self, x, params=None): + """Predict y-coordinates for 2D lines using the estimated model. + + Alias for:: + + predict(x, axis=0)[:, 1] + + Parameters + ---------- + x : array + x-coordinates. + params : (2,) array, optional + Optional custom parameter set in the form (`origin`, `direction`). + + Returns + ------- + y : array + Predicted y-coordinates. + + """ + y = self.predict(x, axis=0, params=params)[:, 1] + return y + + +class CircleModel(BaseModel): + """Total least squares estimator for 2D circles. + + The functional model of the circle is:: + + r**2 = (x - xc)**2 + (y - yc)**2 + + This estimator minimizes the squared distances from all points to the + circle:: + + min{ sum((r - sqrt((x_i - xc)**2 + (y_i - yc)**2))**2) } + + A minimum number of 3 points is required to solve for the parameters. + + Attributes + ---------- + params : tuple + Circle model parameters in the following order `xc`, `yc`, `r`. + + Notes + ----- + The estimation is carried out using a 2D version of the spherical + estimation given in [1]_. + + References + ---------- + .. [1] Jekel, Charles F. Obtaining non-linear orthotropic material models + for pvc-coated polyester via inverse bubble inflation. + Thesis (MEng), Stellenbosch University, 2016. Appendix A, pp. 83-87. + https://hdl.handle.net/10019.1/98627 + + Examples + -------- + >>> t = np.linspace(0, 2 * np.pi, 25) + >>> xy = CircleModel().predict_xy(t, params=(2, 3, 4)) + >>> model = CircleModel() + >>> model.estimate(xy) + True + >>> tuple(np.round(model.params, 5)) + (2.0, 3.0, 4.0) + >>> res = model.residuals(xy) + >>> np.abs(np.round(res, 9)) + array([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.]) + """ + + def estimate(self, data): + """Estimate circle model from data using total least squares. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + + _check_data_dim(data, dim=2) + + # to prevent integer overflow, cast data to float, if it isn't already + float_type = np.promote_types(data.dtype, np.float32) + data = data.astype(float_type, copy=False) + # normalize value range to avoid misfitting due to numeric errors if + # the relative distanceses are small compared to absolute distances + origin = data.mean(axis=0) + data = data - origin + scale = data.std() + if scale < np.finfo(float_type).tiny: + warn( + "Standard deviation of data is too small to estimate " + "circle with meaningful precision.", + category=RuntimeWarning, + stacklevel=2, + ) + return False + data /= scale + + # Adapted from a spherical estimator covered in a blog post by Charles + # Jeckel (see also reference 1 above): + # https://jekel.me/2015/Least-Squares-Sphere-Fit/ + A = np.append(data * 2, np.ones((data.shape[0], 1), dtype=float_type), axis=1) + f = np.sum(data**2, axis=1) + C, _, rank, _ = np.linalg.lstsq(A, f, rcond=None) + + if rank != 3: + warn("Input does not contain enough significant data points.") + return False + + center = C[0:2] + distances = spatial.minkowski_distance(center, data) + r = np.sqrt(np.mean(distances**2)) + + # revert normalization and set params + center *= scale + r *= scale + center += origin + self.params = tuple(center) + (r,) + + return True + + def residuals(self, data): + """Determine residuals of data to model. + + For each point the shortest distance to the circle is returned. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + residuals : (N,) array + Residual for each data point. + + """ + + _check_data_dim(data, dim=2) + + xc, yc, r = self.params + + x = data[:, 0] + y = data[:, 1] + + return r - np.sqrt((x - xc) ** 2 + (y - yc) ** 2) + + def predict_xy(self, t, params=None): + """Predict x- and y-coordinates using the estimated model. + + Parameters + ---------- + t : array + Angles in circle in radians. Angles start to count from positive + x-axis to positive y-axis in a right-handed system. + params : (3,) array, optional + Optional custom parameter set. + + Returns + ------- + xy : (..., 2) array + Predicted x- and y-coordinates. + + """ + if params is None: + params = self.params + xc, yc, r = params + + x = xc + r * np.cos(t) + y = yc + r * np.sin(t) + + return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) + + +class EllipseModel(BaseModel): + """Total least squares estimator for 2D ellipses. + + The functional model of the ellipse is:: + + xt = xc + a*cos(theta)*cos(t) - b*sin(theta)*sin(t) + yt = yc + a*sin(theta)*cos(t) + b*cos(theta)*sin(t) + d = sqrt((x - xt)**2 + (y - yt)**2) + + where ``(xt, yt)`` is the closest point on the ellipse to ``(x, y)``. Thus + d is the shortest distance from the point to the ellipse. + + The estimator is based on a least squares minimization. The optimal + solution is computed directly, no iterations are required. This leads + to a simple, stable and robust fitting method. + + The ``params`` attribute contains the parameters in the following order:: + + xc, yc, a, b, theta + + Attributes + ---------- + params : tuple + Ellipse model parameters in the following order `xc`, `yc`, `a`, `b`, + `theta`. + + Examples + -------- + + >>> xy = EllipseModel().predict_xy(np.linspace(0, 2 * np.pi, 25), + ... params=(10, 15, 8, 4, np.deg2rad(30))) + >>> ellipse = EllipseModel() + >>> ellipse.estimate(xy) + True + >>> np.round(ellipse.params, 2) + array([10. , 15. , 8. , 4. , 0.52]) + >>> np.round(abs(ellipse.residuals(xy)), 5) + array([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.]) + """ + + def estimate(self, data): + """Estimate ellipse model from data using total least squares. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + + References + ---------- + .. [1] Halir, R.; Flusser, J. "Numerically stable direct least squares + fitting of ellipses". In Proc. 6th International Conference in + Central Europe on Computer Graphics and Visualization. + WSCG (Vol. 98, pp. 125-132). + + """ + # Original Implementation: Ben Hammel, Nick Sullivan-Molina + # another REFERENCE: [2] http://mathworld.wolfram.com/Ellipse.html + _check_data_dim(data, dim=2) + + if len(data) < 5: + warn( + "Need at least 5 data points to estimate an ellipse.", + category=RuntimeWarning, + stacklevel=2, + ) + return False + + # to prevent integer overflow, cast data to float, if it isn't already + float_type = np.promote_types(data.dtype, np.float32) + data = data.astype(float_type, copy=False) + + # normalize value range to avoid misfitting due to numeric errors if + # the relative distances are small compared to absolute distances + origin = data.mean(axis=0) + data = data - origin + scale = data.std() + if scale < np.finfo(float_type).tiny: + warn( + "Standard deviation of data is too small to estimate " + "ellipse with meaningful precision.", + category=RuntimeWarning, + stacklevel=2, + ) + return False + data /= scale + + x = data[:, 0] + y = data[:, 1] + + # Quadratic part of design matrix [eqn. 15] from [1] + D1 = np.vstack([x**2, x * y, y**2]).T + # Linear part of design matrix [eqn. 16] from [1] + D2 = np.vstack([x, y, np.ones_like(x)]).T + + # forming scatter matrix [eqn. 17] from [1] + S1 = D1.T @ D1 + S2 = D1.T @ D2 + S3 = D2.T @ D2 + + # Constraint matrix [eqn. 18] + C1 = np.array([[0.0, 0.0, 2.0], [0.0, -1.0, 0.0], [2.0, 0.0, 0.0]]) + + try: + # Reduced scatter matrix [eqn. 29] + M = inv(C1) @ (S1 - S2 @ inv(S3) @ S2.T) + except np.linalg.LinAlgError: # LinAlgError: Singular matrix + return False + + # M*|a b c >=l|a b c >. Find eigenvalues and eigenvectors + # from this equation [eqn. 28] + eig_vals, eig_vecs = np.linalg.eig(M) + + # eigenvector must meet constraint 4ac - b^2 to be valid. + cond = 4 * np.multiply(eig_vecs[0, :], eig_vecs[2, :]) - np.power( + eig_vecs[1, :], 2 + ) + a1 = eig_vecs[:, (cond > 0)] + # seeks for empty matrix + if 0 in a1.shape or len(a1.ravel()) != 3: + return False + a, b, c = a1.ravel() + + # |d f g> = -S3^(-1)*S2^(T)*|a b c> [eqn. 24] + a2 = -inv(S3) @ S2.T @ a1 + d, f, g = a2.ravel() + + # eigenvectors are the coefficients of an ellipse in general form + # a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0 (eqn. 15) from [2] + b /= 2.0 + d /= 2.0 + f /= 2.0 + + # finding center of ellipse [eqn.19 and 20] from [2] + x0 = (c * d - b * f) / (b**2.0 - a * c) + y0 = (a * f - b * d) / (b**2.0 - a * c) + + # Find the semi-axes lengths [eqn. 21 and 22] from [2] + numerator = a * f**2 + c * d**2 + g * b**2 - 2 * b * d * f - a * c * g + term = np.sqrt((a - c) ** 2 + 4 * b**2) + denominator1 = (b**2 - a * c) * (term - (a + c)) + denominator2 = (b**2 - a * c) * (-term - (a + c)) + width = np.sqrt(2 * numerator / denominator1) + height = np.sqrt(2 * numerator / denominator2) + + # angle of counterclockwise rotation of major-axis of ellipse + # to x-axis [eqn. 23] from [2]. + phi = 0.5 * np.arctan((2.0 * b) / (a - c)) + if a > c: + phi += 0.5 * np.pi + + # stabilize parameters: + # sometimes small fluctuations in data can cause + # height and width to swap + if width < height: + width, height = height, width + phi += np.pi / 2 + + phi %= np.pi + + # revert normalization and set params + params = np.nan_to_num([x0, y0, width, height, phi]).real + params[:4] *= scale + params[:2] += origin + + self.params = tuple(float(p) for p in params) + + return True + + def residuals(self, data): + """Determine residuals of data to model. + + For each point the shortest distance to the ellipse is returned. + + Parameters + ---------- + data : (N, 2) array + N points with ``(x, y)`` coordinates, respectively. + + Returns + ------- + residuals : (N,) array + Residual for each data point. + + """ + + _check_data_dim(data, dim=2) + + xc, yc, a, b, theta = self.params + + ctheta = math.cos(theta) + stheta = math.sin(theta) + + x = data[:, 0] + y = data[:, 1] + + N = data.shape[0] + + def fun(t, xi, yi): + ct = math.cos(np.squeeze(t)) + st = math.sin(np.squeeze(t)) + xt = xc + a * ctheta * ct - b * stheta * st + yt = yc + a * stheta * ct + b * ctheta * st + return (xi - xt) ** 2 + (yi - yt) ** 2 + + # def Dfun(t, xi, yi): + # ct = math.cos(t) + # st = math.sin(t) + # xt = xc + a * ctheta * ct - b * stheta * st + # yt = yc + a * stheta * ct + b * ctheta * st + # dfx_t = - 2 * (xi - xt) * (- a * ctheta * st + # - b * stheta * ct) + # dfy_t = - 2 * (yi - yt) * (- a * stheta * st + # + b * ctheta * ct) + # return [dfx_t + dfy_t] + + residuals = np.empty((N,), dtype=np.float64) + + # initial guess for parameter t of closest point on ellipse + t0 = np.arctan2(y - yc, x - xc) - theta + + # determine shortest distance to ellipse for each point + for i in range(N): + xi = x[i] + yi = y[i] + # faster without Dfun, because of the python overhead + t, _ = optimize.leastsq(fun, t0[i], args=(xi, yi)) + residuals[i] = np.sqrt(fun(t, xi, yi)) + + return residuals + + def predict_xy(self, t, params=None): + """Predict x- and y-coordinates using the estimated model. + + Parameters + ---------- + t : array + Angles in circle in radians. Angles start to count from positive + x-axis to positive y-axis in a right-handed system. + params : (5,) array, optional + Optional custom parameter set. + + Returns + ------- + xy : (..., 2) array + Predicted x- and y-coordinates. + + """ + + if params is None: + params = self.params + + xc, yc, a, b, theta = params + + ct = np.cos(t) + st = np.sin(t) + ctheta = math.cos(theta) + stheta = math.sin(theta) + + x = xc + a * ctheta * ct - b * stheta * st + y = yc + a * stheta * ct + b * ctheta * st + + return np.concatenate((x[..., None], y[..., None]), axis=t.ndim) + + +def _dynamic_max_trials(n_inliers, n_samples, min_samples, probability): + """Determine number trials such that at least one outlier-free subset is + sampled for the given inlier/outlier ratio. + + Parameters + ---------- + n_inliers : int + Number of inliers in the data. + n_samples : int + Total number of samples in the data. + min_samples : int + Minimum number of samples chosen randomly from original data. + probability : float + Probability (confidence) that one outlier-free sample is generated. + + Returns + ------- + trials : int + Number of trials. + """ + if probability == 0: + return 0 + if n_inliers == 0: + return np.inf + inlier_ratio = n_inliers / n_samples + nom = 1 - probability + denom = 1 - inlier_ratio**min_samples + # Keep (de-)nominator in the range of [_EPSILON, 1 - _EPSILON] so that + # it is always guaranteed that the logarithm is negative and we return + # a positive number of trials. + nom = np.clip(nom, a_min=_EPSILON, a_max=1 - _EPSILON) + denom = np.clip(denom, a_min=_EPSILON, a_max=1 - _EPSILON) + return np.ceil(np.log(nom) / np.log(denom)) + + +def ransac( + data, + model_class, + min_samples, + residual_threshold, + is_data_valid=None, + is_model_valid=None, + max_trials=100, + stop_sample_num=np.inf, + stop_residuals_sum=0, + stop_probability=1, + rng=None, + initial_inliers=None, +): + """Fit a model to data with the RANSAC (random sample consensus) algorithm. + + RANSAC is an iterative algorithm for the robust estimation of parameters + from a subset of inliers from the complete data set. Each iteration + performs the following tasks: + + 1. Select `min_samples` random samples from the original data and check + whether the set of data is valid (see `is_data_valid`). + 2. Estimate a model to the random subset + (`model_cls.estimate(*data[random_subset]`) and check whether the + estimated model is valid (see `is_model_valid`). + 3. Classify all data as inliers or outliers by calculating the residuals + to the estimated model (`model_cls.residuals(*data)`) - all data samples + with residuals smaller than the `residual_threshold` are considered as + inliers. + 4. Save estimated model as best model if number of inlier samples is + maximal. In case the current estimated model has the same number of + inliers, it is only considered as the best model if it has less sum of + residuals. + + These steps are performed either a maximum number of times or until one of + the special stop criteria are met. The final model is estimated using all + inlier samples of the previously determined best model. + + Parameters + ---------- + data : [list, tuple of] (N, ...) array + Data set to which the model is fitted, where N is the number of data + points and the remaining dimension are depending on model requirements. + If the model class requires multiple input data arrays (e.g. source and + destination coordinates of ``skimage.transform.AffineTransform``), + they can be optionally passed as tuple or list. Note, that in this case + the functions ``estimate(*data)``, ``residuals(*data)``, + ``is_model_valid(model, *random_data)`` and + ``is_data_valid(*random_data)`` must all take each data array as + separate arguments. + model_class : object + Object with the following object methods: + + * ``success = estimate(*data)`` + * ``residuals(*data)`` + + where `success` indicates whether the model estimation succeeded + (`True` or `None` for success, `False` for failure). + min_samples : int in range (0, N) + The minimum number of data points to fit a model to. + residual_threshold : float larger than 0 + Maximum distance for a data point to be classified as an inlier. + is_data_valid : function, optional + This function is called with the randomly selected data before the + model is fitted to it: `is_data_valid(*random_data)`. + is_model_valid : function, optional + This function is called with the estimated model and the randomly + selected data: `is_model_valid(model, *random_data)`, . + max_trials : int, optional + Maximum number of iterations for random sample selection. + stop_sample_num : int, optional + Stop iteration if at least this number of inliers are found. + stop_residuals_sum : float, optional + Stop iteration if sum of residuals is less than or equal to this + threshold. + stop_probability : float in range [0, 1], optional + RANSAC iteration stops if at least one outlier-free set of the + training data is sampled with ``probability >= stop_probability``, + depending on the current best model's inlier ratio and the number + of trials. This requires to generate at least N samples (trials): + + N >= log(1 - probability) / log(1 - e**m) + + where the probability (confidence) is typically set to a high value + such as 0.99, e is the current fraction of inliers w.r.t. the + total number of samples, and m is the min_samples value. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + initial_inliers : array-like of bool, shape (N,), optional + Initial samples selection for model estimation + + + Returns + ------- + model : object + Best model with largest consensus set. + inliers : (N,) array + Boolean mask of inliers classified as ``True``. + + References + ---------- + .. [1] "RANSAC", Wikipedia, https://en.wikipedia.org/wiki/RANSAC + + Examples + -------- + + Generate ellipse data without tilt and add noise: + + >>> t = np.linspace(0, 2 * np.pi, 50) + >>> xc, yc = 20, 30 + >>> a, b = 5, 10 + >>> x = xc + a * np.cos(t) + >>> y = yc + b * np.sin(t) + >>> data = np.column_stack([x, y]) + >>> rng = np.random.default_rng(203560) # do not copy this value + >>> data += rng.normal(size=data.shape) + + Add some faulty data: + + >>> data[0] = (100, 100) + >>> data[1] = (110, 120) + >>> data[2] = (120, 130) + >>> data[3] = (140, 130) + + Estimate ellipse model using all available data: + + >>> model = EllipseModel() + >>> model.estimate(data) + True + >>> np.round(model.params) # doctest: +SKIP + array([ 72., 75., 77., 14., 1.]) + + Estimate ellipse model using RANSAC: + + >>> ransac_model, inliers = ransac(data, EllipseModel, 20, 3, max_trials=50) + >>> abs(np.round(ransac_model.params)) # doctest: +SKIP + array([20., 30., 10., 6., 2.]) + >>> inliers # doctest: +SKIP + array([False, False, False, False, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True], dtype=bool) + >>> sum(inliers) > 40 + True + + RANSAC can be used to robustly estimate a geometric + transformation. In this section, we also show how to use a + proportion of the total samples, rather than an absolute number. + + >>> from skimage.transform import SimilarityTransform + >>> rng = np.random.default_rng() + >>> src = 100 * rng.random((50, 2)) + >>> model0 = SimilarityTransform(scale=0.5, rotation=1, + ... translation=(10, 20)) + >>> dst = model0(src) + >>> dst[0] = (10000, 10000) + >>> dst[1] = (-100, 100) + >>> dst[2] = (50, 50) + >>> ratio = 0.5 # use half of the samples + >>> min_samples = int(ratio * len(src)) + >>> model, inliers = ransac( + ... (src, dst), + ... SimilarityTransform, + ... min_samples, + ... 10, + ... initial_inliers=np.ones(len(src), dtype=bool), + ... ) # doctest: +SKIP + >>> inliers # doctest: +SKIP + array([False, False, False, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True, True, True, True, True, + True, True, True, True, True]) + + """ + + best_inlier_num = 0 + best_inlier_residuals_sum = np.inf + best_inliers = [] + validate_model = is_model_valid is not None + validate_data = is_data_valid is not None + + rng = np.random.default_rng(rng) + + # in case data is not pair of input and output, male it like it + if not isinstance(data, (tuple, list)): + data = (data,) + num_samples = len(data[0]) + + if not (0 < min_samples <= num_samples): + raise ValueError(f"`min_samples` must be in range (0, {num_samples}]") + + if residual_threshold < 0: + raise ValueError("`residual_threshold` must be greater than zero") + + if max_trials < 0: + raise ValueError("`max_trials` must be greater than zero") + + if not (0 <= stop_probability <= 1): + raise ValueError("`stop_probability` must be in range [0, 1]") + + if initial_inliers is not None and len(initial_inliers) != num_samples: + raise ValueError( + f"RANSAC received a vector of initial inliers (length " + f"{len(initial_inliers)}) that didn't match the number of " + f"samples ({num_samples}). The vector of initial inliers should " + f"have the same length as the number of samples and contain only " + f"True (this sample is an initial inlier) and False (this one " + f"isn't) values." + ) + + # for the first run use initial guess of inliers + spl_idxs = ( + initial_inliers + if initial_inliers is not None + else rng.choice(num_samples, min_samples, replace=False) + ) + + # estimate model for current random sample set + model = model_class() + + num_trials = 0 + # max_trials can be updated inside the loop, so this cannot be a for-loop + while num_trials < max_trials: + num_trials += 1 + + # do sample selection according data pairs + samples = [d[spl_idxs] for d in data] + + # for next iteration choose random sample set and be sure that + # no samples repeat + spl_idxs = rng.choice(num_samples, min_samples, replace=False) + + # optional check if random sample set is valid + if validate_data and not is_data_valid(*samples): + continue + + success = model.estimate(*samples) + # backwards compatibility + if success is not None and not success: + continue + + # optional check if estimated model is valid + if validate_model and not is_model_valid(model, *samples): + continue + + residuals = np.abs(model.residuals(*data)) + # consensus set / inliers + inliers = residuals < residual_threshold + residuals_sum = residuals.dot(residuals) + + # choose as new best model if number of inliers is maximal + inliers_count = np.count_nonzero(inliers) + if ( + # more inliers + inliers_count > best_inlier_num + # same number of inliers but less "error" in terms of residuals + or ( + inliers_count == best_inlier_num + and residuals_sum < best_inlier_residuals_sum + ) + ): + best_inlier_num = inliers_count + best_inlier_residuals_sum = residuals_sum + best_inliers = inliers + max_trials = min( + max_trials, + _dynamic_max_trials( + best_inlier_num, num_samples, min_samples, stop_probability + ), + ) + if ( + best_inlier_num >= stop_sample_num + or best_inlier_residuals_sum <= stop_residuals_sum + ): + break + + # estimate final model using all inliers + if any(best_inliers): + # select inliers for each data array + data_inliers = [d[best_inliers] for d in data] + model.estimate(*data_inliers) + if validate_model and not is_model_valid(model, *data_inliers): + warn("Estimated model is not valid. Try increasing max_trials.") + else: + model = None + best_inliers = None + warn("No inliers found. Model not fitted") + + return model, best_inliers diff --git a/lib/python3.10/site-packages/skimage/measure/pnpoly.py b/lib/python3.10/site-packages/skimage/measure/pnpoly.py new file mode 100644 index 0000000000000000000000000000000000000000..610efa4ff5851e5e84c74e27247332158a0b063c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/pnpoly.py @@ -0,0 +1,67 @@ +from ._pnpoly import _grid_points_in_poly, _points_in_poly + + +def grid_points_in_poly(shape, verts, binarize=True): + """Test whether points on a specified grid are inside a polygon. + + For each ``(r, c)`` coordinate on a grid, i.e. ``(0, 0)``, ``(0, 1)`` etc., + test whether that point lies inside a polygon. + + You can control the output type with the `binarize` flag. Please refer to its + documentation for further details. + + Parameters + ---------- + shape : tuple (M, N) + Shape of the grid. + verts : (V, 2) array + Specify the V vertices of the polygon, sorted either clockwise + or anti-clockwise. The first point may (but does not need to be) + duplicated. + binarize: bool + If `True`, the output of the function is a boolean mask. + Otherwise, it is a labeled array. The labels are: + O - outside, 1 - inside, 2 - vertex, 3 - edge. + + See Also + -------- + points_in_poly + + Returns + ------- + mask : (M, N) ndarray + If `binarize` is True, the output is a boolean mask. True means the + corresponding pixel falls inside the polygon. + If `binarize` is False, the output is a labeled array, with pixels + having a label between 0 and 3. The meaning of the values is: + O - outside, 1 - inside, 2 - vertex, 3 - edge. + + """ + output = _grid_points_in_poly(shape, verts) + if binarize: + output = output.astype(bool) + return output + + +def points_in_poly(points, verts): + """Test whether points lie inside a polygon. + + Parameters + ---------- + points : (K, 2) array + Input points, ``(x, y)``. + verts : (L, 2) array + Vertices of the polygon, sorted either clockwise or anti-clockwise. + The first point may (but does not need to be) duplicated. + + See Also + -------- + grid_points_in_poly + + Returns + ------- + mask : (K,) array of bool + True if corresponding point is inside the polygon. + + """ + return _points_in_poly(points, verts) diff --git a/lib/python3.10/site-packages/skimage/measure/profile.py b/lib/python3.10/site-packages/skimage/measure/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..246689553d94f33c36f141ebdf143d1bd0bdaf32 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/measure/profile.py @@ -0,0 +1,190 @@ +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import _validate_interpolation_order, _fix_ndimage_mode + + +def profile_line( + image, + src, + dst, + linewidth=1, + order=None, + mode='reflect', + cval=0.0, + *, + reduce_func=np.mean, +): + """Return the intensity profile of an image measured along a scan line. + + Parameters + ---------- + image : ndarray, shape (M, N[, C]) + The image, either grayscale (2D array) or multichannel + (3D array, where the final axis contains the channel + information). + src : array_like, shape (2,) + The coordinates of the start point of the scan line. + dst : array_like, shape (2,) + The coordinates of the end point of the scan + line. The destination point is *included* in the profile, in + contrast to standard numpy indexing. + linewidth : int, optional + Width of the scan, perpendicular to the line + order : int in {0, 1, 2, 3, 4, 5}, optional + The order of the spline interpolation, default is 0 if + image.dtype is bool and 1 otherwise. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + mode : {'constant', 'nearest', 'reflect', 'mirror', 'wrap'}, optional + How to compute any values falling outside of the image. + cval : float, optional + If `mode` is 'constant', what constant value to use outside the image. + reduce_func : callable, optional + Function used to calculate the aggregation of pixel values + perpendicular to the profile_line direction when `linewidth` > 1. + If set to None the unreduced array will be returned. + + Returns + ------- + return_value : array + The intensity profile along the scan line. The length of the profile + is the ceil of the computed length of the scan line. + + Examples + -------- + >>> x = np.array([[1, 1, 1, 2, 2, 2]]) + >>> img = np.vstack([np.zeros_like(x), x, x, x, np.zeros_like(x)]) + >>> img + array([[0, 0, 0, 0, 0, 0], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [0, 0, 0, 0, 0, 0]]) + >>> profile_line(img, (2, 1), (2, 4)) + array([1., 1., 2., 2.]) + >>> profile_line(img, (1, 0), (1, 6), cval=4) + array([1., 1., 1., 2., 2., 2., 2.]) + + The destination point is included in the profile, in contrast to + standard numpy indexing. + For example: + + >>> profile_line(img, (1, 0), (1, 6)) # The final point is out of bounds + array([1., 1., 1., 2., 2., 2., 2.]) + >>> profile_line(img, (1, 0), (1, 5)) # This accesses the full first row + array([1., 1., 1., 2., 2., 2.]) + + For different reduce_func inputs: + + >>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.mean) + array([0.66666667, 0.66666667, 0.66666667, 1.33333333]) + >>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.max) + array([1, 1, 1, 2]) + >>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.sum) + array([2, 2, 2, 4]) + + The unreduced array will be returned when `reduce_func` is None or when + `reduce_func` acts on each pixel value individually. + + >>> profile_line(img, (1, 2), (4, 2), linewidth=3, order=0, + ... reduce_func=None) + array([[1, 1, 2], + [1, 1, 2], + [1, 1, 2], + [0, 0, 0]]) + >>> profile_line(img, (1, 0), (1, 3), linewidth=3, reduce_func=np.sqrt) + array([[1. , 1. , 0. ], + [1. , 1. , 0. ], + [1. , 1. , 0. ], + [1.41421356, 1.41421356, 0. ]]) + """ + + order = _validate_interpolation_order(image.dtype, order) + mode = _fix_ndimage_mode(mode) + + perp_lines = _line_profile_coordinates(src, dst, linewidth=linewidth) + if image.ndim == 3: + pixels = [ + ndi.map_coordinates( + image[..., i], + perp_lines, + prefilter=order > 1, + order=order, + mode=mode, + cval=cval, + ) + for i in range(image.shape[2]) + ] + pixels = np.transpose(np.asarray(pixels), (1, 2, 0)) + else: + pixels = ndi.map_coordinates( + image, perp_lines, prefilter=order > 1, order=order, mode=mode, cval=cval + ) + # The outputted array with reduce_func=None gives an array where the + # row values (axis=1) are flipped. Here, we make this consistent. + pixels = np.flip(pixels, axis=1) + + if reduce_func is None: + intensities = pixels + else: + try: + intensities = reduce_func(pixels, axis=1) + except TypeError: # function doesn't allow axis kwarg + intensities = np.apply_along_axis(reduce_func, arr=pixels, axis=1) + + return intensities + + +def _line_profile_coordinates(src, dst, linewidth=1): + """Return the coordinates of the profile of an image along a scan line. + + Parameters + ---------- + src : 2-tuple of numeric scalar (float or int) + The start point of the scan line. + dst : 2-tuple of numeric scalar (float or int) + The end point of the scan line. + linewidth : int, optional + Width of the scan, perpendicular to the line + + Returns + ------- + coords : array, shape (2, N, C), float + The coordinates of the profile along the scan line. The length of the + profile is the ceil of the computed length of the scan line. + + Notes + ----- + This is a utility method meant to be used internally by skimage functions. + The destination point is included in the profile, in contrast to + standard numpy indexing. + """ + src_row, src_col = src = np.asarray(src, dtype=float) + dst_row, dst_col = dst = np.asarray(dst, dtype=float) + d_row, d_col = dst - src + theta = np.arctan2(d_row, d_col) + + length = int(np.ceil(np.hypot(d_row, d_col) + 1)) + # we add one above because we include the last point in the profile + # (in contrast to standard numpy indexing) + line_col = np.linspace(src_col, dst_col, length) + line_row = np.linspace(src_row, dst_row, length) + + # we subtract 1 from linewidth to change from pixel-counting + # (make this line 3 pixels wide) to point distances (the + # distance between pixel centers) + col_width = (linewidth - 1) * np.sin(-theta) / 2 + row_width = (linewidth - 1) * np.cos(theta) / 2 + perp_rows = np.stack( + [ + np.linspace(row_i - row_width, row_i + row_width, linewidth) + for row_i in line_row + ] + ) + perp_cols = np.stack( + [ + np.linspace(col_i - col_width, col_i + col_width, linewidth) + for col_i in line_col + ] + ) + return np.stack([perp_rows, perp_cols]) diff --git a/lib/python3.10/site-packages/skimage/metrics/__init__.py b/lib/python3.10/site-packages/skimage/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0ea6126c7dff9565c105cb3eb36408b8f9de38e1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/__init__.py @@ -0,0 +1,5 @@ +"""Metrics corresponding to images, e.g., distance metrics, similarity, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/metrics/__init__.pyi b/lib/python3.10/site-packages/skimage/metrics/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..f47fa2b97737e14e956a0c93f53b5ddc1cea0270 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/__init__.pyi @@ -0,0 +1,28 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + "adapted_rand_error", + "variation_of_information", + "contingency_table", + "mean_squared_error", + "normalized_mutual_information", + "normalized_root_mse", + "peak_signal_noise_ratio", + "structural_similarity", + "hausdorff_distance", + "hausdorff_pair", +] + +from ._adapted_rand_error import adapted_rand_error +from ._contingency_table import contingency_table +from ._structural_similarity import structural_similarity +from ._variation_of_information import variation_of_information +from .set_metrics import hausdorff_distance, hausdorff_pair +from .simple_metrics import ( + mean_squared_error, + normalized_mutual_information, + normalized_root_mse, + peak_signal_noise_ratio, +) diff --git a/lib/python3.10/site-packages/skimage/metrics/_adapted_rand_error.py b/lib/python3.10/site-packages/skimage/metrics/_adapted_rand_error.py new file mode 100644 index 0000000000000000000000000000000000000000..c880a14f661cb562aa687b29ac9dde6b2e259c5c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/_adapted_rand_error.py @@ -0,0 +1,103 @@ +from .._shared.utils import check_shape_equality +from ._contingency_table import contingency_table + +__all__ = ['adapted_rand_error'] + + +def adapted_rand_error( + image_true=None, image_test=None, *, table=None, ignore_labels=(0,), alpha=0.5 +): + r"""Compute Adapted Rand error as defined by the SNEMI3D contest. [1]_ + + Parameters + ---------- + image_true : ndarray of int + Ground-truth label image, same shape as im_test. + image_test : ndarray of int + Test image. + table : scipy.sparse array in crs format, optional + A contingency table built with skimage.evaluate.contingency_table. + If None, it will be computed on the fly. + ignore_labels : sequence of int, optional + Labels to ignore. Any part of the true image labeled with any of these + values will not be counted in the score. + alpha : float, optional + Relative weight given to precision and recall in the adapted Rand error + calculation. + + Returns + ------- + are : float + The adapted Rand error. + prec : float + The adapted Rand precision: this is the number of pairs of pixels that + have the same label in the test label image *and* in the true image, + divided by the number in the test image. + rec : float + The adapted Rand recall: this is the number of pairs of pixels that + have the same label in the test label image *and* in the true image, + divided by the number in the true image. + + Notes + ----- + Pixels with label 0 in the true segmentation are ignored in the score. + + The adapted Rand error is calculated as follows: + + :math:`1 - \frac{\sum_{ij} p_{ij}^{2}}{\alpha \sum_{k} s_{k}^{2} + + (1-\alpha)\sum_{k} t_{k}^{2}}`, + where :math:`p_{ij}` is the probability that a pixel has the same label + in the test image *and* in the true image, :math:`t_{k}` is the + probability that a pixel has label :math:`k` in the true image, + and :math:`s_{k}` is the probability that a pixel has label :math:`k` + in the test image. + + Default behavior is to weight precision and recall equally in the + adapted Rand error calculation. + When alpha = 0, adapted Rand error = recall. + When alpha = 1, adapted Rand error = precision. + + + References + ---------- + .. [1] Arganda-Carreras I, Turaga SC, Berger DR, et al. (2015) + Crowdsourcing the creation of image segmentation algorithms + for connectomics. Front. Neuroanat. 9:142. + :DOI:`10.3389/fnana.2015.00142` + """ + if image_test is not None and image_true is not None: + check_shape_equality(image_true, image_test) + + if table is None: + p_ij = contingency_table( + image_true, + image_test, + ignore_labels=ignore_labels, + normalize=False, + sparse_type="array", + ) + else: + p_ij = table + + if alpha < 0.0 or alpha > 1.0: + raise ValueError('alpha must be between 0 and 1') + + # Sum of the joint distribution squared + sum_p_ij2 = p_ij.data @ p_ij.data - p_ij.sum() + + a_i = p_ij.sum(axis=1).ravel() + b_i = p_ij.sum(axis=0).ravel() + + # Sum of squares of the test segment sizes (this is 2x the number of pairs + # of pixels with the same label in im_test) + sum_a2 = a_i @ a_i - a_i.sum() + # Same for im_true + sum_b2 = b_i @ b_i - b_i.sum() + + precision = sum_p_ij2 / sum_a2 + recall = sum_p_ij2 / sum_b2 + + fscore = sum_p_ij2 / (alpha * sum_a2 + (1 - alpha) * sum_b2) + are = 1.0 - fscore + + return are, precision, recall diff --git a/lib/python3.10/site-packages/skimage/metrics/_contingency_table.py b/lib/python3.10/site-packages/skimage/metrics/_contingency_table.py new file mode 100644 index 0000000000000000000000000000000000000000..85860a09044a0128f616b6e9e2db766f4966ae23 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/_contingency_table.py @@ -0,0 +1,51 @@ +import scipy.sparse as sparse +import numpy as np + +__all__ = ['contingency_table'] + + +def contingency_table( + im_true, im_test, *, ignore_labels=None, normalize=False, sparse_type="matrix" +): + """ + Return the contingency table for all regions in matched segmentations. + + Parameters + ---------- + im_true : ndarray of int + Ground-truth label image, same shape as im_test. + im_test : ndarray of int + Test image. + ignore_labels : sequence of int, optional + Labels to ignore. Any part of the true image labeled with any of these + values will not be counted in the score. + normalize : bool + Determines if the contingency table is normalized by pixel count. + sparse_type : {"matrix", "array"}, optional + The return type of `cont`, either `scipy.sparse.csr_array` or + `scipy.sparse.csr_matrix` (default). + + Returns + ------- + cont : scipy.sparse.csr_matrix or scipy.sparse.csr_array + A contingency table. `cont[i, j]` will equal the number of voxels + labeled `i` in `im_true` and `j` in `im_test`. Depending on `sparse_type`, + this can be returned as a `scipy.sparse.csr_array`. + """ + + if ignore_labels is None: + ignore_labels = [] + im_test_r = im_test.reshape(-1) + im_true_r = im_true.reshape(-1) + data = np.isin(im_true_r, ignore_labels, invert=True).astype(float) + if normalize: + data /= np.count_nonzero(data) + cont = sparse.csr_array((data, (im_true_r, im_test_r))) + + if sparse_type == "matrix": + cont = sparse.csr_matrix(cont) + elif sparse_type != "array": + msg = f"`sparse_type` must be 'array' or 'matrix', got {sparse_type}" + raise ValueError(msg) + + return cont diff --git a/lib/python3.10/site-packages/skimage/metrics/_structural_similarity.py b/lib/python3.10/site-packages/skimage/metrics/_structural_similarity.py new file mode 100644 index 0000000000000000000000000000000000000000..e17d39330c79884a841120912d3a9f3c0b8a0ffc --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/_structural_similarity.py @@ -0,0 +1,292 @@ +import functools + +import numpy as np +from scipy.ndimage import uniform_filter + +from .._shared import utils +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type, check_shape_equality, warn +from ..util.arraycrop import crop +from ..util.dtype import dtype_range + +__all__ = ['structural_similarity'] + + +def structural_similarity( + im1, + im2, + *, + win_size=None, + gradient=False, + data_range=None, + channel_axis=None, + gaussian_weights=False, + full=False, + **kwargs, +): + """ + Compute the mean structural similarity index between two images. + Please pay attention to the `data_range` parameter with floating-point images. + + Parameters + ---------- + im1, im2 : ndarray + Images. Any dimensionality with same shape. + win_size : int or None, optional + The side-length of the sliding window used in comparison. Must be an + odd value. If `gaussian_weights` is True, this is ignored and the + window size will depend on `sigma`. + gradient : bool, optional + If True, also return the gradient with respect to im2. + data_range : float, optional + The data range of the input image (difference between maximum and + minimum possible values). By default, this is estimated from the image + data type. This estimate may be wrong for floating-point image data. + Therefore it is recommended to always pass this scalar value explicitly + (see note below). + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + gaussian_weights : bool, optional + If True, each patch has its mean and variance spatially weighted by a + normalized Gaussian kernel of width sigma=1.5. + full : bool, optional + If True, also return the full structural similarity image. + + Other Parameters + ---------------- + use_sample_covariance : bool + If True, normalize covariances by N-1 rather than, N where N is the + number of pixels within the sliding window. + K1 : float + Algorithm parameter, K1 (small constant, see [1]_). + K2 : float + Algorithm parameter, K2 (small constant, see [1]_). + sigma : float + Standard deviation for the Gaussian when `gaussian_weights` is True. + + Returns + ------- + mssim : float + The mean structural similarity index over the image. + grad : ndarray + The gradient of the structural similarity between im1 and im2 [2]_. + This is only returned if `gradient` is set to True. + S : ndarray + The full SSIM image. This is only returned if `full` is set to True. + + Notes + ----- + If `data_range` is not specified, the range is automatically guessed + based on the image data type. However for floating-point image data, this + estimate yields a result double the value of the desired range, as the + `dtype_range` in `skimage.util.dtype.py` has defined intervals from -1 to + +1. This yields an estimate of 2, instead of 1, which is most often + required when working with image data (as negative light intensities are + nonsensical). In case of working with YCbCr-like color data, note that + these ranges are different per channel (Cb and Cr have double the range + of Y), so one cannot calculate a channel-averaged SSIM with a single call + to this function, as identical ranges are assumed for each channel. + + To match the implementation of Wang et al. [1]_, set `gaussian_weights` + to True, `sigma` to 1.5, `use_sample_covariance` to False, and + specify the `data_range` argument. + + .. versionchanged:: 0.16 + This function was renamed from ``skimage.measure.compare_ssim`` to + ``skimage.metrics.structural_similarity``. + + References + ---------- + .. [1] Wang, Z., Bovik, A. C., Sheikh, H. R., & Simoncelli, E. P. + (2004). Image quality assessment: From error visibility to + structural similarity. IEEE Transactions on Image Processing, + 13, 600-612. + https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf, + :DOI:`10.1109/TIP.2003.819861` + + .. [2] Avanaki, A. N. (2009). Exact global histogram specification + optimized for structural similarity. Optical Review, 16, 613-621. + :arxiv:`0901.0065` + :DOI:`10.1007/s10043-009-0119-z` + + """ + check_shape_equality(im1, im2) + float_type = _supported_float_type(im1.dtype) + + if channel_axis is not None: + # loop over channels + args = dict( + win_size=win_size, + gradient=gradient, + data_range=data_range, + channel_axis=None, + gaussian_weights=gaussian_weights, + full=full, + ) + args.update(kwargs) + nch = im1.shape[channel_axis] + mssim = np.empty(nch, dtype=float_type) + + if gradient: + G = np.empty(im1.shape, dtype=float_type) + if full: + S = np.empty(im1.shape, dtype=float_type) + channel_axis = channel_axis % im1.ndim + _at = functools.partial(utils.slice_at_axis, axis=channel_axis) + for ch in range(nch): + ch_result = structural_similarity(im1[_at(ch)], im2[_at(ch)], **args) + if gradient and full: + mssim[ch], G[_at(ch)], S[_at(ch)] = ch_result + elif gradient: + mssim[ch], G[_at(ch)] = ch_result + elif full: + mssim[ch], S[_at(ch)] = ch_result + else: + mssim[ch] = ch_result + mssim = mssim.mean() + if gradient and full: + return mssim, G, S + elif gradient: + return mssim, G + elif full: + return mssim, S + else: + return mssim + + K1 = kwargs.pop('K1', 0.01) + K2 = kwargs.pop('K2', 0.03) + sigma = kwargs.pop('sigma', 1.5) + if K1 < 0: + raise ValueError("K1 must be positive") + if K2 < 0: + raise ValueError("K2 must be positive") + if sigma < 0: + raise ValueError("sigma must be positive") + use_sample_covariance = kwargs.pop('use_sample_covariance', True) + + if gaussian_weights: + # Set to give an 11-tap filter with the default sigma of 1.5 to match + # Wang et. al. 2004. + truncate = 3.5 + + if win_size is None: + if gaussian_weights: + # set win_size used by crop to match the filter size + r = int(truncate * sigma + 0.5) # radius as in ndimage + win_size = 2 * r + 1 + else: + win_size = 7 # backwards compatibility + + if np.any((np.asarray(im1.shape) - win_size) < 0): + raise ValueError( + 'win_size exceeds image extent. ' + 'Either ensure that your images are ' + 'at least 7x7; or pass win_size explicitly ' + 'in the function call, with an odd value ' + 'less than or equal to the smaller side of your ' + 'images. If your images are multichannel ' + '(with color channels), set channel_axis to ' + 'the axis number corresponding to the channels.' + ) + + if not (win_size % 2 == 1): + raise ValueError('Window size must be odd.') + + if data_range is None: + if np.issubdtype(im1.dtype, np.floating) or np.issubdtype( + im2.dtype, np.floating + ): + raise ValueError( + 'Since image dtype is floating point, you must specify ' + 'the data_range parameter. Please read the documentation ' + 'carefully (including the note). It is recommended that ' + 'you always specify the data_range anyway.' + ) + if im1.dtype != im2.dtype: + warn( + "Inputs have mismatched dtypes. Setting data_range based on im1.dtype.", + stacklevel=2, + ) + dmin, dmax = dtype_range[im1.dtype.type] + data_range = dmax - dmin + if np.issubdtype(im1.dtype, np.integer) and (im1.dtype != np.uint8): + warn( + "Setting data_range based on im1.dtype. " + + f"data_range = {data_range:.0f}. " + + "Please specify data_range explicitly to avoid mistakes.", + stacklevel=2, + ) + + ndim = im1.ndim + + if gaussian_weights: + filter_func = gaussian + filter_args = {'sigma': sigma, 'truncate': truncate, 'mode': 'reflect'} + else: + filter_func = uniform_filter + filter_args = {'size': win_size} + + # ndimage filters need floating point data + im1 = im1.astype(float_type, copy=False) + im2 = im2.astype(float_type, copy=False) + + NP = win_size**ndim + + # filter has already normalized by NP + if use_sample_covariance: + cov_norm = NP / (NP - 1) # sample covariance + else: + cov_norm = 1.0 # population covariance to match Wang et. al. 2004 + + # compute (weighted) means + ux = filter_func(im1, **filter_args) + uy = filter_func(im2, **filter_args) + + # compute (weighted) variances and covariances + uxx = filter_func(im1 * im1, **filter_args) + uyy = filter_func(im2 * im2, **filter_args) + uxy = filter_func(im1 * im2, **filter_args) + vx = cov_norm * (uxx - ux * ux) + vy = cov_norm * (uyy - uy * uy) + vxy = cov_norm * (uxy - ux * uy) + + R = data_range + C1 = (K1 * R) ** 2 + C2 = (K2 * R) ** 2 + + A1, A2, B1, B2 = ( + 2 * ux * uy + C1, + 2 * vxy + C2, + ux**2 + uy**2 + C1, + vx + vy + C2, + ) + D = B1 * B2 + S = (A1 * A2) / D + + # to avoid edge effects will ignore filter radius strip around edges + pad = (win_size - 1) // 2 + + # compute (weighted) mean of ssim. Use float64 for accuracy. + mssim = crop(S, pad).mean(dtype=np.float64) + + if gradient: + # The following is Eqs. 7-8 of Avanaki 2009. + grad = filter_func(A1 / D, **filter_args) * im1 + grad += filter_func(-S / B2, **filter_args) * im2 + grad += filter_func((ux * (A2 - A1) - uy * (B2 - B1) * S) / D, **filter_args) + grad *= 2 / im1.size + + if full: + return mssim, grad, S + else: + return mssim, grad + else: + if full: + return mssim, S + else: + return mssim diff --git a/lib/python3.10/site-packages/skimage/metrics/_variation_of_information.py b/lib/python3.10/site-packages/skimage/metrics/_variation_of_information.py new file mode 100644 index 0000000000000000000000000000000000000000..e2c546fcb739c4d9a479df778e443241cc66f1ed --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/_variation_of_information.py @@ -0,0 +1,133 @@ +import numpy as np +import scipy.sparse as sparse +from ._contingency_table import contingency_table +from .._shared.utils import check_shape_equality + +__all__ = ['variation_of_information'] + + +def variation_of_information(image0=None, image1=None, *, table=None, ignore_labels=()): + """Return symmetric conditional entropies associated with the VI. [1]_ + + The variation of information is defined as VI(X,Y) = H(X|Y) + H(Y|X). + If X is the ground-truth segmentation, then H(X|Y) can be interpreted + as the amount of under-segmentation and H(Y|X) as the amount + of over-segmentation. In other words, a perfect over-segmentation + will have H(X|Y)=0 and a perfect under-segmentation will have H(Y|X)=0. + + Parameters + ---------- + image0, image1 : ndarray of int + Label images / segmentations, must have same shape. + table : scipy.sparse array in csr format, optional + A contingency table built with skimage.evaluate.contingency_table. + If None, it will be computed with skimage.evaluate.contingency_table. + If given, the entropies will be computed from this table and any images + will be ignored. + ignore_labels : sequence of int, optional + Labels to ignore. Any part of the true image labeled with any of these + values will not be counted in the score. + + Returns + ------- + vi : ndarray of float, shape (2,) + The conditional entropies of image1|image0 and image0|image1. + + References + ---------- + .. [1] Marina Meilă (2007), Comparing clusterings—an information based + distance, Journal of Multivariate Analysis, Volume 98, Issue 5, + Pages 873-895, ISSN 0047-259X, :DOI:`10.1016/j.jmva.2006.11.013`. + """ + h0g1, h1g0 = _vi_tables(image0, image1, table=table, ignore_labels=ignore_labels) + # false splits, false merges + return np.array([h1g0.sum(), h0g1.sum()]) + + +def _xlogx(x): + """Compute x * log_2(x). + + We define 0 * log_2(0) = 0 + + Parameters + ---------- + x : ndarray or scipy.sparse.csc_array or scipy.sparse.csr_array + The input array. + + Returns + ------- + y : same type as x + Result of x * log_2(x). + """ + y = x.copy() + if sparse.issparse(y) and y.format in ('csc', 'csr'): + z = y.data + else: + z = np.asarray(y) # ensure np.matrix converted to np.array + nz = z.nonzero() + z[nz] *= np.log2(z[nz]) + return y + + +def _vi_tables(im_true, im_test, table=None, ignore_labels=()): + """Compute probability tables used for calculating VI. + + Parameters + ---------- + im_true, im_test : ndarray of int + Input label images, any dimensionality. + table : csr_array, optional + Pre-computed contingency table. + ignore_labels : sequence of int, optional + Labels to ignore when computing scores. + + Returns + ------- + hxgy, hygx : ndarray of float + Per-segment conditional entropies of ``im_true`` given ``im_test`` and + vice-versa. + """ + check_shape_equality(im_true, im_test) + + if table is None: + # normalize, since it is an identity op if already done + pxy = contingency_table( + im_true, im_test, ignore_labels=ignore_labels, normalize=True + ) + + else: + pxy = table + + # compute marginal probabilities, converting to 1D array + px = np.ravel(pxy.sum(axis=1)) + py = np.ravel(pxy.sum(axis=0)) + + # use sparse matrix linear algebra to compute VI + # first, compute the inverse diagonal matrices + px_inv = sparse.dia_array((_invert_nonzero(px), 0), shape=(px.size, px.size)) + py_inv = sparse.dia_array((_invert_nonzero(py), 0), shape=(py.size, py.size)) + + # then, compute the entropies + hygx = -px @ _xlogx(px_inv @ pxy).sum(axis=1) + hxgy = -_xlogx(pxy @ py_inv).sum(axis=0) @ py + + return list(map(np.asarray, [hxgy, hygx])) + + +def _invert_nonzero(arr): + """Compute the inverse of the non-zero elements of arr, not changing 0. + + Parameters + ---------- + arr : ndarray + + Returns + ------- + arr_inv : ndarray + Array containing the inverse of the non-zero elements of arr, and + zero elsewhere. + """ + arr_inv = arr.copy() + nz = np.nonzero(arr) + arr_inv[nz] = 1 / arr[nz] + return arr_inv diff --git a/lib/python3.10/site-packages/skimage/metrics/set_metrics.py b/lib/python3.10/site-packages/skimage/metrics/set_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..d2eb829b234f54087f9ba60d12579688ce9a1a77 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/set_metrics.py @@ -0,0 +1,147 @@ +import warnings + +import numpy as np +from scipy.spatial import cKDTree + + +def hausdorff_distance(image0, image1, method="standard"): + """Calculate the Hausdorff distance between nonzero elements of given images. + + Parameters + ---------- + image0, image1 : ndarray + Arrays where ``True`` represents a point that is included in a + set of points. Both arrays must have the same shape. + method : {'standard', 'modified'}, optional, default = 'standard' + The method to use for calculating the Hausdorff distance. + ``standard`` is the standard Hausdorff distance, while ``modified`` + is the modified Hausdorff distance. + + Returns + ------- + distance : float + The Hausdorff distance between coordinates of nonzero pixels in + ``image0`` and ``image1``, using the Euclidean distance. + + Notes + ----- + The Hausdorff distance [1]_ is the maximum distance between any point on + ``image0`` and its nearest point on ``image1``, and vice-versa. + The Modified Hausdorff Distance (MHD) has been shown to perform better + than the directed Hausdorff Distance (HD) in the following work by + Dubuisson et al. [2]_. The function calculates forward and backward + mean distances and returns the largest of the two. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Hausdorff_distance + .. [2] M. P. Dubuisson and A. K. Jain. A Modified Hausdorff distance for object + matching. In ICPR94, pages A:566-568, Jerusalem, Israel, 1994. + :DOI:`10.1109/ICPR.1994.576361` + http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.1.8155 + + Examples + -------- + >>> points_a = (3, 0) + >>> points_b = (6, 0) + >>> shape = (7, 1) + >>> image_a = np.zeros(shape, dtype=bool) + >>> image_b = np.zeros(shape, dtype=bool) + >>> image_a[points_a] = True + >>> image_b[points_b] = True + >>> hausdorff_distance(image_a, image_b) + 3.0 + + """ + + if method not in ('standard', 'modified'): + raise ValueError(f'unrecognized method {method}') + + a_points = np.transpose(np.nonzero(image0)) + b_points = np.transpose(np.nonzero(image1)) + + # Handle empty sets properly: + # - if both sets are empty, return zero + # - if only one set is empty, return infinity + if len(a_points) == 0: + return 0 if len(b_points) == 0 else np.inf + elif len(b_points) == 0: + return np.inf + + fwd, bwd = ( + cKDTree(a_points).query(b_points, k=1)[0], + cKDTree(b_points).query(a_points, k=1)[0], + ) + + if method == 'standard': # standard Hausdorff distance + return max(max(fwd), max(bwd)) + elif method == 'modified': # modified Hausdorff distance + return max(np.mean(fwd), np.mean(bwd)) + + +def hausdorff_pair(image0, image1): + """Returns pair of points that are Hausdorff distance apart between nonzero + elements of given images. + + The Hausdorff distance [1]_ is the maximum distance between any point on + ``image0`` and its nearest point on ``image1``, and vice-versa. + + Parameters + ---------- + image0, image1 : ndarray + Arrays where ``True`` represents a point that is included in a + set of points. Both arrays must have the same shape. + + Returns + ------- + point_a, point_b : array + A pair of points that have Hausdorff distance between them. + + References + ---------- + .. [1] http://en.wikipedia.org/wiki/Hausdorff_distance + + Examples + -------- + >>> points_a = (3, 0) + >>> points_b = (6, 0) + >>> shape = (7, 1) + >>> image_a = np.zeros(shape, dtype=bool) + >>> image_b = np.zeros(shape, dtype=bool) + >>> image_a[points_a] = True + >>> image_b[points_b] = True + >>> hausdorff_pair(image_a, image_b) + (array([3, 0]), array([6, 0])) + + """ + a_points = np.transpose(np.nonzero(image0)) + b_points = np.transpose(np.nonzero(image1)) + + # If either of the sets are empty, there is no corresponding pair of points + if len(a_points) == 0 or len(b_points) == 0: + warnings.warn("One or both of the images is empty.", stacklevel=2) + return (), () + + nearest_dists_from_b, nearest_a_point_indices_from_b = cKDTree(a_points).query( + b_points + ) + nearest_dists_from_a, nearest_b_point_indices_from_a = cKDTree(b_points).query( + a_points + ) + + max_index_from_a = nearest_dists_from_b.argmax() + max_index_from_b = nearest_dists_from_a.argmax() + + max_dist_from_a = nearest_dists_from_b[max_index_from_a] + max_dist_from_b = nearest_dists_from_a[max_index_from_b] + + if max_dist_from_b > max_dist_from_a: + return ( + a_points[max_index_from_b], + b_points[nearest_b_point_indices_from_a[max_index_from_b]], + ) + else: + return ( + a_points[nearest_a_point_indices_from_b[max_index_from_a]], + b_points[max_index_from_a], + ) diff --git a/lib/python3.10/site-packages/skimage/metrics/simple_metrics.py b/lib/python3.10/site-packages/skimage/metrics/simple_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..7bb13acf18915b29f5e5298f3ce1c1ebaf4d9de7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/metrics/simple_metrics.py @@ -0,0 +1,270 @@ +import numpy as np +from scipy.stats import entropy + +from ..util.dtype import dtype_range +from .._shared.utils import _supported_float_type, check_shape_equality, warn + +__all__ = [ + 'mean_squared_error', + 'normalized_root_mse', + 'peak_signal_noise_ratio', + 'normalized_mutual_information', +] + + +def _as_floats(image0, image1): + """ + Promote im1, im2 to nearest appropriate floating point precision. + """ + float_type = _supported_float_type((image0.dtype, image1.dtype)) + image0 = np.asarray(image0, dtype=float_type) + image1 = np.asarray(image1, dtype=float_type) + return image0, image1 + + +def mean_squared_error(image0, image1): + """ + Compute the mean-squared error between two images. + + Parameters + ---------- + image0, image1 : ndarray + Images. Any dimensionality, must have same shape. + + Returns + ------- + mse : float + The mean-squared error (MSE) metric. + + Notes + ----- + .. versionchanged:: 0.16 + This function was renamed from ``skimage.measure.compare_mse`` to + ``skimage.metrics.mean_squared_error``. + + """ + check_shape_equality(image0, image1) + image0, image1 = _as_floats(image0, image1) + return np.mean((image0 - image1) ** 2, dtype=np.float64) + + +def normalized_root_mse(image_true, image_test, *, normalization='euclidean'): + """ + Compute the normalized root mean-squared error (NRMSE) between two + images. + + Parameters + ---------- + image_true : ndarray + Ground-truth image, same shape as im_test. + image_test : ndarray + Test image. + normalization : {'euclidean', 'min-max', 'mean'}, optional + Controls the normalization method to use in the denominator of the + NRMSE. There is no standard method of normalization across the + literature [1]_. The methods available here are as follows: + + - 'euclidean' : normalize by the averaged Euclidean norm of + ``im_true``:: + + NRMSE = RMSE * sqrt(N) / || im_true || + + where || . || denotes the Frobenius norm and ``N = im_true.size``. + This result is equivalent to:: + + NRMSE = || im_true - im_test || / || im_true ||. + + - 'min-max' : normalize by the intensity range of ``im_true``. + - 'mean' : normalize by the mean of ``im_true`` + + Returns + ------- + nrmse : float + The NRMSE metric. + + Notes + ----- + .. versionchanged:: 0.16 + This function was renamed from ``skimage.measure.compare_nrmse`` to + ``skimage.metrics.normalized_root_mse``. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Root-mean-square_deviation + + """ + check_shape_equality(image_true, image_test) + image_true, image_test = _as_floats(image_true, image_test) + + # Ensure that both 'Euclidean' and 'euclidean' match + normalization = normalization.lower() + if normalization == 'euclidean': + denom = np.sqrt(np.mean((image_true * image_true), dtype=np.float64)) + elif normalization == 'min-max': + denom = image_true.max() - image_true.min() + elif normalization == 'mean': + denom = image_true.mean() + else: + raise ValueError("Unsupported norm_type") + return np.sqrt(mean_squared_error(image_true, image_test)) / denom + + +def peak_signal_noise_ratio(image_true, image_test, *, data_range=None): + """ + Compute the peak signal to noise ratio (PSNR) for an image. + + Parameters + ---------- + image_true : ndarray + Ground-truth image, same shape as im_test. + image_test : ndarray + Test image. + data_range : int, optional + The data range of the input image (distance between minimum and + maximum possible values). By default, this is estimated from the image + data-type. + + Returns + ------- + psnr : float + The PSNR metric. + + Notes + ----- + .. versionchanged:: 0.16 + This function was renamed from ``skimage.measure.compare_psnr`` to + ``skimage.metrics.peak_signal_noise_ratio``. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio + + """ + check_shape_equality(image_true, image_test) + + if data_range is None: + if image_true.dtype != image_test.dtype: + warn( + "Inputs have mismatched dtype. Setting data_range based on " + "image_true." + ) + dmin, dmax = dtype_range[image_true.dtype.type] + true_min, true_max = np.min(image_true), np.max(image_true) + if true_max > dmax or true_min < dmin: + raise ValueError( + "image_true has intensity values outside the range expected " + "for its data type. Please manually specify the data_range." + ) + if true_min >= 0: + # most common case (255 for uint8, 1 for float) + data_range = dmax + else: + data_range = dmax - dmin + + image_true, image_test = _as_floats(image_true, image_test) + + err = mean_squared_error(image_true, image_test) + data_range = float(data_range) # prevent overflow for small integer types + return 10 * np.log10((data_range**2) / err) + + +def _pad_to(arr, shape): + """Pad an array with trailing zeros to a given target shape. + + Parameters + ---------- + arr : ndarray + The input array. + shape : tuple + The target shape. + + Returns + ------- + padded : ndarray + The padded array. + + Examples + -------- + >>> _pad_to(np.ones((1, 1), dtype=int), (1, 3)) + array([[1, 0, 0]]) + """ + if not all(s >= i for s, i in zip(shape, arr.shape)): + raise ValueError( + f'Target shape {shape} cannot be smaller than input' + f'shape {arr.shape} along any axis.' + ) + padding = [(0, s - i) for s, i in zip(shape, arr.shape)] + return np.pad(arr, pad_width=padding, mode='constant', constant_values=0) + + +def normalized_mutual_information(image0, image1, *, bins=100): + r"""Compute the normalized mutual information (NMI). + + The normalized mutual information of :math:`A` and :math:`B` is given by:: + + .. math:: + + Y(A, B) = \frac{H(A) + H(B)}{H(A, B)} + + where :math:`H(X) := - \sum_{x \in X}{x \log x}` is the entropy. + + It was proposed to be useful in registering images by Colin Studholme and + colleagues [1]_. It ranges from 1 (perfectly uncorrelated image values) + to 2 (perfectly correlated image values, whether positively or negatively). + + Parameters + ---------- + image0, image1 : ndarray + Images to be compared. The two input images must have the same number + of dimensions. + bins : int or sequence of int, optional + The number of bins along each axis of the joint histogram. + + Returns + ------- + nmi : float + The normalized mutual information between the two arrays, computed at + the granularity given by ``bins``. Higher NMI implies more similar + input images. + + Raises + ------ + ValueError + If the images don't have the same number of dimensions. + + Notes + ----- + If the two input images are not the same shape, the smaller image is padded + with zeros. + + References + ---------- + .. [1] C. Studholme, D.L.G. Hill, & D.J. Hawkes (1999). An overlap + invariant entropy measure of 3D medical image alignment. + Pattern Recognition 32(1):71-86 + :DOI:`10.1016/S0031-3203(98)00091-0` + """ + if image0.ndim != image1.ndim: + raise ValueError( + f'NMI requires images of same number of dimensions. ' + f'Got {image0.ndim}D for `image0` and ' + f'{image1.ndim}D for `image1`.' + ) + if image0.shape != image1.shape: + max_shape = np.maximum(image0.shape, image1.shape) + padded0 = _pad_to(image0, max_shape) + padded1 = _pad_to(image1, max_shape) + else: + padded0, padded1 = image0, image1 + + hist, bin_edges = np.histogramdd( + [np.reshape(padded0, -1), np.reshape(padded1, -1)], + bins=bins, + density=True, + ) + + H0 = entropy(np.sum(hist, axis=0)) + H1 = entropy(np.sum(hist, axis=1)) + H01 = entropy(np.reshape(hist, -1)) + + return (H0 + H1) / H01 diff --git a/lib/python3.10/site-packages/skimage/morphology/__init__.py b/lib/python3.10/site-packages/skimage/morphology/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ef513f2bcfeea3749f415a64f58d89a555257d43 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/__init__.py @@ -0,0 +1,91 @@ +"""Morphological algorithms, e.g., closing, opening, skeletonization.""" + +from .binary import binary_closing, binary_dilation, binary_erosion, binary_opening +from .gray import black_tophat, closing, dilation, erosion, opening, white_tophat +from .isotropic import ( + isotropic_erosion, + isotropic_dilation, + isotropic_opening, + isotropic_closing, +) +from .footprints import ( + ball, + cube, + diamond, + disk, + ellipse, + footprint_from_sequence, + footprint_rectangle, + mirror_footprint, + octagon, + octahedron, + pad_footprint, + rectangle, + square, + star, +) +from ..measure._label import label +from ._skeletonize import medial_axis, skeletonize, thin +from .convex_hull import convex_hull_image, convex_hull_object +from .grayreconstruct import reconstruction +from .misc import remove_small_holes, remove_small_objects, remove_objects_by_distance +from .extrema import h_maxima, h_minima, local_minima, local_maxima +from ._flood_fill import flood, flood_fill +from .max_tree import ( + area_opening, + area_closing, + diameter_closing, + diameter_opening, + max_tree, + max_tree_local_maxima, +) + +__all__ = [ + 'area_closing', + 'area_opening', + 'ball', + 'binary_closing', + 'binary_dilation', + 'binary_erosion', + 'binary_opening', + 'black_tophat', + 'closing', + 'convex_hull_image', + 'convex_hull_object', + 'diameter_closing', + 'diameter_opening', + 'diamond', + 'dilation', + 'disk', + 'ellipse', + 'erosion', + 'flood', + 'flood_fill', + 'footprint_from_sequence', + 'footprint_rectangle', + 'h_maxima', + 'h_minima', + 'isotropic_closing', + 'isotropic_dilation', + 'isotropic_erosion', + 'isotropic_opening', + 'label', + 'local_maxima', + 'local_minima', + 'max_tree', + 'max_tree_local_maxima', + 'medial_axis', + 'mirror_footprint', + 'octagon', + 'octahedron', + 'opening', + 'pad_footprint', + 'reconstruction', + 'remove_small_holes', + 'remove_small_objects', + 'remove_objects_by_distance', + 'skeletonize', + 'star', + 'thin', + 'white_tophat', +] diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/_flood_fill.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/_flood_fill.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89efc5f2250c003c69fb157d28c1fb6b663481e0 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/_flood_fill.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/binary.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/binary.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a0f2a06933ee556654ecb77c861ec57f34ecfb6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/binary.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/convex_hull.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/convex_hull.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28bffa028c63068f29ee8b95dc60feebc1e580a0 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/convex_hull.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/extrema.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/extrema.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6dfc6e779c00caa08ee28249246459d7f94d2ba Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/extrema.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/footprints.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/footprints.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5bcf276c6167f9e30cf4859c80e7b697bc3ed475 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/footprints.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/gray.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/gray.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1849acf23316d78d759e2fa69a829358318f2902 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/gray.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/grayreconstruct.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/grayreconstruct.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6da2ef761a168e69f0232fc3e22494b9c5d07da Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/grayreconstruct.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/max_tree.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/max_tree.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..312744ecb62e878d5699a13d3cdae2b9987351b3 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/max_tree.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/__pycache__/misc.cpython-310.pyc b/lib/python3.10/site-packages/skimage/morphology/__pycache__/misc.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dfbc5bc61a0afc31ceee02fffdfc49dcda5fc0c Binary files /dev/null and b/lib/python3.10/site-packages/skimage/morphology/__pycache__/misc.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/morphology/_flood_fill.py b/lib/python3.10/site-packages/skimage/morphology/_flood_fill.py new file mode 100644 index 0000000000000000000000000000000000000000..330e3b1d3da258fbcd20567cc5c9f0bc32c09eee --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/_flood_fill.py @@ -0,0 +1,310 @@ +"""flood_fill.py - in place flood fill algorithm + +This module provides a function to fill all equal (or within tolerance) values +connected to a given seed point with a different value. +""" + +import numpy as np + +from ..util import crop +from ._flood_fill_cy import _flood_fill_equal, _flood_fill_tolerance +from ._util import ( + _offsets_to_raveled_neighbors, + _resolve_neighborhood, + _set_border_values, +) +from .._shared.dtype import numeric_dtype_min_max + + +def flood_fill( + image, + seed_point, + new_value, + *, + footprint=None, + connectivity=None, + tolerance=None, + in_place=False, +): + """Perform flood filling on an image. + + Starting at a specific `seed_point`, connected points equal or within + `tolerance` of the seed value are found, then set to `new_value`. + + Parameters + ---------- + image : ndarray + An n-dimensional array. + seed_point : tuple or int + The point in `image` used as the starting point for the flood fill. If + the image is 1D, this point may be given as an integer. + new_value : `image` type + New value to set the entire fill. This must be chosen in agreement + with the dtype of `image`. + footprint : ndarray, optional + The footprint (structuring element) used to determine the neighborhood + of each evaluated pixel. It must contain only 1's and 0's, have the + same number of dimensions as `image`. If not given, all adjacent pixels + are considered as part of the neighborhood (fully connected). + connectivity : int, optional + A number used to determine the neighborhood of each evaluated pixel. + Adjacent pixels whose squared distance from the center is less than or + equal to `connectivity` are considered neighbors. Ignored if + `footprint` is not None. + tolerance : float or int, optional + If None (default), adjacent values must be strictly equal to the + value of `image` at `seed_point` to be filled. This is fastest. + If a tolerance is provided, adjacent points with values within plus or + minus tolerance from the seed point are filled (inclusive). + in_place : bool, optional + If True, flood filling is applied to `image` in place. If False, the + flood filled result is returned without modifying the input `image` + (default). + + Returns + ------- + filled : ndarray + An array with the same shape as `image` is returned, with values in + areas connected to and equal (or within tolerance of) the seed point + replaced with `new_value`. + + Notes + ----- + The conceptual analogy of this operation is the 'paint bucket' tool in many + raster graphics programs. + + Examples + -------- + >>> from skimage.morphology import flood_fill + >>> image = np.zeros((4, 7), dtype=int) + >>> image[1:3, 1:3] = 1 + >>> image[3, 0] = 1 + >>> image[1:3, 4:6] = 2 + >>> image[3, 6] = 3 + >>> image + array([[0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3]]) + + Fill connected ones with 5, with full connectivity (diagonals included): + + >>> flood_fill(image, (1, 1), 5) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 5, 5, 0, 2, 2, 0], + [0, 5, 5, 0, 2, 2, 0], + [5, 0, 0, 0, 0, 0, 3]]) + + Fill connected ones with 5, excluding diagonal points (connectivity 1): + + >>> flood_fill(image, (1, 1), 5, connectivity=1) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 5, 5, 0, 2, 2, 0], + [0, 5, 5, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3]]) + + Fill with a tolerance: + + >>> flood_fill(image, (0, 0), 5, tolerance=1) + array([[5, 5, 5, 5, 5, 5, 5], + [5, 5, 5, 5, 2, 2, 5], + [5, 5, 5, 5, 2, 2, 5], + [5, 5, 5, 5, 5, 5, 3]]) + """ + mask = flood( + image, + seed_point, + footprint=footprint, + connectivity=connectivity, + tolerance=tolerance, + ) + + if not in_place: + image = image.copy() + + image[mask] = new_value + return image + + +def flood(image, seed_point, *, footprint=None, connectivity=None, tolerance=None): + """Mask corresponding to a flood fill. + + Starting at a specific `seed_point`, connected points equal or within + `tolerance` of the seed value are found. + + Parameters + ---------- + image : ndarray + An n-dimensional array. + seed_point : tuple or int + The point in `image` used as the starting point for the flood fill. If + the image is 1D, this point may be given as an integer. + footprint : ndarray, optional + The footprint (structuring element) used to determine the neighborhood + of each evaluated pixel. It must contain only 1's and 0's, have the + same number of dimensions as `image`. If not given, all adjacent pixels + are considered as part of the neighborhood (fully connected). + connectivity : int, optional + A number used to determine the neighborhood of each evaluated pixel. + Adjacent pixels whose squared distance from the center is less than or + equal to `connectivity` are considered neighbors. Ignored if + `footprint` is not None. + tolerance : float or int, optional + If None (default), adjacent values must be strictly equal to the + initial value of `image` at `seed_point`. This is fastest. If a value + is given, a comparison will be done at every point and if within + tolerance of the initial value will also be filled (inclusive). + + Returns + ------- + mask : ndarray + A Boolean array with the same shape as `image` is returned, with True + values for areas connected to and equal (or within tolerance of) the + seed point. All other values are False. + + Notes + ----- + The conceptual analogy of this operation is the 'paint bucket' tool in many + raster graphics programs. This function returns just the mask + representing the fill. + + If indices are desired rather than masks for memory reasons, the user can + simply run `numpy.nonzero` on the result, save the indices, and discard + this mask. + + Examples + -------- + >>> from skimage.morphology import flood + >>> image = np.zeros((4, 7), dtype=int) + >>> image[1:3, 1:3] = 1 + >>> image[3, 0] = 1 + >>> image[1:3, 4:6] = 2 + >>> image[3, 6] = 3 + >>> image + array([[0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3]]) + + Fill connected ones with 5, with full connectivity (diagonals included): + + >>> mask = flood(image, (1, 1)) + >>> image_flooded = image.copy() + >>> image_flooded[mask] = 5 + >>> image_flooded + array([[0, 0, 0, 0, 0, 0, 0], + [0, 5, 5, 0, 2, 2, 0], + [0, 5, 5, 0, 2, 2, 0], + [5, 0, 0, 0, 0, 0, 3]]) + + Fill connected ones with 5, excluding diagonal points (connectivity 1): + + >>> mask = flood(image, (1, 1), connectivity=1) + >>> image_flooded = image.copy() + >>> image_flooded[mask] = 5 + >>> image_flooded + array([[0, 0, 0, 0, 0, 0, 0], + [0, 5, 5, 0, 2, 2, 0], + [0, 5, 5, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3]]) + + Fill with a tolerance: + + >>> mask = flood(image, (0, 0), tolerance=1) + >>> image_flooded = image.copy() + >>> image_flooded[mask] = 5 + >>> image_flooded + array([[5, 5, 5, 5, 5, 5, 5], + [5, 5, 5, 5, 2, 2, 5], + [5, 5, 5, 5, 2, 2, 5], + [5, 5, 5, 5, 5, 5, 3]]) + """ + # Correct start point in ravelled image - only copy if non-contiguous + image = np.asarray(image) + if image.flags.f_contiguous is True: + order = 'F' + elif image.flags.c_contiguous is True: + order = 'C' + else: + image = np.ascontiguousarray(image) + order = 'C' + + # Shortcut for rank zero + if 0 in image.shape: + return np.zeros(image.shape, dtype=bool) + + # Convenience for 1d input + try: + iter(seed_point) + except TypeError: + seed_point = (seed_point,) + + seed_value = image[seed_point] + seed_point = tuple(np.asarray(seed_point) % image.shape) + + footprint = _resolve_neighborhood( + footprint, connectivity, image.ndim, enforce_adjacency=False + ) + center = tuple(s // 2 for s in footprint.shape) + # Compute padding width as the maximum offset to neighbors on each axis. + # Generates a 2-tuple of (pad_start, pad_end) for each axis. + pad_width = [ + (np.max(np.abs(idx - c)),) * 2 for idx, c in zip(np.nonzero(footprint), center) + ] + + # Must annotate borders + working_image = np.pad( + image, pad_width, mode='constant', constant_values=image.min() + ) + # Stride-aware neighbors - works for both C- and Fortran-contiguity + ravelled_seed_idx = np.ravel_multi_index( + [i + pad_start for i, (pad_start, pad_end) in zip(seed_point, pad_width)], + working_image.shape, + order=order, + ) + neighbor_offsets = _offsets_to_raveled_neighbors( + working_image.shape, footprint, center=center, order=order + ) + + # Use a set of flags; see _flood_fill_cy.pyx for meanings + flags = np.zeros(working_image.shape, dtype=np.uint8, order=order) + _set_border_values(flags, value=2, border_width=pad_width) + + try: + if tolerance is not None: + tolerance = abs(tolerance) + # Account for over- & underflow problems with seed_value ± tolerance + # in a way that works with NumPy 1 & 2 + min_value, max_value = numeric_dtype_min_max(seed_value.dtype) + low_tol = max(min_value.item(), seed_value.item() - tolerance) + high_tol = min(max_value.item(), seed_value.item() + tolerance) + + _flood_fill_tolerance( + working_image.ravel(order), + flags.ravel(order), + neighbor_offsets, + ravelled_seed_idx, + seed_value, + low_tol, + high_tol, + ) + else: + _flood_fill_equal( + working_image.ravel(order), + flags.ravel(order), + neighbor_offsets, + ravelled_seed_idx, + seed_value, + ) + except TypeError: + if working_image.dtype == np.float16: + # Provide the user with clearer error message + raise TypeError( + "dtype of `image` is float16 which is not " + "supported, try upcasting to float32" + ) + else: + raise + + # Output what the user requested; view does not create a new copy. + return crop(flags, pad_width, copy=False).view(bool) diff --git a/lib/python3.10/site-packages/skimage/morphology/_skeletonize.py b/lib/python3.10/site-packages/skimage/morphology/_skeletonize.py new file mode 100644 index 0000000000000000000000000000000000000000..62e01f9685d510f13e0b202c9162f233fb296e8f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/_skeletonize.py @@ -0,0 +1,655 @@ +""" +Algorithms for computing the skeleton of a binary image +""" + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import check_nD +from ..util import crop +from ._skeletonize_lee_cy import _compute_thin_image +from ._skeletonize_various_cy import ( + _fast_skeletonize, + _skeletonize_loop, + _table_lookup_index, +) + + +def skeletonize(image, *, method=None): + """Compute the skeleton of the input image via thinning. + + Parameters + ---------- + image : (M, N[, P]) ndarray of bool or int + The image containing the objects to be skeletonized. Each connected component + in the image is reduced to a single-pixel wide skeleton. The image is binarized + prior to thinning; thus, adjacent objects of different intensities are + considered as one. Zero or ``False`` values represent the background, nonzero + or ``True`` values -- foreground. + method : {'zhang', 'lee'}, optional + Which algorithm to use. Zhang's algorithm [Zha84]_ only works for + 2D images, and is the default for 2D. Lee's algorithm [Lee94]_ + works for 2D or 3D images and is the default for 3D. + + Returns + ------- + skeleton : (M, N[, P]) ndarray of bool + The thinned image. + + See Also + -------- + medial_axis + + References + ---------- + .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models + via 3-D medial surface/axis thinning algorithms. + Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. + + .. [Zha84] A fast parallel algorithm for thinning digital patterns, + T. Y. Zhang and C. Y. Suen, Communications of the ACM, + March 1984, Volume 27, Number 3. + + Examples + -------- + >>> X, Y = np.ogrid[0:9, 0:9] + >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool) + >>> ellipse.view(np.uint8) + array([[0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8) + >>> skel = skeletonize(ellipse) + >>> skel.view(np.uint8) + array([[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, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + """ + image = image.astype(bool, order="C", copy=False) + + if method not in {'zhang', 'lee', None}: + raise ValueError( + f'skeletonize method should be either "lee" or "zhang", ' f'got {method}.' + ) + if image.ndim == 2 and (method is None or method == 'zhang'): + skeleton = _skeletonize_zhang(image) + elif image.ndim == 3 and method == 'zhang': + raise ValueError('skeletonize method "zhang" only works for 2D ' 'images.') + elif image.ndim == 3 or (image.ndim == 2 and method == 'lee'): + skeleton = _skeletonize_lee(image) + else: + raise ValueError( + f'skeletonize requires a 2D or 3D image as input, ' f'got {image.ndim}D.' + ) + return skeleton + + +def _skeletonize_zhang(image): + """Return the skeleton of a 2D binary image. + + Thinning is used to reduce each connected component in a binary image + to a single-pixel wide skeleton. + + Parameters + ---------- + image : numpy.ndarray + An image containing the objects to be skeletonized. Zeros or ``False`` + represent background, nonzero values or ``True`` are foreground. + + Returns + ------- + skeleton : ndarray + A matrix containing the thinned image. + + See Also + -------- + medial_axis, skeletonize, thin + + Notes + ----- + The algorithm [Zha84]_ works by making successive passes of the image, + removing pixels on object borders. This continues until no + more pixels can be removed. The image is correlated with a + mask that assigns each pixel a number in the range [0...255] + corresponding to each possible pattern of its 8 neighboring + pixels. A look up table is then used to assign the pixels a + value of 0, 1, 2 or 3, which are selectively removed during + the iterations. + + Note that this algorithm will give different results than a + medial axis transform, which is also often referred to as + "skeletonization". + + References + ---------- + .. [Zha84] A fast parallel algorithm for thinning digital patterns, + T. Y. Zhang and C. Y. Suen, Communications of the ACM, + March 1984, Volume 27, Number 3. + + Examples + -------- + >>> X, Y = np.ogrid[0:9, 0:9] + >>> ellipse = (1./3 * (X - 4)**2 + (Y - 4)**2 < 3**2).astype(bool) + >>> ellipse.view(np.uint8) + array([[0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0]], dtype=uint8) + >>> skel = skeletonize(ellipse) + >>> skel.view(np.uint8) + array([[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, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + """ + if image.ndim != 2: + raise ValueError("Zhang's skeletonize method requires a 2D array") + return _fast_skeletonize(image) + + +# --------- Skeletonization and thinning based on Guo and Hall 1989 --------- + + +def _generate_thin_luts(): + """generate LUTs for thinning algorithm (for reference)""" + + def nabe(n): + return np.array([n >> i & 1 for i in range(0, 9)]).astype(bool) + + def G1(n): + s = 0 + bits = nabe(n) + for i in (0, 2, 4, 6): + if not (bits[i]) and (bits[i + 1] or bits[(i + 2) % 8]): + s += 1 + return s == 1 + + g1_lut = np.array([G1(n) for n in range(256)]) + + def G2(n): + n1, n2 = 0, 0 + bits = nabe(n) + for k in (1, 3, 5, 7): + if bits[k] or bits[k - 1]: + n1 += 1 + if bits[k] or bits[(k + 1) % 8]: + n2 += 1 + return min(n1, n2) in [2, 3] + + g2_lut = np.array([G2(n) for n in range(256)]) + + g12_lut = g1_lut & g2_lut + + def G3(n): + bits = nabe(n) + return not ((bits[1] or bits[2] or not (bits[7])) and bits[0]) + + def G3p(n): + bits = nabe(n) + return not ((bits[5] or bits[6] or not (bits[3])) and bits[4]) + + g3_lut = np.array([G3(n) for n in range(256)]) + g3p_lut = np.array([G3p(n) for n in range(256)]) + + g123_lut = g12_lut & g3_lut + g123p_lut = g12_lut & g3p_lut + + return g123_lut, g123p_lut + + +# fmt: off +G123_LUT = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, + 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, + 0, 1, 1, 0, 0, 1, 0, 0, 0], dtype=bool) + +G123P_LUT = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 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, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 1, 0, 1, 0, 1, 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, 1, 0, + 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 1, 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, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=bool) +# fmt: on + + +def thin(image, max_num_iter=None): + """ + Perform morphological thinning of a binary image. + + Parameters + ---------- + image : binary (M, N) ndarray + The image to thin. If this input isn't already a binary image, + it gets converted into one: In this case, zero values are considered + background (False), nonzero values are considered foreground (True). + max_num_iter : int, number of iterations, optional + Regardless of the value of this parameter, the thinned image + is returned immediately if an iteration produces no change. + If this parameter is specified it thus sets an upper bound on + the number of iterations performed. + + Returns + ------- + out : ndarray of bool + Thinned image. + + See Also + -------- + skeletonize, medial_axis + + Notes + ----- + This algorithm [1]_ works by making multiple passes over the image, + removing pixels matching a set of criteria designed to thin + connected regions while preserving eight-connected components and + 2 x 2 squares [2]_. In each of the two sub-iterations the algorithm + correlates the intermediate skeleton image with a neighborhood mask, + then looks up each neighborhood in a lookup table indicating whether + the central pixel should be deleted in that sub-iteration. + + References + ---------- + .. [1] Z. Guo and R. W. Hall, "Parallel thinning with + two-subiteration algorithms," Comm. ACM, vol. 32, no. 3, + pp. 359-373, 1989. :DOI:`10.1145/62065.62074` + .. [2] Lam, L., Seong-Whan Lee, and Ching Y. Suen, "Thinning + Methodologies-A Comprehensive Survey," IEEE Transactions on + Pattern Analysis and Machine Intelligence, Vol 14, No. 9, + p. 879, 1992. :DOI:`10.1109/34.161346` + + Examples + -------- + >>> square = np.zeros((7, 7), dtype=bool) + >>> square[1:-1, 2:-2] = 1 + >>> square[0, 1] = 1 + >>> square.view(np.uint8) + array([[0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> skel = thin(square) + >>> skel.view(np.uint8) + array([[0, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + """ + # check that image is 2d + check_nD(image, 2) + + # convert image to uint8 with values in {0, 1} + skel = np.asanyarray(image, dtype=bool).copy().view(np.uint8) + + # neighborhood mask + mask = np.array([[8, 4, 2], [16, 0, 1], [32, 64, 128]], dtype=np.uint8) + + # iterate until convergence, up to the iteration limit + max_num_iter = max_num_iter or np.inf + num_iter = 0 + n_pts_old, n_pts_new = np.inf, np.sum(skel) + while n_pts_old != n_pts_new and num_iter < max_num_iter: + n_pts_old = n_pts_new + + # perform the two "subiterations" described in the paper + for lut in [G123_LUT, G123P_LUT]: + # correlate image with neighborhood mask + N = ndi.correlate(skel, mask, mode='constant') + # take deletion decision from this subiteration's LUT + D = np.take(lut, N) + # perform deletion + skel[D] = 0 + + n_pts_new = np.sum(skel) # count points after thinning + num_iter += 1 + + return skel.astype(bool) + + +# --------- Skeletonization by medial axis transform -------- + +_eight_connect = ndi.generate_binary_structure(2, 2) + + +def medial_axis(image, mask=None, return_distance=False, *, rng=None): + """Compute the medial axis transform of a binary image. + + Parameters + ---------- + image : binary ndarray, shape (M, N) + The image of the shape to skeletonize. If this input isn't already a + binary image, it gets converted into one: In this case, zero values are + considered background (False), nonzero values are considered + foreground (True). + mask : binary ndarray, shape (M, N), optional + If a mask is given, only those elements in `image` with a true + value in `mask` are used for computing the medial axis. + return_distance : bool, optional + If true, the distance transform is returned as well as the skeleton. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + The PRNG determines the order in which pixels are processed for + tiebreaking. + + .. versionadded:: 0.19 + + Returns + ------- + out : ndarray of bools + Medial axis transform of the image + dist : ndarray of ints, optional + Distance transform of the image (only returned if `return_distance` + is True) + + See Also + -------- + skeletonize, thin + + Notes + ----- + This algorithm computes the medial axis transform of an image + as the ridges of its distance transform. + + The different steps of the algorithm are as follows + * A lookup table is used, that assigns 0 or 1 to each configuration of + the 3x3 binary square, whether the central pixel should be removed + or kept. We want a point to be removed if it has more than one neighbor + and if removing it does not change the number of connected components. + + * The distance transform to the background is computed, as well as + the cornerness of the pixel. + + * The foreground (value of 1) points are ordered by + the distance transform, then the cornerness. + + * A cython function is called to reduce the image to its skeleton. It + processes pixels in the order determined at the previous step, and + removes or maintains a pixel according to the lookup table. Because + of the ordering, it is possible to process all pixels in only one + pass. + + Examples + -------- + >>> square = np.zeros((7, 7), dtype=bool) + >>> square[1:-1, 2:-2] = 1 + >>> square.view(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> medial_axis(square).view(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + + """ + global _eight_connect + if mask is None: + masked_image = image.astype(bool) + else: + masked_image = image.astype(bool).copy() + masked_image[~mask] = False + # + # Build lookup table - three conditions + # 1. Keep only positive pixels (center_is_foreground array). + # AND + # 2. Keep if removing the pixel results in a different connectivity + # (if the number of connected components is different with and + # without the central pixel) + # OR + # 3. Keep if # pixels in neighborhood is 2 or less + # Note that table is independent of image + center_is_foreground = (np.arange(512) & 2**4).astype(bool) + table = ( + center_is_foreground # condition 1. + & ( + np.array( + [ + ndi.label(_pattern_of(index), _eight_connect)[1] + != ndi.label(_pattern_of(index & ~(2**4)), _eight_connect)[1] + for index in range(512) + ] + ) # condition 2 + | np.array([np.sum(_pattern_of(index)) < 3 for index in range(512)]) + ) + # condition 3 + ) + + # Build distance transform + distance = ndi.distance_transform_edt(masked_image) + if return_distance: + store_distance = distance.copy() + + # Corners + # The processing order along the edge is critical to the shape of the + # resulting skeleton: if you process a corner first, that corner will + # be eroded and the skeleton will miss the arm from that corner. Pixels + # with fewer neighbors are more "cornery" and should be processed last. + # We use a cornerness_table lookup table where the score of a + # configuration is the number of background (0-value) pixels in the + # 3x3 neighborhood + cornerness_table = np.array( + [9 - np.sum(_pattern_of(index)) for index in range(512)] + ) + corner_score = _table_lookup(masked_image, cornerness_table) + + # Define arrays for inner loop + i, j = np.mgrid[0 : image.shape[0], 0 : image.shape[1]] + result = masked_image.copy() + distance = distance[result] + i = np.ascontiguousarray(i[result], dtype=np.intp) + j = np.ascontiguousarray(j[result], dtype=np.intp) + result = np.ascontiguousarray(result, np.uint8) + + # Determine the order in which pixels are processed. + # We use a random # for tiebreaking. Assign each pixel in the image a + # predictable, random # so that masking doesn't affect arbitrary choices + # of skeletons + # + generator = np.random.default_rng(rng) + tiebreaker = generator.permutation(np.arange(masked_image.sum())) + order = np.lexsort((tiebreaker, corner_score[masked_image], distance)) + order = np.ascontiguousarray(order, dtype=np.int32) + + table = np.ascontiguousarray(table, dtype=np.uint8) + # Remove pixels not belonging to the medial axis + _skeletonize_loop(result, i, j, order, table) + + result = result.astype(bool) + if mask is not None: + result[~mask] = image[~mask] + if return_distance: + return result, store_distance + else: + return result + + +def _pattern_of(index): + """ + Return the pattern represented by an index value + Byte decomposition of index + """ + return np.array( + [ + [index & 2**0, index & 2**1, index & 2**2], + [index & 2**3, index & 2**4, index & 2**5], + [index & 2**6, index & 2**7, index & 2**8], + ], + bool, + ) + + +def _table_lookup(image, table): + """ + Perform a morphological transform on an image, directed by its + neighbors + + Parameters + ---------- + image : ndarray + A binary image + table : ndarray + A 512-element table giving the transform of each pixel given + the values of that pixel and its 8-connected neighbors. + + Returns + ------- + result : ndarray of same shape as `image` + Transformed image + + Notes + ----- + The pixels are numbered like this:: + + 0 1 2 + 3 4 5 + 6 7 8 + + The index at a pixel is the sum of 2** for pixels + that evaluate to true. + """ + # + # We accumulate into the indexer to get the index into the table + # at each point in the image + # + if image.shape[0] < 3 or image.shape[1] < 3: + image = image.astype(bool) + indexer = np.zeros(image.shape, int) + indexer[1:, 1:] += image[:-1, :-1] * 2**0 + indexer[1:, :] += image[:-1, :] * 2**1 + indexer[1:, :-1] += image[:-1, 1:] * 2**2 + + indexer[:, 1:] += image[:, :-1] * 2**3 + indexer[:, :] += image[:, :] * 2**4 + indexer[:, :-1] += image[:, 1:] * 2**5 + + indexer[:-1, 1:] += image[1:, :-1] * 2**6 + indexer[:-1, :] += image[1:, :] * 2**7 + indexer[:-1, :-1] += image[1:, 1:] * 2**8 + else: + indexer = _table_lookup_index(np.ascontiguousarray(image, np.uint8)) + image = table[indexer] + return image + + +def _skeletonize_lee(image): + """Compute the skeleton of a binary image. + + Thinning is used to reduce each connected component in a binary image + to a single-pixel wide skeleton. + + Parameters + ---------- + image : ndarray, 2D or 3D + An image containing the objects to be skeletonized. Zeros or ``False`` + represent background, nonzero values or ``True`` are foreground. + + Returns + ------- + skeleton : ndarray of bool + The thinned image. + + See Also + -------- + skeletonize, medial_axis + + Notes + ----- + The method of [Lee94]_ uses an octree data structure to examine a 3x3x3 + neighborhood of a pixel. The algorithm proceeds by iteratively sweeping + over the image, and removing pixels at each iteration until the image + stops changing. Each iteration consists of two steps: first, a list of + candidates for removal is assembled; then pixels from this list are + rechecked sequentially, to better preserve connectivity of the image. + + The algorithm this function implements is different from the algorithms + used by either `skeletonize` or `medial_axis`, thus for 2D images the + results produced by this function are generally different. + + References + ---------- + .. [Lee94] T.-C. Lee, R.L. Kashyap and C.-N. Chu, Building skeleton models + via 3-D medial surface/axis thinning algorithms. + Computer Vision, Graphics, and Image Processing, 56(6):462-478, 1994. + + """ + # make sure the image is 3D or 2D + if image.ndim < 2 or image.ndim > 3: + raise ValueError( + "skeletonize can only handle 2D or 3D images; " + f"got image.ndim = {image.ndim} instead." + ) + + image_o = image.astype(bool, order="C", copy=False) + + # make a 2D input image 3D and pad it w/ zeros to simplify dealing w/ boundaries + # NB: careful here to not clobber the original *and* minimize copying + if image.ndim == 2: + image_o = image_o[np.newaxis, ...] + image_o = np.pad(image_o, pad_width=1, mode='constant') # copies + + # do the computation + image_o = _compute_thin_image(image_o) + + # crop it back and restore the original intensity range + image_o = crop(image_o, crop_width=1) + if image.ndim == 2: + image_o = image_o[0] + + return image_o diff --git a/lib/python3.10/site-packages/skimage/morphology/_util.py b/lib/python3.10/site-packages/skimage/morphology/_util.py new file mode 100644 index 0000000000000000000000000000000000000000..bcdfdb643e08b95f2d6a36ef1719991180353113 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/_util.py @@ -0,0 +1,328 @@ +"""Utility functions used in the morphology subpackage.""" + +import numpy as np +from scipy import ndimage as ndi + + +def _validate_connectivity(image_dim, connectivity, offset): + """Convert any valid connectivity to a footprint and offset. + + Parameters + ---------- + image_dim : int + The number of dimensions of the input image. + connectivity : int, array, or None + The neighborhood connectivity. An integer is interpreted as in + ``scipy.ndimage.generate_binary_structure``, as the maximum number + of orthogonal steps to reach a neighbor. An array is directly + interpreted as a footprint and its shape is validated against + the input image shape. ``None`` is interpreted as a connectivity of 1. + offset : tuple of int, or None + The coordinates of the center of the footprint. + + Returns + ------- + c_connectivity : array of bool + The footprint (structuring element) corresponding to the input + `connectivity`. + offset : array of int + The offset corresponding to the center of the footprint. + + Raises + ------ + ValueError: + If the image dimension and the connectivity or offset dimensions don't + match. + """ + if connectivity is None: + connectivity = 1 + + if np.isscalar(connectivity): + c_connectivity = ndi.generate_binary_structure(image_dim, connectivity) + else: + c_connectivity = np.array(connectivity, bool) + if c_connectivity.ndim != image_dim: + raise ValueError("Connectivity dimension must be same as image") + + if offset is None: + if any([x % 2 == 0 for x in c_connectivity.shape]): + raise ValueError("Connectivity array must have an unambiguous " "center") + + offset = np.array(c_connectivity.shape) // 2 + + return c_connectivity, offset + + +def _raveled_offsets_and_distances( + image_shape, + *, + footprint=None, + connectivity=1, + center=None, + spacing=None, + order='C', +): + """Compute offsets to neighboring pixels in raveled coordinate space. + + This function also returns the corresponding distances from the center + pixel given a spacing (assumed to be 1 along each axis by default). + + Parameters + ---------- + image_shape : tuple of int + The shape of the image for which the offsets are being computed. + footprint : array of bool + The footprint of the neighborhood, expressed as an n-dimensional array + of 1s and 0s. If provided, the connectivity argument is ignored. + connectivity : {1, ..., ndim} + The square connectivity of the neighborhood: the number of orthogonal + steps allowed to consider a pixel a neighbor. See + `scipy.ndimage.generate_binary_structure`. Ignored if footprint is + provided. + center : tuple of int + Tuple of indices to the center of the footprint. If not provided, it + is assumed to be the center of the footprint, either provided or + generated by the connectivity argument. + spacing : tuple of float + The spacing between pixels/voxels along each axis. + order : 'C' or 'F' + The ordering of the array, either C or Fortran ordering. + + Returns + ------- + raveled_offsets : ndarray + Linear offsets to a samples neighbors in the raveled image, sorted by + their distance from the center. + distances : ndarray + The pixel distances corresponding to each offset. + + Notes + ----- + This function will return values even if `image_shape` contains a dimension + length that is smaller than `footprint`. + + Examples + -------- + >>> off, d = _raveled_offsets_and_distances( + ... (4, 5), footprint=np.ones((4, 3)), center=(1, 1) + ... ) + >>> off + array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11]) + >>> d[0] + 1.0 + >>> d[-1] # distance from (1, 1) to (3, 2) + 2.236... + """ + ndim = len(image_shape) + if footprint is None: + footprint = ndi.generate_binary_structure(rank=ndim, connectivity=connectivity) + if center is None: + center = tuple(s // 2 for s in footprint.shape) + + if not footprint.ndim == ndim == len(center): + raise ValueError( + "number of dimensions in image shape, footprint and its" + "center index does not match" + ) + + offsets = np.stack( + [(idx - c) for idx, c in zip(np.nonzero(footprint), center)], axis=-1 + ) + + if order == 'F': + offsets = offsets[:, ::-1] + image_shape = image_shape[::-1] + elif order != 'C': + raise ValueError("order must be 'C' or 'F'") + + # Scale offsets in each dimension and sum + ravel_factors = image_shape[1:] + (1,) + ravel_factors = np.cumprod(ravel_factors[::-1])[::-1] + raveled_offsets = (offsets * ravel_factors).sum(axis=1) + + # Sort by distance + if spacing is None: + spacing = np.ones(ndim) + weighted_offsets = offsets * spacing + distances = np.sqrt(np.sum(weighted_offsets**2, axis=1)) + sorted_raveled_offsets = raveled_offsets[np.argsort(distances, kind="stable")] + sorted_distances = np.sort(distances, kind="stable") + + # If any dimension in image_shape is smaller than footprint.shape + # duplicates might occur, remove them + if any(x < y for x, y in zip(image_shape, footprint.shape)): + # np.unique reorders, which we don't want + _, indices = np.unique(sorted_raveled_offsets, return_index=True) + indices = np.sort(indices, kind="stable") + sorted_raveled_offsets = sorted_raveled_offsets[indices] + sorted_distances = sorted_distances[indices] + + # Remove "offset to center" + sorted_raveled_offsets = sorted_raveled_offsets[1:] + sorted_distances = sorted_distances[1:] + + return sorted_raveled_offsets, sorted_distances + + +def _offsets_to_raveled_neighbors(image_shape, footprint, center, order='C'): + """Compute offsets to a samples neighbors if the image would be raveled. + + Parameters + ---------- + image_shape : tuple + The shape of the image for which the offsets are computed. + footprint : ndarray + The footprint (structuring element) determining the neighborhood + expressed as an n-D array of 1's and 0's. + center : tuple + Tuple of indices to the center of `footprint`. + order : {"C", "F"}, optional + Whether the image described by `image_shape` is in row-major (C-style) + or column-major (Fortran-style) order. + + Returns + ------- + raveled_offsets : ndarray + Linear offsets to a samples neighbors in the raveled image, sorted by + their distance from the center. + + Notes + ----- + This function will return values even if `image_shape` contains a dimension + length that is smaller than `footprint`. + + Examples + -------- + >>> _offsets_to_raveled_neighbors((4, 5), np.ones((4, 3)), (1, 1)) + array([-5, -1, 1, 5, -6, -4, 4, 6, 10, 9, 11]) + >>> _offsets_to_raveled_neighbors((2, 3, 2), np.ones((3, 3, 3)), (1, 1, 1)) + array([-6, -2, -1, 1, 2, 6, -8, -7, -5, -4, -3, 3, 4, 5, 7, 8, -9, + 9]) + """ + raveled_offsets = _raveled_offsets_and_distances( + image_shape, footprint=footprint, center=center, order=order + )[0] + + return raveled_offsets + + +def _resolve_neighborhood(footprint, connectivity, ndim, enforce_adjacency=True): + """Validate or create a footprint (structuring element). + + Depending on the values of `connectivity` and `footprint` this function + either creates a new footprint (`footprint` is None) using `connectivity` + or validates the given footprint (`footprint` is not None). + + Parameters + ---------- + footprint : ndarray + The footprint (structuring) element used to determine the neighborhood + of each evaluated pixel (``True`` denotes a connected pixel). It must + be a boolean array and have the same number of dimensions as `image`. + If neither `footprint` nor `connectivity` are given, all adjacent + pixels are considered as part of the neighborhood. + connectivity : int + A number used to determine the neighborhood of each evaluated pixel. + Adjacent pixels whose squared distance from the center is less than or + equal to `connectivity` are considered neighbors. Ignored if + `footprint` is not None. + ndim : int + Number of dimensions `footprint` ought to have. + enforce_adjacency : bool + A boolean that determines whether footprint must only specify direct + neighbors. + + Returns + ------- + footprint : ndarray + Validated or new footprint specifying the neighborhood. + + Examples + -------- + >>> _resolve_neighborhood(None, 1, 2) + array([[False, True, False], + [ True, True, True], + [False, True, False]]) + >>> _resolve_neighborhood(None, None, 3).shape + (3, 3, 3) + """ + if footprint is None: + if connectivity is None: + connectivity = ndim + footprint = ndi.generate_binary_structure(ndim, connectivity) + else: + # Validate custom structured element + footprint = np.asarray(footprint, dtype=bool) + # Must specify neighbors for all dimensions + if footprint.ndim != ndim: + raise ValueError( + "number of dimensions in image and footprint do not" "match" + ) + # Must only specify direct neighbors + if enforce_adjacency and any(s != 3 for s in footprint.shape): + raise ValueError("dimension size in footprint is not 3") + elif any((s % 2 != 1) for s in footprint.shape): + raise ValueError("footprint size must be odd along all dimensions") + + return footprint + + +def _set_border_values(image, value, border_width=1): + """Set edge values along all axes to a constant value. + + Parameters + ---------- + image : ndarray + The array to modify inplace. + value : scalar + The value to use. Should be compatible with `image`'s dtype. + border_width : int or sequence of tuples + A sequence with one 2-tuple per axis where the first and second values + are the width of the border at the start and end of the axis, + respectively. If an int is provided, a uniform border width along all + axes is used. + + Examples + -------- + >>> image = np.zeros((4, 5), dtype=int) + >>> _set_border_values(image, 1) + >>> image + array([[1, 1, 1, 1, 1], + [1, 0, 0, 0, 1], + [1, 0, 0, 0, 1], + [1, 1, 1, 1, 1]]) + >>> image = np.zeros((8, 8), dtype=int) + >>> _set_border_values(image, 1, border_width=((1, 1), (2, 3))) + >>> image + array([[1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1]]) + """ + if np.isscalar(border_width): + border_width = ((border_width, border_width),) * image.ndim + elif len(border_width) != image.ndim: + raise ValueError('length of `border_width` must match image.ndim') + for axis, npad in enumerate(border_width): + if len(npad) != 2: + raise ValueError('each sequence in `border_width` must have ' 'length 2') + w_start, w_end = npad + if w_start == w_end == 0: + continue + elif w_start == w_end == 1: + # Index first and last element in the current dimension + sl = (slice(None),) * axis + ((0, -1),) + (...,) + image[sl] = value + continue + if w_start > 0: + # set first w_start entries along axis to value + sl = (slice(None),) * axis + (slice(0, w_start),) + (...,) + image[sl] = value + if w_end > 0: + # set last w_end entries along axis to value + sl = (slice(None),) * axis + (slice(-w_end, None),) + (...,) + image[sl] = value diff --git a/lib/python3.10/site-packages/skimage/morphology/binary.py b/lib/python3.10/site-packages/skimage/morphology/binary.py new file mode 100644 index 0000000000000000000000000000000000000000..f3f9bc06a75aab26945e9623315e2b605c1169b2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/binary.py @@ -0,0 +1,320 @@ +""" +Binary morphological operations +""" + +import numpy as np +from scipy import ndimage as ndi + +from .footprints import _footprint_is_sequence, pad_footprint +from .misc import default_footprint + + +def _iterate_binary_func(binary_func, image, footprint, out, border_value): + """Helper to call `binary_func` for each footprint in a sequence. + + binary_func is a binary morphology function that accepts "structure", + "output" and "iterations" keyword arguments + (e.g. `scipy.ndimage.binary_erosion`). + """ + fp, num_iter = footprint[0] + binary_func( + image, structure=fp, output=out, iterations=num_iter, border_value=border_value + ) + for fp, num_iter in footprint[1:]: + # Note: out.copy() because the computation cannot be in-place! + # SciPy <= 1.7 did not automatically make a copy if needed. + binary_func( + out.copy(), + structure=fp, + output=out, + iterations=num_iter, + border_value=border_value, + ) + return out + + +# The default_footprint decorator provides a diamond footprint as +# default with the same dimension as the input image and size 3 along each +# axis. +@default_footprint +def binary_erosion(image, footprint=None, out=None, *, mode='ignore'): + """Return fast binary morphological erosion of an image. + + This function returns the same result as grayscale erosion but performs + faster for binary images. + + Morphological erosion sets a pixel at ``(i,j)`` to the minimum over all + pixels in the neighborhood centered at ``(i,j)``. Erosion shrinks bright + regions and enlarges dark regions. + + Parameters + ---------- + image : ndarray + Binary input image. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray of bool, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'max', 'min', 'ignore'. + If 'max' or 'ignore', pixels outside the image domain are assumed + to be `True`, which causes them to not influence the result. + Default is 'ignore'. + + .. versionadded:: 0.23 + `mode` was added in 0.23. + + Returns + ------- + eroded : ndarray of bool or uint + The result of the morphological erosion taking values in + ``[False, True]``. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate a + footprint sequence of this type. + + For even-sized footprints, :func:`skimage.morphology.erosion` and + this function produce an output that differs: one is shifted by one pixel + compared to the other. + + See also + -------- + skimage.morphology.isotropic_erosion + + """ + if out is None: + out = np.empty(image.shape, dtype=bool) + + if mode not in {"max", "min", "ignore"}: + raise ValueError(f"unsupported mode, got {mode!r}") + border_value = False if mode == 'min' else True + + footprint = pad_footprint(footprint, pad_end=True) + if not _footprint_is_sequence(footprint): + footprint = [(footprint, 1)] + + out = _iterate_binary_func( + binary_func=ndi.binary_erosion, + image=image, + footprint=footprint, + out=out, + border_value=border_value, + ) + return out + + +@default_footprint +def binary_dilation(image, footprint=None, out=None, *, mode='ignore'): + """Return fast binary morphological dilation of an image. + + This function returns the same result as grayscale dilation but performs + faster for binary images. + + Morphological dilation sets a pixel at ``(i,j)`` to the maximum over all + pixels in the neighborhood centered at ``(i,j)``. Dilation enlarges bright + regions and shrinks dark regions. + + Parameters + ---------- + image : ndarray + Binary input image. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray of bool, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'max', 'min', 'ignore'. + If 'min' or 'ignore', pixels outside the image domain are assumed + to be `False`, which causes them to not influence the result. + Default is 'ignore'. + + .. versionadded:: 0.23 + `mode` was added in 0.23. + + Returns + ------- + dilated : ndarray of bool or uint + The result of the morphological dilation with values in + ``[False, True]``. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate a + footprint sequence of this type. + + For non-symmetric footprints, :func:`skimage.morphology.binary_dilation` + and :func:`skimage.morphology.dilation` produce an output that differs: + `binary_dilation` mirrors the footprint, whereas `dilation` does not. + + See also + -------- + skimage.morphology.isotropic_dilation + + """ + if out is None: + out = np.empty(image.shape, dtype=bool) + + if mode not in {"max", "min", "ignore"}: + raise ValueError(f"unsupported mode, got {mode!r}") + border_value = True if mode == 'max' else False + + footprint = pad_footprint(footprint, pad_end=True) + if not _footprint_is_sequence(footprint): + footprint = [(footprint, 1)] + + out = _iterate_binary_func( + binary_func=ndi.binary_dilation, + image=image, + footprint=footprint, + out=out, + border_value=border_value, + ) + return out + + +@default_footprint +def binary_opening(image, footprint=None, out=None, *, mode='ignore'): + """Return fast binary morphological opening of an image. + + This function returns the same result as grayscale opening but performs + faster for binary images. + + The morphological opening on an image is defined as an erosion followed by + a dilation. Opening can remove small bright spots (i.e. "salt") and connect + small dark cracks. This tends to "open" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Binary input image. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray of bool, optional + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'max', 'min', 'ignore'. + If 'ignore', pixels outside the image domain are assumed to be `True` + for the erosion and `False` for the dilation, which causes them to not + influence the result. Default is 'ignore'. + + .. versionadded:: 0.23 + `mode` was added in 0.23. + + Returns + ------- + opening : ndarray of bool + The result of the morphological opening. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate a + footprint sequence of this type. + + See also + -------- + skimage.morphology.isotropic_opening + + """ + tmp = binary_erosion(image, footprint, mode=mode) + out = binary_dilation(tmp, footprint, out=out, mode=mode) + return out + + +@default_footprint +def binary_closing(image, footprint=None, out=None, *, mode='ignore'): + """Return fast binary morphological closing of an image. + + This function returns the same result as grayscale closing but performs + faster for binary images. + + The morphological closing on an image is defined as a dilation followed by + an erosion. Closing can remove small dark spots (i.e. "pepper") and connect + small bright cracks. This tends to "close" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Binary input image. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray of bool, optional + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'max', 'min', 'ignore'. + If 'ignore', pixels outside the image domain are assumed to be `True` + for the erosion and `False` for the dilation, which causes them to not + influence the result. Default is 'ignore'. + + .. versionadded:: 0.23 + `mode` was added in 0.23. + + Returns + ------- + closing : ndarray of bool + The result of the morphological closing. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate a + footprint sequence of this type. + + See also + -------- + skimage.morphology.isotropic_closing + + """ + tmp = binary_dilation(image, footprint, mode=mode) + out = binary_erosion(tmp, footprint, out=out, mode=mode) + return out diff --git a/lib/python3.10/site-packages/skimage/morphology/convex_hull.py b/lib/python3.10/site-packages/skimage/morphology/convex_hull.py new file mode 100644 index 0000000000000000000000000000000000000000..5e8563955b180b85ac902a2b5fcaf084fa31cea3 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/convex_hull.py @@ -0,0 +1,222 @@ +"""Convex Hull.""" + +from itertools import product +import numpy as np +from scipy.spatial import ConvexHull, QhullError +from ..measure.pnpoly import grid_points_in_poly +from ._convex_hull import possible_hull +from ..measure._label import label +from ..util import unique_rows +from .._shared.utils import warn + +__all__ = ['convex_hull_image', 'convex_hull_object'] + + +def _offsets_diamond(ndim): + offsets = np.zeros((2 * ndim, ndim)) + for vertex, (axis, offset) in enumerate(product(range(ndim), (-0.5, 0.5))): + offsets[vertex, axis] = offset + return offsets + + +def _check_coords_in_hull(gridcoords, hull_equations, tolerance): + r"""Checks all the coordinates for inclusiveness in the convex hull. + + Parameters + ---------- + gridcoords : (M, N) ndarray + Coordinates of ``N`` points in ``M`` dimensions. + hull_equations : (M, N) ndarray + Hyperplane equations of the facets of the convex hull. + tolerance : float + Tolerance when determining whether a point is inside the hull. Due + to numerical floating point errors, a tolerance of 0 can result in + some points erroneously being classified as being outside the hull. + + Returns + ------- + coords_in_hull : ndarray of bool + Binary 1D ndarray representing points in n-dimensional space + with value ``True`` set for points inside the convex hull. + + Notes + ----- + Checking the inclusiveness of coordinates in a convex hull requires + intermediate calculations of dot products which are memory-intensive. + Thus, the convex hull equations are checked individually with all + coordinates to keep within the memory limit. + + References + ---------- + .. [1] https://github.com/scikit-image/scikit-image/issues/5019 + + """ + ndim, n_coords = gridcoords.shape + n_hull_equations = hull_equations.shape[0] + coords_in_hull = np.ones(n_coords, dtype=bool) + + # Pre-allocate arrays to cache intermediate results for reducing overheads + dot_array = np.empty(n_coords, dtype=np.float64) + test_ineq_temp = np.empty(n_coords, dtype=np.float64) + coords_single_ineq = np.empty(n_coords, dtype=bool) + + # A point is in the hull if it satisfies all of the hull's inequalities + for idx in range(n_hull_equations): + # Tests a hyperplane equation on all coordinates of volume + np.dot(hull_equations[idx, :ndim], gridcoords, out=dot_array) + np.add(dot_array, hull_equations[idx, ndim:], out=test_ineq_temp) + np.less(test_ineq_temp, tolerance, out=coords_single_ineq) + coords_in_hull *= coords_single_ineq + + return coords_in_hull + + +def convex_hull_image( + image, offset_coordinates=True, tolerance=1e-10, include_borders=True +): + """Compute the convex hull image of a binary image. + + The convex hull is the set of pixels included in the smallest convex + polygon that surround all white pixels in the input image. + + Parameters + ---------- + image : array + Binary input image. This array is cast to bool before processing. + offset_coordinates : bool, optional + If ``True``, a pixel at coordinate, e.g., (4, 7) will be represented + by coordinates (3.5, 7), (4.5, 7), (4, 6.5), and (4, 7.5). This adds + some "extent" to a pixel when computing the hull. + tolerance : float, optional + Tolerance when determining whether a point is inside the hull. Due + to numerical floating point errors, a tolerance of 0 can result in + some points erroneously being classified as being outside the hull. + include_borders: bool, optional + If ``False``, vertices/edges are excluded from the final hull mask. + + Returns + ------- + hull : (M, N) array of bool + Binary image with pixels in convex hull set to True. + + References + ---------- + .. [1] https://blogs.mathworks.com/steve/2011/10/04/binary-image-convex-hull-algorithm-notes/ + + """ + ndim = image.ndim + if np.count_nonzero(image) == 0: + warn( + "Input image is entirely zero, no valid convex hull. " + "Returning empty image", + UserWarning, + ) + return np.zeros(image.shape, dtype=bool) + # In 2D, we do an optimisation by choosing only pixels that are + # the starting or ending pixel of a row or column. This vastly + # limits the number of coordinates to examine for the virtual hull. + if ndim == 2: + coords = possible_hull(np.ascontiguousarray(image, dtype=np.uint8)) + else: + coords = np.transpose(np.nonzero(image)) + if offset_coordinates: + # when offsetting, we multiply number of vertices by 2 * ndim. + # therefore, we reduce the number of coordinates by using a + # convex hull on the original set, before offsetting. + try: + hull0 = ConvexHull(coords) + except QhullError as err: + warn( + f"Failed to get convex hull image. " + f"Returning empty image, see error message below:\n" + f"{err}" + ) + return np.zeros(image.shape, dtype=bool) + coords = hull0.points[hull0.vertices] + + # Add a vertex for the middle of each pixel edge + if offset_coordinates: + offsets = _offsets_diamond(image.ndim) + coords = (coords[:, np.newaxis, :] + offsets).reshape(-1, ndim) + + # repeated coordinates can *sometimes* cause problems in + # scipy.spatial.ConvexHull, so we remove them. + coords = unique_rows(coords) + + # Find the convex hull + try: + hull = ConvexHull(coords) + except QhullError as err: + warn( + f"Failed to get convex hull image. " + f"Returning empty image, see error message below:\n" + f"{err}" + ) + return np.zeros(image.shape, dtype=bool) + vertices = hull.points[hull.vertices] + + # If 2D, use fast Cython function to locate convex hull pixels + if ndim == 2: + labels = grid_points_in_poly(image.shape, vertices, binarize=False) + # If include_borders is True, we include vertices (2) and edge + # points (3) in the mask, otherwise only the inside of the hull (1) + mask = labels >= 1 if include_borders else labels == 1 + else: + gridcoords = np.reshape(np.mgrid[tuple(map(slice, image.shape))], (ndim, -1)) + + coords_in_hull = _check_coords_in_hull(gridcoords, hull.equations, tolerance) + mask = np.reshape(coords_in_hull, image.shape) + + return mask + + +def convex_hull_object(image, *, connectivity=2): + r"""Compute the convex hull image of individual objects in a binary image. + + The convex hull is the set of pixels included in the smallest convex + polygon that surround all white pixels in the input image. + + Parameters + ---------- + image : (M, N) ndarray + Binary input image. + connectivity : {1, 2}, int, optional + Determines the neighbors of each pixel. Adjacent elements + within a squared distance of ``connectivity`` from pixel center + are considered neighbors.:: + + 1-connectivity 2-connectivity + [ ] [ ] [ ] [ ] + | \ | / + [ ]--[x]--[ ] [ ]--[x]--[ ] + | / | \ + [ ] [ ] [ ] [ ] + + Returns + ------- + hull : ndarray of bool + Binary image with pixels inside convex hull set to ``True``. + + Notes + ----- + This function uses ``skimage.morphology.label`` to define unique objects, + finds the convex hull of each using ``convex_hull_image``, and combines + these regions with logical OR. Be aware the convex hulls of unconnected + objects may overlap in the result. If this is suspected, consider using + convex_hull_image separately on each object or adjust ``connectivity``. + """ + if image.ndim > 2: + raise ValueError("Input must be a 2D image") + + if connectivity not in (1, 2): + raise ValueError('`connectivity` must be either 1 or 2.') + + labeled_im = label(image, connectivity=connectivity, background=0) + convex_obj = np.zeros(image.shape, dtype=bool) + convex_img = np.zeros(image.shape, dtype=bool) + + for i in range(1, labeled_im.max() + 1): + convex_obj = convex_hull_image(labeled_im == i) + convex_img = np.logical_or(convex_img, convex_obj) + + return convex_img diff --git a/lib/python3.10/site-packages/skimage/morphology/extrema.py b/lib/python3.10/site-packages/skimage/morphology/extrema.py new file mode 100644 index 0000000000000000000000000000000000000000..a87cef0c0a2e335bf6bfe773c0660a9f6451ba0a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/extrema.py @@ -0,0 +1,549 @@ +"""extrema.py - local minima and maxima + +This module provides functions to find local maxima and minima of an image. +Here, local maxima (minima) are defined as connected sets of pixels with equal +gray level which is strictly greater (smaller) than the gray level of all +pixels in direct neighborhood of the connected set. In addition, the module +provides the related functions h-maxima and h-minima. + +Soille, P. (2003). Morphological Image Analysis: Principles and Applications +(2nd ed.), Chapter 6. Springer-Verlag New York, Inc. +""" + +import numpy as np + +from .._shared.utils import warn +from ..util import dtype_limits, invert, crop +from . import grayreconstruct, _util +from ._extrema_cy import _local_maxima + + +def _add_constant_clip(image, const_value): + """Add constant to the image while handling overflow issues gracefully.""" + min_dtype, max_dtype = dtype_limits(image, clip_negative=False) + + if const_value > (max_dtype - min_dtype): + raise ValueError( + "The added constant is not compatible" "with the image data type." + ) + + result = image + const_value + result[image > max_dtype - const_value] = max_dtype + return result + + +def _subtract_constant_clip(image, const_value): + """Subtract constant from image while handling underflow issues.""" + min_dtype, max_dtype = dtype_limits(image, clip_negative=False) + + if const_value > (max_dtype - min_dtype): + raise ValueError( + "The subtracted constant is not compatible" "with the image data type." + ) + + result = image - const_value + result[image < (const_value + min_dtype)] = min_dtype + return result + + +def h_maxima(image, h, footprint=None): + """Determine all maxima of the image with height >= h. + + The local maxima are defined as connected sets of pixels with equal + gray level strictly greater than the gray level of all pixels in direct + neighborhood of the set. + + A local maximum M of height h is a local maximum for which + there is at least one path joining M with an equal or higher local maximum + on which the minimal value is f(M) - h (i.e. the values along the path + are not decreasing by more than h with respect to the maximum's value) + and no path to an equal or higher local maximum for which the minimal + value is greater. + + The global maxima of the image are also found by this function. + + Parameters + ---------- + image : ndarray + The input image for which the maxima are to be calculated. + h : unsigned integer + The minimal height of all extracted maxima. + footprint : ndarray, optional + The neighborhood expressed as an n-D array of 1's and 0's. + Default is the ball of radius 1 according to the maximum norm + (i.e. a 3x3 square for 2D images, a 3x3x3 cube for 3D images, etc.) + + Returns + ------- + h_max : ndarray + The local maxima of height >= h and the global maxima. + The resulting image is a binary image, where pixels belonging to + the determined maxima take value 1, the others take value 0. + + See Also + -------- + skimage.morphology.h_minima + skimage.morphology.local_maxima + skimage.morphology.local_minima + + References + ---------- + .. [1] Soille, P., "Morphological Image Analysis: Principles and + Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883. + + Examples + -------- + >>> import numpy as np + >>> from skimage.morphology import extrema + + We create an image (quadratic function with a maximum in the center and + 4 additional constant maxima. + The heights of the maxima are: 1, 21, 41, 61, 81 + + >>> w = 10 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:4,2:4] = 40; f[2:4,7:9] = 60; f[7:9,2:4] = 80; f[7:9,7:9] = 100 + >>> f = f.astype(int) + + We can calculate all maxima with a height of at least 40: + + >>> maxima = extrema.h_maxima(f, 40) + + The resulting image will contain 3 local maxima. + """ + + # Check for h value that is larger then range of the image. If this + # is True then there are no h-maxima in the image. + if h > np.ptp(image): + return np.zeros(image.shape, dtype=np.uint8) + + # Check for floating point h value. For this to work properly + # we need to explicitly convert image to float64. + # + # FIXME: This could give incorrect results if image is int64 and + # has a very high dynamic range. The dtype of image is + # changed to float64, and different integer values could + # become the same float due to rounding. + # + # >>> ii64 = np.iinfo(np.int64) + # >>> a = np.array([ii64.max, ii64.max - 2]) + # >>> a[0] == a[1] + # False + # >>> b = a.astype(np.float64) + # >>> b[0] == b[1] + # True + # + if np.issubdtype(type(h), np.floating) and np.issubdtype(image.dtype, np.integer): + if (h % 1) != 0: + warn( + 'possible precision loss converting image to ' + 'floating point. To silence this warning, ' + 'ensure image and h have same data type.', + stacklevel=2, + ) + image = image.astype(float) + else: + h = image.dtype.type(h) + + if h == 0: + raise ValueError("h = 0 is ambiguous, use local_maxima() " "instead?") + + if np.issubdtype(image.dtype, np.floating): + # The purpose of the resolution variable is to allow for the + # small rounding errors that inevitably occur when doing + # floating point arithmetic. We want shifted_img to be + # guaranteed to be h less than image. If we only subtract h + # there may be pixels were shifted_img ends up being + # slightly greater than image - h. + # + # The resolution is scaled based on the pixel values in the + # image because floating point precision is relative. A + # very large value of 1.0e10 will have a large precision, + # say +-1.0e4, and a very small value of 1.0e-10 will have + # a very small precision, say +-1.0e-16. + # + resolution = 2 * np.finfo(image.dtype).resolution * np.abs(image) + shifted_img = image - h - resolution + else: + shifted_img = _subtract_constant_clip(image, h) + + rec_img = grayreconstruct.reconstruction( + shifted_img, image, method='dilation', footprint=footprint + ) + residue_img = image - rec_img + return (residue_img >= h).astype(np.uint8) + + +def h_minima(image, h, footprint=None): + """Determine all minima of the image with depth >= h. + + The local minima are defined as connected sets of pixels with equal + gray level strictly smaller than the gray levels of all pixels in direct + neighborhood of the set. + + A local minimum M of depth h is a local minimum for which + there is at least one path joining M with an equal or lower local minimum + on which the maximal value is f(M) + h (i.e. the values along the path + are not increasing by more than h with respect to the minimum's value) + and no path to an equal or lower local minimum for which the maximal + value is smaller. + + The global minima of the image are also found by this function. + + Parameters + ---------- + image : ndarray + The input image for which the minima are to be calculated. + h : unsigned integer + The minimal depth of all extracted minima. + footprint : ndarray, optional + The neighborhood expressed as an n-D array of 1's and 0's. + Default is the ball of radius 1 according to the maximum norm + (i.e. a 3x3 square for 2D images, a 3x3x3 cube for 3D images, etc.) + + Returns + ------- + h_min : ndarray + The local minima of depth >= h and the global minima. + The resulting image is a binary image, where pixels belonging to + the determined minima take value 1, the others take value 0. + + See Also + -------- + skimage.morphology.h_maxima + skimage.morphology.local_maxima + skimage.morphology.local_minima + + References + ---------- + .. [1] Soille, P., "Morphological Image Analysis: Principles and + Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883. + + Examples + -------- + >>> import numpy as np + >>> from skimage.morphology import extrema + + We create an image (quadratic function with a minimum in the center and + 4 additional constant maxima. + The depth of the minima are: 1, 21, 41, 61, 81 + + >>> w = 10 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:4,2:4] = 160; f[2:4,7:9] = 140; f[7:9,2:4] = 120; f[7:9,7:9] = 100 + >>> f = f.astype(int) + + We can calculate all minima with a depth of at least 40: + + >>> minima = extrema.h_minima(f, 40) + + The resulting image will contain 3 local minima. + """ + if h > np.ptp(image): + return np.zeros(image.shape, dtype=np.uint8) + + if np.issubdtype(type(h), np.floating) and np.issubdtype(image.dtype, np.integer): + if (h % 1) != 0: + warn( + 'possible precision loss converting image to ' + 'floating point. To silence this warning, ' + 'ensure image and h have same data type.', + stacklevel=2, + ) + image = image.astype(float) + else: + h = image.dtype.type(h) + + if h == 0: + raise ValueError("h = 0 is ambiguous, use local_minima() " "instead?") + + if np.issubdtype(image.dtype, np.floating): + resolution = 2 * np.finfo(image.dtype).resolution * np.abs(image) + shifted_img = image + h + resolution + else: + shifted_img = _add_constant_clip(image, h) + + rec_img = grayreconstruct.reconstruction( + shifted_img, image, method='erosion', footprint=footprint + ) + residue_img = rec_img - image + return (residue_img >= h).astype(np.uint8) + + +def local_maxima( + image, footprint=None, connectivity=None, indices=False, allow_borders=True +): + """Find local maxima of n-dimensional array. + + The local maxima are defined as connected sets of pixels with equal gray + level (plateaus) strictly greater than the gray levels of all pixels in the + neighborhood. + + Parameters + ---------- + image : ndarray + An n-dimensional array. + footprint : ndarray, optional + The footprint (structuring element) used to determine the neighborhood + of each evaluated pixel (``True`` denotes a connected pixel). It must + be a boolean array and have the same number of dimensions as `image`. + If neither `footprint` nor `connectivity` are given, all adjacent + pixels are considered as part of the neighborhood. + connectivity : int, optional + A number used to determine the neighborhood of each evaluated pixel. + Adjacent pixels whose squared distance from the center is less than or + equal to `connectivity` are considered neighbors. Ignored if + `footprint` is not None. + indices : bool, optional + If True, the output will be a tuple of one-dimensional arrays + representing the indices of local maxima in each dimension. If False, + the output will be a boolean array with the same shape as `image`. + allow_borders : bool, optional + If true, plateaus that touch the image border are valid maxima. + + Returns + ------- + maxima : ndarray or tuple[ndarray] + If `indices` is false, a boolean array with the same shape as `image` + is returned with ``True`` indicating the position of local maxima + (``False`` otherwise). If `indices` is true, a tuple of one-dimensional + arrays containing the coordinates (indices) of all found maxima. + + Warns + ----- + UserWarning + If `allow_borders` is false and any dimension of the given `image` is + shorter than 3 samples, maxima can't exist and a warning is shown. + + See Also + -------- + skimage.morphology.local_minima + skimage.morphology.h_maxima + skimage.morphology.h_minima + + Notes + ----- + This function operates on the following ideas: + + 1. Make a first pass over the image's last dimension and flag candidates + for local maxima by comparing pixels in only one direction. + If the pixels aren't connected in the last dimension all pixels are + flagged as candidates instead. + + For each candidate: + + 2. Perform a flood-fill to find all connected pixels that have the same + gray value and are part of the plateau. + 3. Consider the connected neighborhood of a plateau: if no bordering sample + has a higher gray level, mark the plateau as a definite local maximum. + + Examples + -------- + >>> from skimage.morphology import local_maxima + >>> image = np.zeros((4, 7), dtype=int) + >>> image[1:3, 1:3] = 1 + >>> image[3, 0] = 1 + >>> image[1:3, 4:6] = 2 + >>> image[3, 6] = 3 + >>> image + array([[0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3]]) + + Find local maxima by comparing to all neighboring pixels (maximal + connectivity): + + >>> local_maxima(image) + array([[False, False, False, False, False, False, False], + [False, True, True, False, False, False, False], + [False, True, True, False, False, False, False], + [ True, False, False, False, False, False, True]]) + >>> local_maxima(image, indices=True) + (array([1, 1, 2, 2, 3, 3]), array([1, 2, 1, 2, 0, 6])) + + Find local maxima without comparing to diagonal pixels (connectivity 1): + + >>> local_maxima(image, connectivity=1) + array([[False, False, False, False, False, False, False], + [False, True, True, False, True, True, False], + [False, True, True, False, True, True, False], + [ True, False, False, False, False, False, True]]) + + and exclude maxima that border the image edge: + + >>> local_maxima(image, connectivity=1, allow_borders=False) + array([[False, False, False, False, False, False, False], + [False, True, True, False, True, True, False], + [False, True, True, False, True, True, False], + [False, False, False, False, False, False, False]]) + """ + image = np.asarray(image, order="C") + if image.size == 0: + # Return early for empty input + if indices: + # Make sure that output is a tuple of 1 empty array per dimension + return np.nonzero(image) + else: + return np.zeros(image.shape, dtype=bool) + + if allow_borders: + # Ensure that local maxima are always at least one smaller sample away + # from the image border + image = np.pad(image, 1, mode='constant', constant_values=image.min()) + + # Array of flags used to store the state of each pixel during evaluation. + # See _extrema_cy.pyx for their meaning + flags = np.zeros(image.shape, dtype=np.uint8) + _util._set_border_values(flags, value=3) + + if any(s < 3 for s in image.shape): + # Warn and skip if any dimension is smaller than 3 + # -> no maxima can exist & footprint can't be applied + warn( + "maxima can't exist for an image with any dimension smaller 3 " + "if borders aren't allowed", + stacklevel=3, + ) + else: + footprint = _util._resolve_neighborhood(footprint, connectivity, image.ndim) + neighbor_offsets = _util._offsets_to_raveled_neighbors( + image.shape, footprint, center=((1,) * image.ndim) + ) + + try: + _local_maxima(image.ravel(), flags.ravel(), neighbor_offsets) + except TypeError: + if image.dtype == np.float16: + # Provide the user with clearer error message + raise TypeError( + "dtype of `image` is float16 which is not " + "supported, try upcasting to float32" + ) + else: + raise # Otherwise raise original message + + if allow_borders: + # Revert padding performed at the beginning of the function + flags = crop(flags, 1) + else: + # No padding was performed but set edge values back to 0 + _util._set_border_values(flags, value=0) + + if indices: + return np.nonzero(flags) + else: + return flags.view(bool) + + +def local_minima( + image, footprint=None, connectivity=None, indices=False, allow_borders=True +): + """Find local minima of n-dimensional array. + + The local minima are defined as connected sets of pixels with equal gray + level (plateaus) strictly smaller than the gray levels of all pixels in the + neighborhood. + + Parameters + ---------- + image : ndarray + An n-dimensional array. + footprint : ndarray, optional + The footprint (structuring element) used to determine the neighborhood + of each evaluated pixel (``True`` denotes a connected pixel). It must + be a boolean array and have the same number of dimensions as `image`. + If neither `footprint` nor `connectivity` are given, all adjacent + pixels are considered as part of the neighborhood. + connectivity : int, optional + A number used to determine the neighborhood of each evaluated pixel. + Adjacent pixels whose squared distance from the center is less than or + equal to `connectivity` are considered neighbors. Ignored if + `footprint` is not None. + indices : bool, optional + If True, the output will be a tuple of one-dimensional arrays + representing the indices of local minima in each dimension. If False, + the output will be a boolean array with the same shape as `image`. + allow_borders : bool, optional + If true, plateaus that touch the image border are valid minima. + + Returns + ------- + minima : ndarray or tuple[ndarray] + If `indices` is false, a boolean array with the same shape as `image` + is returned with ``True`` indicating the position of local minima + (``False`` otherwise). If `indices` is true, a tuple of one-dimensional + arrays containing the coordinates (indices) of all found minima. + + See Also + -------- + skimage.morphology.local_maxima + skimage.morphology.h_maxima + skimage.morphology.h_minima + + Notes + ----- + This function operates on the following ideas: + + 1. Make a first pass over the image's last dimension and flag candidates + for local minima by comparing pixels in only one direction. + If the pixels aren't connected in the last dimension all pixels are + flagged as candidates instead. + + For each candidate: + + 2. Perform a flood-fill to find all connected pixels that have the same + gray value and are part of the plateau. + 3. Consider the connected neighborhood of a plateau: if no bordering sample + has a smaller gray level, mark the plateau as a definite local minimum. + + Examples + -------- + >>> from skimage.morphology import local_minima + >>> image = np.zeros((4, 7), dtype=int) + >>> image[1:3, 1:3] = -1 + >>> image[3, 0] = -1 + >>> image[1:3, 4:6] = -2 + >>> image[3, 6] = -3 + >>> image + array([[ 0, 0, 0, 0, 0, 0, 0], + [ 0, -1, -1, 0, -2, -2, 0], + [ 0, -1, -1, 0, -2, -2, 0], + [-1, 0, 0, 0, 0, 0, -3]]) + + Find local minima by comparing to all neighboring pixels (maximal + connectivity): + + >>> local_minima(image) + array([[False, False, False, False, False, False, False], + [False, True, True, False, False, False, False], + [False, True, True, False, False, False, False], + [ True, False, False, False, False, False, True]]) + >>> local_minima(image, indices=True) + (array([1, 1, 2, 2, 3, 3]), array([1, 2, 1, 2, 0, 6])) + + Find local minima without comparing to diagonal pixels (connectivity 1): + + >>> local_minima(image, connectivity=1) + array([[False, False, False, False, False, False, False], + [False, True, True, False, True, True, False], + [False, True, True, False, True, True, False], + [ True, False, False, False, False, False, True]]) + + and exclude minima that border the image edge: + + >>> local_minima(image, connectivity=1, allow_borders=False) + array([[False, False, False, False, False, False, False], + [False, True, True, False, True, True, False], + [False, True, True, False, True, True, False], + [False, False, False, False, False, False, False]]) + """ + return local_maxima( + image=invert(image, signed_float=True), + footprint=footprint, + connectivity=connectivity, + indices=indices, + allow_borders=allow_borders, + ) diff --git a/lib/python3.10/site-packages/skimage/morphology/footprints.py b/lib/python3.10/site-packages/skimage/morphology/footprints.py new file mode 100644 index 0000000000000000000000000000000000000000..e7d77d18af2072643a956483b89eabdf45944a4b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/footprints.py @@ -0,0 +1,1110 @@ +import os +import warnings +from collections.abc import Sequence +from numbers import Integral + +import numpy as np + +from .. import draw +from skimage import morphology +from .._shared.utils import deprecate_func + + +# Precomputed ball and disk decompositions were saved as 2D arrays where the +# radius of the desired decomposition is used to index into the first axis of +# the array. The values at a given radius corresponds to the number of +# repetitions of 3 different types elementary of structuring elements. +# +# See _nsphere_series_decomposition for full details. +_nsphere_decompositions = {} +_nsphere_decompositions[2] = np.load( + os.path.join(os.path.dirname(__file__), 'disk_decompositions.npy') +) +_nsphere_decompositions[3] = np.load( + os.path.join(os.path.dirname(__file__), 'ball_decompositions.npy') +) + + +def _footprint_is_sequence(footprint): + if hasattr(footprint, '__array_interface__'): + return False + + def _validate_sequence_element(t): + return ( + isinstance(t, Sequence) + and len(t) == 2 + and hasattr(t[0], '__array_interface__') + and isinstance(t[1], Integral) + ) + + if isinstance(footprint, Sequence): + if not all(_validate_sequence_element(t) for t in footprint): + raise ValueError( + "All elements of footprint sequence must be a 2-tuple where " + "the first element of the tuple is an ndarray and the second " + "is an integer indicating the number of iterations." + ) + else: + raise ValueError("footprint must be either an ndarray or Sequence") + return True + + +def _shape_from_sequence(footprints, require_odd_size=False): + """Determine the shape of composite footprint + + In the future if we only want to support odd-sized square, we may want to + change this to require_odd_size + """ + if not _footprint_is_sequence(footprints): + raise ValueError("expected a sequence of footprints") + ndim = footprints[0][0].ndim + shape = [0] * ndim + + def _odd_size(size, require_odd_size): + if require_odd_size and size % 2 == 0: + raise ValueError("expected all footprint elements to have odd size") + + for d in range(ndim): + fp, nreps = footprints[0] + _odd_size(fp.shape[d], require_odd_size) + shape[d] = fp.shape[d] + (nreps - 1) * (fp.shape[d] - 1) + for fp, nreps in footprints[1:]: + _odd_size(fp.shape[d], require_odd_size) + shape[d] += nreps * (fp.shape[d] - 1) + return tuple(shape) + + +def footprint_from_sequence(footprints): + """Convert a footprint sequence into an equivalent ndarray. + + Parameters + ---------- + footprints : tuple of 2-tuples + A sequence of footprint tuples where the first element of each tuple + is an array corresponding to a footprint and the second element is the + number of times it is to be applied. Currently, all footprints should + have odd size. + + Returns + ------- + footprint : ndarray + An single array equivalent to applying the sequence of ``footprints``. + """ + + # Create a single pixel image of sufficient size and apply binary dilation. + shape = _shape_from_sequence(footprints) + imag = np.zeros(shape, dtype=bool) + imag[tuple(s // 2 for s in shape)] = 1 + return morphology.binary_dilation(imag, footprints) + + +def footprint_rectangle(shape, *, dtype=np.uint8, decomposition=None): + """Generate a rectangular or hyper-rectangular footprint. + + Generates, depending on the length and dimensions requested with `shape`, + a square, rectangle, cube, cuboid, or even higher-dimensional versions + of these shapes. + + Parameters + ---------- + shape : tuple[int, ...] + The length of the footprint in each dimension. The length of the + sequence determines the number of dimensions of the footprint. + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'separable', 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + give an identical result to a single, larger footprint, but often with + better computational performance. See Notes for more details. + With 'separable', this function uses separable 1D footprints for each + axis. Whether 'sequence' or 'separable' is computationally faster may + be architecture-dependent. + + Returns + ------- + footprint : array or tuple[tuple[ndarray, int], ...] + A footprint consisting only of ones, i.e. every pixel belongs to the + neighborhood. When `decomposition` is None, this is just an array. + Otherwise, this will be a tuple whose length is equal to the number of + unique structuring elements to apply (see Examples for more detail). + + Examples + -------- + >>> import skimage as ski + >>> ski.morphology.footprint_rectangle((3, 5)) + array([[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], dtype=uint8) + + Decomposition will return multiple footprints that combine into a simple + footprint of the requested shape. + + >>> ski.morphology.footprint_rectangle((9, 9), decomposition="sequence") + ((array([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=uint8), + 4),) + + `"sequence"` makes sure that the decomposition only returns 1D footprints. + + >>> ski.morphology.footprint_rectangle((3, 5), decomposition="separable") + ((array([[1], + [1], + [1]], dtype=uint8), + 1), + (array([[1, 1, 1, 1, 1]], dtype=uint8), 1)) + + Generate a 5-dimensional hypercube with 3 samples in each dimension + + >>> ski.morphology.footprint_rectangle((3,) * 5).shape + (3, 3, 3, 3, 3) + """ + has_even_width = any(width % 2 == 0 for width in shape) + if decomposition == "sequence" and has_even_width: + warnings.warn( + "decomposition='sequence' is only supported for uneven footprints, " + "falling back to decomposition='separable'", + stacklevel=2, + ) + decomposition = "sequence_fallback" + + def partial_footprint(dim, width): + shape_ = (1,) * dim + (width,) + (1,) * (len(shape) - dim - 1) + fp = (np.ones(shape_, dtype=dtype), 1) + return fp + + if decomposition is None: + footprint = np.ones(shape, dtype=dtype) + + elif decomposition in ("separable", "sequence_fallback"): + footprint = tuple( + partial_footprint(dim, width) for dim, width in enumerate(shape) + ) + + elif decomposition == "sequence": + min_width = min(shape) + sq_reps = _decompose_size(min_width, 3) + footprint = [(np.ones((3,) * len(shape), dtype=dtype), sq_reps)] + for dim, width in enumerate(shape): + if width > min_width: + nextra = width - min_width + 1 + component = partial_footprint(dim, nextra) + footprint.append(component) + footprint = tuple(footprint) + + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + + return footprint + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Use `skimage.morphology.footprint_rectangle` instead.", +) +def square(width, dtype=np.uint8, *, decomposition=None): + """Generates a flat, square-shaped footprint. + + Every pixel along the perimeter has a chessboard distance + no greater than radius (radius=floor(width/2)) pixels. + + Parameters + ---------- + width : int + The width and height of the square. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'separable', 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + give an identical result to a single, larger footprint, but often with + better computational performance. See Notes for more details. + With 'separable', this function uses separable 1D footprints for each + axis. Whether 'sequence' or 'separable' is computationally faster may + be architecture-dependent. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + When `decomposition` is None, this is just a numpy.ndarray. Otherwise, + this will be a tuple whose length is equal to the number of unique + structuring elements to apply (see Notes for more detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For binary morphology, using ``decomposition='sequence'`` or + ``decomposition='separable'`` were observed to give better performance than + ``decomposition=None``, with the magnitude of the performance increase + rapidly increasing with footprint size. For grayscale morphology with + square footprints, it is recommended to use ``decomposition=None`` since + the internal SciPy functions that are called already have a fast + implementation based on separable 1D sliding windows. + + The 'sequence' decomposition mode only supports odd valued `width`. If + `width` is even, the sequence used will be identical to the 'separable' + mode. + """ + footprint = footprint_rectangle( + shape=(width, width), dtype=dtype, decomposition=decomposition + ) + return footprint + + +def _decompose_size(size, kernel_size=3): + """Determine number of repeated iterations for a `kernel_size` kernel. + + Returns how many repeated morphology operations with an element of size + `kernel_size` is equivalent to a morphology with a single kernel of size + `n`. + + """ + if kernel_size % 2 != 1: + raise ValueError("only odd length kernel_size is supported") + return 1 + (size - kernel_size) // (kernel_size - 1) + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Use `skimage.morphology.footprint_rectangle` instead.", +) +def rectangle(nrows, ncols, dtype=np.uint8, *, decomposition=None): + """Generates a flat, rectangular-shaped footprint. + + Every pixel in the rectangle generated for a given width and given height + belongs to the neighborhood. + + Parameters + ---------- + nrows : int + The number of rows of the rectangle. + ncols : int + The number of columns of the rectangle. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'separable', 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but often with + better computational performance. See Notes for more details. + With 'separable', this function uses separable 1D footprints for each + axis. Whether 'sequence' or 'separable' is computationally faster may + be architecture-dependent. + + Returns + ------- + footprint : ndarray or tuple + A footprint consisting only of ones, i.e. every pixel belongs to the + neighborhood. When `decomposition` is None, this is just a + numpy.ndarray. Otherwise, this will be a tuple whose length is equal to + the number of unique structuring elements to apply (see Notes for more + detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For binary morphology, using ``decomposition='sequence'`` + was observed to give better performance, with the magnitude of the + performance increase rapidly increasing with footprint size. For grayscale + morphology with rectangular footprints, it is recommended to use + ``decomposition=None`` since the internal SciPy functions that are called + already have a fast implementation based on separable 1D sliding windows. + + The `sequence` decomposition mode only supports odd valued `nrows` and + `ncols`. If either `nrows` or `ncols` is even, the sequence used will be + identical to ``decomposition='separable'``. + + - The use of ``width`` and ``height`` has been deprecated in + version 0.18.0. Use ``nrows`` and ``ncols`` instead. + """ + footprint = footprint_rectangle( + shape=(nrows, ncols), dtype=dtype, decomposition=decomposition + ) + return footprint + + +def diamond(radius, dtype=np.uint8, *, decomposition=None): + """Generates a flat, diamond-shaped footprint. + + A pixel is part of the neighborhood (i.e. labeled 1) if + the city block/Manhattan distance between it and the center of + the neighborhood is no greater than radius. + + Parameters + ---------- + radius : int + The radius of the diamond-shaped footprint. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but with + better computational performance. See Notes for more details. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + When `decomposition` is None, this is just a numpy.ndarray. Otherwise, + this will be a tuple whose length is equal to the number of unique + structuring elements to apply (see Notes for more detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For either binary or grayscale morphology, using + ``decomposition='sequence'`` was observed to have a performance benefit, + with the magnitude of the benefit increasing with increasing footprint + size. + + """ + if decomposition is None: + L = np.arange(0, radius * 2 + 1) + I, J = np.meshgrid(L, L) + footprint = np.array( + np.abs(I - radius) + np.abs(J - radius) <= radius, dtype=dtype + ) + elif decomposition == 'sequence': + fp = diamond(1, dtype=dtype, decomposition=None) + nreps = _decompose_size(2 * radius + 1, fp.shape[0]) + footprint = ((fp, nreps),) + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + return footprint + + +def _nsphere_series_decomposition(radius, ndim, dtype=np.uint8): + """Generate a sequence of footprints approximating an n-sphere. + + Morphological operations with an n-sphere (hypersphere) footprint can be + approximated by applying a series of smaller footprints of extent 3 along + each axis. Specific solutions for this are given in [1]_ for the case of + 2D disks with radius 2 through 10. + + Here we used n-dimensional extensions of the "square", "diamond" and + "t-shaped" elements from that publication. All of these elementary elements + have size ``(3,) * ndim``. We numerically computed the number of + repetitions of each element that gives the closest match to the disk + (in 2D) or ball (in 3D) computed with ``decomposition=None``. + + The approach can be extended to higher dimensions, but we have only stored + results for 2D and 3D at this point. + + Empirically, the shapes at large radius approach a hexadecagon + (16-sides [2]_) in 2D and a rhombicuboctahedron (26-faces, [3]_) in 3D. + + References + ---------- + .. [1] Park, H and Chin R.T. Decomposition of structuring elements for + optimal implementation of morphological operations. In Proceedings: + 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London, + UK. + https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf + .. [2] https://en.wikipedia.org/wiki/Hexadecagon + .. [3] https://en.wikipedia.org/wiki/Rhombicuboctahedron + """ + + if radius == 1: + # for radius 1 just use the exact shape (3,) * ndim solution + kwargs = dict(dtype=dtype, strict_radius=False, decomposition=None) + if ndim == 2: + return ((disk(1, **kwargs), 1),) + elif ndim == 3: + return ((ball(1, **kwargs), 1),) + + # load precomputed decompositions + if ndim not in _nsphere_decompositions: + raise ValueError( + "sequence decompositions are only currently available for " + "2d disks or 3d balls" + ) + precomputed_decompositions = _nsphere_decompositions[ndim] + max_radius = precomputed_decompositions.shape[0] + if radius > max_radius: + raise ValueError( + f"precomputed {ndim}D decomposition unavailable for " + f"radius > {max_radius}" + ) + num_t_series, num_diamond, num_square = precomputed_decompositions[radius] + + sequence = [] + if num_t_series > 0: + # shape (3,) * ndim "T-shaped" footprints + all_t = _t_shaped_element_series(ndim=ndim, dtype=dtype) + [sequence.append((t, num_t_series)) for t in all_t] + if num_diamond > 0: + d = np.zeros((3,) * ndim, dtype=dtype) + sl = [slice(1, 2)] * ndim + for ax in range(ndim): + sl[ax] = slice(None) + d[tuple(sl)] = 1 + sl[ax] = slice(1, 2) + sequence.append((d, num_diamond)) + if num_square > 0: + sq = np.ones((3,) * ndim, dtype=dtype) + sequence.append((sq, num_square)) + return tuple(sequence) + + +def _t_shaped_element_series(ndim=2, dtype=np.uint8): + """A series of T-shaped structuring elements. + + In the 2D case this is a T-shaped element and its rotation at multiples of + 90 degrees. This series is used in efficient decompositions of disks of + various radius as published in [1]_. + + The generalization to the n-dimensional case can be performed by having the + "top" of the T to extend in (ndim - 1) dimensions and then producing a + series of rotations such that the bottom end of the T points along each of + ``2 * ndim`` orthogonal directions. + """ + if ndim == 2: + # The n-dimensional case produces the same set of footprints, but + # the 2D example is retained here for clarity. + t0 = np.array([[1, 1, 1], [0, 1, 0], [0, 1, 0]], dtype=dtype) + t90 = np.rot90(t0, 1) + t180 = np.rot90(t0, 2) + t270 = np.rot90(t0, 3) + return t0, t90, t180, t270 + else: + # ndimensional generalization of the 2D case above + all_t = [] + for ax in range(ndim): + for idx in [0, 2]: + t = np.zeros((3,) * ndim, dtype=dtype) + sl = [slice(None)] * ndim + sl[ax] = slice(idx, idx + 1) + t[tuple(sl)] = 1 + sl = [slice(1, 2)] * ndim + sl[ax] = slice(None) + t[tuple(sl)] = 1 + all_t.append(t) + return tuple(all_t) + + +def disk(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None): + """Generates a flat, disk-shaped footprint. + + A pixel is within the neighborhood if the Euclidean distance between + it and the origin is no greater than radius (This is only approximately + True, when `decomposition == 'sequence'`). + + Parameters + ---------- + radius : int + The radius of the disk-shaped footprint. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + strict_radius : bool, optional + If False, extend the radius by 0.5. This allows the circle to expand + further within a cube that remains of size ``2 * radius + 1`` along + each axis. This parameter is ignored if decomposition is not None. + decomposition : {None, 'sequence', 'crosses'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given a result equivalent to a single, larger footprint, but with + better computational performance. For disk footprints, the 'sequence' + or 'crosses' decompositions are not always exactly equivalent to + ``decomposition=None``. See Notes for more details. + + Returns + ------- + footprint : ndarray + The footprint where elements of the neighborhood are 1 and 0 otherwise. + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + The disk produced by the ``decomposition='sequence'`` mode may not be + identical to that with ``decomposition=None``. A disk footprint can be + approximated by applying a series of smaller footprints of extent 3 along + each axis. Specific solutions for this are given in [1]_ for the case of + 2D disks with radius 2 through 10. Here, we numerically computed the number + of repetitions of each element that gives the closest match to the disk + computed with kwargs ``strict_radius=False, decomposition=None``. + + Empirically, the series decomposition at large radius approaches a + hexadecagon (a 16-sided polygon [2]_). In [3]_, the authors demonstrate + that a hexadecagon is the closest approximation to a disk that can be + achieved for decomposition with footprints of shape (3, 3). + + The disk produced by the ``decomposition='crosses'`` is often but not + always identical to that with ``decomposition=None``. It tends to give a + closer approximation than ``decomposition='sequence'``, at a performance + that is fairly comparable. The individual cross-shaped elements are not + limited to extent (3, 3) in size. Unlike the 'seqeuence' decomposition, the + 'crosses' decomposition can also accurately approximate the shape of disks + with ``strict_radius=True``. The method is based on an adaption of + algorithm 1 given in [4]_. + + References + ---------- + .. [1] Park, H and Chin R.T. Decomposition of structuring elements for + optimal implementation of morphological operations. In Proceedings: + 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London, + UK. + https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf + .. [2] https://en.wikipedia.org/wiki/Hexadecagon + .. [3] Vanrell, M and Vitrià, J. Optimal 3 × 3 decomposable disks for + morphological transformations. Image and Vision Computing, Vol. 15, + Issue 11, 1997. + :DOI:`10.1016/S0262-8856(97)00026-7` + .. [4] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric + Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological + Image Processing, (1 November 1990). + :DOI:`10.1117/12.23608` + """ + if decomposition is None: + L = np.arange(-radius, radius + 1) + X, Y = np.meshgrid(L, L) + if not strict_radius: + radius += 0.5 + return np.array((X**2 + Y**2) <= radius**2, dtype=dtype) + elif decomposition == 'sequence': + sequence = _nsphere_series_decomposition(radius, ndim=2, dtype=dtype) + elif decomposition == 'crosses': + fp = disk(radius, dtype, strict_radius=strict_radius, decomposition=None) + sequence = _cross_decomposition(fp) + return sequence + + +def _cross(r0, r1, dtype=np.uint8): + """Cross-shaped structuring element of shape (r0, r1). + + Only the central row and column are ones. + """ + s0 = int(2 * r0 + 1) + s1 = int(2 * r1 + 1) + c = np.zeros((s0, s1), dtype=dtype) + if r1 != 0: + c[r0, :] = 1 + if r0 != 0: + c[:, r1] = 1 + return c + + +def _cross_decomposition(footprint, dtype=np.uint8): + """Decompose a symmetric convex footprint into cross-shaped elements. + + This is a decomposition of the footprint into a sequence of + (possibly asymmetric) cross-shaped elements. This technique was proposed in + [1]_ and corresponds roughly to algorithm 1 of that publication (some + details had to be modified to get reliable operation). + + .. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric + Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological + Image Processing, (1 November 1990). + :DOI:`10.1117/12.23608` + """ + quadrant = footprint[footprint.shape[0] // 2 :, footprint.shape[1] // 2 :] + col_sums = quadrant.sum(0, dtype=int) + col_sums = np.concatenate((col_sums, np.asarray([0], dtype=int))) + i_prev = 0 + idx = {} + sum0 = 0 + for i in range(col_sums.size - 1): + if col_sums[i] > col_sums[i + 1]: + if i == 0: + continue + key = (col_sums[i_prev] - col_sums[i], i - i_prev) + sum0 += key[0] + if key not in idx: + idx[key] = 1 + else: + idx[key] += 1 + i_prev = i + n = quadrant.shape[0] - 1 - sum0 + if n > 0: + key = (n, 0) + idx[key] = idx.get(key, 0) + 1 + return tuple([(_cross(r0, r1, dtype), n) for (r0, r1), n in idx.items()]) + + +def ellipse(width, height, dtype=np.uint8, *, decomposition=None): + """Generates a flat, ellipse-shaped footprint. + + Every pixel along the perimeter of ellipse satisfies + the equation ``(x/width+1)**2 + (y/height+1)**2 = 1``. + + Parameters + ---------- + width : int + The width of the ellipse-shaped footprint. + height : int + The height of the ellipse-shaped footprint. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'crosses'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but with + better computational performance. See Notes for more details. + + Returns + ------- + footprint : ndarray + The footprint where elements of the neighborhood are 1 and 0 otherwise. + The footprint will have shape ``(2 * height + 1, 2 * width + 1)``. + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + The ellipse produced by the ``decomposition='crosses'`` is often but not + always identical to that with ``decomposition=None``. The method is based + on an adaption of algorithm 1 given in [1]_. + + References + ---------- + .. [1] Li, D. and Ritter, G.X. Decomposition of Separable and Symmetric + Convex Templates. Proc. SPIE 1350, Image Algebra and Morphological + Image Processing, (1 November 1990). + :DOI:`10.1117/12.23608` + + Examples + -------- + >>> from skimage.morphology import footprints + >>> footprints.ellipse(5, 3) + array([[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0]], dtype=uint8) + + """ + if decomposition is None: + footprint = np.zeros((2 * height + 1, 2 * width + 1), dtype=dtype) + rows, cols = draw.ellipse(height, width, height + 1, width + 1) + footprint[rows, cols] = 1 + return footprint + elif decomposition == 'crosses': + fp = ellipse(width, height, dtype, decomposition=None) + sequence = _cross_decomposition(fp) + return sequence + + +@deprecate_func( + deprecated_version="0.25", + removed_version="0.27", + hint="Use `skimage.morphology.footprint_rectangle` instead.", +) +def cube(width, dtype=np.uint8, *, decomposition=None): + """Generates a cube-shaped footprint. + + This is the 3D equivalent of a square. + Every pixel along the perimeter has a chessboard distance + no greater than radius (radius=floor(width/2)) pixels. + + Parameters + ---------- + width : int + The width, height and depth of the cube. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'separable', 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but often with + better computational performance. See Notes for more details. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + When `decomposition` is None, this is just a numpy.ndarray. Otherwise, + this will be a tuple whose length is equal to the number of unique + structuring elements to apply (see Notes for more detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For binary morphology, using ``decomposition='sequence'`` + was observed to give better performance, with the magnitude of the + performance increase rapidly increasing with footprint size. For grayscale + morphology with square footprints, it is recommended to use + ``decomposition=None`` since the internal SciPy functions that are called + already have a fast implementation based on separable 1D sliding windows. + + The 'sequence' decomposition mode only supports odd valued `width`. If + `width` is even, the sequence used will be identical to the 'separable' + mode. + """ + footprint = footprint_rectangle( + shape=(width, width, width), dtype=dtype, decomposition=decomposition + ) + return footprint + + +def octahedron(radius, dtype=np.uint8, *, decomposition=None): + """Generates a octahedron-shaped footprint. + + This is the 3D equivalent of a diamond. + A pixel is part of the neighborhood (i.e. labeled 1) if + the city block/Manhattan distance between it and the center of + the neighborhood is no greater than radius. + + Parameters + ---------- + radius : int + The radius of the octahedron-shaped footprint. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but with + better computational performance. See Notes for more details. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + When `decomposition` is None, this is just a numpy.ndarray. Otherwise, + this will be a tuple whose length is equal to the number of unique + structuring elements to apply (see Notes for more detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For either binary or grayscale morphology, using + ``decomposition='sequence'`` was observed to have a performance benefit, + with the magnitude of the benefit increasing with increasing footprint + size. + """ + # note that in contrast to diamond(), this method allows non-integer radii + if decomposition is None: + n = 2 * radius + 1 + Z, Y, X = np.mgrid[ + -radius : radius : n * 1j, + -radius : radius : n * 1j, + -radius : radius : n * 1j, + ] + s = np.abs(X) + np.abs(Y) + np.abs(Z) + footprint = np.array(s <= radius, dtype=dtype) + elif decomposition == 'sequence': + fp = octahedron(1, dtype=dtype, decomposition=None) + nreps = _decompose_size(2 * radius + 1, fp.shape[0]) + footprint = ((fp, nreps),) + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + return footprint + + +def ball(radius, dtype=np.uint8, *, strict_radius=True, decomposition=None): + """Generates a ball-shaped footprint. + + This is the 3D equivalent of a disk. + A pixel is within the neighborhood if the Euclidean distance between + it and the origin is no greater than radius. + + Parameters + ---------- + radius : float + The radius of the ball-shaped footprint. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + strict_radius : bool, optional + If False, extend the radius by 0.5. This allows the circle to expand + further within a cube that remains of size ``2 * radius + 1`` along + each axis. This parameter is ignored if decomposition is not None. + decomposition : {None, 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given a result equivalent to a single, larger footprint, but with + better computational performance. For ball footprints, the sequence + decomposition is not exactly equivalent to decomposition=None. + See Notes for more details. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + + Notes + ----- + The disk produced by the decomposition='sequence' mode is not identical + to that with decomposition=None. Here we extend the approach taken in [1]_ + for disks to the 3D case, using 3-dimensional extensions of the "square", + "diamond" and "t-shaped" elements from that publication. All of these + elementary elements have size ``(3,) * ndim``. We numerically computed the + number of repetitions of each element that gives the closest match to the + ball computed with kwargs ``strict_radius=False, decomposition=None``. + + Empirically, the equivalent composite footprint to the sequence + decomposition approaches a rhombicuboctahedron (26-faces [2]_). + + References + ---------- + .. [1] Park, H and Chin R.T. Decomposition of structuring elements for + optimal implementation of morphological operations. In Proceedings: + 1997 IEEE Workshop on Nonlinear Signal and Image Processing, London, + UK. + https://www.iwaenc.org/proceedings/1997/nsip97/pdf/scan/ns970226.pdf + .. [2] https://en.wikipedia.org/wiki/Rhombicuboctahedron + """ + if decomposition is None: + n = 2 * radius + 1 + Z, Y, X = np.mgrid[ + -radius : radius : n * 1j, + -radius : radius : n * 1j, + -radius : radius : n * 1j, + ] + s = X**2 + Y**2 + Z**2 + if not strict_radius: + radius += 0.5 + return np.array(s <= radius * radius, dtype=dtype) + elif decomposition == 'sequence': + sequence = _nsphere_series_decomposition(radius, ndim=3, dtype=dtype) + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + return sequence + + +def octagon(m, n, dtype=np.uint8, *, decomposition=None): + """Generates an octagon shaped footprint. + + For a given size of (m) horizontal and vertical sides + and a given (n) height or width of slanted sides octagon is generated. + The slanted sides are 45 or 135 degrees to the horizontal axis + and hence the widths and heights are equal. The overall size of the + footprint along a single axis will be ``m + 2 * n``. + + Parameters + ---------- + m : int + The size of the horizontal and vertical sides. + n : int + The height or width of the slanted sides. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + decomposition : {None, 'sequence'}, optional + If None, a single array is returned. For 'sequence', a tuple of smaller + footprints is returned. Applying this series of smaller footprints will + given an identical result to a single, larger footprint, but with + better computational performance. See Notes for more details. + + Returns + ------- + footprint : ndarray or tuple + The footprint where elements of the neighborhood are 1 and 0 otherwise. + When `decomposition` is None, this is just a numpy.ndarray. Otherwise, + this will be a tuple whose length is equal to the number of unique + structuring elements to apply (see Notes for more detail) + + Notes + ----- + When `decomposition` is not None, each element of the `footprint` + tuple is a 2-tuple of the form ``(ndarray, num_iter)`` that specifies a + footprint array and the number of iterations it is to be applied. + + For either binary or grayscale morphology, using + ``decomposition='sequence'`` was observed to have a performance benefit, + with the magnitude of the benefit increasing with increasing footprint + size. + """ + if m == n == 0: + raise ValueError("m and n cannot both be zero") + + # TODO?: warn about even footprint size when m is even + + if decomposition is None: + from . import convex_hull_image + + footprint = np.zeros((m + 2 * n, m + 2 * n)) + footprint[0, n] = 1 + footprint[n, 0] = 1 + footprint[0, m + n - 1] = 1 + footprint[m + n - 1, 0] = 1 + footprint[-1, n] = 1 + footprint[n, -1] = 1 + footprint[-1, m + n - 1] = 1 + footprint[m + n - 1, -1] = 1 + footprint = convex_hull_image(footprint).astype(dtype) + elif decomposition == 'sequence': + # special handling for edge cases with small m and/or n + if m <= 2 and n <= 2: + return ((octagon(m, n, dtype=dtype, decomposition=None), 1),) + + # general approach for larger m and/or n + if m == 0: + m = 2 + n -= 1 + sequence = [] + if m > 1: + sequence += list( + footprint_rectangle((m, m), dtype=dtype, decomposition='sequence') + ) + if n > 0: + sequence += [(diamond(1, dtype=dtype, decomposition=None), n)] + footprint = tuple(sequence) + else: + raise ValueError(f"Unrecognized decomposition: {decomposition}") + return footprint + + +def star(a, dtype=np.uint8): + """Generates a star shaped footprint. + + Start has 8 vertices and is an overlap of square of size `2*a + 1` + with its 45 degree rotated version. + The slanted sides are 45 or 135 degrees to the horizontal axis. + + Parameters + ---------- + a : int + Parameter deciding the size of the star structural element. The side + of the square array returned is `2*a + 1 + 2*floor(a / 2)`. + + Other Parameters + ---------------- + dtype : data-type, optional + The data type of the footprint. + + Returns + ------- + footprint : ndarray + The footprint where elements of the neighborhood are 1 and 0 otherwise. + + """ + from . import convex_hull_image + + if a == 1: + bfilter = np.zeros((3, 3), dtype) + bfilter[:] = 1 + return bfilter + + m = 2 * a + 1 + n = a // 2 + footprint_square = np.zeros((m + 2 * n, m + 2 * n)) + footprint_square[n : m + n, n : m + n] = 1 + + c = (m + 2 * n - 1) // 2 + footprint_rotated = np.zeros((m + 2 * n, m + 2 * n)) + footprint_rotated[0, c] = footprint_rotated[-1, c] = 1 + footprint_rotated[c, 0] = footprint_rotated[c, -1] = 1 + footprint_rotated = convex_hull_image(footprint_rotated).astype(int) + + footprint = footprint_square + footprint_rotated + footprint[footprint > 0] = 1 + + return footprint.astype(dtype) + + +def mirror_footprint(footprint): + """Mirror each dimension in the footprint. + + Parameters + ---------- + footprint : ndarray or tuple + The input footprint or sequence of footprints + + Returns + ------- + inverted : ndarray or tuple + The footprint, mirrored along each dimension. + + Examples + -------- + >>> footprint = np.array([[0, 0, 0], + ... [0, 1, 1], + ... [0, 1, 1]], np.uint8) + >>> mirror_footprint(footprint) + array([[1, 1, 0], + [1, 1, 0], + [0, 0, 0]], dtype=uint8) + + """ + if _footprint_is_sequence(footprint): + return tuple((mirror_footprint(fp), n) for fp, n in footprint) + footprint = np.asarray(footprint) + return footprint[(slice(None, None, -1),) * footprint.ndim] + + +def pad_footprint(footprint, *, pad_end=True): + """Pad the footprint to an odd size along each dimension. + + Parameters + ---------- + footprint : ndarray or tuple + The input footprint or sequence of footprints + pad_end : bool, optional + If ``True``, pads at the end of each dimension (right side), otherwise + pads on the front (left side). + + Returns + ------- + padded : ndarray or tuple + The footprint, padded to an odd size along each dimension. + + Examples + -------- + >>> footprint = np.array([[0, 0], + ... [1, 1], + ... [1, 1]], np.uint8) + >>> pad_footprint(footprint) + array([[0, 0, 0], + [1, 1, 0], + [1, 1, 0]], dtype=uint8) + + """ + if _footprint_is_sequence(footprint): + return tuple((pad_footprint(fp, pad_end=pad_end), n) for fp, n in footprint) + footprint = np.asarray(footprint) + padding = [] + for sz in footprint.shape: + padding.append(((0, 1) if pad_end else (1, 0)) if sz % 2 == 0 else (0, 0)) + return np.pad(footprint, padding) diff --git a/lib/python3.10/site-packages/skimage/morphology/gray.py b/lib/python3.10/site-packages/skimage/morphology/gray.py new file mode 100644 index 0000000000000000000000000000000000000000..337bb8f3f6596fbf22ec29fbf72c80726aaa5a4f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/gray.py @@ -0,0 +1,703 @@ +""" +Grayscale morphological operations +""" + +import warnings + +import numpy as np +from scipy import ndimage as ndi + +from .footprints import _footprint_is_sequence, mirror_footprint, pad_footprint +from .misc import default_footprint +from .._shared.utils import DEPRECATED + + +__all__ = ['erosion', 'dilation', 'opening', 'closing', 'white_tophat', 'black_tophat'] + + +def _iterate_gray_func(gray_func, image, footprints, out, mode, cval): + """Helper to call `gray_func` for each footprint in a sequence. + + `gray_func` is a morphology function that accepts `footprint`, `output`, + `mode` and `cval` keyword arguments (e.g. `scipy.ndimage.grey_erosion`). + """ + fp, num_iter = footprints[0] + gray_func(image, footprint=fp, output=out, mode=mode, cval=cval) + for _ in range(1, num_iter): + gray_func(out.copy(), footprint=fp, output=out, mode=mode, cval=cval) + for fp, num_iter in footprints[1:]: + # Note: out.copy() because the computation cannot be in-place! + for _ in range(num_iter): + gray_func(out.copy(), footprint=fp, output=out, mode=mode, cval=cval) + return out + + +def _shift_footprint(footprint, shift_x, shift_y): + """Shift the binary image `footprint` in the left and/or up. + + This only affects 2D footprints with even number of rows + or columns. + + Parameters + ---------- + footprint : 2D array, shape (M, N) + The input footprint. + shift_x, shift_y : bool or None + Whether to move `footprint` along each axis. If ``None``, the + array is not modified along that dimension. + + Returns + ------- + out : 2D array, shape (M + int(shift_x), N + int(shift_y)) + The shifted footprint. + """ + footprint = np.asarray(footprint) + if footprint.ndim != 2: + # do nothing for 1D or 3D or higher footprints + return footprint + m, n = footprint.shape + if m % 2 == 0: + extra_row = np.zeros((1, n), footprint.dtype) + if shift_x: + footprint = np.vstack((footprint, extra_row)) + else: + footprint = np.vstack((extra_row, footprint)) + m += 1 + if n % 2 == 0: + extra_col = np.zeros((m, 1), footprint.dtype) + if shift_y: + footprint = np.hstack((footprint, extra_col)) + else: + footprint = np.hstack((extra_col, footprint)) + return footprint + + +def _shift_footprints(footprint, shift_x, shift_y): + """Shifts the footprints, whether it's a single array or a sequence. + + See `_shift_footprint`, which is called for each array in the sequence. + """ + if shift_x is DEPRECATED and shift_y is DEPRECATED: + return footprint + + warning_msg = ( + "The parameters `shift_x` and `shift_y` are deprecated since v0.23 and " + "will be removed in v0.26. Use `pad_footprint` or modify the footprint" + "manually instead." + ) + warnings.warn(warning_msg, FutureWarning, stacklevel=4) + + if _footprint_is_sequence(footprint): + return tuple((_shift_footprint(fp, shift_x, shift_y), n) for fp, n in footprint) + return _shift_footprint(footprint, shift_x, shift_y) + + +def _min_max_to_constant_mode(dtype, mode, cval): + """Replace 'max' and 'min' with appropriate 'cval' and 'constant' mode.""" + if mode == "max": + mode = "constant" + if np.issubdtype(dtype, bool): + cval = True + elif np.issubdtype(dtype, np.integer): + cval = np.iinfo(dtype).max + else: + cval = np.inf + elif mode == "min": + mode = "constant" + if np.issubdtype(dtype, bool): + cval = False + elif np.issubdtype(dtype, np.integer): + cval = np.iinfo(dtype).min + else: + cval = -np.inf + return mode, cval + + +_SUPPORTED_MODES = { + "reflect", + "constant", + "nearest", + "mirror", + "wrap", + "max", + "min", + "ignore", +} + + +@default_footprint +def erosion( + image, + footprint=None, + out=None, + shift_x=DEPRECATED, + shift_y=DEPRECATED, + *, + mode="reflect", + cval=0.0, +): + """Return grayscale morphological erosion of an image. + + Morphological erosion sets a pixel at (i,j) to the minimum over all pixels + in the neighborhood centered at (i,j). Erosion shrinks bright regions and + enlarges dark regions. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarrays, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. + If 'max' or 'ignore', pixels outside the image domain are assumed + to be the maximum for the image's dtype, which causes them to not + influence the result. Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + eroded : array, same shape as `image` + The result of the morphological erosion. + + Other Parameters + ---------------- + shift_x, shift_y : DEPRECATED + + .. deprecated:: 0.23 + + Notes + ----- + For ``uint8`` (and ``uint16`` up to a certain bit-depth) data, the + lower algorithm complexity makes the :func:`skimage.filters.rank.minimum` + function more efficient for larger images and footprints. + + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + For even-sized footprints, :func:`skimage.morphology.binary_erosion` and + this function produce an output that differs: one is shifted by one pixel + compared to the other. + + Examples + -------- + >>> # Erosion shrinks bright regions + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> bright_square = np.array([[0, 0, 0, 0, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> erosion(bright_square, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + if out is None: + out = np.empty_like(image) + + if mode not in _SUPPORTED_MODES: + raise ValueError(f"unsupported mode, got {mode!r}") + if mode == "ignore": + mode = "max" + mode, cval = _min_max_to_constant_mode(image.dtype, mode, cval) + + footprint = _shift_footprints(footprint, shift_x, shift_y) + footprint = pad_footprint(footprint, pad_end=False) + if not _footprint_is_sequence(footprint): + footprint = [(footprint, 1)] + + out = _iterate_gray_func( + gray_func=ndi.grey_erosion, + image=image, + footprints=footprint, + out=out, + mode=mode, + cval=cval, + ) + return out + + +@default_footprint +def dilation( + image, + footprint=None, + out=None, + shift_x=DEPRECATED, + shift_y=DEPRECATED, + *, + mode="reflect", + cval=0.0, +): + """Return grayscale morphological dilation of an image. + + Morphological dilation sets the value of a pixel to the maximum over all + pixel values within a local neighborhood centered about it. The values + where the footprint is 1 define this neighborhood. + Dilation enlarges bright regions and shrinks dark regions. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. + If 'min' or 'ignore', pixels outside the image domain are assumed + to be the maximum for the image's dtype, which causes them to not + influence the result. Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + dilated : uint8 array, same shape and type as `image` + The result of the morphological dilation. + + Other Parameters + ---------------- + shift_x, shift_y : DEPRECATED + + .. deprecated:: 0.23 + + Notes + ----- + For ``uint8`` (and ``uint16`` up to a certain bit-depth) data, the lower + algorithm complexity makes the :func:`skimage.filters.rank.maximum` + function more efficient for larger images and footprints. + + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + For non-symmetric footprints, :func:`skimage.morphology.binary_dilation` + and :func:`skimage.morphology.dilation` produce an output that differs: + `binary_dilation` mirrors the footprint, whereas `dilation` does not. + + Examples + -------- + >>> # Dilation enlarges bright regions + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> bright_pixel = np.array([[0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0], + ... [0, 0, 1, 0, 0], + ... [0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> dilation(bright_pixel, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + if out is None: + out = np.empty_like(image) + + if mode not in _SUPPORTED_MODES: + raise ValueError(f"unsupported mode, got {mode!r}") + if mode == "ignore": + mode = "min" + mode, cval = _min_max_to_constant_mode(image.dtype, mode, cval) + + footprint = _shift_footprints(footprint, shift_x, shift_y) + footprint = pad_footprint(footprint, pad_end=False) + # Note that `ndi.grey_dilation` mirrors the footprint and this + # additional inversion should be removed in skimage2, see gh-6676. + footprint = mirror_footprint(footprint) + if not _footprint_is_sequence(footprint): + footprint = [(footprint, 1)] + + out = _iterate_gray_func( + gray_func=ndi.grey_dilation, + image=image, + footprints=footprint, + out=out, + mode=mode, + cval=cval, + ) + return out + + +@default_footprint +def opening(image, footprint=None, out=None, *, mode="reflect", cval=0.0): + """Return grayscale morphological opening of an image. + + The morphological opening of an image is defined as an erosion followed by + a dilation. Opening can remove small bright spots (i.e. "salt") and connect + small dark cracks. This tends to "open" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray, optional + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. + If 'ignore', pixels outside the image domain are assumed + to be the maximum for the image's dtype in the erosion, and minimum + in the dilation, which causes them to not influence the result. + Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + opening : array, same shape and type as `image` + The result of the morphological opening. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + Examples + -------- + >>> # Open up gap between two bright regions (but also shrink regions) + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> bad_connection = np.array([[1, 0, 0, 0, 1], + ... [1, 1, 0, 1, 1], + ... [1, 1, 1, 1, 1], + ... [1, 1, 0, 1, 1], + ... [1, 0, 0, 0, 1]], dtype=np.uint8) + >>> opening(bad_connection, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [1, 1, 0, 1, 1], + [1, 1, 0, 1, 1], + [1, 1, 0, 1, 1], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + footprint = pad_footprint(footprint, pad_end=False) + eroded = erosion(image, footprint, mode=mode, cval=cval) + out = dilation(eroded, mirror_footprint(footprint), out=out, mode=mode, cval=cval) + return out + + +@default_footprint +def closing(image, footprint=None, out=None, *, mode="reflect", cval=0.0): + """Return grayscale morphological closing of an image. + + The morphological closing of an image is defined as a dilation followed by + an erosion. Closing can remove small dark spots (i.e. "pepper") and connect + small bright cracks. This tends to "close" up (dark) gaps between (bright) + features. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray, optional + The array to store the result of the morphology. If None, + a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. + If 'ignore', pixels outside the image domain are assumed + to be the maximum for the image's dtype in the erosion, and minimum + in the dilation, which causes them to not influence the result. + Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + closing : array, same shape and type as `image` + The result of the morphological closing. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + Examples + -------- + >>> # Close a gap between two bright lines + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> broken_line = np.array([[0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0], + ... [1, 1, 0, 1, 1], + ... [0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0]], dtype=np.uint8) + >>> closing(broken_line, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [1, 1, 1, 1, 1], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + footprint = pad_footprint(footprint, pad_end=False) + dilated = dilation(image, footprint, mode=mode, cval=cval) + out = erosion(dilated, mirror_footprint(footprint), out=out, mode=mode, cval=cval) + return out + + +@default_footprint +def white_tophat(image, footprint=None, out=None, *, mode="reflect", cval=0.0): + """Return white top hat of an image. + + The white top hat of an image is defined as the image minus its + morphological opening. This operation returns the bright spots of the image + that are smaller than the footprint. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray, optional + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. See :func:`skimage.morphology.opening`. + Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + out : array, same shape and type as `image` + The result of the morphological white top hat. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + See Also + -------- + black_tophat + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Top-hat_transform + + Examples + -------- + >>> # Subtract gray background from bright peak + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> bright_on_gray = np.array([[2, 3, 3, 3, 2], + ... [3, 4, 5, 4, 3], + ... [3, 5, 9, 5, 3], + ... [3, 4, 5, 4, 3], + ... [2, 3, 3, 3, 2]], dtype=np.uint8) + >>> white_tophat(bright_on_gray, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 5, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + if out is image: + # We need a temporary image + opened = opening(image, footprint, mode=mode, cval=cval) + if np.issubdtype(opened.dtype, bool): + np.logical_xor(out, opened, out=out) + else: + out -= opened + return out + + # Else write intermediate result into output image + out = opening(image, footprint, out=out, mode=mode, cval=cval) + if np.issubdtype(out.dtype, bool): + np.logical_xor(image, out, out=out) + else: + np.subtract(image, out, out=out) + return out + + +@default_footprint +def black_tophat(image, footprint=None, out=None, *, mode="reflect", cval=0.0): + """Return black top hat of an image. + + The black top hat of an image is defined as its morphological closing minus + the original image. This operation returns the dark spots of the image that + are smaller than the footprint. Note that dark spots in the + original image are bright spots after the black top hat. + + Parameters + ---------- + image : ndarray + Image array. + footprint : ndarray or tuple, optional + The neighborhood expressed as a 2-D array of 1's and 0's. + If None, use a cross-shaped footprint (connectivity=1). The footprint + can also be provided as a sequence of smaller footprints as described + in the notes below. + out : ndarray, optional + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + mode : str, optional + The `mode` parameter determines how the array borders are handled. + Valid modes are: 'reflect', 'constant', 'nearest', 'mirror', 'wrap', + 'max', 'min', or 'ignore'. See :func:`skimage.morphology.closing`. + Default is 'reflect'. + cval : scalar, optional + Value to fill past edges of input if `mode` is 'constant'. Default + is 0.0. + + .. versionadded:: 0.23 + `mode` and `cval` were added in 0.23. + + Returns + ------- + out : array, same shape and type as `image` + The result of the morphological black top hat. + + Notes + ----- + The footprint can also be a provided as a sequence of 2-tuples where the + first element of each 2-tuple is a footprint ndarray and the second element + is an integer describing the number of times it should be iterated. For + example ``footprint=[(np.ones((9, 1)), 1), (np.ones((1, 9)), 1)]`` + would apply a 9x1 footprint followed by a 1x9 footprint resulting in a net + effect that is the same as ``footprint=np.ones((9, 9))``, but with lower + computational cost. Most of the builtin footprints such as + :func:`skimage.morphology.disk` provide an option to automatically generate + a footprint sequence of this type. + + See Also + -------- + white_tophat + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Top-hat_transform + + Examples + -------- + >>> # Change dark peak to bright peak and subtract background + >>> import numpy as np + >>> from skimage.morphology import footprint_rectangle + >>> dark_on_gray = np.array([[7, 6, 6, 6, 7], + ... [6, 5, 4, 5, 6], + ... [6, 4, 0, 4, 6], + ... [6, 5, 4, 5, 6], + ... [7, 6, 6, 6, 7]], dtype=np.uint8) + >>> black_tophat(dark_on_gray, footprint_rectangle((3, 3))) + array([[0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 1, 5, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0]], dtype=uint8) + + """ + if out is image: + # We need a temporary image + closed = closing(image, footprint, mode=mode, cval=cval) + if np.issubdtype(closed.dtype, bool): + np.logical_xor(closed, out, out=out) + else: + np.subtract(closed, out, out=out) + return out + + out = closing(image, footprint, out=out, mode=mode, cval=cval) + if np.issubdtype(out.dtype, np.bool_): + np.logical_xor(out, image, out=out) + else: + out -= image + return out diff --git a/lib/python3.10/site-packages/skimage/morphology/grayreconstruct.py b/lib/python3.10/site-packages/skimage/morphology/grayreconstruct.py new file mode 100644 index 0000000000000000000000000000000000000000..e924fab6c8a347b5d5e061b5bb5b056f11c2d1a2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/grayreconstruct.py @@ -0,0 +1,217 @@ +import numpy as np + +from .._shared.utils import _supported_float_type +from ..filters._rank_order import rank_order +from ._grayreconstruct import reconstruction_loop + + +def reconstruction(seed, mask, method='dilation', footprint=None, offset=None): + """Perform a morphological reconstruction of an image. + + Morphological reconstruction by dilation is similar to basic morphological + dilation: high-intensity values will replace nearby low-intensity values. + The basic dilation operator, however, uses a footprint to + determine how far a value in the input image can spread. In contrast, + reconstruction uses two images: a "seed" image, which specifies the values + that spread, and a "mask" image, which gives the maximum allowed value at + each pixel. The mask image, like the footprint, limits the spread + of high-intensity values. Reconstruction by erosion is simply the inverse: + low-intensity values spread from the seed image and are limited by the mask + image, which represents the minimum allowed value. + + Alternatively, you can think of reconstruction as a way to isolate the + connected regions of an image. For dilation, reconstruction connects + regions marked by local maxima in the seed image: neighboring pixels + less-than-or-equal-to those seeds are connected to the seeded region. + Local maxima with values larger than the seed image will get truncated to + the seed value. + + Parameters + ---------- + seed : ndarray + The seed image (a.k.a. marker image), which specifies the values that + are dilated or eroded. + mask : ndarray + The maximum (dilation) / minimum (erosion) allowed value at each pixel. + method : {'dilation'|'erosion'}, optional + Perform reconstruction by dilation or erosion. In dilation (or + erosion), the seed image is dilated (or eroded) until limited by the + mask image. For dilation, each seed value must be less than or equal + to the corresponding mask value; for erosion, the reverse is true. + Default is 'dilation'. + footprint : ndarray, optional + The neighborhood expressed as an n-D array of 1's and 0's. + Default is the n-D square of radius equal to 1 (i.e. a 3x3 square + for 2D images, a 3x3x3 cube for 3D images, etc.) + offset : ndarray, optional + The coordinates of the center of the footprint. + Default is located on the geometrical center of the footprint, in that + case footprint dimensions must be odd. + + Returns + ------- + reconstructed : ndarray + The result of morphological reconstruction. + + Examples + -------- + >>> import numpy as np + >>> from skimage.morphology import reconstruction + + First, we create a sinusoidal mask image with peaks at middle and ends. + + >>> x = np.linspace(0, 4 * np.pi) + >>> y_mask = np.cos(x) + + Then, we create a seed image initialized to the minimum mask value (for + reconstruction by dilation, min-intensity values don't spread) and add + "seeds" to the left and right peak, but at a fraction of peak value (1). + + >>> y_seed = y_mask.min() * np.ones_like(x) + >>> y_seed[0] = 0.5 + >>> y_seed[-1] = 0 + >>> y_rec = reconstruction(y_seed, y_mask) + + The reconstructed image (or curve, in this case) is exactly the same as the + mask image, except that the peaks are truncated to 0.5 and 0. The middle + peak disappears completely: Since there were no seed values in this peak + region, its reconstructed value is truncated to the surrounding value (-1). + + As a more practical example, we try to extract the bright features of an + image by subtracting a background image created by reconstruction. + + >>> y, x = np.mgrid[:20:0.5, :20:0.5] + >>> bumps = np.sin(x) + np.sin(y) + + To create the background image, set the mask image to the original image, + and the seed image to the original image with an intensity offset, `h`. + + >>> h = 0.3 + >>> seed = bumps - h + >>> background = reconstruction(seed, bumps) + + The resulting reconstructed image looks exactly like the original image, + but with the peaks of the bumps cut off. Subtracting this reconstructed + image from the original image leaves just the peaks of the bumps + + >>> hdome = bumps - background + + This operation is known as the h-dome of the image and leaves features + of height `h` in the subtracted image. + + Notes + ----- + The algorithm is taken from [1]_. Applications for grayscale reconstruction + are discussed in [2]_ and [3]_. + + References + ---------- + .. [1] Robinson, "Efficient morphological reconstruction: a downhill + filter", Pattern Recognition Letters 25 (2004) 1759-1767. + .. [2] Vincent, L., "Morphological Grayscale Reconstruction in Image + Analysis: Applications and Efficient Algorithms", IEEE Transactions + on Image Processing (1993) + .. [3] Soille, P., "Morphological Image Analysis: Principles and + Applications", Chapter 6, 2nd edition (2003), ISBN 3540429883. + """ + assert tuple(seed.shape) == tuple(mask.shape) + if method == 'dilation' and np.any(seed > mask): + raise ValueError( + "Intensity of seed image must be less than that " + "of the mask image for reconstruction by dilation." + ) + elif method == 'erosion' and np.any(seed < mask): + raise ValueError( + "Intensity of seed image must be greater than that " + "of the mask image for reconstruction by erosion." + ) + + if footprint is None: + footprint = np.ones([3] * seed.ndim, dtype=bool) + else: + footprint = footprint.astype(bool, copy=True) + + if offset is None: + if not all([d % 2 == 1 for d in footprint.shape]): + raise ValueError("Footprint dimensions must all be odd") + offset = np.array([d // 2 for d in footprint.shape]) + else: + if offset.ndim != footprint.ndim: + raise ValueError("Offset and footprint ndims must be equal.") + if not all([(0 <= o < d) for o, d in zip(offset, footprint.shape)]): + raise ValueError("Offset must be included inside footprint") + + # Cross out the center of the footprint + footprint[tuple(slice(d, d + 1) for d in offset)] = False + + # Make padding for edges of reconstructed image so we can ignore boundaries + dims = np.zeros(seed.ndim + 1, dtype=int) + dims[1:] = np.array(seed.shape) + (np.array(footprint.shape) - 1) + dims[0] = 2 + inside_slices = tuple(slice(o, o + s) for o, s in zip(offset, seed.shape)) + # Set padded region to minimum image intensity and mask along first axis so + # we can interleave image and mask pixels when sorting. + if method == 'dilation': + pad_value = np.min(seed) + elif method == 'erosion': + pad_value = np.max(seed) + else: + raise ValueError( + "Reconstruction method can be one of 'erosion' " + f"or 'dilation'. Got '{method}'." + ) + float_dtype = _supported_float_type(mask.dtype) + images = np.full(dims, pad_value, dtype=float_dtype) + images[(0, *inside_slices)] = seed + images[(1, *inside_slices)] = mask + + # determine whether image is large enough to require 64-bit integers + isize = images.size + # use -isize so we get a signed dtype rather than an unsigned one + signed_int_dtype = np.result_type(np.min_scalar_type(-isize), np.int32) + # the corresponding unsigned type has same char, but uppercase + unsigned_int_dtype = np.dtype(signed_int_dtype.char.upper()) + + # Create a list of strides across the array to get the neighbors within + # a flattened array + value_stride = np.array(images.strides[1:]) // images.dtype.itemsize + image_stride = images.strides[0] // images.dtype.itemsize + footprint_mgrid = np.mgrid[ + [slice(-o, d - o) for d, o in zip(footprint.shape, offset)] + ] + footprint_offsets = footprint_mgrid[:, footprint].transpose() + nb_strides = np.array( + [ + np.sum(value_stride * footprint_offset) + for footprint_offset in footprint_offsets + ], + signed_int_dtype, + ) + images = images.reshape(-1) + + # Erosion goes smallest to largest; dilation goes largest to smallest. + index_sorted = np.argsort(images).astype(signed_int_dtype, copy=False) + if method == 'dilation': + index_sorted = index_sorted[::-1] + + # Make a linked list of pixels sorted by value. -1 is the list terminator. + prev = np.full(isize, -1, signed_int_dtype) + next = np.full(isize, -1, signed_int_dtype) + prev[index_sorted[1:]] = index_sorted[:-1] + next[index_sorted[:-1]] = index_sorted[1:] + + # Cython inner-loop compares the rank of pixel values. + if method == 'dilation': + value_rank, value_map = rank_order(images) + elif method == 'erosion': + value_rank, value_map = rank_order(-images) + value_map = -value_map + + start = index_sorted[0] + value_rank = value_rank.astype(unsigned_int_dtype, copy=False) + reconstruction_loop(value_rank, prev, next, nb_strides, start, image_stride) + + # Reshape reconstructed image to original image shape and remove padding. + rec_img = value_map[value_rank[:image_stride]] + rec_img.shape = np.array(seed.shape) + (np.array(footprint.shape) - 1) + return rec_img[inside_slices] diff --git a/lib/python3.10/site-packages/skimage/morphology/isotropic.py b/lib/python3.10/site-packages/skimage/morphology/isotropic.py new file mode 100644 index 0000000000000000000000000000000000000000..6234a5f21e600632e85e21d85422c7805546ce95 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/isotropic.py @@ -0,0 +1,194 @@ +""" +Binary morphological operations +""" + +import numpy as np +from scipy import ndimage as ndi + + +def isotropic_erosion(image, radius, out=None, spacing=None): + """Return binary morphological erosion of an image. + + This function returns the same result as :func:`skimage.morphology.binary_erosion` + but performs faster for large circular structuring elements. + This works by applying a threshold to the exact Euclidean distance map + of the image [1]_, [2]_. + The implementation is based on: func:`scipy.ndimage.distance_transform_edt`. + + Parameters + ---------- + image : ndarray + Binary input image. + radius : float + The radius by which regions should be eroded. + out : ndarray of bool, optional + The array to store the result of the morphology. If None, + a new array will be allocated. + spacing : float, or sequence of float, optional + Spacing of elements along each dimension. + If a sequence, must be of length equal to the input's dimension (number of axes). + If a single number, this value is used for all axes. + If not specified, a grid spacing of unity is implied. + + Returns + ------- + eroded : ndarray of bool + The result of the morphological erosion taking values in + ``[False, True]``. + + References + ---------- + .. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators + using local distance transformation by propagation, and applications," + Image Processing And Its Applications, 1999. Seventh International + Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2. + :DOI:`10.1049/cp:19990446` + + .. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing + and thresholding of distance maps, Pattern Recognition Letters, + Volume 13, Issue 3, 1992, Pages 161-166. + :DOI:`10.1016/0167-8655(92)90055-5` + """ + + dist = ndi.distance_transform_edt(image, sampling=spacing) + return np.greater(dist, radius, out=out) + + +def isotropic_dilation(image, radius, out=None, spacing=None): + """Return binary morphological dilation of an image. + + This function returns the same result as :func:`skimage.morphology.binary_dilation` + but performs faster for large circular structuring elements. + This works by applying a threshold to the exact Euclidean distance map + of the inverted image [1]_, [2]_. + The implementation is based on: func:`scipy.ndimage.distance_transform_edt`. + + Parameters + ---------- + image : ndarray + Binary input image. + radius : float + The radius by which regions should be dilated. + out : ndarray of bool, optional + The array to store the result of the morphology. If None is + passed, a new array will be allocated. + spacing : float, or sequence of float, optional + Spacing of elements along each dimension. + If a sequence, must be of length equal to the input's dimension (number of axes). + If a single number, this value is used for all axes. + If not specified, a grid spacing of unity is implied. + + Returns + ------- + dilated : ndarray of bool + The result of the morphological dilation with values in + ``[False, True]``. + + References + ---------- + .. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators + using local distance transformation by propagation, and applications," + Image Processing And Its Applications, 1999. Seventh International + Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2. + :DOI:`10.1049/cp:19990446` + + .. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing + and thresholding of distance maps, Pattern Recognition Letters, + Volume 13, Issue 3, 1992, Pages 161-166. + :DOI:`10.1016/0167-8655(92)90055-5` + """ + + dist = ndi.distance_transform_edt(np.logical_not(image), sampling=spacing) + return np.less_equal(dist, radius, out=out) + + +def isotropic_opening(image, radius, out=None, spacing=None): + """Return binary morphological opening of an image. + + This function returns the same result as :func:`skimage.morphology.binary_opening` + but performs faster for large circular structuring elements. + This works by thresholding the exact Euclidean distance map [1]_, [2]_. + The implementation is based on: func:`scipy.ndimage.distance_transform_edt`. + + Parameters + ---------- + image : ndarray + Binary input image. + radius : float + The radius with which the regions should be opened. + out : ndarray of bool, optional + The array to store the result of the morphology. If None + is passed, a new array will be allocated. + spacing : float, or sequence of float, optional + Spacing of elements along each dimension. + If a sequence, must be of length equal to the input's dimension (number of axes). + If a single number, this value is used for all axes. + If not specified, a grid spacing of unity is implied. + + Returns + ------- + opened : ndarray of bool + The result of the morphological opening. + + References + ---------- + .. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators + using local distance transformation by propagation, and applications," + Image Processing And Its Applications, 1999. Seventh International + Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2. + :DOI:`10.1049/cp:19990446` + + .. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing + and thresholding of distance maps, Pattern Recognition Letters, + Volume 13, Issue 3, 1992, Pages 161-166. + :DOI:`10.1016/0167-8655(92)90055-5` + """ + + eroded = isotropic_erosion(image, radius, out=out, spacing=spacing) + return isotropic_dilation(eroded, radius, out=out, spacing=spacing) + + +def isotropic_closing(image, radius, out=None, spacing=None): + """Return binary morphological closing of an image. + + This function returns the same result as binary :func:`skimage.morphology.binary_closing` + but performs faster for large circular structuring elements. + This works by thresholding the exact Euclidean distance map [1]_, [2]_. + The implementation is based on: func:`scipy.ndimage.distance_transform_edt`. + + Parameters + ---------- + image : ndarray + Binary input image. + radius : float + The radius with which the regions should be closed. + out : ndarray of bool, optional + The array to store the result of the morphology. If None, + is passed, a new array will be allocated. + spacing : float, or sequence of float, optional + Spacing of elements along each dimension. + If a sequence, must be of length equal to the input's dimension (number of axes). + If a single number, this value is used for all axes. + If not specified, a grid spacing of unity is implied. + + Returns + ------- + closed : ndarray of bool + The result of the morphological closing. + + References + ---------- + .. [1] Cuisenaire, O. and Macq, B., "Fast Euclidean morphological operators + using local distance transformation by propagation, and applications," + Image Processing And Its Applications, 1999. Seventh International + Conference on (Conf. Publ. No. 465), 1999, pp. 856-860 vol.2. + :DOI:`10.1049/cp:19990446` + + .. [2] Ingemar Ragnemalm, Fast erosion and dilation by contour processing + and thresholding of distance maps, Pattern Recognition Letters, + Volume 13, Issue 3, 1992, Pages 161-166. + :DOI:`10.1016/0167-8655(92)90055-5` + """ + + dilated = isotropic_dilation(image, radius, out=out, spacing=spacing) + return isotropic_erosion(dilated, radius, out=out, spacing=spacing) diff --git a/lib/python3.10/site-packages/skimage/morphology/max_tree.py b/lib/python3.10/site-packages/skimage/morphology/max_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..d385b0e4d13c4e570d928371665864939d1dfa03 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/max_tree.py @@ -0,0 +1,700 @@ +"""max_tree.py - max_tree representation of images. + +This module provides operators based on the max-tree representation of images. +A grayscale image can be seen as a pile of nested sets, each of which is the +result of a threshold operation. These sets can be efficiently represented by +max-trees, where the inclusion relation between connected components at +different levels are represented by parent-child relationships. + +These representations allow efficient implementations of many algorithms, such +as attribute operators. Unlike morphological openings and closings, these +operators do not require a fixed footprint, but rather act with a flexible +footprint that meets a certain criterion. + +This implementation provides functions for: +1. max-tree generation +2. area openings / closings +3. diameter openings / closings +4. local maxima + +References: + .. [1] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive + Connected Operators for Image and Sequence Processing. + IEEE Transactions on Image Processing, 7(4), 555-570. + :DOI:`10.1109/83.663500` + .. [2] Berger, C., Geraud, T., Levillain, R., Widynski, N., Baillard, A., + Bertin, E. (2007). Effective Component Tree Computation with + Application to Pattern Recognition in Astronomical Imaging. + In International Conference on Image Processing (ICIP) (pp. 41-44). + :DOI:`10.1109/ICIP.2007.4379949` + .. [3] Najman, L., & Couprie, M. (2006). Building the component tree in + quasi-linear time. IEEE Transactions on Image Processing, 15(11), + 3531-3539. + :DOI:`10.1109/TIP.2006.877518` + .. [4] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` +""" + +import numpy as np + +from ._util import _validate_connectivity, _offsets_to_raveled_neighbors +from ..util import invert + +from . import _max_tree + +unsigned_int_types = [np.uint8, np.uint16, np.uint32, np.uint64] +signed_int_types = [np.int8, np.int16, np.int32, np.int64] +signed_float_types = [np.float16, np.float32, np.float64] + + +# building the max tree. +def max_tree(image, connectivity=1): + """Build the max tree from an image. + + Component trees represent the hierarchical structure of the connected + components resulting from sequential thresholding operations applied to an + image. A connected component at one level is parent of a component at a + higher level if the latter is included in the first. A max-tree is an + efficient representation of a component tree. A connected component at + one level is represented by one reference pixel at this level, which is + parent to all other pixels at that level and to the reference pixel at the + level above. The max-tree is the basis for many morphological operators, + namely connected operators. + + Parameters + ---------- + image : ndarray + The input image for which the max-tree is to be calculated. + This image can be of any type. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + + Returns + ------- + parent : ndarray, int64 + Array of same shape as image. The value of each pixel is the index of + its parent in the ravelled array. + tree_traverser : 1D array, int64 + The ordered pixel indices (referring to the ravelled array). The pixels + are ordered such that every pixel is preceded by its parent (except for + the root which has no parent). + + References + ---------- + .. [1] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive + Connected Operators for Image and Sequence Processing. + IEEE Transactions on Image Processing, 7(4), 555-570. + :DOI:`10.1109/83.663500` + .. [2] Berger, C., Geraud, T., Levillain, R., Widynski, N., Baillard, A., + Bertin, E. (2007). Effective Component Tree Computation with + Application to Pattern Recognition in Astronomical Imaging. + In International Conference on Image Processing (ICIP) (pp. 41-44). + :DOI:`10.1109/ICIP.2007.4379949` + .. [3] Najman, L., & Couprie, M. (2006). Building the component tree in + quasi-linear time. IEEE Transactions on Image Processing, 15(11), + 3531-3539. + :DOI:`10.1109/TIP.2006.877518` + .. [4] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create a small sample image (Figure 1 from [4]) and build the max-tree. + + >>> image = np.array([[15, 13, 16], [12, 12, 10], [16, 12, 14]]) + >>> P, S = max_tree(image, connectivity=2) + """ + # User defined masks are not allowed, as there might be more than one + # connected component in the mask (and therefore not a single tree that + # represents the image). Mask here is an image that is 0 on the border + # and 1 everywhere else. + mask = np.ones(image.shape) + for k in range(len(image.shape)): + np.moveaxis(mask, k, 0)[0] = 0 + np.moveaxis(mask, k, 0)[-1] = 0 + + neighbors, offset = _validate_connectivity(image.ndim, connectivity, offset=None) + + # initialization of the parent image + parent = np.zeros(image.shape, dtype=np.int64) + + # flat_neighborhood contains a list of offsets allowing one to find the + # neighbors in the ravelled image. + flat_neighborhood = _offsets_to_raveled_neighbors( + image.shape, neighbors, offset + ).astype(np.int32) + + # pixels need to be sorted according to their gray level. + tree_traverser = np.argsort(image.ravel(), kind="stable").astype(np.int64) + + # call of cython function. + _max_tree._max_tree( + image.ravel(), + mask.ravel().astype(np.uint8), + flat_neighborhood, + offset.astype(np.int32), + np.array(image.shape, dtype=np.int32), + parent.ravel(), + tree_traverser, + ) + + return parent, tree_traverser + + +def area_opening( + image, area_threshold=64, connectivity=1, parent=None, tree_traverser=None +): + """Perform an area opening of the image. + + Area opening removes all bright structures of an image with + a surface smaller than area_threshold. + The output image is thus the largest image smaller than the input + for which all local maxima have at least a surface of + area_threshold pixels. + + Area openings are similar to morphological openings, but + they do not use a fixed footprint, but rather a deformable + one, with surface = area_threshold. Consequently, the area_opening + with area_threshold=1 is the identity. + + In the binary case, area openings are equivalent to + remove_small_objects; this operator is thus extended to gray-level images. + + Technically, this operator is based on the max-tree representation of + the image. + + Parameters + ---------- + image : ndarray + The input image for which the area_opening is to be calculated. + This image can be of any type. + area_threshold : unsigned int + The size parameter (number of pixels). The default value is arbitrarily + chosen to be 64. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + parent : ndarray, int64, optional + Parent image representing the max tree of the image. The + value of each pixel is the index of its parent in the ravelled array. + tree_traverser : 1D array, int64, optional + The ordered pixel indices (referring to the ravelled array). The pixels + are ordered such that every pixel is preceded by its parent (except for + the root which has no parent). + + Returns + ------- + output : ndarray + Output image of the same shape and type as the input image. + + See Also + -------- + skimage.morphology.area_closing + skimage.morphology.diameter_opening + skimage.morphology.diameter_closing + skimage.morphology.max_tree + skimage.morphology.remove_small_objects + skimage.morphology.remove_small_holes + + References + ---------- + .. [1] Vincent L., Proc. "Grayscale area openings and closings, + their efficient implementation and applications", + EURASIP Workshop on Mathematical Morphology and its + Applications to Signal Processing, Barcelona, Spain, pp.22-27, + May 1993. + .. [2] Soille, P., "Morphological Image Analysis: Principles and + Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883. + :DOI:`10.1007/978-3-662-05088-0` + .. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive + Connected Operators for Image and Sequence Processing. + IEEE Transactions on Image Processing, 7(4), 555-570. + :DOI:`10.1109/83.663500` + .. [4] Najman, L., & Couprie, M. (2006). Building the component tree in + quasi-linear time. IEEE Transactions on Image Processing, 15(11), + 3531-3539. + :DOI:`10.1109/TIP.2006.877518` + .. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create an image (quadratic function with a maximum in the center and + 4 additional local maxima. + + >>> w = 12 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:3,1:5] = 40; f[2:4,9:11] = 60; f[9:11,2:4] = 80 + >>> f[9:10,9:11] = 100; f[10,10] = 100 + >>> f = f.astype(int) + + We can calculate the area opening: + + >>> open = area_opening(f, 8, connectivity=1) + + The peaks with a surface smaller than 8 are removed. + """ + output = image.copy() + + if parent is None or tree_traverser is None: + parent, tree_traverser = max_tree(image, connectivity) + + area = _max_tree._compute_area(image.ravel(), parent.ravel(), tree_traverser) + + _max_tree._direct_filter( + image.ravel(), + output.ravel(), + parent.ravel(), + tree_traverser, + area, + area_threshold, + ) + return output + + +def diameter_opening( + image, diameter_threshold=8, connectivity=1, parent=None, tree_traverser=None +): + """Perform a diameter opening of the image. + + Diameter opening removes all bright structures of an image with + maximal extension smaller than diameter_threshold. The maximal + extension is defined as the maximal extension of the bounding box. + The operator is also called Bounding Box Opening. In practice, + the result is similar to a morphological opening, but long and thin + structures are not removed. + + Technically, this operator is based on the max-tree representation of + the image. + + Parameters + ---------- + image : ndarray + The input image for which the area_opening is to be calculated. + This image can be of any type. + diameter_threshold : unsigned int + The maximal extension parameter (number of pixels). The default value + is 8. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + parent : ndarray, int64, optional + Parent image representing the max tree of the image. The + value of each pixel is the index of its parent in the ravelled array. + tree_traverser : 1D array, int64, optional + The ordered pixel indices (referring to the ravelled array). The pixels + are ordered such that every pixel is preceded by its parent (except for + the root which has no parent). + + Returns + ------- + output : ndarray + Output image of the same shape and type as the input image. + + See Also + -------- + skimage.morphology.area_opening + skimage.morphology.area_closing + skimage.morphology.diameter_closing + skimage.morphology.max_tree + + References + ---------- + .. [1] Walter, T., & Klein, J.-C. (2002). Automatic Detection of + Microaneurysms in Color Fundus Images of the Human Retina by Means + of the Bounding Box Closing. In A. Colosimo, P. Sirabella, + A. Giuliani (Eds.), Medical Data Analysis. Lecture Notes in Computer + Science, vol 2526, pp. 210-220. Springer Berlin Heidelberg. + :DOI:`10.1007/3-540-36104-9_23` + .. [2] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create an image (quadratic function with a maximum in the center and + 4 additional local maxima. + + >>> w = 12 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:3,1:5] = 40; f[2:4,9:11] = 60; f[9:11,2:4] = 80 + >>> f[9:10,9:11] = 100; f[10,10] = 100 + >>> f = f.astype(int) + + We can calculate the diameter opening: + + >>> open = diameter_opening(f, 3, connectivity=1) + + The peaks with a maximal extension of 2 or less are removed. + The remaining peaks have all a maximal extension of at least 3. + """ + output = image.copy() + + if parent is None or tree_traverser is None: + parent, tree_traverser = max_tree(image, connectivity) + + diam = _max_tree._compute_extension( + image.ravel(), + np.array(image.shape, dtype=np.int32), + parent.ravel(), + tree_traverser, + ) + + _max_tree._direct_filter( + image.ravel(), + output.ravel(), + parent.ravel(), + tree_traverser, + diam, + diameter_threshold, + ) + return output + + +def area_closing( + image, area_threshold=64, connectivity=1, parent=None, tree_traverser=None +): + """Perform an area closing of the image. + + Area closing removes all dark structures of an image with + a surface smaller than area_threshold. + The output image is larger than or equal to the input image + for every pixel and all local minima have at least a surface of + area_threshold pixels. + + Area closings are similar to morphological closings, but + they do not use a fixed footprint, but rather a deformable + one, with surface = area_threshold. + + In the binary case, area closings are equivalent to + remove_small_holes; this operator is thus extended to gray-level images. + + Technically, this operator is based on the max-tree representation of + the image. + + Parameters + ---------- + image : ndarray + The input image for which the area_closing is to be calculated. + This image can be of any type. + area_threshold : unsigned int + The size parameter (number of pixels). The default value is arbitrarily + chosen to be 64. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + parent : ndarray, int64, optional + Parent image representing the max tree of the inverted image. The + value of each pixel is the index of its parent in the ravelled array. + See Note for further details. + tree_traverser : 1D array, int64, optional + The ordered pixel indices (referring to the ravelled array). The pixels + are ordered such that every pixel is preceded by its parent (except for + the root which has no parent). + + Returns + ------- + output : ndarray + Output image of the same shape and type as input image. + + See Also + -------- + skimage.morphology.area_opening + skimage.morphology.diameter_opening + skimage.morphology.diameter_closing + skimage.morphology.max_tree + skimage.morphology.remove_small_objects + skimage.morphology.remove_small_holes + + References + ---------- + .. [1] Vincent L., Proc. "Grayscale area openings and closings, + their efficient implementation and applications", + EURASIP Workshop on Mathematical Morphology and its + Applications to Signal Processing, Barcelona, Spain, pp.22-27, + May 1993. + .. [2] Soille, P., "Morphological Image Analysis: Principles and + Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883. + :DOI:`10.1007/978-3-662-05088-0` + .. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive + Connected Operators for Image and Sequence Processing. + IEEE Transactions on Image Processing, 7(4), 555-570. + :DOI:`10.1109/83.663500` + .. [4] Najman, L., & Couprie, M. (2006). Building the component tree in + quasi-linear time. IEEE Transactions on Image Processing, 15(11), + 3531-3539. + :DOI:`10.1109/TIP.2006.877518` + .. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create an image (quadratic function with a minimum in the center and + 4 additional local minima. + + >>> w = 12 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:3,1:5] = 160; f[2:4,9:11] = 140; f[9:11,2:4] = 120 + >>> f[9:10,9:11] = 100; f[10,10] = 100 + >>> f = f.astype(int) + + We can calculate the area closing: + + >>> closed = area_closing(f, 8, connectivity=1) + + All small minima are removed, and the remaining minima have at least + a size of 8. + + Notes + ----- + If a max-tree representation (parent and tree_traverser) are given to the + function, they must be calculated from the inverted image for this + function, i.e.: + >>> P, S = max_tree(invert(f)) + >>> closed = diameter_closing(f, 3, parent=P, tree_traverser=S) + """ + # inversion of the input image + image_inv = invert(image) + output = image_inv.copy() + + if parent is None or tree_traverser is None: + parent, tree_traverser = max_tree(image_inv, connectivity) + + area = _max_tree._compute_area(image_inv.ravel(), parent.ravel(), tree_traverser) + + _max_tree._direct_filter( + image_inv.ravel(), + output.ravel(), + parent.ravel(), + tree_traverser, + area, + area_threshold, + ) + + # inversion of the output image + output = invert(output) + + return output + + +def diameter_closing( + image, diameter_threshold=8, connectivity=1, parent=None, tree_traverser=None +): + """Perform a diameter closing of the image. + + Diameter closing removes all dark structures of an image with + maximal extension smaller than diameter_threshold. The maximal + extension is defined as the maximal extension of the bounding box. + The operator is also called Bounding Box Closing. In practice, + the result is similar to a morphological closing, but long and thin + structures are not removed. + + Technically, this operator is based on the max-tree representation of + the image. + + Parameters + ---------- + image : ndarray + The input image for which the diameter_closing is to be calculated. + This image can be of any type. + diameter_threshold : unsigned int + The maximal extension parameter (number of pixels). The default value + is 8. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + parent : ndarray, int64, optional + Precomputed parent image representing the max tree of the inverted + image. This function is fast, if precomputed parent and tree_traverser + are provided. See Note for further details. + tree_traverser : 1D array, int64, optional + Precomputed traverser, where the pixels are ordered such that every + pixel is preceded by its parent (except for the root which has no + parent). This function is fast, if precomputed parent and + tree_traverser are provided. See Note for further details. + + Returns + ------- + output : ndarray + Output image of the same shape and type as input image. + + See Also + -------- + skimage.morphology.area_opening + skimage.morphology.area_closing + skimage.morphology.diameter_opening + skimage.morphology.max_tree + + References + ---------- + .. [1] Walter, T., & Klein, J.-C. (2002). Automatic Detection of + Microaneurysms in Color Fundus Images of the Human Retina by Means + of the Bounding Box Closing. In A. Colosimo, P. Sirabella, + A. Giuliani (Eds.), Medical Data Analysis. Lecture Notes in Computer + Science, vol 2526, pp. 210-220. Springer Berlin Heidelberg. + :DOI:`10.1007/3-540-36104-9_23` + .. [2] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create an image (quadratic function with a minimum in the center and + 4 additional local minima. + + >>> w = 12 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 180 + 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:3,1:5] = 160; f[2:4,9:11] = 140; f[9:11,2:4] = 120 + >>> f[9:10,9:11] = 100; f[10,10] = 100 + >>> f = f.astype(int) + + We can calculate the diameter closing: + + >>> closed = diameter_closing(f, 3, connectivity=1) + + All small minima with a maximal extension of 2 or less are removed. + The remaining minima have all a maximal extension of at least 3. + + Notes + ----- + If a max-tree representation (parent and tree_traverser) are given to the + function, they must be calculated from the inverted image for this + function, i.e.: + >>> P, S = max_tree(invert(f)) + >>> closed = diameter_closing(f, 3, parent=P, tree_traverser=S) + """ + # inversion of the input image + image_inv = invert(image) + output = image_inv.copy() + + if parent is None or tree_traverser is None: + parent, tree_traverser = max_tree(image_inv, connectivity) + + diam = _max_tree._compute_extension( + image_inv.ravel(), + np.array(image_inv.shape, dtype=np.int32), + parent.ravel(), + tree_traverser, + ) + + _max_tree._direct_filter( + image_inv.ravel(), + output.ravel(), + parent.ravel(), + tree_traverser, + diam, + diameter_threshold, + ) + output = invert(output) + return output + + +def max_tree_local_maxima(image, connectivity=1, parent=None, tree_traverser=None): + """Determine all local maxima of the image. + + The local maxima are defined as connected sets of pixels with equal + gray level strictly greater than the gray levels of all pixels in direct + neighborhood of the set. The function labels the local maxima. + + Technically, the implementation is based on the max-tree representation + of an image. The function is very efficient if the max-tree representation + has already been computed. Otherwise, it is preferable to use + the function local_maxima. + + Parameters + ---------- + image : ndarray + The input image for which the maxima are to be calculated. + connectivity : unsigned int, optional + The neighborhood connectivity. The integer represents the maximum + number of orthogonal steps to reach a neighbor. In 2D, it is 1 for + a 4-neighborhood and 2 for a 8-neighborhood. Default value is 1. + parent : ndarray, int64, optional + The value of each pixel is the index of its parent in the ravelled + array. + tree_traverser : 1D array, int64, optional + The ordered pixel indices (referring to the ravelled array). The pixels + are ordered such that every pixel is preceded by its parent (except for + the root which has no parent). + + Returns + ------- + local_max : ndarray, uint64 + Labeled local maxima of the image. + + See Also + -------- + skimage.morphology.local_maxima + skimage.morphology.max_tree + + References + ---------- + .. [1] Vincent L., Proc. "Grayscale area openings and closings, + their efficient implementation and applications", + EURASIP Workshop on Mathematical Morphology and its + Applications to Signal Processing, Barcelona, Spain, pp.22-27, + May 1993. + .. [2] Soille, P., "Morphological Image Analysis: Principles and + Applications" (Chapter 6), 2nd edition (2003), ISBN 3540429883. + :DOI:`10.1007/978-3-662-05088-0` + .. [3] Salembier, P., Oliveras, A., & Garrido, L. (1998). Antiextensive + Connected Operators for Image and Sequence Processing. + IEEE Transactions on Image Processing, 7(4), 555-570. + :DOI:`10.1109/83.663500` + .. [4] Najman, L., & Couprie, M. (2006). Building the component tree in + quasi-linear time. IEEE Transactions on Image Processing, 15(11), + 3531-3539. + :DOI:`10.1109/TIP.2006.877518` + .. [5] Carlinet, E., & Geraud, T. (2014). A Comparative Review of + Component Tree Computation Algorithms. IEEE Transactions on Image + Processing, 23(9), 3885-3895. + :DOI:`10.1109/TIP.2014.2336551` + + Examples + -------- + We create an image (quadratic function with a maximum in the center and + 4 additional constant maxima. + + >>> w = 10 + >>> x, y = np.mgrid[0:w,0:w] + >>> f = 20 - 0.2*((x - w/2)**2 + (y-w/2)**2) + >>> f[2:4,2:4] = 40; f[2:4,7:9] = 60; f[7:9,2:4] = 80; f[7:9,7:9] = 100 + >>> f = f.astype(int) + + We can calculate all local maxima: + + >>> maxima = max_tree_local_maxima(f) + + The resulting image contains the labeled local maxima. + """ + + output = np.ones(image.shape, dtype=np.uint64) + + if parent is None or tree_traverser is None: + parent, tree_traverser = max_tree(image, connectivity) + + _max_tree._max_tree_local_maxima( + image.ravel(), output.ravel(), parent.ravel(), tree_traverser + ) + + return output diff --git a/lib/python3.10/site-packages/skimage/morphology/misc.py b/lib/python3.10/site-packages/skimage/morphology/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..30d302c00a1eb890ce63eda7b1c6a53127f8947a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/misc.py @@ -0,0 +1,454 @@ +"""Miscellaneous morphology functions.""" + +import numpy as np +import functools +from scipy import ndimage as ndi +from scipy.spatial import cKDTree + +from .._shared.utils import warn +from ._misc_cy import _remove_objects_by_distance + + +# Our function names don't exactly correspond to ndimages. +# This dictionary translates from our names to scipy's. +funcs = ('erosion', 'dilation', 'opening', 'closing') +skimage2ndimage = {x: 'grey_' + x for x in funcs} + +# These function names are the same in ndimage. +funcs = ( + 'binary_erosion', + 'binary_dilation', + 'binary_opening', + 'binary_closing', + 'black_tophat', + 'white_tophat', +) +skimage2ndimage.update({x: x for x in funcs}) + + +def default_footprint(func): + """Decorator to add a default footprint to morphology functions. + + Parameters + ---------- + func : function + A morphology function such as erosion, dilation, opening, closing, + white_tophat, or black_tophat. + + Returns + ------- + func_out : function + The function, using a default footprint of same dimension + as the input image with connectivity 1. + + """ + + @functools.wraps(func) + def func_out(image, footprint=None, *args, **kwargs): + if footprint is None: + footprint = ndi.generate_binary_structure(image.ndim, 1) + return func(image, footprint=footprint, *args, **kwargs) + + return func_out + + +def _check_dtype_supported(ar): + # Should use `issubdtype` for bool below, but there's a bug in numpy 1.7 + if not (ar.dtype == bool or np.issubdtype(ar.dtype, np.integer)): + raise TypeError( + "Only bool or integer image types are supported. " f"Got {ar.dtype}." + ) + + +def remove_small_objects(ar, min_size=64, connectivity=1, *, out=None): + """Remove objects smaller than the specified size. + + Expects ar to be an array with labeled objects, and removes objects + smaller than min_size. If `ar` is bool, the image is first labeled. + This leads to potentially different behavior for bool and 0-and-1 + arrays. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the objects of interest. If the array type is + int, the ints must be non-negative. + min_size : int, optional (default: 64) + The smallest allowable object size. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. Used during + labelling if `ar` is bool. + out : ndarray + Array of the same shape as `ar`, into which the output is + placed. By default, a new array is created. + + Raises + ------ + TypeError + If the input array is of an invalid type, such as float or string. + ValueError + If the input array contains negative values. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small connected components removed. + + See Also + -------- + skimage.morphology.remove_objects_by_distance + + Examples + -------- + >>> from skimage import morphology + >>> a = np.array([[0, 0, 0, 1, 0], + ... [1, 1, 1, 0, 0], + ... [1, 1, 1, 0, 1]], bool) + >>> b = morphology.remove_small_objects(a, 6) + >>> b + array([[False, False, False, False, False], + [ True, True, True, False, False], + [ True, True, True, False, False]]) + >>> c = morphology.remove_small_objects(a, 7, connectivity=2) + >>> c + array([[False, False, False, True, False], + [ True, True, True, False, False], + [ True, True, True, False, False]]) + >>> d = morphology.remove_small_objects(a, 6, out=a) + >>> d is a + True + + """ + # Raising type error if not int or bool + _check_dtype_supported(ar) + + if out is None: + out = ar.copy() + else: + out[:] = ar + + if min_size == 0: # shortcut for efficiency + return out + + if out.dtype == bool: + footprint = ndi.generate_binary_structure(ar.ndim, connectivity) + ccs = np.zeros_like(ar, dtype=np.int32) + ndi.label(ar, footprint, output=ccs) + else: + ccs = out + + try: + component_sizes = np.bincount(ccs.ravel()) + except ValueError: + raise ValueError( + "Negative value labels are not supported. Try " + "relabeling the input with `scipy.ndimage.label` or " + "`skimage.morphology.label`." + ) + + if len(component_sizes) == 2 and out.dtype != bool: + warn( + "Only one label was provided to `remove_small_objects`. " + "Did you mean to use a boolean array?" + ) + + too_small = component_sizes < min_size + too_small_mask = too_small[ccs] + out[too_small_mask] = 0 + + return out + + +def remove_small_holes(ar, area_threshold=64, connectivity=1, *, out=None): + """Remove contiguous holes smaller than the specified size. + + Parameters + ---------- + ar : ndarray (arbitrary shape, int or bool type) + The array containing the connected components of interest. + area_threshold : int, optional (default: 64) + The maximum area, in pixels, of a contiguous hole that will be filled. + Replaces `min_size`. + connectivity : int, {1, 2, ..., ar.ndim}, optional (default: 1) + The connectivity defining the neighborhood of a pixel. + out : ndarray + Array of the same shape as `ar` and bool dtype, into which the + output is placed. By default, a new array is created. + + Raises + ------ + TypeError + If the input array is of an invalid type, such as float or string. + ValueError + If the input array contains negative values. + + Returns + ------- + out : ndarray, same shape and type as input `ar` + The input array with small holes within connected components removed. + + Examples + -------- + >>> from skimage import morphology + >>> a = np.array([[1, 1, 1, 1, 1, 0], + ... [1, 1, 1, 0, 1, 0], + ... [1, 0, 0, 1, 1, 0], + ... [1, 1, 1, 1, 1, 0]], bool) + >>> b = morphology.remove_small_holes(a, 2) + >>> b + array([[ True, True, True, True, True, False], + [ True, True, True, True, True, False], + [ True, False, False, True, True, False], + [ True, True, True, True, True, False]]) + >>> c = morphology.remove_small_holes(a, 2, connectivity=2) + >>> c + array([[ True, True, True, True, True, False], + [ True, True, True, False, True, False], + [ True, False, False, True, True, False], + [ True, True, True, True, True, False]]) + >>> d = morphology.remove_small_holes(a, 2, out=a) + >>> d is a + True + + Notes + ----- + If the array type is int, it is assumed that it contains already-labeled + objects. The labels are not kept in the output image (this function always + outputs a bool image). It is suggested that labeling is completed after + using this function. + + """ + _check_dtype_supported(ar) + + # Creates warning if image is an integer image + if ar.dtype != bool: + warn( + "Any labeled images will be returned as a boolean array. " + "Did you mean to use a boolean array?", + UserWarning, + ) + + if out is not None: + if out.dtype != bool: + raise TypeError("out dtype must be bool") + else: + out = ar.astype(bool, copy=True) + + # Creating the inverse of ar + np.logical_not(ar, out=out) + + # removing small objects from the inverse of ar + out = remove_small_objects(out, area_threshold, connectivity, out=out) + + np.logical_not(out, out=out) + + return out + + +def remove_objects_by_distance( + label_image, + min_distance, + *, + priority=None, + p_norm=2, + spacing=None, + out=None, +): + """Remove objects, in specified order, until remaining are a minimum distance apart. + + Remove labeled objects from an image until the remaining ones are spaced + more than a given distance from one another. By default, smaller objects + are removed first. + + Parameters + ---------- + label_image : ndarray of integers + An n-dimensional array containing object labels, e.g. as returned by + :func:`~.label`. A value of zero is considered background, all other + object IDs must be positive integers. + min_distance : int or float + Remove objects whose distance to other objects is not greater than this + positive value. Objects with a lower `priority` are removed first. + priority : ndarray, optional + Defines the priority with which objects are removed. Expects a + 1-dimensional array of length + :func:`np.amax(label_image) + 1 ` that contains the priority + for each object's label at the respective index. Objects with a lower value + are removed first until all remaining objects fulfill the distance + requirement. If not given, priority is given to objects with a higher + number of samples and their label value second. + p_norm : int or float, optional + The Minkowski distance of order p, used to calculate the distance + between objects. The default ``2`` corresponds to the Euclidean + distance, ``1`` to the "Manhattan" distance, and ``np.inf`` to the + Chebyshev distance. + spacing : sequence of float, optional + The pixel spacing along each axis of `label_image`. If not specified, + a grid spacing of unity (1) is implied. + out : ndarray, optional + Array of the same shape and dtype as `image`, into which the output is + placed. By default, a new array is created. + + Returns + ------- + out : ndarray + Array of the same shape as `label_image`, for which objects that violate + the `min_distance` condition were removed. + + See Also + -------- + skimage.morphology.remove_small_objects + Remove objects smaller than the specified size. + + Notes + ----- + The basic steps of this algorithm work as follows: + + 1. Find the indices for of all given objects and separate them depending on + if they point to an object's border or not. + 2. Sort indices by their label value, ensuring that indices which point to + the same object are next to each other. This optimization allows finding + all parts of an object, simply by stepping to the neighboring indices. + 3. Sort boundary indices by `priority`. Use a stable-sort to preserve the + ordering from the previous sorting step. If `priority` is not given, + use :func:`numpy.bincount` as a fallback. + 4. Construct a :class:`scipy.spatial.cKDTree` from the boundary indices. + 5. Iterate across boundary indices in priority-sorted order, and query the + kd-tree for objects that are too close. Remove ones that are and don't + take them into account when evaluating other objects later on. + + The performance of this algorithm depends on the number of samples in + `label_image` that belong to an object's border. + + Examples + -------- + >>> import skimage as ski + >>> ski.morphology.remove_objects_by_distance(np.array([2, 0, 1, 1]), 2) + array([0, 0, 1, 1]) + >>> ski.morphology.remove_objects_by_distance( + ... np.array([2, 0, 1, 1]), 2, priority=np.array([0, 1, 9]) + ... ) + array([2, 0, 0, 0]) + >>> label_image = np.array( + ... [[8, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9], + ... [8, 8, 8, 0, 0, 0, 0, 0, 0, 9, 9], + ... [0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ... [0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0], + ... [2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7]] + ... ) + >>> ski.morphology.remove_objects_by_distance( + ... label_image, min_distance=3 + ... ) + array([[8, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9], + [8, 8, 8, 0, 0, 0, 0, 0, 0, 9, 9], + [0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 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, 7, 7]]) + """ + if min_distance < 0: + raise ValueError(f"min_distance must be >= 0, was {min_distance}") + if not np.issubdtype(label_image.dtype, np.integer): + raise ValueError( + f"`label_image` must be of integer dtype, got {label_image.dtype}" + ) + if out is None: + out = label_image.copy(order="C") + elif out is not label_image: + out[:] = label_image + # May create a copy if order is not C, account for that later + out_raveled = out.ravel(order="C") + + if spacing is not None: + spacing = np.array(spacing) + if spacing.shape != (out.ndim,) or spacing.min() <= 0: + raise ValueError( + "`spacing` must contain exactly one positive factor " + "for each dimension of `label_image`" + ) + + indices = np.flatnonzero(out_raveled) + # Optimization: Split indices into those on the object boundaries and inner + # ones. The KDTree is built only from the boundary indices, which reduces + # the size of the critical loop significantly! Remaining indices are only + # used to remove the inner parts of objects as well. + if (spacing is None or np.all(spacing[0] == spacing)) and p_norm <= 2: + # For unity spacing we can make the borders more sparse by using a + # lower connectivity + footprint = ndi.generate_binary_structure(out.ndim, 1) + else: + footprint = ndi.generate_binary_structure(out.ndim, out.ndim) + border = ( + ndi.maximum_filter(out, footprint=footprint) + != ndi.minimum_filter(out, footprint=footprint) + ).ravel()[indices] + border_indices = indices[border] + inner_indices = indices[~border] + + if border_indices.size == 0: + # Image without any or only one object, return early + return out + + # Sort by label ID first, so that IDs of the same object are contiguous + # in the sorted index. This allows fast discovery of the whole object by + # simple iteration up or down the index! + border_indices = border_indices[np.argsort(out_raveled[border_indices])] + inner_indices = inner_indices[np.argsort(out_raveled[inner_indices])] + + if priority is None: + if not np.can_cast(out.dtype, np.intp, casting="safe"): + # bincount expects intp (32-bit) on WASM or i386, so down-cast to that + priority = np.bincount(out_raveled.astype(np.intp, copy=False)) + else: + priority = np.bincount(out_raveled) + # `priority` can only be indexed by positive object IDs, + # `border_indices` contains all unique sorted IDs so check the lowest / first + smallest_id = out_raveled[border_indices[0]] + if smallest_id < 0: + raise ValueError(f"found object with negative ID {smallest_id!r}") + + try: + # Sort by priority second using a stable sort to preserve the contiguous + # sorting of objects. Because each pixel in an object has the same + # priority we don't need to worry about separating objects. + border_indices = border_indices[ + np.argsort(priority[out_raveled[border_indices]], kind="stable")[::-1] + ] + except IndexError as error: + # Use np.amax only for the exception path to provide a nicer error message + expected_shape = (np.amax(out_raveled) + 1,) + if priority.shape != expected_shape: + raise ValueError( + "shape of `priority` must be (np.amax(label_image) + 1,), " + f"expected {expected_shape}, got {priority.shape} instead" + ) from error + else: + raise + + # Construct kd-tree from unraveled border indices (optionally scale by `spacing`) + unraveled_indices = np.unravel_index(border_indices, out.shape) + if spacing is not None: + unraveled_indices = tuple( + unraveled_indices[dim] * spacing[dim] for dim in range(out.ndim) + ) + kdtree = cKDTree(data=np.asarray(unraveled_indices, dtype=np.float64).T) + + _remove_objects_by_distance( + out=out_raveled, + border_indices=border_indices, + inner_indices=inner_indices, + kdtree=kdtree, + min_distance=min_distance, + p_norm=p_norm, + shape=label_image.shape, + ) + + if out_raveled.base is not out: + # `out_raveled` is a copy, re-assign + out[:] = out_raveled.reshape(out.shape) + return out diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/__init__.py b/lib/python3.10/site-packages/skimage/morphology/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_binary.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_binary.py new file mode 100644 index 0000000000000000000000000000000000000000..a35e8447f5caf6fa872c5df03b8c8d763134c704 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_binary.py @@ -0,0 +1,349 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal, assert_equal +from scipy import ndimage as ndi + +from skimage import data, color, morphology +from skimage.util import img_as_bool +from skimage.morphology import binary, footprints, gray, footprint_rectangle + + +img = color.rgb2gray(data.astronaut()) +bw_img = img > 100 / 255.0 + + +def test_non_square_image(): + footprint = footprint_rectangle((3, 3)) + binary_res = binary.binary_erosion(bw_img[:100, :200], footprint) + gray_res = img_as_bool(gray.erosion(bw_img[:100, :200], footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_binary_erosion(): + footprint = footprint_rectangle((3, 3)) + binary_res = binary.binary_erosion(bw_img, footprint) + gray_res = img_as_bool(gray.erosion(bw_img, footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_binary_dilation(): + footprint = footprint_rectangle((3, 3)) + binary_res = binary.binary_dilation(bw_img, footprint) + gray_res = img_as_bool(gray.dilation(bw_img, footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_binary_closing(): + footprint = footprint_rectangle((3, 3)) + binary_res = binary.binary_closing(bw_img, footprint) + gray_res = img_as_bool(gray.closing(bw_img, footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_binary_closing_extensive(): + footprint = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]]) + + result_default = binary.binary_closing(bw_img, footprint=footprint) + assert np.all(result_default >= bw_img) + + # mode="min" is expected to be not extensive + result_min = binary.binary_closing(img, footprint=footprint, mode="min") + assert not np.all(result_min >= bw_img) + + +def test_binary_opening(): + footprint = footprint_rectangle((3, 3)) + binary_res = binary.binary_opening(bw_img, footprint) + gray_res = img_as_bool(gray.opening(bw_img, footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_binary_opening_anti_extensive(): + footprint = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]]) + + result_default = binary.binary_opening(bw_img, footprint=footprint) + assert np.all(result_default <= bw_img) + + # mode="max" is expected to be not extensive + result_max = binary.binary_opening(bw_img, footprint=footprint, mode="max") + assert not np.all(result_max <= bw_img) + + +def _get_decomp_test_data(function, ndim=2): + if function == 'binary_erosion': + img = np.ones((17,) * ndim, dtype=np.uint8) + img[8, 8] = 0 + elif function == 'binary_dilation': + img = np.zeros((17,) * ndim, dtype=np.uint8) + img[8, 8] = 1 + else: + img = data.binary_blobs(32, n_dim=ndim, rng=1) + return img + + +@pytest.mark.parametrize( + "function", + ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], +) +@pytest.mark.parametrize("nrows", (3, 7, 11)) +@pytest.mark.parametrize("ncols", (3, 7, 11)) +@pytest.mark.parametrize("decomposition", ['separable', 'sequence']) +def test_rectangle_decomposition(function, nrows, ncols, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprint_rectangle((nrows, ncols), decomposition=None) + footprint = footprint_rectangle((nrows, ncols), decomposition=decomposition) + img = _get_decomp_test_data(function) + func = getattr(binary, function) + expected = func(img, footprint=footprint_ndarray) + out = func(img, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], +) +@pytest.mark.parametrize("m", (0, 1, 2, 3, 4, 5)) +@pytest.mark.parametrize("n", (0, 1, 2, 3, 4, 5)) +@pytest.mark.parametrize("decomposition", ['sequence']) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) +def test_octagon_decomposition(function, m, n, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + if m == 0 and n == 0: + with pytest.raises(ValueError): + footprints.octagon(m, n, decomposition=decomposition) + else: + footprint_ndarray = footprints.octagon(m, n, decomposition=None) + footprint = footprints.octagon(m, n, decomposition=decomposition) + img = _get_decomp_test_data(function) + func = getattr(binary, function) + expected = func(img, footprint=footprint_ndarray) + out = func(img, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], +) +@pytest.mark.parametrize("radius", (1, 2, 5)) +@pytest.mark.parametrize("decomposition", ['sequence']) +def test_diamond_decomposition(function, radius, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprints.diamond(radius, decomposition=None) + footprint = footprints.diamond(radius, decomposition=decomposition) + img = _get_decomp_test_data(function) + func = getattr(binary, function) + expected = func(img, footprint=footprint_ndarray) + out = func(img, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], +) +@pytest.mark.parametrize("shape", [(3, 3, 3), (3, 4, 5)]) +@pytest.mark.parametrize("decomposition", ['separable', 'sequence']) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) +def test_cube_decomposition(function, shape, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprint_rectangle(shape, decomposition=None) + footprint = footprint_rectangle(shape, decomposition=decomposition) + img = _get_decomp_test_data(function, ndim=3) + func = getattr(binary, function) + expected = func(img, footprint=footprint_ndarray) + out = func(img, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"], +) +@pytest.mark.parametrize("radius", (1, 2, 3)) +@pytest.mark.parametrize("decomposition", ['sequence']) +def test_octahedron_decomposition(function, radius, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprints.octahedron(radius, decomposition=None) + footprint = footprints.octahedron(radius, decomposition=decomposition) + img = _get_decomp_test_data(function, ndim=3) + func = getattr(binary, function) + expected = func(img, footprint=footprint_ndarray) + out = func(img, footprint=footprint) + assert_array_equal(expected, out) + + +def test_footprint_overflow(): + footprint = np.ones((17, 17), dtype=np.uint8) + img = np.zeros((20, 20), dtype=bool) + img[2:19, 2:19] = True + binary_res = binary.binary_erosion(img, footprint) + gray_res = img_as_bool(gray.erosion(img, footprint)) + assert_array_equal(binary_res, gray_res) + + +def test_out_argument(): + for func in (binary.binary_erosion, binary.binary_dilation): + footprint = np.ones((3, 3), dtype=np.uint8) + img = np.ones((10, 10)) + out = np.zeros_like(img) + out_saved = out.copy() + func(img, footprint, out=out) + assert np.any(out != out_saved) + assert_array_equal(out, func(img, footprint)) + + +binary_functions = [ + binary.binary_erosion, + binary.binary_dilation, + binary.binary_opening, + binary.binary_closing, +] + + +@pytest.mark.parametrize("func", binary_functions) +@pytest.mark.parametrize("mode", ['max', 'min', 'ignore']) +def test_supported_mode(func, mode): + img = np.ones((10, 10), dtype=bool) + func(img, mode=mode) + + +@pytest.mark.parametrize("func", binary_functions) +@pytest.mark.parametrize("mode", ["reflect", 3, None]) +def test_unsupported_mode(func, mode): + img = np.ones((10, 10)) + with pytest.raises(ValueError, match="unsupported mode"): + func(img, mode=mode) + + +@pytest.mark.parametrize("function", binary_functions) +def test_default_footprint(function): + footprint = morphology.diamond(radius=1) + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + np.uint8, + ) + im_expected = function(image, footprint) + im_test = function(image) + assert_array_equal(im_expected, im_test) + + +def test_3d_fallback_default_footprint(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = binary.binary_opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndi.generate_binary_structure(3, 1) + assert_array_equal(opened, image_expected) + + +binary_3d_fallback_functions = [binary.binary_opening, binary.binary_closing] + + +@pytest.mark.parametrize("function", binary_3d_fallback_functions) +def test_3d_fallback_cube_footprint(function): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + new_image = function(image, cube) + assert_array_equal(new_image, image) + + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16 - 1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + footprint = ndi.generate_binary_structure(2, 1) + ndimage_opened = ndi.binary_opening(image, structure=footprint) + ndimage_closed = ndi.binary_closing(image, structure=footprint) + + assert_array_equal(bin_opened, ndimage_opened) + assert_array_equal(bin_closed, ndimage_closed) + + +def test_binary_output_2d(): + image = np.zeros((9, 9), np.uint16) + image[2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3] = 2**15 + image[4, 4] = 2**16 - 1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + assert_equal(bin_opened.dtype, bool) + assert_equal(bin_closed.dtype, bool) + + assert_equal(int_opened.dtype, np.uint8) + assert_equal(int_closed.dtype, np.uint8) + + +def test_binary_output_3d(): + image = np.zeros((9, 9, 9), np.uint16) + image[2:-2, 2:-2, 2:-2] = 2**14 + image[3:-3, 3:-3, 3:-3] = 2**15 + image[4, 4, 4] = 2**16 - 1 + + bin_opened = binary.binary_opening(image) + bin_closed = binary.binary_closing(image) + + int_opened = np.empty_like(image, dtype=np.uint8) + int_closed = np.empty_like(image, dtype=np.uint8) + binary.binary_opening(image, out=int_opened) + binary.binary_closing(image, out=int_closed) + + assert_equal(bin_opened.dtype, bool) + assert_equal(bin_closed.dtype, bool) + + assert_equal(int_opened.dtype, np.uint8) + assert_equal(int_closed.dtype, np.uint8) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_convex_hull.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_convex_hull.py new file mode 100644 index 0000000000000000000000000000000000000000..e4495a0941155c861517d313423a79b8dbabb629 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_convex_hull.py @@ -0,0 +1,349 @@ +import numpy as np +from skimage.morphology import convex_hull_image, convex_hull_object +from skimage.morphology._convex_hull import possible_hull + +from skimage._shared import testing +from skimage._shared.testing import assert_array_equal +from skimage._shared._warnings import expected_warnings + + +def test_basic(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + assert_array_equal(convex_hull_image(image), expected) + + +def test_empty_image(): + image = np.zeros((6, 6), dtype=bool) + with expected_warnings(['entirely zero']): + assert_array_equal(convex_hull_image(image), image) + + +def test_qhull_offset_example(): + nonzeros = ( + ( + [ + 1367, + 1368, + 1368, + 1368, + 1369, + 1369, + 1369, + 1369, + 1369, + 1370, + 1370, + 1370, + 1370, + 1370, + 1370, + 1370, + 1371, + 1371, + 1371, + 1371, + 1371, + 1371, + 1371, + 1371, + 1371, + 1372, + 1372, + 1372, + 1372, + 1372, + 1372, + 1372, + 1372, + 1372, + 1373, + 1373, + 1373, + 1373, + 1373, + 1373, + 1373, + 1373, + 1373, + 1374, + 1374, + 1374, + 1374, + 1374, + 1374, + 1374, + 1375, + 1375, + 1375, + 1375, + 1375, + 1376, + 1376, + 1376, + 1377, + 1372, + ] + ), + ( + [ + 151, + 150, + 151, + 152, + 149, + 150, + 151, + 152, + 153, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 148, + 149, + 150, + 151, + 152, + 149, + 150, + 151, + 150, + 155, + ] + ), + ) + image = np.zeros((1392, 1040), dtype=bool) + image[nonzeros] = True + expected = image.copy() + assert_array_equal(convex_hull_image(image), expected) + + +def test_pathological_qhull_example(): + image = np.array( + [[0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 0, 0, 0, 0]], + dtype=bool, + ) + expected = np.array( + [[0, 0, 0, 1, 1, 1, 0], [0, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 0]], + dtype=bool, + ) + assert_array_equal(convex_hull_image(image), expected) + + +def test_pathological_qhull_labels(): + image = np.array( + [[0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 0, 0, 0, 0]], + dtype=bool, + ) + + expected = np.array( + [[0, 0, 0, 0, 1, 0, 0], [0, 0, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0, 0]], + dtype=bool, + ) + + actual = convex_hull_image(image, include_borders=False) + assert_array_equal(actual, expected) + + +def test_possible_hull(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + expected = np.array( + [ + [1, 4], + [2, 3], + [3, 2], + [4, 1], + [4, 1], + [3, 2], + [2, 3], + [1, 4], + [2, 5], + [3, 6], + [4, 7], + [2, 5], + [3, 6], + [4, 7], + [4, 2], + [4, 3], + [4, 4], + [4, 5], + [4, 6], + ] + ) + + ph = possible_hull(image) + assert_array_equal(ph, expected) + + +def test_object(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + expected_conn_1 = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 0, 1], + [1, 1, 1, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 0, 0, 1, 0, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + assert_array_equal(convex_hull_object(image, connectivity=1), expected_conn_1) + + expected_conn_2 = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 0, 0, 0, 0, 1, 1, 1], + [1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + assert_array_equal(convex_hull_object(image, connectivity=2), expected_conn_2) + + with testing.raises(ValueError): + convex_hull_object(image, connectivity=3) + + out = convex_hull_object(image, connectivity=1) + assert_array_equal(out, expected_conn_1) + + +def test_non_c_contiguous(): + # 2D Fortran-contiguous + image = np.ones((2, 2), order='F', dtype=bool) + assert_array_equal(convex_hull_image(image), image) + # 3D Fortran-contiguous + image = np.ones((2, 2, 2), order='F', dtype=bool) + assert_array_equal(convex_hull_image(image), image) + # 3D non-contiguous + image = np.transpose(np.ones((2, 2, 2), dtype=bool), [0, 2, 1]) + assert_array_equal(convex_hull_image(image), image) + + +@testing.fixture +def images2d3d(): + from ...measure.tests.test_regionprops import SAMPLE as image + + image3d = np.stack((image, image, image)) + return image, image3d + + +def test_consistent_2d_3d_hulls(images2d3d): + image, image3d = images2d3d + chimage = convex_hull_image(image) + chimage[8, 0] = True # correct for single point exactly on hull edge + chimage3d = convex_hull_image(image3d) + assert_array_equal(chimage3d[1], chimage) + + +def test_few_points(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + image3d = np.stack([image, image, image]) + with testing.assert_warns(UserWarning): + chimage3d = convex_hull_image(image3d) + assert_array_equal(chimage3d, np.zeros(image3d.shape, dtype=bool)) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_extrema.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_extrema.py new file mode 100644 index 0000000000000000000000000000000000000000..1966ecb9b19e15f70a9d34d7ea52843e4fdc8fde --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_extrema.py @@ -0,0 +1,713 @@ +import math +import unittest + +import numpy as np +from numpy.testing import assert_equal +from pytest import raises, warns + +from skimage._shared.testing import expected_warnings +from skimage.morphology import extrema + + +eps = 1e-12 + + +def diff(a, b): + a = np.asarray(a, dtype=np.float64) + b = np.asarray(b, dtype=np.float64) + t = ((a - b) ** 2).sum() + return math.sqrt(t) + + +class TestExtrema: + def test_saturated_arithmetic(self): + """Adding/subtracting a constant and clipping""" + # Test for unsigned integer + data = np.array( + [[250, 251, 5, 5], [100, 200, 253, 252], [4, 10, 1, 3]], dtype=np.uint8 + ) + # adding the constant + img_constant_added = extrema._add_constant_clip(data, 4) + expected = np.array( + [[254, 255, 9, 9], [104, 204, 255, 255], [8, 14, 5, 7]], dtype=np.uint8 + ) + error = diff(img_constant_added, expected) + assert error < eps + img_constant_subtracted = extrema._subtract_constant_clip(data, 4) + expected = np.array( + [[246, 247, 1, 1], [96, 196, 249, 248], [0, 6, 0, 0]], dtype=np.uint8 + ) + error = diff(img_constant_subtracted, expected) + assert error < eps + + # Test for signed integer + data = np.array([[32767, 32766], [-32768, -32767]], dtype=np.int16) + img_constant_added = extrema._add_constant_clip(data, 1) + expected = np.array([[32767, 32767], [-32767, -32766]], dtype=np.int16) + error = diff(img_constant_added, expected) + assert error < eps + img_constant_subtracted = extrema._subtract_constant_clip(data, 1) + expected = np.array([[32766, 32765], [-32768, -32768]], dtype=np.int16) + error = diff(img_constant_subtracted, expected) + assert error < eps + + def test_h_maxima(self): + """h-maxima for various data types""" + + data = np.array( + [ + [10, 11, 13, 14, 14, 15, 14, 14, 13, 11], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + [13, 15, 40, 40, 18, 18, 18, 60, 60, 15], + [14, 16, 40, 40, 19, 19, 19, 60, 60, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [15, 16, 18, 19, 19, 20, 19, 19, 18, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [14, 16, 80, 80, 19, 19, 19, 100, 100, 16], + [13, 15, 80, 80, 18, 18, 18, 100, 100, 15], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + ], + dtype=np.uint8, + ) + + expected_result = np.array( + [ + [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, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 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, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + for dtype in [np.uint8, np.uint64, np.int8, np.int64]: + data = data.astype(dtype) + out = extrema.h_maxima(data, 40) + + error = diff(expected_result, out) + assert error < eps + + def test_h_minima(self): + """h-minima for various data types""" + + data = np.array( + [ + [10, 11, 13, 14, 14, 15, 14, 14, 13, 11], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + [13, 15, 40, 40, 18, 18, 18, 60, 60, 15], + [14, 16, 40, 40, 19, 19, 19, 60, 60, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [15, 16, 18, 19, 19, 20, 19, 19, 18, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [14, 16, 80, 80, 19, 19, 19, 100, 100, 16], + [13, 15, 80, 80, 18, 18, 18, 100, 100, 15], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + ], + dtype=np.uint8, + ) + data = 100 - data + expected_result = np.array( + [ + [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, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 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, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + for dtype in [np.uint8, np.uint64, np.int8, np.int64]: + data = data.astype(dtype) + out = extrema.h_minima(data, 40) + + error = diff(expected_result, out) + assert error < eps + assert out.dtype == expected_result.dtype + + def test_extrema_float(self): + """specific tests for float type""" + data = np.array( + [ + [0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14, 0.14, 0.13, 0.11], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + [0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18, 0.60, 0.60, 0.15], + [0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19, 0.60, 0.60, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19, 1.0, 1.0, 0.16], + [0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18, 1.0, 1.0, 0.15], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + ], + dtype=np.float32, + ) + inverted_data = 1.0 - data + + out = extrema.h_maxima(data, 0.003) + expected_result = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + error = diff(expected_result, out) + assert error < eps + + out = extrema.h_minima(inverted_data, 0.003) + error = diff(expected_result, out) + assert error < eps + + def test_h_maxima_float_image(self): + """specific tests for h-maxima float image type""" + w = 10 + x, y = np.mgrid[0:w, 0:w] + data = 20 - 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2) + data[2:4, 2:4] = 40 + data[2:4, 7:9] = 60 + data[7:9, 2:4] = 80 + data[7:9, 7:9] = 100 + data = data.astype(np.float32) + + expected_result = np.zeros_like(data) + expected_result[(data > 19.9)] = 1.0 + + for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]: + out = extrema.h_maxima(data, h) + error = diff(expected_result, out) + assert error < eps + + def test_h_maxima_float_h(self): + """specific tests for h-maxima float h parameter""" + data = np.array( + [ + [0, 0, 0, 0, 0], + [0, 3, 3, 3, 0], + [0, 3, 4, 3, 0], + [0, 3, 3, 3, 0], + [0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + h_vals = np.linspace(1.0, 2.0, 100) + failures = 0 + + for h in h_vals: + if h % 1 != 0: + msgs = ['possible precision loss converting image'] + else: + msgs = [] + + with expected_warnings(msgs): + maxima = extrema.h_maxima(data, h) + + if maxima[2, 2] == 0: + failures += 1 + + assert failures == 0 + + def test_h_maxima_large_h(self): + """test that h-maxima works correctly for large h""" + data = np.array( + [ + [10, 10, 10, 10, 10], + [10, 13, 13, 13, 10], + [10, 13, 14, 13, 10], + [10, 13, 13, 13, 10], + [10, 10, 10, 10, 10], + ], + dtype=np.uint8, + ) + + maxima = extrema.h_maxima(data, 5) + assert np.sum(maxima) == 0 + + data = np.array( + [ + [10, 10, 10, 10, 10], + [10, 13, 13, 13, 10], + [10, 13, 14, 13, 10], + [10, 13, 13, 13, 10], + [10, 10, 10, 10, 10], + ], + dtype=np.float32, + ) + + maxima = extrema.h_maxima(data, 5.0) + assert np.sum(maxima) == 0 + + def test_h_minima_float_image(self): + """specific tests for h-minima float image type""" + w = 10 + x, y = np.mgrid[0:w, 0:w] + data = 180 + 0.2 * ((x - w / 2) ** 2 + (y - w / 2) ** 2) + data[2:4, 2:4] = 160 + data[2:4, 7:9] = 140 + data[7:9, 2:4] = 120 + data[7:9, 7:9] = 100 + data = data.astype(np.float32) + + expected_result = np.zeros_like(data) + expected_result[(data < 180.1)] = 1.0 + + for h in [1.0e-12, 1.0e-6, 1.0e-3, 1.0e-2, 1.0e-1, 0.1]: + out = extrema.h_minima(data, h) + error = diff(expected_result, out) + assert error < eps + + def test_h_minima_float_h(self): + """specific tests for h-minima float h parameter""" + data = np.array( + [ + [4, 4, 4, 4, 4], + [4, 1, 1, 1, 4], + [4, 1, 0, 1, 4], + [4, 1, 1, 1, 4], + [4, 4, 4, 4, 4], + ], + dtype=np.uint8, + ) + + h_vals = np.linspace(1.0, 2.0, 100) + failures = 0 + for h in h_vals: + if h % 1 != 0: + msgs = ['possible precision loss converting image'] + else: + msgs = [] + + with expected_warnings(msgs): + minima = extrema.h_minima(data, h) + + if minima[2, 2] == 0: + failures += 1 + + assert failures == 0 + + def test_h_minima_large_h(self): + """test that h-minima works correctly for large h""" + data = np.array( + [ + [14, 14, 14, 14, 14], + [14, 11, 11, 11, 14], + [14, 11, 10, 11, 14], + [14, 11, 11, 11, 14], + [14, 14, 14, 14, 14], + ], + dtype=np.uint8, + ) + + maxima = extrema.h_minima(data, 5) + assert np.sum(maxima) == 0 + + data = np.array( + [ + [14, 14, 14, 14, 14], + [14, 11, 11, 11, 14], + [14, 11, 10, 11, 14], + [14, 11, 11, 11, 14], + [14, 14, 14, 14, 14], + ], + dtype=np.float32, + ) + + maxima = extrema.h_minima(data, 5.0) + assert np.sum(maxima) == 0 + + +class TestLocalMaxima(unittest.TestCase): + """Some tests for local_minima are included as well.""" + + supported_dtypes = [ + np.uint8, + np.uint16, + np.uint32, + np.uint64, + np.int8, + np.int16, + np.int32, + np.int64, + np.float32, + np.float64, + ] + image = np.array( + [ + [1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 2, 0, 0, 3, 3, 0, 0, 4, 0, 2, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 4, 4, 0, 3, 0, 0, 0], + [0, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 3, 0, 0, 0], + [0, 0, 2, 0, 2, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + # Connectivity 2, maxima can touch border, returned with default values + expected_default = np.array( + [ + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + # Connectivity 1 (cross), maxima can touch border + expected_cross = np.array( + [ + [1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], + [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + def test_empty(self): + """Test result with empty image.""" + result = extrema.local_maxima(np.array([[]]), indices=False) + assert result.size == 0 + assert result.dtype == bool + assert result.shape == (1, 0) + + result = extrema.local_maxima(np.array([]), indices=True) + assert isinstance(result, tuple) + assert len(result) == 1 + assert result[0].size == 0 + assert result[0].dtype == np.intp + + result = extrema.local_maxima(np.array([[]]), indices=True) + assert isinstance(result, tuple) + assert len(result) == 2 + assert result[0].size == 0 + assert result[0].dtype == np.intp + assert result[1].size == 0 + assert result[1].dtype == np.intp + + def test_dtypes(self): + """Test results with default configuration for all supported dtypes.""" + for dtype in self.supported_dtypes: + result = extrema.local_maxima(self.image.astype(dtype)) + assert result.dtype == bool + assert_equal(result, self.expected_default) + + def test_dtypes_old(self): + """ + Test results with default configuration and data copied from old unit + tests for all supported dtypes. + """ + data = np.array( + [ + [10, 11, 13, 14, 14, 15, 14, 14, 13, 11], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + [13, 15, 40, 40, 18, 18, 18, 60, 60, 15], + [14, 16, 40, 40, 19, 19, 19, 60, 60, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [15, 16, 18, 19, 19, 20, 19, 19, 18, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [14, 16, 80, 80, 19, 19, 19, 100, 100, 16], + [13, 15, 80, 80, 18, 18, 18, 100, 100, 15], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + ], + dtype=np.uint8, + ) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + for dtype in self.supported_dtypes: + image = data.astype(dtype) + result = extrema.local_maxima(image) + assert result.dtype == bool + assert_equal(result, expected) + + def test_connectivity(self): + """Test results if footprint is a scalar.""" + # Connectivity 1: generates cross shaped footprint + result_conn1 = extrema.local_maxima(self.image, connectivity=1) + assert result_conn1.dtype == bool + assert_equal(result_conn1, self.expected_cross) + + # Connectivity 2: generates square shaped footprint + result_conn2 = extrema.local_maxima(self.image, connectivity=2) + assert result_conn2.dtype == bool + assert_equal(result_conn2, self.expected_default) + + # Connectivity 3: generates square shaped footprint + result_conn3 = extrema.local_maxima(self.image, connectivity=3) + assert result_conn3.dtype == bool + assert_equal(result_conn3, self.expected_default) + + def test_footprint(self): + """Test results if footprint is given.""" + footprint_cross = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=bool) + result_footprint_cross = extrema.local_maxima( + self.image, footprint=footprint_cross + ) + assert result_footprint_cross.dtype == bool + assert_equal(result_footprint_cross, self.expected_cross) + + for footprint in [ + ((True,) * 3,) * 3, + np.ones((3, 3), dtype=np.float64), + np.ones((3, 3), dtype=np.uint8), + np.ones((3, 3), dtype=bool), + ]: + # Test different dtypes for footprint which expects a boolean array + # but will accept and convert other types if possible + result_footprint_square = extrema.local_maxima( + self.image, footprint=footprint + ) + assert result_footprint_square.dtype == bool + assert_equal(result_footprint_square, self.expected_default) + + footprint_x = np.array([[1, 0, 1], [0, 1, 0], [1, 0, 1]], dtype=bool) + expected_footprint_x = np.array( + [ + [1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + result_footprint_x = extrema.local_maxima(self.image, footprint=footprint_x) + assert result_footprint_x.dtype == bool + assert_equal(result_footprint_x, expected_footprint_x) + + def test_indices(self): + """Test output if indices of peaks are desired.""" + # Connectivity 1 + expected_conn1 = np.nonzero(self.expected_cross) + result_conn1 = extrema.local_maxima(self.image, connectivity=1, indices=True) + assert_equal(result_conn1, expected_conn1) + + # Connectivity 2 + expected_conn2 = np.nonzero(self.expected_default) + result_conn2 = extrema.local_maxima(self.image, connectivity=2, indices=True) + assert_equal(result_conn2, expected_conn2) + + def test_allow_borders(self): + """Test maxima detection at the image border.""" + # Use connectivity 1 to allow many maxima, only filtering at border is + # of interest + result_with_boder = extrema.local_maxima( + self.image, connectivity=1, allow_borders=True + ) + assert result_with_boder.dtype == bool + assert_equal(result_with_boder, self.expected_cross) + + expected_without_border = np.array( + [ + [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, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0], + [0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + result_without_border = extrema.local_maxima( + self.image, connectivity=1, allow_borders=False + ) + assert result_with_boder.dtype == bool + assert_equal(result_without_border, expected_without_border) + + def test_nd(self): + """Test one- and three-dimensional case.""" + # One-dimension + x_1d = np.array([1, 1, 0, 1, 2, 3, 0, 2, 1, 2, 0]) + expected_1d = np.array([1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0], dtype=bool) + result_1d = extrema.local_maxima(x_1d) + assert result_1d.dtype == bool + assert_equal(result_1d, expected_1d) + + # 3-dimensions (adapted from old unit test) + x_3d = np.zeros((8, 8, 8), dtype=np.uint8) + expected_3d = np.zeros((8, 8, 8), dtype=bool) + # first maximum: only one pixel + x_3d[1, 1:3, 1:3] = 100 + x_3d[2, 2, 2] = 200 + x_3d[3, 1:3, 1:3] = 100 + expected_3d[2, 2, 2] = 1 + # second maximum: three pixels in z-direction + x_3d[5:8, 1, 1] = 200 + expected_3d[5:8, 1, 1] = 1 + # third: two maxima in 0 and 3. + x_3d[0, 5:8, 5:8] = 200 + x_3d[1, 6, 6] = 100 + x_3d[2, 5:7, 5:7] = 200 + x_3d[0:3, 5:8, 5:8] += 50 + expected_3d[0, 5:8, 5:8] = 1 + expected_3d[2, 5:7, 5:7] = 1 + # four : one maximum in the corner of the square + x_3d[6:8, 6:8, 6:8] = 200 + x_3d[7, 7, 7] = 255 + expected_3d[7, 7, 7] = 1 + result_3d = extrema.local_maxima(x_3d) + assert result_3d.dtype == bool + assert_equal(result_3d, expected_3d) + + def test_constant(self): + """Test behaviour for 'flat' images.""" + const_image = np.full((7, 6), 42, dtype=np.uint8) + expected = np.zeros((7, 6), dtype=np.uint8) + for dtype in self.supported_dtypes: + const_image = const_image.astype(dtype) + # test for local maxima + result = extrema.local_maxima(const_image) + assert result.dtype == bool + assert_equal(result, expected) + # test for local minima + result = extrema.local_minima(const_image) + assert result.dtype == bool + assert_equal(result, expected) + + def test_extrema_float(self): + """Specific tests for float type.""" + # Copied from old unit test for local_maxima + image = np.array( + [ + [0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14, 0.14, 0.13, 0.11], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + [0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18, 0.60, 0.60, 0.15], + [0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19, 0.60, 0.60, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19, 1.0, 1.0, 0.16], + [0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18, 1.0, 1.0, 0.15], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + ], + dtype=np.float32, + ) + inverted_image = 1.0 - image + expected_result = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + + # Test for local maxima with automatic step calculation + result = extrema.local_maxima(image) + assert result.dtype == bool + assert_equal(result, expected_result) + + # Test for local minima with automatic step calculation + result = extrema.local_minima(inverted_image) + assert result.dtype == bool + assert_equal(result, expected_result) + + def test_extrema_small_float(self): + image = np.array( + [ + [9.89232736e-20, 8.78543302e-20, 8.78543302e-20, 9.89232736e-20], + [8.78543302e-20, 6.38842355e-20, 6.38842355e-20, 8.78543302e-20], + [8.78543302e-20, 6.38842355e-20, 6.38842355e-20, 8.78543302e-20], + [9.89232736e-20, 8.78543302e-20, 8.78543302e-20, 9.89232736e-20], + ] + ) + + result = extrema.local_minima(image) + + expected_result = np.array( + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ) + + assert_equal(result, expected_result) + + def test_exceptions(self): + """Test if input validation triggers correct exceptions.""" + # Mismatching number of dimensions + with raises(ValueError, match="number of dimensions"): + extrema.local_maxima(self.image, footprint=np.ones((3, 3, 3), dtype=bool)) + with raises(ValueError, match="number of dimensions"): + extrema.local_maxima(self.image, footprint=np.ones((3,), dtype=bool)) + + # All dimensions in footprint must be of size 3 + with raises(ValueError, match="dimension size"): + extrema.local_maxima(self.image, footprint=np.ones((2, 3), dtype=bool)) + with raises(ValueError, match="dimension size"): + extrema.local_maxima(self.image, footprint=np.ones((5, 5), dtype=bool)) + + with raises(TypeError, match="float16 which is not supported"): + extrema.local_maxima(np.empty(1, dtype=np.float16)) + + def test_small_array(self): + """Test output for arrays with dimension smaller 3. + + If any dimension of an array is smaller than 3 and `allow_borders` is + false a footprint, which has at least 3 elements in each + dimension, can't be applied. This is an implementation detail so + `local_maxima` should still return valid output (see gh-3261). + + If `allow_borders` is true the array is padded internally and there is + no problem. + """ + warning_msg = "maxima can't exist .* any dimension smaller 3 .*" + x = np.array([0, 1]) + extrema.local_maxima(x, allow_borders=True) # no warning + with warns(UserWarning, match=warning_msg): + result = extrema.local_maxima(x, allow_borders=False) + assert_equal(result, [0, 0]) + assert result.dtype == bool + + x = np.array([[1, 2], [2, 2]]) + extrema.local_maxima(x, allow_borders=True, indices=True) # no warning + with warns(UserWarning, match=warning_msg): + result = extrema.local_maxima(x, allow_borders=False, indices=True) + assert_equal(result, np.zeros((2, 0), dtype=np.intp)) + assert result[0].dtype == np.intp + assert result[1].dtype == np.intp diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_flood_fill.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_flood_fill.py new file mode 100644 index 0000000000000000000000000000000000000000..6f2c9eb19d3a3dfad34d93107532d1b4e22cdb60 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_flood_fill.py @@ -0,0 +1,360 @@ +import numpy as np +import pytest + +from skimage.morphology import flood, flood_fill + +eps = 1e-12 + + +def test_empty_input(): + # Test shortcut + output = flood_fill(np.empty(0), (), 2) + assert output.size == 0 + + # Boolean output type + assert flood(np.empty(0), ()).dtype == bool + + # Maintain shape, even with zero size present + assert flood(np.empty((20, 0, 4)), ()).shape == (20, 0, 4) + + +def test_float16(): + image = np.array([9.0, 0.1, 42], dtype=np.float16) + with pytest.raises(TypeError, match="dtype of `image` is float16"): + flood_fill(image, 0, 1) + + +@pytest.mark.parametrize("tolerance", [-150, 150, -379, 379]) +def test_overrange_tolerance_int(tolerance): + image = np.arange(256, dtype=np.uint8).reshape((8, 8, 4)) + seed = (3, 4, 2) + expected = np.zeros_like(image) + output = flood_fill(image, seed, 0, tolerance=tolerance) + np.testing.assert_equal(output, expected) + + +def test_overrange_tolerance_float(): + max_value = np.finfo(np.float32).max + + image = np.random.uniform(size=(64, 64), low=-1.0, high=1.0).astype(np.float32) + image *= max_value + + expected = np.ones_like(image) + output = flood_fill(image, (0, 1), 1.0, tolerance=max_value.item() * 10) + + np.testing.assert_equal(output, expected) + + +def test_inplace_int(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3], + [0, 1, 1, 1, 3, 3, 4], + ] + ) + + flood_fill(image, (0, 0), 5, in_place=True) + + expected = np.array( + [ + [5, 5, 5, 5, 5, 5, 5], + [5, 1, 1, 5, 2, 2, 5], + [5, 1, 1, 5, 2, 2, 5], + [1, 5, 5, 5, 5, 5, 3], + [5, 1, 1, 1, 3, 3, 4], + ] + ) + + np.testing.assert_array_equal(image, expected) + + +def test_inplace_float(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3], + [0, 1, 1, 1, 3, 3, 4], + ], + dtype=np.float32, + ) + + flood_fill(image, (0, 0), 5, in_place=True) + + expected = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0], + [5.0, 1.0, 1.0, 5.0, 2.0, 2.0, 5.0], + [5.0, 1.0, 1.0, 5.0, 2.0, 2.0, 5.0], + [1.0, 5.0, 5.0, 5.0, 5.0, 5.0, 3.0], + [5.0, 1.0, 1.0, 1.0, 3.0, 3.0, 4.0], + ], + dtype=np.float32, + ) + + np.testing.assert_allclose(image, expected) + + +def test_inplace_noncontiguous(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3], + [0, 1, 1, 1, 3, 3, 4], + ] + ) + + # Transpose is noncontiguous + image2 = image[::2, ::2] + + flood_fill(image2, (0, 0), 5, in_place=True) + + # The inplace modified result + expected2 = np.array([[5, 5, 5, 5], [5, 1, 2, 5], [5, 1, 3, 4]]) + + np.testing.assert_allclose(image2, expected2) + + # Projected back through the view, `image` also modified + expected = np.array( + [ + [5, 0, 5, 0, 5, 0, 5], + [0, 1, 1, 0, 2, 2, 0], + [5, 1, 1, 0, 2, 2, 5], + [1, 0, 0, 0, 0, 0, 3], + [5, 1, 1, 1, 3, 3, 4], + ] + ) + + np.testing.assert_allclose(image, expected) + + +def test_1d(): + image = np.arange(11) + expected = np.array([0, 1, -20, -20, -20, -20, -20, -20, -20, 9, 10]) + + output = flood_fill(image, 5, -20, tolerance=3) + output2 = flood_fill(image, (5,), -20, tolerance=3) + + np.testing.assert_equal(output, expected) + np.testing.assert_equal(output, output2) + + +def test_wraparound(): + # If the borders (or neighbors) aren't correctly accounted for, this fails, + # because the algorithm uses an ravelled array. + test = np.zeros((5, 7), dtype=np.float64) + test[:, 3] = 100 + + expected = np.array( + [ + [-1.0, -1.0, -1.0, 100.0, 0.0, 0.0, 0.0], + [-1.0, -1.0, -1.0, 100.0, 0.0, 0.0, 0.0], + [-1.0, -1.0, -1.0, 100.0, 0.0, 0.0, 0.0], + [-1.0, -1.0, -1.0, 100.0, 0.0, 0.0, 0.0], + [-1.0, -1.0, -1.0, 100.0, 0.0, 0.0, 0.0], + ] + ) + + np.testing.assert_equal(flood_fill(test, (0, 0), -1), expected) + + +def test_neighbors(): + # This test will only pass if the neighbors are exactly correct + test = np.zeros((5, 7), dtype=np.float64) + test[:, 3] = 100 + + expected = np.array( + [ + [0, 0, 0, 255, 0, 0, 0], + [0, 0, 0, 255, 0, 0, 0], + [0, 0, 0, 255, 0, 0, 0], + [0, 0, 0, 255, 0, 0, 0], + [0, 0, 0, 255, 0, 0, 0], + ] + ) + output = flood_fill(test, (0, 3), 255) + + np.testing.assert_equal(output, expected) + + test[2] = 100 + expected[2] = 255 + + output2 = flood_fill(test, (2, 3), 255) + + np.testing.assert_equal(output2, expected) + + +def test_footprint(): + # Basic tests for nonstandard footprints + footprint = np.array([[0, 1, 1], [0, 1, 1], [0, 0, 0]]) # Cannot grow left or down + + output = flood_fill( + np.zeros((5, 6), dtype=np.uint8), (3, 1), 255, footprint=footprint + ) + + expected = np.array( + [ + [0, 255, 255, 255, 255, 255], + [0, 255, 255, 255, 255, 255], + [0, 255, 255, 255, 255, 255], + [0, 255, 255, 255, 255, 255], + [0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + np.testing.assert_equal(output, expected) + + footprint = np.array([[0, 0, 0], [1, 1, 0], [1, 1, 0]]) # Cannot grow right or up + + output = flood_fill( + np.zeros((5, 6), dtype=np.uint8), (1, 4), 255, footprint=footprint + ) + + expected = np.array( + [ + [0, 0, 0, 0, 0, 0], + [255, 255, 255, 255, 255, 0], + [255, 255, 255, 255, 255, 0], + [255, 255, 255, 255, 255, 0], + [255, 255, 255, 255, 255, 0], + ], + dtype=np.uint8, + ) + + np.testing.assert_equal(output, expected) + + +def test_basic_nd(): + for dimension in (3, 4, 5): + shape = (5,) * dimension + hypercube = np.zeros(shape) + slice_mid = tuple(slice(1, -1, None) for dim in range(dimension)) + hypercube[slice_mid] = 1 # sum is 3**dimension + filled = flood_fill(hypercube, (2,) * dimension, 2) + + # Test that the middle sum is correct + assert filled.sum() == 3**dimension * 2 + + # Test that the entire array is as expected + np.testing.assert_equal( + filled, np.pad(np.ones((3,) * dimension) * 2, 1, 'constant') + ) + + +@pytest.mark.parametrize("tolerance", [None, 0]) +def test_f_order(tolerance): + image = np.array( + [ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 1, 0, 0], + ], + order="F", + ) + expected = np.array( + [ + [0, 0, 0, 0], + [1, 0, 0, 0], + [0, 1, 0, 0], + ], + dtype=bool, + ) + + mask = flood(image, seed_point=(1, 0), tolerance=tolerance) + np.testing.assert_array_equal(expected, mask) + + mask = flood(image, seed_point=(2, 1), tolerance=tolerance) + np.testing.assert_array_equal(expected, mask) + + +def test_negative_indexing_seed_point(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 2, 2, 0], + [0, 1, 1, 0, 2, 2, 0], + [1, 0, 0, 0, 0, 0, 3], + [0, 1, 1, 1, 3, 3, 4], + ], + dtype=np.float32, + ) + + expected = np.array( + [ + [5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0], + [5.0, 1.0, 1.0, 5.0, 2.0, 2.0, 5.0], + [5.0, 1.0, 1.0, 5.0, 2.0, 2.0, 5.0], + [1.0, 5.0, 5.0, 5.0, 5.0, 5.0, 3.0], + [5.0, 1.0, 1.0, 1.0, 3.0, 3.0, 4.0], + ], + dtype=np.float32, + ) + + image = flood_fill(image, (0, -1), 5) + + np.testing.assert_allclose(image, expected) + + +def test_non_adjacent_footprint(): + # Basic tests for non-adjacent footprints + footprint = np.array( + [ + [1, 0, 0, 0, 1], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [1, 0, 0, 0, 1], + ] + ) + + output = flood_fill( + np.zeros((5, 6), dtype=np.uint8), (2, 3), 255, footprint=footprint + ) + + expected = np.array( + [ + [0, 255, 0, 0, 0, 255], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 255, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 255, 0, 0, 0, 255], + ], + dtype=np.uint8, + ) + + np.testing.assert_equal(output, expected) + + footprint = np.array( + [ + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + ] + ) + + image = np.zeros((5, 10), dtype=np.uint8) + image[:, (3, 7, 8)] = 100 + + output = flood_fill(image, (0, 0), 255, footprint=footprint) + + expected = np.array( + [ + [255, 255, 255, 100, 255, 255, 255, 100, 100, 0], + [255, 255, 255, 100, 255, 255, 255, 100, 100, 0], + [255, 255, 255, 100, 255, 255, 255, 100, 100, 0], + [255, 255, 255, 100, 255, 255, 255, 100, 100, 0], + [255, 255, 255, 100, 255, 255, 255, 100, 100, 0], + ], + dtype=np.uint8, + ) + + np.testing.assert_equal(output, expected) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_footprints.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_footprints.py new file mode 100644 index 0000000000000000000000000000000000000000..7889b2a22900c6b24152294d0dc7f1b1fb0fb361 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_footprints.py @@ -0,0 +1,281 @@ +""" +Tests for Morphological footprints +(skimage.morphology.footprint) + +Author: Damian Eads +""" + +import numpy as np +import pytest +from numpy.testing import assert_equal + +from skimage._shared.testing import fetch, assert_stacklevel +from skimage.morphology import footprints +from skimage.morphology import footprint_rectangle, footprint_from_sequence + + +class TestFootprints: + def strel_worker(self, fn, func): + matlab_masks = np.load(fetch(fn)) + k = 0 + for arrname in sorted(matlab_masks): + expected_mask = matlab_masks[arrname] + actual_mask = func(k) + if expected_mask.shape == (1,): + expected_mask = expected_mask[:, np.newaxis] + assert_equal(expected_mask, actual_mask) + k = k + 1 + + def strel_worker_3d(self, fn, func): + matlab_masks = np.load(fetch(fn)) + k = 0 + for arrname in sorted(matlab_masks): + expected_mask = matlab_masks[arrname] + actual_mask = func(k) + if expected_mask.shape == (1,): + expected_mask = expected_mask[:, np.newaxis] + # Test center slice for each dimension. This gives a good + # indication of validity without the need for a 3D reference + # mask. + c = int(expected_mask.shape[0] / 2) + assert_equal(expected_mask, actual_mask[c, :, :]) + assert_equal(expected_mask, actual_mask[:, c, :]) + assert_equal(expected_mask, actual_mask[:, :, c]) + k = k + 1 + + def test_footprint_disk(self): + """Test disk footprints""" + self.strel_worker("data/disk-matlab-output.npz", footprints.disk) + + def test_footprint_diamond(self): + """Test diamond footprints""" + self.strel_worker("data/diamond-matlab-output.npz", footprints.diamond) + + def test_footprint_ball(self): + """Test ball footprints""" + self.strel_worker_3d("data/disk-matlab-output.npz", footprints.ball) + + def test_footprint_octahedron(self): + """Test octahedron footprints""" + self.strel_worker_3d("data/diamond-matlab-output.npz", footprints.octahedron) + + def test_footprint_octagon(self): + """Test octagon footprints""" + expected_mask1 = np.array( + [ + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + ], + dtype=np.uint8, + ) + actual_mask1 = footprints.octagon(5, 3) + expected_mask2 = np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]], dtype=np.uint8) + actual_mask2 = footprints.octagon(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + + def test_footprint_ellipse(self): + """Test ellipse footprints""" + expected_mask1 = np.array( + [ + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + ], + dtype=np.uint8, + ) + actual_mask1 = footprints.ellipse(5, 3) + expected_mask2 = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8) + actual_mask2 = footprints.ellipse(1, 1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + assert_equal(expected_mask1, footprints.ellipse(3, 5).T) + assert_equal(expected_mask2, footprints.ellipse(1, 1).T) + + def test_footprint_star(self): + """Test star footprints""" + expected_mask1 = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + actual_mask1 = footprints.star(4) + expected_mask2 = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.uint8) + actual_mask2 = footprints.star(1) + assert_equal(expected_mask1, actual_mask1) + assert_equal(expected_mask2, actual_mask2) + + +@pytest.mark.parametrize( + 'function, args, supports_sequence_decomposition', + [ + (footprints.disk, (3,), True), + (footprints.ball, (3,), True), + (footprints.diamond, (3,), True), + (footprints.octahedron, (3,), True), + (footprint_rectangle, ((3, 5),), True), + (footprints.ellipse, (3, 4), False), + (footprints.octagon, (3, 4), True), + (footprints.star, (3,), False), + ], +) +@pytest.mark.parametrize("dtype", [np.uint8, np.float64]) +def test_footprint_dtype(function, args, supports_sequence_decomposition, dtype): + # make sure footprint dtype matches what was requested + footprint = function(*args, dtype=dtype) + assert footprint.dtype == dtype + + if supports_sequence_decomposition: + sequence = function(*args, dtype=dtype, decomposition='sequence') + assert all([fp_tuple[0].dtype == dtype for fp_tuple in sequence]) + + +@pytest.mark.parametrize("function", ["disk", "ball"]) +@pytest.mark.parametrize("radius", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 50, 75, 100]) +def test_nsphere_series_approximation(function, radius): + fp_func = getattr(footprints, function) + expected = fp_func(radius, strict_radius=False, decomposition=None) + footprint_sequence = fp_func(radius, strict_radius=False, decomposition="sequence") + approximate = footprints.footprint_from_sequence(footprint_sequence) + assert approximate.shape == expected.shape + + # verify that maximum error does not exceed some fraction of the size + error = np.sum(np.abs(expected.astype(int) - approximate.astype(int))) + if radius == 1: + assert error == 0 + else: + max_error = 0.1 if function == "disk" else 0.15 + assert error / expected.size <= max_error + + +@pytest.mark.parametrize("radius", [1, 2, 3, 4, 5, 10, 20, 50, 75]) +@pytest.mark.parametrize("strict_radius", [False, True]) +def test_disk_crosses_approximation(radius, strict_radius): + fp_func = footprints.disk + expected = fp_func(radius, strict_radius=strict_radius, decomposition=None) + footprint_sequence = fp_func( + radius, strict_radius=strict_radius, decomposition="crosses" + ) + approximate = footprints.footprint_from_sequence(footprint_sequence) + assert approximate.shape == expected.shape + + # verify that maximum error does not exceed some fraction of the size + error = np.sum(np.abs(expected.astype(int) - approximate.astype(int))) + max_error = 0.05 + assert error / expected.size <= max_error + + +@pytest.mark.parametrize("width", [3, 8, 20, 50]) +@pytest.mark.parametrize("height", [3, 8, 20, 50]) +def test_ellipse_crosses_approximation(width, height): + fp_func = footprints.ellipse + expected = fp_func(width, height, decomposition=None) + footprint_sequence = fp_func(width, height, decomposition="crosses") + approximate = footprints.footprint_from_sequence(footprint_sequence) + assert approximate.shape == expected.shape + + # verify that maximum error does not exceed some fraction of the size + error = np.sum(np.abs(expected.astype(int) - approximate.astype(int))) + max_error = 0.05 + assert error / expected.size <= max_error + + +def test_disk_series_approximation_unavailable(): + # ValueError if radius is too large (only precomputed up to radius=250) + with pytest.raises(ValueError): + footprints.disk(radius=10000, decomposition="sequence") + + +def test_ball_series_approximation_unavailable(): + # ValueError if radius is too large (only precomputed up to radius=100) + with pytest.raises(ValueError): + footprints.ball(radius=10000, decomposition="sequence") + + +@pytest.mark.parametrize("as_sequence", [tuple, None]) +def test_mirror_footprint(as_sequence): + footprint = np.array([[0, 0, 0], [0, 1, 1], [0, 1, 1]], np.uint8) + expected_res = np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]], dtype=np.uint8) + if as_sequence is not None: + footprint = as_sequence([(footprint, 2), (footprint.T, 3)]) + expected_res = as_sequence([(expected_res, 2), (expected_res.T, 3)]) + + actual_res = footprints.mirror_footprint(footprint) + assert type(expected_res) is type(actual_res) + assert_equal(expected_res, actual_res) + + +@pytest.mark.parametrize("as_sequence", [tuple, None]) +@pytest.mark.parametrize("pad_end", [True, False]) +def test_pad_footprint(as_sequence, pad_end): + footprint = np.array([[0, 0], [1, 0], [1, 1]], np.uint8) + pad_width = [(0, 0), (0, 1)] if pad_end is True else [(0, 0), (1, 0)] + expected_res = np.pad(footprint, pad_width) + if as_sequence is not None: + footprint = as_sequence([(footprint, 2), (footprint.T, 3)]) + expected_res = as_sequence([(expected_res, 2), (expected_res.T, 3)]) + + actual_res = footprints.pad_footprint(footprint, pad_end=pad_end) + assert type(expected_res) is type(actual_res) + assert_equal(expected_res, actual_res) + + +class Test_footprint_rectangule: + @pytest.mark.parametrize("i", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("j", [0, 1, 2, 3, 4]) + def test_rectangle(self, i, j): + desired = np.ones((i, j), dtype='uint8') + actual = footprint_rectangle((i, j)) + assert_equal(actual, desired) + + @pytest.mark.parametrize("i", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("j", [0, 1, 2, 3, 4]) + @pytest.mark.parametrize("k", [0, 1, 2, 3, 4]) + def test_cuboid(self, i, j, k): + desired = np.ones((i, j, k), dtype='uint8') + actual = footprint_rectangle((i, j, k)) + assert_equal(actual, desired) + + @pytest.mark.parametrize("shape", [(3,), (5, 5), (5, 5, 7)]) + @pytest.mark.parametrize("decomposition", ["separable", "sequence"]) + def test_decomposition(self, shape, decomposition): + regular = footprint_rectangle(shape) + decomposed = footprint_rectangle(shape, decomposition=decomposition) + recomposed = footprint_from_sequence(decomposed) + assert_equal(recomposed, regular) + + @pytest.mark.parametrize("shape", [(2,), (3, 4)]) + def test_uneven_sequence_decomposition_warning(self, shape): + """Should fall back to decomposition="separable" for uneven footprint size.""" + desired = footprint_rectangle(shape, decomposition="separable") + regex = "decomposition='sequence' is only supported for uneven footprints" + with pytest.warns(UserWarning, match=regex) as record: + actual = footprint_rectangle(shape, decomposition="sequence") + assert_stacklevel(record) + assert_equal(actual, desired) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_gray.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_gray.py new file mode 100644 index 0000000000000000000000000000000000000000..7073f22845c2d9078cca6dadcd500235c4371264 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_gray.py @@ -0,0 +1,500 @@ +import numpy as np +import pytest +from scipy import ndimage as ndi +from numpy.testing import assert_allclose, assert_array_equal, assert_equal + +from skimage import color, data, transform +from skimage._shared._warnings import expected_warnings +from skimage._shared.testing import fetch, assert_stacklevel +from skimage.morphology import gray, footprints, footprint_rectangle +from skimage.util import img_as_uint, img_as_ubyte + + +@pytest.fixture +def cam_image(): + from skimage import data + + return np.ascontiguousarray(data.camera()[64:112, 64:96]) + + +@pytest.fixture +def cell3d_image(): + from skimage import data + + return np.ascontiguousarray(data.cells3d()[30:48, 0, 20:36, 20:32]) + + +gray_morphology_funcs = ( + gray.erosion, + gray.dilation, + gray.opening, + gray.closing, + gray.white_tophat, + gray.black_tophat, +) + + +class TestMorphology: + # These expected outputs were generated with skimage v0.22.0 + PR #6695 + # using: + # + # from skimage.morphology.tests.test_gray import TestMorphology + # import numpy as np + # output = TestMorphology()._build_expected_output() + # np.savez_compressed('gray_morph_output.npz', **output) + + def _build_expected_output(self): + def square(n): + return footprint_rectangle((n, n)) + + footprints_2D = ( + square, + footprints.diamond, + footprints.disk, + footprints.star, + ) + + image = img_as_ubyte( + transform.downscale_local_mean(color.rgb2gray(data.coffee()), (20, 20)) + ) + + output = {} + for n in range(1, 4): + for strel in footprints_2D: + for func in gray_morphology_funcs: + key = f'{strel.__name__}_{n}_{func.__name__}' + output[key] = func(image, strel(n)) + + return output + + def test_gray_morphology(self): + expected = dict(np.load(fetch('data/gray_morph_output.npz'))) + calculated = self._build_expected_output() + assert_equal(expected, calculated) + + def test_gray_closing_extensive(self): + img = data.coins() + footprint = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]]) + + # Default mode="reflect" is not extensive for backwards-compatibility + result_default = gray.closing(img, footprint=footprint) + assert not np.all(result_default >= img) + + result = gray.closing(img, footprint=footprint, mode="ignore") + assert np.all(result >= img) + + def test_gray_opening_anti_extensive(self): + img = data.coins() + footprint = np.array([[0, 0, 1], [0, 1, 1], [1, 1, 1]]) + + # Default mode="reflect" is not extensive for backwards-compatibility + result_default = gray.opening(img, footprint=footprint) + assert not np.all(result_default <= img) + + result_ignore = gray.opening(img, footprint=footprint, mode="ignore") + assert np.all(result_ignore <= img) + + @pytest.mark.parametrize("func", gray_morphology_funcs) + @pytest.mark.parametrize("mode", gray._SUPPORTED_MODES) + def test_supported_mode(self, func, mode): + img = np.ones((10, 10)) + func(img, mode=mode) + + @pytest.mark.parametrize("func", gray_morphology_funcs) + @pytest.mark.parametrize("mode", ["", "symmetric", 3, None]) + def test_unsupported_mode(self, func, mode): + img = np.ones((10, 10)) + with pytest.raises(ValueError, match="unsupported mode"): + func(img, mode=mode) + + +class TestEccentricStructuringElements: + def setup_class(self): + self.black_pixel = 255 * np.ones((6, 6), dtype=np.uint8) + self.black_pixel[2, 2] = 0 + self.white_pixel = 255 - self.black_pixel + self.footprints = [ + footprint_rectangle((2, 2)), + footprint_rectangle((2, 1)), + footprint_rectangle((2, 1)), + ] + + def test_dilate_erode_symmetry(self): + for s in self.footprints: + c = gray.erosion(self.black_pixel, s) + d = gray.dilation(self.white_pixel, s) + assert np.all(c == (255 - d)) + + def test_open_black_pixel(self): + for s in self.footprints: + gray_open = gray.opening(self.black_pixel, s) + assert np.all(gray_open == self.black_pixel) + + def test_close_white_pixel(self): + for s in self.footprints: + gray_close = gray.closing(self.white_pixel, s) + assert np.all(gray_close == self.white_pixel) + + def test_open_white_pixel(self): + for s in self.footprints: + assert np.all(gray.opening(self.white_pixel, s) == 0) + + def test_close_black_pixel(self): + for s in self.footprints: + assert np.all(gray.closing(self.black_pixel, s) == 255) + + def test_white_tophat_white_pixel(self): + for s in self.footprints: + tophat = gray.white_tophat(self.white_pixel, s) + assert np.all(tophat == self.white_pixel) + + def test_black_tophat_black_pixel(self): + for s in self.footprints: + tophat = gray.black_tophat(self.black_pixel, s) + assert np.all(tophat == self.white_pixel) + + def test_white_tophat_black_pixel(self): + for s in self.footprints: + tophat = gray.white_tophat(self.black_pixel, s) + assert np.all(tophat == 0) + + def test_black_tophat_white_pixel(self): + for s in self.footprints: + tophat = gray.black_tophat(self.white_pixel, s) + assert np.all(tophat == 0) + + +gray_functions = [ + gray.erosion, + gray.dilation, + gray.opening, + gray.closing, + gray.white_tophat, + gray.black_tophat, +] + + +@pytest.mark.parametrize("function", gray_functions) +def test_default_footprint(function): + strel = footprints.diamond(radius=1) + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + np.uint8, + ) + im_expected = function(image, strel) + im_test = function(image) + assert_array_equal(im_expected, im_test) + + +def test_3d_fallback_default_footprint(): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), bool) + image[2:-2, 2:-2, 2:-2] = 1 + + opened = gray.opening(image) + + # expect a "hyper-cross" centered in the 5x5x5: + image_expected = np.zeros((7, 7, 7), dtype=bool) + image_expected[2:5, 2:5, 2:5] = ndi.generate_binary_structure(3, 1) + assert_array_equal(opened, image_expected) + + +gray_3d_fallback_functions = [gray.closing, gray.opening] + + +@pytest.mark.parametrize("function", gray_3d_fallback_functions) +def test_3d_fallback_cube_footprint(function): + # 3x3x3 cube inside a 7x7x7 image: + image = np.zeros((7, 7, 7), bool) + image[2:-2, 2:-2, 2:-2] = 1 + + cube = np.ones((3, 3, 3), dtype=np.uint8) + + new_image = function(image, cube) + assert_array_equal(new_image, image) + + +def test_3d_fallback_white_tophat(): + image = np.zeros((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 1 + image[3, 2:5, 2:5] = 1 + image[4, 3:5, 3:5] = 1 + + with expected_warnings([r'operator.*deprecated|\A\Z']): + new_image = gray.white_tophat(image) + footprint = ndi.generate_binary_structure(3, 1) + with expected_warnings([r'operator.*deprecated|\A\Z']): + image_expected = ndi.white_tophat( + image.view(dtype=np.uint8), footprint=footprint + ) + assert_array_equal(new_image, image_expected) + + +def test_3d_fallback_black_tophat(): + image = np.ones((7, 7, 7), dtype=bool) + image[2, 2:4, 2:4] = 0 + image[3, 2:5, 2:5] = 0 + image[4, 3:5, 3:5] = 0 + + with expected_warnings([r'operator.*deprecated|\A\Z']): + new_image = gray.black_tophat(image) + footprint = ndi.generate_binary_structure(3, 1) + with expected_warnings([r'operator.*deprecated|\A\Z']): + image_expected = ndi.black_tophat( + image.view(dtype=np.uint8), footprint=footprint + ) + assert_array_equal(new_image, image_expected) + + +def test_2d_ndimage_equivalence(): + image = np.zeros((9, 9), np.uint8) + image[2:-2, 2:-2] = 128 + image[3:-3, 3:-3] = 196 + image[4, 4] = 255 + + opened = gray.opening(image) + closed = gray.closing(image) + + footprint = ndi.generate_binary_structure(2, 1) + ndimage_opened = ndi.grey_opening(image, footprint=footprint) + ndimage_closed = ndi.grey_closing(image, footprint=footprint) + + assert_array_equal(opened, ndimage_opened) + assert_array_equal(closed, ndimage_closed) + + +# float test images +im = np.array( + [ + [0.55, 0.72, 0.6, 0.54, 0.42], + [0.65, 0.44, 0.89, 0.96, 0.38], + [0.79, 0.53, 0.57, 0.93, 0.07], + [0.09, 0.02, 0.83, 0.78, 0.87], + [0.98, 0.8, 0.46, 0.78, 0.12], + ] +) + +eroded = np.array( + [ + [0.55, 0.44, 0.54, 0.42, 0.38], + [0.44, 0.44, 0.44, 0.38, 0.07], + [0.09, 0.02, 0.53, 0.07, 0.07], + [0.02, 0.02, 0.02, 0.78, 0.07], + [0.09, 0.02, 0.46, 0.12, 0.12], + ] +) + +dilated = np.array( + [ + [0.72, 0.72, 0.89, 0.96, 0.54], + [0.79, 0.89, 0.96, 0.96, 0.96], + [0.79, 0.79, 0.93, 0.96, 0.93], + [0.98, 0.83, 0.83, 0.93, 0.87], + [0.98, 0.98, 0.83, 0.78, 0.87], + ] +) + +opened = np.array( + [ + [0.55, 0.55, 0.54, 0.54, 0.42], + [0.55, 0.44, 0.54, 0.44, 0.38], + [0.44, 0.53, 0.53, 0.78, 0.07], + [0.09, 0.02, 0.78, 0.78, 0.78], + [0.09, 0.46, 0.46, 0.78, 0.12], + ] +) + +closed = np.array( + [ + [0.72, 0.72, 0.72, 0.54, 0.54], + [0.72, 0.72, 0.89, 0.96, 0.54], + [0.79, 0.79, 0.79, 0.93, 0.87], + [0.79, 0.79, 0.83, 0.78, 0.87], + [0.98, 0.83, 0.78, 0.78, 0.78], + ] +) + + +def test_float(): + assert_allclose(gray.erosion(im), eroded) + assert_allclose(gray.dilation(im), dilated) + assert_allclose(gray.opening(im), opened) + assert_allclose(gray.closing(im), closed) + + +def test_uint16(): + im16, eroded16, dilated16, opened16, closed16 = map( + img_as_uint, [im, eroded, dilated, opened, closed] + ) + assert_allclose(gray.erosion(im16), eroded16) + assert_allclose(gray.dilation(im16), dilated16) + assert_allclose(gray.opening(im16), opened16) + assert_allclose(gray.closing(im16), closed16) + + +def test_discontiguous_out_array(): + image = np.array([[5, 6, 2], [7, 2, 2], [3, 5, 1]], np.uint8) + out_array_big = np.zeros((5, 5), np.uint8) + out_array = out_array_big[::2, ::2] + expected_dilation = np.array( + [ + [7, 0, 6, 0, 6], + [0, 0, 0, 0, 0], + [7, 0, 7, 0, 2], + [0, 0, 0, 0, 0], + [7, 0, 5, 0, 5], + ], + np.uint8, + ) + expected_erosion = np.array( + [ + [5, 0, 2, 0, 2], + [0, 0, 0, 0, 0], + [2, 0, 2, 0, 1], + [0, 0, 0, 0, 0], + [3, 0, 1, 0, 1], + ], + np.uint8, + ) + gray.dilation(image, out=out_array) + assert_array_equal(out_array_big, expected_dilation) + gray.erosion(image, out=out_array) + assert_array_equal(out_array_big, expected_erosion) + + +def test_1d_erosion(): + image = np.array([1, 2, 3, 2, 1]) + expected = np.array([1, 1, 2, 1, 1]) + eroded = gray.erosion(image) + assert_array_equal(eroded, expected) + + +@pytest.mark.parametrize( + "function", + ["erosion", "dilation", "closing", "opening", "white_tophat", "black_tophat"], +) +@pytest.mark.parametrize("nrows", [3, 7, 11]) +@pytest.mark.parametrize("ncols", [3, 7, 11]) +@pytest.mark.parametrize("decomposition", ['separable', 'sequence']) +def test_rectangle_decomposition(cam_image, function, nrows, ncols, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprint_rectangle((nrows, ncols), decomposition=None) + footprint = footprint_rectangle((nrows, ncols), decomposition=decomposition) + func = getattr(gray, function) + expected = func(cam_image, footprint=footprint_ndarray) + out = func(cam_image, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["erosion", "dilation", "closing", "opening", "white_tophat", "black_tophat"], +) +@pytest.mark.parametrize("radius", (2, 3)) +@pytest.mark.parametrize("decomposition", ['sequence']) +def test_diamond_decomposition(cam_image, function, radius, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprints.diamond(radius, decomposition=None) + footprint = footprints.diamond(radius, decomposition=decomposition) + func = getattr(gray, function) + expected = func(cam_image, footprint=footprint_ndarray) + out = func(cam_image, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["erosion", "dilation", "closing", "opening", "white_tophat", "black_tophat"], +) +@pytest.mark.parametrize("m", (0, 1, 3, 5)) +@pytest.mark.parametrize("n", (0, 1, 2, 3)) +@pytest.mark.parametrize("decomposition", ['sequence']) +@pytest.mark.filterwarnings( + "ignore:.*falling back to decomposition='separable':UserWarning:skimage" +) +def test_octagon_decomposition(cam_image, function, m, n, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + if m == 0 and n == 0: + with pytest.raises(ValueError): + footprints.octagon(m, n, decomposition=decomposition) + else: + footprint_ndarray = footprints.octagon(m, n, decomposition=None) + footprint = footprints.octagon(m, n, decomposition=decomposition) + func = getattr(gray, function) + expected = func(cam_image, footprint=footprint_ndarray) + out = func(cam_image, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["erosion", "dilation", "closing", "opening", "white_tophat", "black_tophat"], +) +@pytest.mark.parametrize("shape", [(5, 5, 5), (5, 5, 7)]) +@pytest.mark.parametrize("decomposition", ['separable', 'sequence']) +def test_cube_decomposition(cell3d_image, function, shape, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprint_rectangle(shape, decomposition=None) + footprint = footprint_rectangle(shape, decomposition=decomposition) + func = getattr(gray, function) + expected = func(cell3d_image, footprint=footprint_ndarray) + out = func(cell3d_image, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize( + "function", + ["erosion", "dilation", "closing", "opening", "white_tophat", "black_tophat"], +) +@pytest.mark.parametrize("radius", (3,)) +@pytest.mark.parametrize("decomposition", ['sequence']) +def test_octahedron_decomposition(cell3d_image, function, radius, decomposition): + """Validate footprint decomposition for various shapes. + + comparison is made to the case without decomposition. + """ + footprint_ndarray = footprints.octahedron(radius, decomposition=None) + footprint = footprints.octahedron(radius, decomposition=decomposition) + func = getattr(gray, function) + expected = func(cell3d_image, footprint=footprint_ndarray) + out = func(cell3d_image, footprint=footprint) + assert_array_equal(expected, out) + + +@pytest.mark.parametrize("func", [gray.erosion, gray.dilation]) +@pytest.mark.parametrize("name", ["shift_x", "shift_y"]) +@pytest.mark.parametrize("value", [True, False, None]) +def test_deprecated_shift(func, name, value): + img = np.ones(10) + func(img) # Shouldn't warn + + regex = "`shift_x` and `shift_y` are deprecated" + with pytest.warns(FutureWarning, match=regex) as record: + func(img, **{name: value}) + assert_stacklevel(record) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_isotropic.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_isotropic.py new file mode 100644 index 0000000000000000000000000000000000000000..d74f5198ee632b9fd8a7a4471d3688224646c89d --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_isotropic.py @@ -0,0 +1,83 @@ +import numpy as np +from numpy.testing import assert_array_equal + +from skimage import color, data, morphology +from skimage.morphology import binary, isotropic +from skimage.util import img_as_bool + +img = color.rgb2gray(data.astronaut()) +bw_img = img > 100 / 255.0 + + +def test_non_square_image(): + isotropic_res = isotropic.isotropic_erosion(bw_img[:100, :200], 3) + binary_res = img_as_bool( + binary.binary_erosion(bw_img[:100, :200], morphology.disk(3)) + ) + assert_array_equal(isotropic_res, binary_res) + + +def test_isotropic_erosion(): + isotropic_res = isotropic.isotropic_erosion(bw_img, 3) + binary_res = img_as_bool(binary.binary_erosion(bw_img, morphology.disk(3))) + assert_array_equal(isotropic_res, binary_res) + + +def _disk_with_spacing(radius, dtype=np.uint8, *, strict_radius=True, spacing=None): + # Identical to morphology.disk, but with a spacing parameter and without decomposition. + # This is different from morphology.ellipse which produces a slightly different footprint. + L = np.arange(-radius, radius + 1) + X, Y = np.meshgrid(L, L) + + if spacing is not None: + X *= spacing[1] + Y *= spacing[0] + + if not strict_radius: + radius += 0.5 + return np.array((X**2 + Y**2) <= radius**2, dtype=dtype) + + +def test_isotropic_erosion_spacing(): + isotropic_res = isotropic.isotropic_dilation(bw_img, 6, spacing=(1, 2)) + binary_res = img_as_bool( + binary.binary_dilation(bw_img, _disk_with_spacing(6, spacing=(1, 2))) + ) + assert_array_equal(isotropic_res, binary_res) + + +def test_isotropic_dilation(): + isotropic_res = isotropic.isotropic_dilation(bw_img, 3) + binary_res = img_as_bool(binary.binary_dilation(bw_img, morphology.disk(3))) + assert_array_equal(isotropic_res, binary_res) + + +def test_isotropic_closing(): + isotropic_res = isotropic.isotropic_closing(bw_img, 3) + binary_res = img_as_bool(binary.binary_closing(bw_img, morphology.disk(3))) + assert_array_equal(isotropic_res, binary_res) + + +def test_isotropic_opening(): + isotropic_res = isotropic.isotropic_opening(bw_img, 3) + binary_res = img_as_bool(binary.binary_opening(bw_img, morphology.disk(3))) + assert_array_equal(isotropic_res, binary_res) + + +def test_footprint_overflow(): + img = np.zeros((20, 20), dtype=bool) + img[2:19, 2:19] = True + isotropic_res = isotropic.isotropic_erosion(img, 9) + binary_res = img_as_bool(binary.binary_erosion(img, morphology.disk(9))) + assert_array_equal(isotropic_res, binary_res) + + +def test_out_argument(): + for func in (isotropic.isotropic_erosion, isotropic.isotropic_dilation): + radius = 3 + img = np.ones((10, 10)) + out = np.zeros_like(img) + out_saved = out.copy() + func(img, radius, out=out) + assert np.any(out != out_saved) + assert_array_equal(out, func(img, radius)) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_max_tree.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_max_tree.py new file mode 100644 index 0000000000000000000000000000000000000000..c37d4c4c1d41c35598b62d89aba9054b2213dcb8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_max_tree.py @@ -0,0 +1,469 @@ +import numpy as np +from skimage.morphology import max_tree, area_closing, area_opening +from skimage.morphology import max_tree_local_maxima, diameter_opening +from skimage.morphology import diameter_closing +from skimage.util import invert + +from skimage._shared.testing import assert_array_equal, TestCase + +eps = 1e-12 + + +def _full_type_test(img, param, expected, func, param_scale=False, **keywords): + # images as they are + out = func(img, param, **keywords) + assert_array_equal(out, expected) + + # unsigned int + for dt in [np.uint32, np.uint64]: + img_cast = img.astype(dt) + out = func(img_cast, param, **keywords) + exp_cast = expected.astype(dt) + assert_array_equal(out, exp_cast) + + # float + data_float = img.astype(np.float64) + data_float = data_float / 255.0 + expected_float = expected.astype(np.float64) + expected_float = expected_float / 255.0 + if param_scale: + param_cast = param / 255.0 + else: + param_cast = param + for dt in [np.float32, np.float64]: + data_cast = data_float.astype(dt) + out = func(data_cast, param_cast, **keywords) + exp_cast = expected_float.astype(dt) + error_img = 255.0 * exp_cast - 255.0 * out + error = (error_img >= 1.0).sum() + assert error < eps + + # signed images + img_signed = img.astype(np.int16) + img_signed = img_signed - 128 + exp_signed = expected.astype(np.int16) + exp_signed = exp_signed - 128 + for dt in [np.int8, np.int16, np.int32, np.int64]: + img_s = img_signed.astype(dt) + out = func(img_s, param, **keywords) + exp_s = exp_signed.astype(dt) + assert_array_equal(out, exp_s) + + +class TestMaxtree(TestCase): + def test_max_tree(self): + "Test for max tree" + img_type = np.uint8 + img = np.array( + [[10, 8, 8, 9], [7, 7, 9, 9], [8, 7, 10, 10], [9, 9, 10, 10]], + dtype=img_type, + ) + + P_exp = np.array( + [[1, 4, 1, 1], [4, 4, 3, 3], [1, 4, 3, 10], [3, 3, 10, 10]], dtype=np.int64 + ) + + S_exp = np.array( + [4, 5, 9, 1, 2, 8, 3, 6, 7, 12, 13, 0, 10, 11, 14, 15], dtype=np.int64 + ) + + for img_type in [np.uint8, np.uint16, np.uint32, np.uint64]: + img = img.astype(img_type) + P, S = max_tree(img, connectivity=2) + assert_array_equal(P, P_exp) + assert_array_equal(S, S_exp) + + for img_type in [np.int8, np.int16, np.int32, np.int64]: + img = img.astype(img_type) + img_shifted = img - 9 + P, S = max_tree(img_shifted, connectivity=2) + assert_array_equal(P, P_exp) + assert_array_equal(S, S_exp) + + img_float = img.astype(float) + img_float = (img_float - 8) / 2.0 + for img_type in [np.float32, np.float64]: + img_float = img_float.astype(img_type) + P, S = max_tree(img_float, connectivity=2) + assert_array_equal(P, P_exp) + assert_array_equal(S, S_exp) + + return + + def test_area_closing(self): + "Test for Area Closing (2 thresholds, all types)" + + # original image + img = np.array( + [ + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 200, 200, 240, 200, 240, 200, 200, 240, 240, 200, 240], + [240, 200, 40, 240, 240, 240, 240, 240, 240, 240, 40, 240], + [240, 240, 240, 240, 100, 240, 100, 100, 240, 240, 200, 240], + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255], + [200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 40], + [200, 200, 200, 100, 200, 200, 200, 240, 255, 255, 255, 255], + [200, 200, 200, 100, 200, 200, 200, 240, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 40, 200, 240, 240, 100, 255, 255], + [200, 40, 255, 255, 255, 40, 200, 255, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255], + ], + dtype=np.uint8, + ) + + # expected area closing with area 2 + expected_2 = np.array( + [ + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 200, 200, 240, 240, 240, 200, 200, 240, 240, 200, 240], + [240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 200, 240], + [240, 240, 240, 240, 240, 240, 100, 100, 240, 240, 200, 240], + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255], + [200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 255], + [200, 200, 200, 100, 200, 200, 200, 240, 255, 255, 255, 255], + [200, 200, 200, 100, 200, 200, 200, 240, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 40, 200, 240, 240, 200, 255, 255], + [200, 200, 255, 255, 255, 40, 200, 255, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255], + ], + dtype=np.uint8, + ) + + # expected diameter closing with diameter 4 + expected_4 = np.array( + [ + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 200, 200, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240, 240], + [200, 200, 200, 200, 200, 200, 200, 240, 240, 240, 255, 255], + [200, 255, 200, 200, 200, 255, 200, 240, 255, 255, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 240, 255, 255, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 240, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 240, 240, 200, 255, 255], + [200, 200, 255, 255, 255, 200, 200, 255, 200, 200, 255, 255], + [200, 200, 200, 200, 200, 200, 200, 255, 255, 255, 255, 255], + ], + dtype=np.uint8, + ) + + # _full_type_test makes a test with many image types. + _full_type_test(img, 2, expected_2, area_closing, connectivity=2) + _full_type_test(img, 4, expected_4, area_closing, connectivity=2) + + P, S = max_tree(invert(img), connectivity=2) + _full_type_test(img, 4, expected_4, area_closing, parent=P, tree_traverser=S) + + def test_area_opening(self): + "Test for Area Opening (2 thresholds, all types)" + + # original image + img = np.array( + [ + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 55, 55, 15, 55, 15, 55, 55, 15, 15, 55, 15], + [15, 55, 215, 15, 15, 15, 15, 15, 15, 15, 215, 15], + [15, 15, 15, 15, 155, 15, 155, 155, 15, 15, 55, 15], + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [55, 55, 55, 55, 55, 55, 55, 15, 55, 55, 0, 0], + [55, 0, 55, 55, 55, 0, 55, 15, 0, 0, 0, 215], + [55, 55, 55, 155, 55, 55, 55, 15, 0, 0, 0, 0], + [55, 55, 55, 155, 55, 55, 55, 15, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 215, 55, 15, 15, 155, 0, 0], + [55, 215, 0, 0, 0, 215, 55, 0, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + # expected area closing with area 2 + expected_2 = np.array( + [ + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 55, 55, 15, 15, 15, 55, 55, 15, 15, 55, 15], + [15, 55, 55, 15, 15, 15, 15, 15, 15, 15, 55, 15], + [15, 15, 15, 15, 15, 15, 155, 155, 15, 15, 55, 15], + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [55, 55, 55, 55, 55, 55, 55, 15, 55, 55, 0, 0], + [55, 0, 55, 55, 55, 0, 55, 15, 0, 0, 0, 0], + [55, 55, 55, 155, 55, 55, 55, 15, 0, 0, 0, 0], + [55, 55, 55, 155, 55, 55, 55, 15, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 215, 55, 15, 15, 55, 0, 0], + [55, 55, 0, 0, 0, 215, 55, 0, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + # expected diameter closing with diameter 4 + expected_4 = np.array( + [ + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 55, 55, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 55, 55, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15], + [55, 55, 55, 55, 55, 55, 55, 15, 15, 15, 0, 0], + [55, 0, 55, 55, 55, 0, 55, 15, 0, 0, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 15, 0, 0, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 15, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 15, 15, 55, 0, 0], + [55, 55, 0, 0, 0, 55, 55, 0, 55, 55, 0, 0], + [55, 55, 55, 55, 55, 55, 55, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + # _full_type_test makes a test with many image types. + _full_type_test(img, 2, expected_2, area_opening, connectivity=2) + _full_type_test(img, 4, expected_4, area_opening, connectivity=2) + + P, S = max_tree(img, connectivity=2) + _full_type_test(img, 4, expected_4, area_opening, parent=P, tree_traverser=S) + + def test_diameter_closing(self): + "Test for Diameter Opening (2 thresholds, all types)" + img = np.array( + [ + [97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + [93, 63, 63, 63, 63, 86, 86, 86, 87, 43, 43, 91], + [92, 89, 88, 86, 85, 85, 84, 85, 85, 43, 43, 89], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [90, 88, 86, 84, 83, 83, 82, 83, 83, 84, 86, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [92, 89, 23, 23, 85, 85, 84, 85, 85, 3, 3, 89], + [93, 91, 23, 23, 87, 86, 86, 86, 87, 88, 3, 91], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + ], + dtype=np.uint8, + ) + + ex2 = np.array( + [ + [97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + [93, 63, 63, 63, 63, 86, 86, 86, 87, 43, 43, 91], + [92, 89, 88, 86, 85, 85, 84, 85, 85, 43, 43, 89], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [90, 88, 86, 84, 83, 83, 83, 83, 83, 84, 86, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [92, 89, 23, 23, 85, 85, 84, 85, 85, 3, 3, 89], + [93, 91, 23, 23, 87, 86, 86, 86, 87, 88, 3, 91], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + ], + dtype=np.uint8, + ) + + ex4 = np.array( + [ + [97, 95, 93, 92, 91, 90, 90, 90, 91, 92, 93, 95], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + [93, 63, 63, 63, 63, 86, 86, 86, 87, 84, 84, 91], + [92, 89, 88, 86, 85, 85, 84, 85, 85, 84, 84, 89], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [90, 88, 86, 84, 83, 83, 83, 83, 83, 84, 86, 88], + [90, 88, 86, 85, 84, 83, 83, 83, 84, 85, 86, 88], + [91, 88, 87, 85, 84, 84, 83, 84, 84, 85, 87, 88], + [92, 89, 84, 84, 85, 85, 84, 85, 85, 84, 84, 89], + [93, 91, 84, 84, 87, 86, 86, 86, 87, 88, 84, 91], + [95, 93, 91, 89, 88, 88, 88, 88, 88, 89, 91, 93], + ], + dtype=np.uint8, + ) + + # _full_type_test makes a test with many image types. + _full_type_test(img, 2, ex2, diameter_closing, connectivity=2) + _full_type_test(img, 4, ex4, diameter_closing, connectivity=2) + + P, S = max_tree(invert(img), connectivity=2) + _full_type_test(img, 4, ex4, diameter_opening, parent=P, tree_traverser=S) + + def test_diameter_opening(self): + "Test for Diameter Opening (2 thresholds, all types)" + img = np.array( + [ + [5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + [9, 40, 40, 40, 40, 16, 16, 16, 16, 60, 60, 11], + [11, 13, 15, 16, 17, 18, 18, 18, 17, 60, 60, 13], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 15, 16, 18, 19, 19, 20, 19, 19, 18, 16, 15], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [11, 13, 80, 80, 17, 18, 18, 18, 17, 100, 100, 13], + [9, 11, 80, 80, 16, 16, 16, 16, 16, 15, 100, 11], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + ] + ) + + ex2 = np.array( + [ + [5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + [9, 40, 40, 40, 40, 16, 16, 16, 16, 60, 60, 11], + [11, 13, 15, 16, 17, 18, 18, 18, 17, 60, 60, 13], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 15, 16, 18, 19, 19, 19, 19, 19, 18, 16, 15], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [11, 13, 80, 80, 17, 18, 18, 18, 17, 100, 100, 13], + [9, 11, 80, 80, 16, 16, 16, 16, 16, 15, 100, 11], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + ] + ) + + ex4 = np.array( + [ + [5, 7, 9, 11, 12, 12, 12, 12, 12, 11, 9, 7], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + [9, 40, 40, 40, 40, 16, 16, 16, 16, 18, 18, 11], + [11, 13, 15, 16, 17, 18, 18, 18, 17, 18, 18, 13], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 15, 16, 18, 19, 19, 19, 19, 19, 18, 16, 15], + [12, 14, 16, 18, 19, 19, 19, 19, 19, 18, 16, 14], + [12, 14, 16, 17, 18, 19, 19, 19, 18, 17, 16, 14], + [11, 13, 18, 18, 17, 18, 18, 18, 17, 18, 18, 13], + [9, 11, 18, 18, 16, 16, 16, 16, 16, 15, 18, 11], + [7, 10, 11, 13, 14, 14, 15, 14, 14, 13, 11, 10], + ] + ) + + # _full_type_test makes a test with many image types. + _full_type_test(img, 2, ex2, diameter_opening, connectivity=2) + _full_type_test(img, 4, ex4, diameter_opening, connectivity=2) + + P, S = max_tree(img, connectivity=2) + _full_type_test(img, 4, ex4, diameter_opening, parent=P, tree_traverser=S) + + def test_local_maxima(self): + "local maxima for various data types" + data = np.array( + [ + [10, 11, 13, 14, 14, 15, 14, 14, 13, 11], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + [13, 15, 40, 40, 18, 18, 18, 60, 60, 15], + [14, 16, 40, 40, 19, 19, 19, 60, 60, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [15, 16, 18, 19, 19, 20, 19, 19, 18, 16], + [14, 16, 18, 19, 19, 19, 19, 19, 18, 16], + [14, 16, 80, 80, 19, 19, 19, 100, 100, 16], + [13, 15, 80, 80, 18, 18, 18, 100, 100, 15], + [11, 13, 15, 16, 16, 16, 16, 16, 15, 13], + ], + dtype=np.uint8, + ) + expected_result = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint64, + ) + for dtype in [np.uint8, np.uint64, np.int8, np.int64]: + test_data = data.astype(dtype) + out = max_tree_local_maxima(test_data, connectivity=1) + out_bin = out > 0 + assert_array_equal(expected_result, out_bin) + assert out.dtype == expected_result.dtype + assert np.max(out) == 5 + + P, S = max_tree(test_data) + out = max_tree_local_maxima(test_data, parent=P, tree_traverser=S) + + assert_array_equal(expected_result, out_bin) + + assert out.dtype == expected_result.dtype + assert np.max(out) == 5 + + def test_extrema_float(self): + "specific tests for float type" + data = np.array( + [ + [0.10, 0.11, 0.13, 0.14, 0.14, 0.15, 0.14, 0.14, 0.13, 0.11], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + [0.13, 0.15, 0.40, 0.40, 0.18, 0.18, 0.18, 0.60, 0.60, 0.15], + [0.14, 0.16, 0.40, 0.40, 0.19, 0.19, 0.19, 0.60, 0.60, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.15, 0.182, 0.18, 0.19, 0.204, 0.20, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.18, 0.19, 0.19, 0.19, 0.19, 0.19, 0.18, 0.16], + [0.14, 0.16, 0.80, 0.80, 0.19, 0.19, 0.19, 4.0, 1.0, 0.16], + [0.13, 0.15, 0.80, 0.80, 0.18, 0.18, 0.18, 1.0, 1.0, 0.15], + [0.11, 0.13, 0.15, 0.16, 0.16, 0.16, 0.16, 0.16, 0.15, 0.13], + ], + dtype=np.float32, + ) + + expected_result = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=np.uint8, + ) + + # test for local maxima + out = max_tree_local_maxima(data, connectivity=1) + out_bin = out > 0 + assert_array_equal(expected_result, out_bin) + assert np.max(out) == 6 + + def test_3d(self): + """tests the detection of maxima in 3D.""" + img = np.zeros((8, 8, 8), dtype=np.uint8) + local_maxima = np.zeros((8, 8, 8), dtype=np.uint64) + + # first maximum: only one pixel + img[1, 1:3, 1:3] = 100 + img[2, 2, 2] = 200 + img[3, 1:3, 1:3] = 100 + local_maxima[2, 2, 2] = 1 + + # second maximum: three pixels in z-direction + img[5:8, 1, 1] = 200 + local_maxima[5:8, 1, 1] = 1 + + # third: two maxima in 0 and 3. + img[0, 5:8, 5:8] = 200 + img[1, 6, 6] = 100 + img[2, 5:7, 5:7] = 200 + img[0:3, 5:8, 5:8] += 50 + local_maxima[0, 5:8, 5:8] = 1 + local_maxima[2, 5:7, 5:7] = 1 + + # four : one maximum in the corner of the square + img[6:8, 6:8, 6:8] = 200 + img[7, 7, 7] = 255 + local_maxima[7, 7, 7] = 1 + + out = max_tree_local_maxima(img) + out_bin = out > 0 + assert_array_equal(local_maxima, out_bin) + assert np.max(out) == 5 diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_misc.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_misc.py new file mode 100644 index 0000000000000000000000000000000000000000..82d68ca1aa1468dd804f4ecd603b3c7e70e6bafb --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_misc.py @@ -0,0 +1,521 @@ +import numpy as np +import pytest +import scipy as sp + +from skimage.morphology import ( + remove_small_objects, + remove_small_holes, + remove_objects_by_distance, + local_maxima, + label, +) + +from skimage._shared import testing +from skimage._shared.testing import assert_array_equal, assert_equal +from skimage._shared._warnings import expected_warnings + + +test_image = np.array([[0, 0, 0, 1, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 1]], bool) + +# Dtypes supported by the `label_image` parameter in `remove_objects_by_distance` +supported_dtypes = [ + np.uint8, + np.uint16, + np.uint32, + np.int8, + np.int16, + np.int32, + np.int64, +] + + +def test_one_connectivity(): + expected = np.array([[0, 0, 0, 0, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=6) + assert_array_equal(observed, expected) + + +def test_two_connectivity(): + expected = np.array([[0, 0, 0, 1, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 0]], bool) + observed = remove_small_objects(test_image, min_size=7, connectivity=2) + assert_array_equal(observed, expected) + + +def test_in_place(): + image = test_image.copy() + observed = remove_small_objects(image, min_size=6, out=image) + assert_equal( + observed is image, True, "remove_small_objects in_place argument failed." + ) + + +@pytest.mark.parametrize("in_dtype", [bool, int, np.int32]) +@pytest.mark.parametrize("out_dtype", [bool, int, np.int32]) +def test_out(in_dtype, out_dtype): + image = test_image.astype(in_dtype, copy=True) + expected_out = np.empty_like(test_image, dtype=out_dtype) + + if out_dtype != bool: + # object with only 1 label will warn on non-bool output dtype + exp_warn = ["Only one label was provided"] + else: + exp_warn = [] + + with expected_warnings(exp_warn): + out = remove_small_objects(image, min_size=6, out=expected_out) + + assert out is expected_out + + +def test_labeled_image(): + labeled_image = np.array( + [[2, 2, 2, 0, 1], [2, 2, 2, 0, 1], [2, 0, 0, 0, 0], [0, 0, 3, 3, 3]], dtype=int + ) + expected = np.array( + [[2, 2, 2, 0, 0], [2, 2, 2, 0, 0], [2, 0, 0, 0, 0], [0, 0, 3, 3, 3]], dtype=int + ) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + +def test_uint_image(): + labeled_image = np.array( + [[2, 2, 2, 0, 1], [2, 2, 2, 0, 1], [2, 0, 0, 0, 0], [0, 0, 3, 3, 3]], + dtype=np.uint8, + ) + expected = np.array( + [[2, 2, 2, 0, 0], [2, 2, 2, 0, 0], [2, 0, 0, 0, 0], [0, 0, 3, 3, 3]], + dtype=np.uint8, + ) + observed = remove_small_objects(labeled_image, min_size=3) + assert_array_equal(observed, expected) + + +def test_single_label_warning(): + image = np.array([[0, 0, 0, 1, 0], [1, 1, 1, 0, 0], [1, 1, 1, 0, 0]], int) + with expected_warnings(['use a boolean array?']): + remove_small_objects(image, min_size=6) + + +def test_float_input(): + float_test = np.random.rand(5, 5) + with testing.raises(TypeError): + remove_small_objects(float_test) + + +def test_negative_input(): + negative_int = np.random.randint(-4, -1, size=(5, 5)) + with testing.raises(ValueError): + remove_small_objects(negative_int) + + +test_holes_image = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + ], + bool, +) + + +def test_one_connectivity_holes(): + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + ], + bool, + ) + observed = remove_small_holes(test_holes_image, area_threshold=3) + assert_array_equal(observed, expected) + + +def test_two_connectivity_holes(): + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + ], + bool, + ) + observed = remove_small_holes(test_holes_image, area_threshold=3, connectivity=2) + assert_array_equal(observed, expected) + + +def test_in_place_holes(): + image = test_holes_image.copy() + observed = remove_small_holes(image, area_threshold=3, out=image) + assert_equal( + observed is image, True, "remove_small_holes in_place argument failed." + ) + + +def test_out_remove_small_holes(): + image = test_holes_image.copy() + expected_out = np.empty_like(image) + out = remove_small_holes(image, area_threshold=3, out=expected_out) + + assert out is expected_out + + +def test_non_bool_out(): + image = test_holes_image.copy() + expected_out = np.empty_like(image, dtype=int) + with testing.raises(TypeError): + remove_small_holes(image, area_threshold=3, out=expected_out) + + +def test_labeled_image_holes(): + labeled_holes_image = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 0, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + ], + dtype=int, + ) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + ], + dtype=bool, + ) + with expected_warnings(['returned as a boolean array']): + observed = remove_small_holes(labeled_holes_image, area_threshold=3) + assert_array_equal(observed, expected) + + +def test_uint_image_holes(): + labeled_holes_image = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 0, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + ], + dtype=np.uint8, + ) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + ], + dtype=bool, + ) + with expected_warnings(['returned as a boolean array']): + observed = remove_small_holes(labeled_holes_image, area_threshold=3) + assert_array_equal(observed, expected) + + +def test_label_warning_holes(): + labeled_holes_image = np.array( + [ + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 0, 2], + [0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + ], + dtype=int, + ) + with expected_warnings(['use a boolean array?']): + remove_small_holes(labeled_holes_image, area_threshold=3) + remove_small_holes(labeled_holes_image.astype(bool), area_threshold=3) + + +def test_float_input_holes(): + float_test = np.random.rand(5, 5) + with testing.raises(TypeError): + remove_small_holes(float_test) + + +class Test_remove_near_objects: + @pytest.mark.parametrize("min_distance", [2.1, 5, 30.99, 49]) + @pytest.mark.parametrize("dtype", supported_dtypes) + def test_min_distance_1d(self, min_distance, dtype): + # First 3 objects are only just to close, last one is just far enough + d = int(np.floor(min_distance)) + labels = np.zeros(d * 3 + 2, dtype=dtype) + labels[[0, d, 2 * d, 3 * d + 1]] = 1 + labels, _ = sp.ndimage.label(labels, output=dtype) + desired = labels.copy() + desired[d] = 0 + + result = remove_objects_by_distance(labels, min_distance) + assert result.dtype == desired.dtype + assert_array_equal(result, desired) + + @pytest.mark.parametrize("dtype", supported_dtypes) + @pytest.mark.parametrize("order", ["C", "F"]) + def test_handcrafted_2d(self, dtype, order): + label = np.array( + [ + [8, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9], + [8, 8, 8, 0, 0, 0, 0, 0, 0, 9, 9], + [0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0], + [2, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7], + ], + dtype=dtype, + order=order, + ) + priority = np.arange(10) + desired = np.array( + [ + [8, 0, 0, 0, 0, 0, 0, 0, 0, 9, 9], + [8, 8, 8, 0, 0, 0, 0, 0, 0, 9, 9], + [0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [2, 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, 7, 7], + ], + dtype=dtype, + ) + result = remove_objects_by_distance(label, 3, priority=priority) + assert result.flags["C_CONTIGUOUS"] + assert_array_equal(result, desired) + + @pytest.mark.parametrize("ndim", [1, 2, 3, 4, 5]) + def test_large_objects_nd(self, ndim): + shape = (5,) * ndim + a = np.ones(shape, dtype=np.uint8) + a[-2, ...] = 0 + labels, _ = sp.ndimage.label(a) + desired = labels.copy() + desired[-2:, ...] = 0 + + result = remove_objects_by_distance(labels, 2) + assert_array_equal(result, desired) + + @pytest.mark.parametrize("distance", [5, 50, 100]) + @pytest.mark.parametrize("p_norm", [1, 2, np.inf]) + def test_random(self, distance, p_norm): + rng = np.random.default_rng(1713648513) + image = rng.random(size=(400, 400)) + maxima = local_maxima(image) + objects = label(maxima) + + spaced_objects = remove_objects_by_distance(objects, distance, p_norm=p_norm) + kdtree = sp.spatial.cKDTree( + np.array(np.nonzero(spaced_objects), dtype=np.float64).transpose(), + ) + + # Compute distance between all objects that are equal or smaller `distance` + distances = kdtree.sparse_distance_matrix( + kdtree, max_distance=distance, p=p_norm + ) + # There should be no objects left + assert distances.count_nonzero() == 0 + + # But increasing by 1 should reveal a few objects + distances = kdtree.sparse_distance_matrix( + kdtree, max_distance=distance + 1, p=p_norm + ) + assert distances.count_nonzero() > 0 + + @pytest.mark.parametrize("value", [0, 1]) + @pytest.mark.parametrize("dtype", supported_dtypes) + def test_constant(self, value, dtype): + labels = np.empty((10, 10), dtype=dtype) + labels.fill(value) + + result = remove_objects_by_distance(labels, 3) + assert_array_equal(labels, result) + + def test_empty(self): + labels = np.empty((3, 3, 0), dtype=int) + result = remove_objects_by_distance(labels, 3) + assert_equal(labels, result) + + def test_priority(self): + labels = np.array([0, 1, 4, 1]) + + # Object with more samples takes precedence + result = remove_objects_by_distance(labels, 3) + desired = np.array([0, 1, 0, 1]) + assert_array_equal(result, desired) + + # Assigning priority with equal values, sorts by higher label ID second + priority = np.array([0, 1, 1, 1, 1]) + result = remove_objects_by_distance(labels, 3, priority=priority) + desired = np.array([0, 0, 4, 0]) + assert_array_equal(result, desired) + + # But given a different priority that order can be overruled + priority = np.array([0, 1, 1, 1, -1]) + result = remove_objects_by_distance(labels, 3, priority=priority) + desired = np.array([0, 1, 0, 1]) + assert_array_equal(result, desired) + + @pytest.mark.parametrize("order", ["C", "F"]) + def test_out(self, order): + labels_original = np.array([[1, 0, 2], [1, 0, 2]], order=order) + desired = np.array([[0, 0, 2], [0, 0, 2]], order=order) + + # By default, input image is not modified + labels = labels_original.copy(order=order) + remove_objects_by_distance(labels, 2) + assert_array_equal(labels, labels_original) + + # But modified if passed to `out` + remove_objects_by_distance(labels, 2, out=labels) + assert labels.flags[f"{order}_CONTIGUOUS"] + assert_array_equal(labels, desired) + + @pytest.mark.parametrize("min_distance", [-10, -0.1]) + def test_negative_min_distance(self, min_distance): + labels = np.array([1, 0, 2]) + with pytest.raises(ValueError, match="must be >= 0"): + remove_objects_by_distance(labels, min_distance) + + def test_p_norm(self): + labels = np.array([[2, 0], [0, 1]]) + removed = np.array([[2, 0], [0, 0]]) + + # p_norm=2, default (Euclidean distance) + result = remove_objects_by_distance(labels, 1.4) + assert_array_equal(result, labels) + result = remove_objects_by_distance(labels, np.sqrt(2)) + assert_array_equal(result, removed) + + # p_norm=1 (Manhatten distance) + result = remove_objects_by_distance( + labels, + min_distance=1.9, + p_norm=1, + ) + assert_array_equal(result, labels) + result = remove_objects_by_distance(labels, 2, p_norm=1) + assert_array_equal(result, removed) + + # p_norm=np.inf (Chebyshev distance) + result = remove_objects_by_distance(labels, 0.9, p_norm=np.inf) + assert_array_equal(result, labels) + result = remove_objects_by_distance(labels, 1, p_norm=np.inf) + assert_array_equal(result, removed) + + @pytest.mark.parametrize( + "shape", + [ + (0,), + ], + ) + def test_priority_shape(self, shape): + remove_objects_by_distance(np.array([0, 0, 0]), 3, priority=np.ones((0,))) + remove_objects_by_distance(np.array([0, 0, 0]), 3, priority=np.ones((1,))) + + error_msg = r"shape of `priority` must be \(np\.amax\(label_image\) \+ 1,\)" + with pytest.raises(ValueError, match=error_msg): + remove_objects_by_distance(np.array([1, 0, 0]), 3, priority=np.ones((0,))) + with pytest.raises(ValueError, match=error_msg): + remove_objects_by_distance(np.array([1, 0, 0]), 3, priority=np.ones((1,))) + with pytest.raises(ValueError, match=error_msg): + remove_objects_by_distance(np.array([1, 0, 0]), 3, priority=np.ones((1,))) + + def test_negative_label_ids(self): + labels = np.array( + [ + [1, 1, -1, 2, 2, 2], + [1, 1, 3, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [3, 3, 3, 3, 3, 3], + ] + ) + with pytest.raises(ValueError, match=".*object with negative ID"): + remove_objects_by_distance(labels, 1, priority=np.ones(4)) + + def test_objects_with_inside(self): + labels = np.array( + [ + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [3, 3, 3, 3, 3, 3], + ] + ) + desired = np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [3, 3, 3, 3, 3, 3], + ] + ) + result = remove_objects_by_distance(labels, 1, priority=np.arange(4)) + assert_array_equal(result, desired) + + def test_spacing(self): + labels = np.array( + [[1, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], dtype=int + ) + + # Stretch second dimension + result = remove_objects_by_distance(labels, 3, spacing=(1, 3)) + expected = np.array( + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], dtype=int + ) + np.testing.assert_array_equal(result, expected) + + # Compress second dimension + result = remove_objects_by_distance(labels, 1, spacing=(1, 1 / 3)) + expected = np.array( + [[0, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 4]], dtype=int + ) + np.testing.assert_array_equal(result, expected) + + @pytest.mark.parametrize("spacing", [(-1, -1), (1,), (1, 1, 1), [[1, 1]], 1]) + def test_spacing_raises(self, spacing): + labels = np.array( + [[1, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], dtype=int + ) + regex = ".*must contain exactly one positive factor for each dimension" + with pytest.raises(ValueError, match=regex): + remove_objects_by_distance(labels, 3, spacing=spacing) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_reconstruction.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_reconstruction.py new file mode 100644 index 0000000000000000000000000000000000000000..5a69020ed8aa9670012bbadf83e0e716a733b151 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_reconstruction.py @@ -0,0 +1,190 @@ +import math +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal + +from skimage._shared.utils import _supported_float_type +from skimage.morphology.grayreconstruct import reconstruction + + +def test_zeros(): + """Test reconstruction with image and mask of zeros""" + assert_array_almost_equal(reconstruction(np.zeros((5, 7)), np.zeros((5, 7))), 0) + + +def test_image_equals_mask(): + """Test reconstruction where the image and mask are the same""" + assert_array_almost_equal(reconstruction(np.ones((7, 5)), np.ones((7, 5))), 1) + + +def test_image_less_than_mask(): + """Test reconstruction where the image is uniform and less than mask""" + image = np.ones((5, 5)) + mask = np.ones((5, 5)) * 2 + assert_array_almost_equal(reconstruction(image, mask), 1) + + +def test_one_image_peak(): + """Test reconstruction with one peak pixel""" + image = np.ones((5, 5)) + image[2, 2] = 2 + mask = np.ones((5, 5)) * 3 + assert_array_almost_equal(reconstruction(image, mask), 2) + + +# minsize chosen to test sizes covering use of 8, 16 and 32-bit integers +# internally +@pytest.mark.parametrize('minsize', [None, 200, 20000, 40000, 80000]) +@pytest.mark.parametrize('dtype', [np.uint8, np.float32]) +def test_two_image_peaks(minsize, dtype): + """Test reconstruction with two peak pixels isolated by the mask""" + image = np.array( + [ + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 2, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 3, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + ], + dtype=dtype, + ) + + mask = np.array( + [ + [4, 4, 4, 1, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 4, 4, 4, 1], + [1, 1, 1, 1, 1, 4, 4, 4, 1], + [1, 1, 1, 1, 1, 4, 4, 4, 1], + ], + dtype=dtype, + ) + + expected = np.array( + [ + [2, 2, 2, 1, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1, 1], + [2, 2, 2, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 3, 3, 3, 1], + [1, 1, 1, 1, 1, 3, 3, 3, 1], + [1, 1, 1, 1, 1, 3, 3, 3, 1], + ], + dtype=dtype, + ) + if minsize is not None: + # increase data size by tiling (done to test various int types) + nrow = math.ceil(math.sqrt(minsize / image.size)) + ncol = math.ceil(minsize / (image.size * nrow)) + image = np.tile(image, (nrow, ncol)) + mask = np.tile(mask, (nrow, ncol)) + expected = np.tile(expected, (nrow, ncol)) + out = reconstruction(image, mask) + assert out.dtype == _supported_float_type(mask.dtype) + assert_array_almost_equal(out, expected) + + +def test_zero_image_one_mask(): + """Test reconstruction with an image of all zeros and a mask that's not""" + result = reconstruction(np.zeros((10, 10)), np.ones((10, 10))) + assert_array_almost_equal(result, 0) + + +@pytest.mark.parametrize( + 'dtype', + [ + np.int8, + np.uint8, + np.int16, + np.uint16, + np.int32, + np.uint32, + np.int64, + np.uint64, + np.float16, + np.float32, + np.float64, + ], +) +def test_fill_hole(dtype): + """Test reconstruction by erosion, which should fill holes in mask.""" + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0], dtype=dtype) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0], dtype=dtype) + result = reconstruction(seed, mask, method='erosion') + assert result.dtype == _supported_float_type(mask.dtype) + expected = np.array([0, 3, 6, 4, 4, 4, 4, 4, 2, 0], dtype=dtype) + assert_array_almost_equal(result, expected) + + +def test_invalid_seed(): + seed = np.ones((5, 5)) + mask = np.ones((5, 5)) + with pytest.raises(ValueError): + reconstruction(seed * 2, mask, method='dilation') + with pytest.raises(ValueError): + reconstruction(seed * 0.5, mask, method='erosion') + + +def test_invalid_footprint(): + seed = np.ones((5, 5)) + mask = np.ones((5, 5)) + with pytest.raises(ValueError): + reconstruction(seed, mask, footprint=np.ones((4, 4))) + with pytest.raises(ValueError): + reconstruction(seed, mask, footprint=np.ones((3, 4))) + reconstruction(seed, mask, footprint=np.ones((3, 3))) + + +def test_invalid_method(): + seed = np.array([0, 8, 8, 8, 8, 8, 8, 8, 8, 0]) + mask = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + with pytest.raises(ValueError): + reconstruction(seed, mask, method='foo') + + +def test_invalid_offset_not_none(): + """Test reconstruction with invalid not None offset parameter""" + image = np.array( + [ + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 2, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 3, 1], + [1, 1, 1, 1, 1, 1, 1, 1], + ] + ) + + mask = np.array( + [ + [4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [4, 4, 4, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4], + [1, 1, 1, 1, 1, 4, 4, 4], + ] + ) + with pytest.raises(ValueError): + reconstruction( + image, + mask, + method='dilation', + footprint=np.ones((3, 3)), + offset=np.array([3, 0]), + ) + + +def test_offset_not_none(): + """Test reconstruction with valid offset parameter""" + seed = np.array([0, 3, 6, 2, 1, 1, 1, 4, 2, 0]) + mask = np.array([0, 8, 6, 8, 8, 8, 8, 4, 4, 0]) + expected = np.array([0, 3, 6, 6, 6, 6, 6, 4, 4, 0]) + + assert_array_almost_equal( + reconstruction( + seed, mask, method='dilation', footprint=np.ones(3), offset=np.array([0]) + ), + expected, + ) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_skeletonize.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_skeletonize.py new file mode 100644 index 0000000000000000000000000000000000000000..17b84e3eb183b42f40b2503b52e56b2d8d88d95a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_skeletonize.py @@ -0,0 +1,368 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal +import scipy.ndimage as ndi + +from skimage import io, draw +from skimage._shared.testing import fetch +from skimage.data import binary_blobs +from skimage.morphology import medial_axis, skeletonize, thin +from skimage.morphology._skeletonize import G123_LUT, G123P_LUT, _generate_thin_luts + + +class TestSkeletonize: + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_no_foreground(self, method): + image = np.zeros((5, 5)) + result = skeletonize(image, method=method) + assert_array_equal(result, np.zeros((5, 5))) + + @pytest.mark.parametrize( + "ndim,method", [(1, "zhang"), (3, "zhang"), (1, "lee"), (4, "lee")] + ) + def test_wrong_ndim(self, ndim, method): + image = np.zeros((5,) * ndim, dtype=bool) + with pytest.raises(ValueError): + skeletonize(image, method=method) + + def test_wrong_method(self): + image = np.ones((5, 5), dtype=bool) + with pytest.raises(ValueError): + skeletonize(image, method="foo") + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_skeletonize_all_foreground(self, method): + image = np.ones((3, 4), dtype=bool) + result = skeletonize(image, method=method) + if method == "zhang": + expected = np.array([[0, 0, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]], dtype=bool) + else: # "lee" + expected = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]], dtype=bool) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_single_point(self, method): + image = np.zeros((5, 5), dtype=bool) + image[3, 3] = 1 + result = skeletonize(image, method=method) + assert_array_equal(result, image) + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_vec_1d(self, method): + # Corner case of a 2D image, which is a 1D vector + image = np.ones((5, 1), dtype=bool) + result = skeletonize(image, method=method) + assert_array_equal(result, image) + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_already_thinned(self, method): + image = np.array( + [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 1, 1, 1, 0], + [1, 0, 0, 0, 0], + ], + dtype=bool, + ) + result = skeletonize(image, method=method) + assert_array_equal(result, image) + + def test_output(self): + image = io.imread(fetch("data/bw_text.png"), as_gray=True) + + # make black the foreground + image = image == 0 + result = skeletonize(image) + + expected = np.load(fetch("data/bw_text_skeleton.npy")) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_num_neighbors(self, method, dtype): + # an empty image + image = np.zeros((300, 300), dtype=dtype) + + # foreground object 1 + image[10:-10, 10:100] = 1 + image[-100:-10, 10:-10] = 2 + image[10:-10, -100:-10] = 3 + + # foreground object 2 + rs, cs = draw.line(250, 150, 10, 280) + for i in range(10): + image[rs + i, cs] = 4 + rs, cs = draw.line(10, 150, 250, 280) + for i in range(20): + image[rs + i, cs] = 5 + + # foreground object 3 + ir, ic = np.indices(image.shape) + circle1 = (ic - 135) ** 2 + (ir - 150) ** 2 < 30**2 + circle2 = (ic - 135) ** 2 + (ir - 150) ** 2 < 20**2 + image[circle1] = 1 + image[circle2] = 0 + result = skeletonize(image, method=method).astype(np.uint8) + + # there should never be a 2x2 block of foreground pixels in a skeleton + mask = np.array([[1, 1], [1, 1]], np.uint8) + blocks = ndi.correlate(result, mask, mode="constant") + assert not np.any(blocks == 4) + + def test_lut_fix(self): + image = np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 0], + [0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + result = skeletonize(image) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + assert np.all(result == expected) + + @pytest.mark.parametrize("ndim,method", [(2, "zhang"), (2, "lee"), (3, "lee")]) + @pytest.mark.parametrize("dtype", [bool, np.uint8]) + def test_input_not_modified(self, method, ndim, dtype): + # Skeletonize must not modify the input image + image = np.ones((3,) * ndim, dtype=dtype) + image = np.pad(image, 1) + original = image.copy() + _ = skeletonize(image, method=method) + np.testing.assert_array_equal(image, original) + + @pytest.mark.parametrize("method", ["zhang", "lee"]) + def test_input_float_conv(self, method): + # Check that the floats are correctly handled. Also check non-contiguous input + image = np.random.random((16, 16))[::2, ::2] + image[image < 0.5] = 0.0 + + original = image.copy() + result = skeletonize(image, method=method) + + assert result.dtype == bool + assert_array_equal(image, original) + + def test_two_hole_image_vs_fiji(self): + # Test a simple 2D image against FIJI + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + expected = np.array( + [ + [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, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 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], + ], + dtype=bool, + ) + result = skeletonize(image, method="lee") + assert_array_equal(result, expected) + + def test_3d_vs_fiji(self): + # Generate an image with blobs and compare its skeleton + # to the one generated by FIJI (Plugins>Skeleton->Skeletonize) + image = binary_blobs(32, 0.05, n_dim=3, rng=1234) + image = image[:-2, ...] + + result = skeletonize(image) + expected = io.imread(fetch("data/_blobs_3d_fiji_skeleton.tif")).astype(bool) + assert_array_equal(result, expected) + + +class TestThin: + @property + def input_image(self): + # Image to test thinning with + ii = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 2, 3, 4, 5, 0], + [0, 1, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 6, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + dtype=float, + ) + return ii + + def test_all_zeros(self): + image = np.zeros((10, 10), dtype=bool) + assert np.all(thin(image) == False) + + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_thin_copies_input(self, dtype): + """Ensure thinning does not modify the input image.""" + image = self.input_image.astype(dtype) + original = image.copy() + thin(image) + np.testing.assert_array_equal(image, original) + + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_iter_1(self, dtype): + image = self.input_image.astype(dtype) + result = thin(image, 1).astype(bool) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 1, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_noiter(self, dtype): + image = self.input_image.astype(dtype) + result = thin(image).astype(bool) + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0], + [0, 1, 0, 1, 0, 0, 0], + [0, 0, 1, 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], + ], + dtype=bool, + ) + assert_array_equal(result, expected) + + def test_baddim(self): + for ii in [np.zeros(3, dtype=bool), np.zeros((3, 3, 3), dtype=bool)]: + with pytest.raises(ValueError): + thin(ii) + + def test_lut_generation(self): + g123, g123p = _generate_thin_luts() + + assert_array_equal(g123, G123_LUT) + assert_array_equal(g123p, G123P_LUT) + + +class TestMedialAxis: + def test_all_zeros(self): + result = medial_axis(np.zeros((10, 10), dtype=bool)) + assert np.all(result == False) + + def test_all_zeros_masked(self): + result = medial_axis( + np.zeros((10, 10), dtype=bool), np.zeros((10, 10), dtype=bool) + ) + assert np.all(result == False) + + @pytest.mark.parametrize("dtype", [bool, float, int]) + def test_vertical_line(self, dtype): + # Image is a thick vertical line (see gh-3861) + image = np.zeros((9, 9), dtype=dtype) + image[:, 2] = 1 + image[:, 3] = 2 + image[:, 4] = 3 + + expected = np.full(image.shape, False) + expected[:, 3] = True + + result = medial_axis(image) + assert_array_equal(result, expected) + + def test_rectangle(self): + image = np.zeros((9, 15), dtype=bool) + image[1:-1, 1:-1] = True + # Excepted are four diagonals from the corners, meeting in a horizontal line + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + result = medial_axis(image) + assert np.all(result == expected) + result, distance = medial_axis(image, return_distance=True) + assert distance.max() == 4 + + def test_rectange_with_hole(self): + image = np.zeros((9, 15), dtype=bool) + image[1:-1, 1:-1] = True + image[4, 4:-4] = False + expected = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], + dtype=bool, + ) + result = medial_axis(image) + assert np.all(result == expected) + + def test_narrow_image(self): + # Image is a 1-pixel thin strip + image = np.zeros((1, 5), dtype=bool) + image[:, 1:-1] = True + result = medial_axis(image) + assert np.all(result == image) diff --git a/lib/python3.10/site-packages/skimage/morphology/tests/test_util.py b/lib/python3.10/site-packages/skimage/morphology/tests/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..1d50dcdec2c0c466a29db381d71c33a44450ba4f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/morphology/tests/test_util.py @@ -0,0 +1,221 @@ +"""Tests for `_util`.""" + +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +from skimage.morphology import _util + + +@pytest.mark.parametrize("image_shape", [(111,), (33, 44), (22, 55, 11), (6, 5, 4, 3)]) +@pytest.mark.parametrize("order", ["C", "F"]) +def test_offsets_to_raveled_neighbors_highest_connectivity(image_shape, order): + """ + Check scenarios where footprint is always of the highest connectivity + and all dimensions are > 2. + """ + footprint = np.ones((3,) * len(image_shape), dtype=bool) + center = (1,) * len(image_shape) + offsets = _util._offsets_to_raveled_neighbors(image_shape, footprint, center, order) + + # Assert only neighbors are present, center was removed + assert len(offsets) == footprint.sum() - 1 + assert 0 not in offsets + # Assert uniqueness + assert len(set(offsets)) == offsets.size + # offsets form pairs of with same value but different signs + # if footprint is symmetric around center + assert all(-x in offsets for x in offsets) + + # Construct image whose values are the Manhattan distance to its center + image_center = tuple(s // 2 for s in image_shape) + coords = [ + np.abs(np.arange(s, dtype=np.intp) - c) + for s, c in zip(image_shape, image_center) + ] + grid = np.meshgrid(*coords, indexing="ij") + image = np.sum(grid, axis=0) + + image_raveled = image.ravel(order) + image_center_raveled = np.ravel_multi_index(image_center, image_shape, order=order) + + # Sample raveled image around its center + samples = [] + for offset in offsets: + index = image_center_raveled + offset + samples.append(image_raveled[index]) + + # Assert that center with value 0 wasn't selected + assert np.min(samples) == 1 + # Assert that only neighbors where selected + # (highest value == connectivity) + assert np.max(samples) == len(image_shape) + # Assert that nearest neighbors are selected first + assert list(sorted(samples)) == samples + + +@pytest.mark.parametrize( + "image_shape", [(2,), (2, 2), (2, 1, 2), (2, 2, 1, 2), (0, 2, 1, 2)] +) +@pytest.mark.parametrize("order", ["C", "F"]) +def test_offsets_to_raveled_neighbors_footprint_smaller_image(image_shape, order): + """ + Test if a dimension indicated by `image_shape` is smaller than in + `footprint`. + """ + footprint = np.ones((3,) * len(image_shape), dtype=bool) + center = (1,) * len(image_shape) + offsets = _util._offsets_to_raveled_neighbors(image_shape, footprint, center, order) + + # Assert only neighbors are present, center and duplicates (possible + # for this scenario) where removed + assert len(offsets) <= footprint.sum() - 1 + assert 0 not in offsets + # Assert uniqueness + assert len(set(offsets)) == offsets.size + # offsets form pairs of with same value but different signs + # if footprint is symmetric around center + assert all(-x in offsets for x in offsets) + + +def test_offsets_to_raveled_neighbors_explicit_0(): + """Check reviewed example.""" + image_shape = (100, 200, 3) + footprint = np.ones((3, 3, 3), dtype=bool) + center = (1, 1, 1) + offsets = _util._offsets_to_raveled_neighbors(image_shape, footprint, center) + + desired = np.array( + [ + -600, + -3, + -1, + 1, + 3, + 600, + -603, + -601, + -599, + -597, + -4, + -2, + 2, + 4, + 597, + 599, + 601, + 603, + -604, + -602, + -598, + -596, + 596, + 598, + 602, + 604, + ] + ) + assert_array_equal(offsets, desired) + + +def test_offsets_to_raveled_neighbors_explicit_1(): + """Check reviewed example where footprint is larger in last dimension.""" + image_shape = (10, 9, 8, 3) + footprint = np.ones((3, 3, 3, 4), dtype=bool) + center = (1, 1, 1, 1) + offsets = _util._offsets_to_raveled_neighbors(image_shape, footprint, center) + + desired = np.array( + [ + -216, + -24, + -3, + -1, + 1, + 3, + 24, + 216, + -240, + -219, + -217, + -215, + -213, + -192, + -27, + -25, + -23, + -21, + -4, + -2, + 2, + 4, + 21, + 23, + 25, + 27, + 192, + 213, + 215, + 217, + 219, + 240, + -243, + -241, + -239, + -237, + -220, + -218, + -214, + -212, + -195, + -193, + -191, + -189, + -28, + -26, + -22, + -20, + 20, + 22, + 26, + 28, + 189, + 191, + 193, + 195, + 212, + 214, + 218, + 220, + 237, + 239, + 241, + 243, + -244, + -242, + -238, + -236, + -196, + -194, + -190, + -188, + 188, + 190, + 194, + 196, + 236, + 238, + 242, + 244, + 5, + -211, + -19, + 29, + 221, + -235, + -187, + 197, + 245, + ] + ) + assert_array_equal(offsets, desired) diff --git a/lib/python3.10/site-packages/skimage/py.typed b/lib/python3.10/site-packages/skimage/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/registration/__init__.py b/lib/python3.10/site-packages/skimage/registration/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8691f90519adedffb3a80eff78b24307ac8949cb --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/__init__.py @@ -0,0 +1,5 @@ +"""Image registration algorithms, e.g., optical flow or phase cross correlation.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/registration/__init__.pyi b/lib/python3.10/site-packages/skimage/registration/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..bf941f9a362bb804e6cd58af5b5cdbe26abd9925 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/__init__.pyi @@ -0,0 +1,8 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = ['optical_flow_ilk', 'optical_flow_tvl1', 'phase_cross_correlation'] + +from ._optical_flow import optical_flow_tvl1, optical_flow_ilk +from ._phase_cross_correlation import phase_cross_correlation diff --git a/lib/python3.10/site-packages/skimage/registration/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..054c396e7688aeb9f9f3d977ee7ad6f5aa8ee48f Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/__pycache__/_masked_phase_cross_correlation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/__pycache__/_masked_phase_cross_correlation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e36d1ffd767f9562a635d2a613dfc4fffaa5bacc Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/__pycache__/_masked_phase_cross_correlation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fa1f6440337079474506244e0151450ad2c57cf Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow_utils.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow_utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..190a14efa456417d27695d4bfd2239bde1850db6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/__pycache__/_optical_flow_utils.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/__pycache__/_phase_cross_correlation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/__pycache__/_phase_cross_correlation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fa3b0542c25a6fca0305a8e14fd96aae4b20ded Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/__pycache__/_phase_cross_correlation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/_masked_phase_cross_correlation.py b/lib/python3.10/site-packages/skimage/registration/_masked_phase_cross_correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..a37e8789b5b98604a7513287d4fc8472d30dfac2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/_masked_phase_cross_correlation.py @@ -0,0 +1,306 @@ +""" +Implementation of the masked normalized cross-correlation. + +Based on the following publication: +D. Padfield. Masked object registration in the Fourier domain. +IEEE Transactions on Image Processing (2012) + +and the author's original MATLAB implementation, available on this website: +http://www.dirkpadfield.com/ +""" + +from functools import partial + +import numpy as np +import scipy.fft as fftmodule +from scipy.fft import next_fast_len + +from .._shared.utils import _supported_float_type + + +def _masked_phase_cross_correlation( + reference_image, moving_image, reference_mask, moving_mask=None, overlap_ratio=0.3 +): + """Masked image translation registration by masked normalized + cross-correlation. + + Parameters + ---------- + reference_image : ndarray + Reference image. + moving_image : ndarray + Image to register. Must be same dimensionality as ``reference_image``, + but not necessarily the same size. + reference_mask : ndarray + Boolean mask for ``reference_image``. The mask should evaluate + to ``True`` (or 1) on valid pixels. ``reference_mask`` should + have the same shape as ``reference_image``. + moving_mask : ndarray or None, optional + Boolean mask for ``moving_image``. The mask should evaluate to ``True`` + (or 1) on valid pixels. ``moving_mask`` should have the same shape + as ``moving_image``. If ``None``, ``reference_mask`` will be used. + overlap_ratio : float, optional + Minimum allowed overlap ratio between images. The correlation for + translations corresponding with an overlap ratio lower than this + threshold will be ignored. A lower `overlap_ratio` leads to smaller + maximum translation, while a higher `overlap_ratio` leads to greater + robustness against spurious matches due to small overlap between + masked images. + + Returns + ------- + shifts : ndarray + Shift vector (in pixels) required to register ``moving_image`` + with ``reference_image``. Axis ordering is consistent with numpy. + + References + ---------- + .. [1] Dirk Padfield. Masked Object Registration in the Fourier Domain. + IEEE Transactions on Image Processing, vol. 21(5), + pp. 2706-2718 (2012). :DOI:`10.1109/TIP.2011.2181402` + .. [2] D. Padfield. "Masked FFT registration". In Proc. Computer Vision and + Pattern Recognition, pp. 2918-2925 (2010). + :DOI:`10.1109/CVPR.2010.5540032` + + """ + if moving_mask is None: + if reference_image.shape != moving_image.shape: + raise ValueError( + "Input images have different shapes, moving_mask must " + "be explicitly set." + ) + moving_mask = reference_mask.astype(bool) + + # We need masks to be of the same size as their respective images + for im, mask in [(reference_image, reference_mask), (moving_image, moving_mask)]: + if im.shape != mask.shape: + raise ValueError("Image sizes must match their respective mask sizes.") + + xcorr = cross_correlate_masked( + moving_image, + reference_image, + moving_mask, + reference_mask, + axes=tuple(range(moving_image.ndim)), + mode='full', + overlap_ratio=overlap_ratio, + ) + + # Generalize to the average of multiple equal maxima + maxima = np.stack(np.nonzero(xcorr == xcorr.max()), axis=1) + center = np.mean(maxima, axis=0) + shifts = center - np.array(reference_image.shape) + 1 + + # The mismatch in size will impact the center location of the + # cross-correlation + size_mismatch = np.array(moving_image.shape) - np.array(reference_image.shape) + + return -shifts + (size_mismatch / 2) + + +def cross_correlate_masked( + arr1, arr2, m1, m2, mode='full', axes=(-2, -1), overlap_ratio=0.3 +): + """ + Masked normalized cross-correlation between arrays. + + Parameters + ---------- + arr1 : ndarray + First array. + arr2 : ndarray + Seconds array. The dimensions of `arr2` along axes that are not + transformed should be equal to that of `arr1`. + m1 : ndarray + Mask of `arr1`. The mask should evaluate to `True` + (or 1) on valid pixels. `m1` should have the same shape as `arr1`. + m2 : ndarray + Mask of `arr2`. The mask should evaluate to `True` + (or 1) on valid pixels. `m2` should have the same shape as `arr2`. + mode : {'full', 'same'}, optional + 'full': + This returns the convolution at each point of overlap. At + the end-points of the convolution, the signals do not overlap + completely, and boundary effects may be seen. + 'same': + The output is the same size as `arr1`, centered with respect + to the `‘full’` output. Boundary effects are less prominent. + axes : tuple of ints, optional + Axes along which to compute the cross-correlation. + overlap_ratio : float, optional + Minimum allowed overlap ratio between images. The correlation for + translations corresponding with an overlap ratio lower than this + threshold will be ignored. A lower `overlap_ratio` leads to smaller + maximum translation, while a higher `overlap_ratio` leads to greater + robustness against spurious matches due to small overlap between + masked images. + + Returns + ------- + out : ndarray + Masked normalized cross-correlation. + + Raises + ------ + ValueError : if correlation `mode` is not valid, or array dimensions along + non-transformation axes are not equal. + + References + ---------- + .. [1] Dirk Padfield. Masked Object Registration in the Fourier Domain. + IEEE Transactions on Image Processing, vol. 21(5), + pp. 2706-2718 (2012). :DOI:`10.1109/TIP.2011.2181402` + .. [2] D. Padfield. "Masked FFT registration". In Proc. Computer Vision and + Pattern Recognition, pp. 2918-2925 (2010). + :DOI:`10.1109/CVPR.2010.5540032` + """ + if mode not in {'full', 'same'}: + raise ValueError(f"Correlation mode '{mode}' is not valid.") + + fixed_image = np.asarray(arr1) + moving_image = np.asarray(arr2) + float_dtype = _supported_float_type((fixed_image.dtype, moving_image.dtype)) + if float_dtype.kind == 'c': + raise ValueError("complex-valued arr1, arr2 are not supported") + + fixed_image = fixed_image.astype(float_dtype) + fixed_mask = np.array(m1, dtype=bool) + moving_image = moving_image.astype(float_dtype) + moving_mask = np.array(m2, dtype=bool) + eps = np.finfo(float_dtype).eps + + # Array dimensions along non-transformation axes should be equal. + all_axes = set(range(fixed_image.ndim)) + for axis in all_axes - set(axes): + if fixed_image.shape[axis] != moving_image.shape[axis]: + raise ValueError( + f'Array shapes along non-transformation axes should be ' + f'equal, but dimensions along axis {axis} are not.' + ) + + # Determine final size along transformation axes + # Note that it might be faster to compute Fourier transform in a slightly + # larger shape (`fast_shape`). Then, after all fourier transforms are done, + # we slice back to`final_shape` using `final_slice`. + final_shape = list(arr1.shape) + for axis in axes: + final_shape[axis] = fixed_image.shape[axis] + moving_image.shape[axis] - 1 + final_shape = tuple(final_shape) + final_slice = tuple([slice(0, int(sz)) for sz in final_shape]) + + # Extent transform axes to the next fast length (i.e. multiple of 3, 5, or + # 7) + fast_shape = tuple([next_fast_len(final_shape[ax]) for ax in axes]) + + # We use the new scipy.fft because they allow leaving the transform axes + # unchanged which was not possible with scipy.fftpack's + # fftn/ifftn in older versions of SciPy. + # E.g. arr shape (2, 3, 7), transform along axes (0, 1) with shape (4, 4) + # results in arr_fft shape (4, 4, 7) + fft = partial(fftmodule.fftn, s=fast_shape, axes=axes) + _ifft = partial(fftmodule.ifftn, s=fast_shape, axes=axes) + + def ifft(x): + return _ifft(x).real + + fixed_image[np.logical_not(fixed_mask)] = 0.0 + moving_image[np.logical_not(moving_mask)] = 0.0 + + # N-dimensional analog to rotation by 180deg is flip over all relevant axes. + # See [1] for discussion. + rotated_moving_image = _flip(moving_image, axes=axes) + rotated_moving_mask = _flip(moving_mask, axes=axes) + + fixed_fft = fft(fixed_image) + rotated_moving_fft = fft(rotated_moving_image) + fixed_mask_fft = fft(fixed_mask.astype(float_dtype)) + rotated_moving_mask_fft = fft(rotated_moving_mask.astype(float_dtype)) + + # Calculate overlap of masks at every point in the convolution. + # Locations with high overlap should not be taken into account. + number_overlap_masked_px = ifft(rotated_moving_mask_fft * fixed_mask_fft) + number_overlap_masked_px[:] = np.round(number_overlap_masked_px) + number_overlap_masked_px[:] = np.fmax(number_overlap_masked_px, eps) + masked_correlated_fixed_fft = ifft(rotated_moving_mask_fft * fixed_fft) + masked_correlated_rotated_moving_fft = ifft(fixed_mask_fft * rotated_moving_fft) + + numerator = ifft(rotated_moving_fft * fixed_fft) + numerator -= ( + masked_correlated_fixed_fft + * masked_correlated_rotated_moving_fft + / number_overlap_masked_px + ) + + fixed_squared_fft = fft(np.square(fixed_image)) + fixed_denom = ifft(rotated_moving_mask_fft * fixed_squared_fft) + fixed_denom -= np.square(masked_correlated_fixed_fft) / number_overlap_masked_px + fixed_denom[:] = np.fmax(fixed_denom, 0.0) + + rotated_moving_squared_fft = fft(np.square(rotated_moving_image)) + moving_denom = ifft(fixed_mask_fft * rotated_moving_squared_fft) + moving_denom -= ( + np.square(masked_correlated_rotated_moving_fft) / number_overlap_masked_px + ) + moving_denom[:] = np.fmax(moving_denom, 0.0) + + denom = np.sqrt(fixed_denom * moving_denom) + + # Slice back to expected convolution shape. + numerator = numerator[final_slice] + denom = denom[final_slice] + number_overlap_masked_px = number_overlap_masked_px[final_slice] + + if mode == 'same': + _centering = partial(_centered, newshape=fixed_image.shape, axes=axes) + denom = _centering(denom) + numerator = _centering(numerator) + number_overlap_masked_px = _centering(number_overlap_masked_px) + + # Pixels where `denom` is very small will introduce large + # numbers after division. To get around this problem, + # we zero-out problematic pixels. + tol = 1e3 * eps * np.max(np.abs(denom), axis=axes, keepdims=True) + nonzero_indices = denom > tol + + # explicitly set out dtype for compatibility with SciPy < 1.4, where + # fftmodule will be numpy.fft which always uses float64 dtype. + out = np.zeros_like(denom, dtype=float_dtype) + out[nonzero_indices] = numerator[nonzero_indices] / denom[nonzero_indices] + np.clip(out, a_min=-1, a_max=1, out=out) + + # Apply overlap ratio threshold + number_px_threshold = overlap_ratio * np.max( + number_overlap_masked_px, axis=axes, keepdims=True + ) + out[number_overlap_masked_px < number_px_threshold] = 0.0 + + return out + + +def _centered(arr, newshape, axes): + """Return the center `newshape` portion of `arr`, leaving axes not + in `axes` untouched.""" + newshape = np.asarray(newshape) + currshape = np.array(arr.shape) + + slices = [slice(None, None)] * arr.ndim + + for ax in axes: + startind = (currshape[ax] - newshape[ax]) // 2 + endind = startind + newshape[ax] + slices[ax] = slice(startind, endind) + + return arr[tuple(slices)] + + +def _flip(arr, axes=None): + """Reverse array over many axes. Generalization of arr[::-1] for many + dimensions. If `axes` is `None`, flip along all axes.""" + if axes is None: + reverse = [slice(None, None, -1)] * arr.ndim + else: + reverse = [slice(None, None, None)] * arr.ndim + for axis in axes: + reverse[axis] = slice(None, None, -1) + + return arr[tuple(reverse)] diff --git a/lib/python3.10/site-packages/skimage/registration/_optical_flow.py b/lib/python3.10/site-packages/skimage/registration/_optical_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..e8b11f363a8ceb1e9806c4621871656c10871f8a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/_optical_flow.py @@ -0,0 +1,429 @@ +"""TV-L1 optical flow algorithm implementation.""" + +from functools import partial +from itertools import combinations_with_replacement + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.filters import gaussian as gaussian_filter +from .._shared.utils import _supported_float_type +from ..transform import warp +from ._optical_flow_utils import _coarse_to_fine, _get_warp_points + + +def _tvl1( + reference_image, + moving_image, + flow0, + attachment, + tightness, + num_warp, + num_iter, + tol, + prefilter, +): + """TV-L1 solver for optical flow estimation. + + Parameters + ---------- + reference_image : ndarray, shape (M, N[, P[, ...]]) + The first grayscale image of the sequence. + moving_image : ndarray, shape (M, N[, P[, ...]]) + The second grayscale image of the sequence. + flow0 : ndarray, shape (image0.ndim, M, N[, P[, ...]]) + Initialization for the vector field. + attachment : float + Attachment parameter. The smaller this parameter is, + the smoother is the solutions. + tightness : float + Tightness parameter. It should have a small value in order to + maintain attachment and regularization parts in + correspondence. + num_warp : int + Number of times moving_image is warped. + num_iter : int + Number of fixed point iteration. + tol : float + Tolerance used as stopping criterion based on the L² distance + between two consecutive values of (u, v). + prefilter : bool + Whether to prefilter the estimated optical flow before each + image warp. + + Returns + ------- + flow : ndarray, shape (image0.ndim, M, N[, P[, ...]]) + The estimated optical flow components for each axis. + + """ + + dtype = reference_image.dtype + grid = np.meshgrid( + *[np.arange(n, dtype=dtype) for n in reference_image.shape], + indexing='ij', + sparse=True, + ) + + # dt corresponds to tau in [3]_, i.e. the time step + dt = 0.5 / reference_image.ndim + reg_num_iter = 2 + f0 = attachment * tightness + f1 = dt / tightness + tol *= reference_image.size + + flow_current = flow_previous = flow0 + + g = np.zeros((reference_image.ndim,) + reference_image.shape, dtype=dtype) + proj = np.zeros( + ( + reference_image.ndim, + reference_image.ndim, + ) + + reference_image.shape, + dtype=dtype, + ) + + s_g = [ + slice(None), + ] * g.ndim + s_p = [ + slice(None), + ] * proj.ndim + s_d = [ + slice(None), + ] * (proj.ndim - 2) + + for _ in range(num_warp): + if prefilter: + flow_current = ndi.median_filter( + flow_current, [1] + reference_image.ndim * [3] + ) + + image1_warp = warp( + moving_image, _get_warp_points(grid, flow_current), mode='edge' + ) + grad = np.array(np.gradient(image1_warp)) + NI = (grad * grad).sum(0) + NI[NI == 0] = 1 + + rho_0 = image1_warp - reference_image - (grad * flow_current).sum(0) + + for _ in range(num_iter): + # Data term + + rho = rho_0 + (grad * flow_current).sum(0) + + idx = abs(rho) <= f0 * NI + + flow_auxiliary = flow_current + + flow_auxiliary[:, idx] -= rho[idx] * grad[:, idx] / NI[idx] + + idx = ~idx + srho = f0 * np.sign(rho[idx]) + flow_auxiliary[:, idx] -= srho * grad[:, idx] + + # Regularization term + flow_current = flow_auxiliary.copy() + + for idx in range(reference_image.ndim): + s_p[0] = idx + for _ in range(reg_num_iter): + for ax in range(reference_image.ndim): + s_g[0] = ax + s_g[ax + 1] = slice(0, -1) + g[tuple(s_g)] = np.diff(flow_current[idx], axis=ax) + s_g[ax + 1] = slice(None) + + norm = np.sqrt((g**2).sum(0))[np.newaxis, ...] + norm *= f1 + norm += 1.0 + proj[idx] -= dt * g + proj[idx] /= norm + + # d will be the (negative) divergence of proj[idx] + d = -proj[idx].sum(0) + for ax in range(reference_image.ndim): + s_p[1] = ax + s_p[ax + 2] = slice(0, -1) + s_d[ax] = slice(1, None) + d[tuple(s_d)] += proj[tuple(s_p)] + s_p[ax + 2] = slice(None) + s_d[ax] = slice(None) + + flow_current[idx] = flow_auxiliary[idx] + d + + flow_previous -= flow_current # The difference as stopping criteria + if (flow_previous * flow_previous).sum() < tol: + break + + flow_previous = flow_current + + return flow_current + + +def optical_flow_tvl1( + reference_image, + moving_image, + *, + attachment=15, + tightness=0.3, + num_warp=5, + num_iter=10, + tol=1e-4, + prefilter=False, + dtype=np.float32, +): + r"""Coarse to fine optical flow estimator. + + The TV-L1 solver is applied at each level of the image + pyramid. TV-L1 is a popular algorithm for optical flow estimation + introduced by Zack et al. [1]_, improved in [2]_ and detailed in [3]_. + + Parameters + ---------- + reference_image : ndarray, shape (M, N[, P[, ...]]) + The first grayscale image of the sequence. + moving_image : ndarray, shape (M, N[, P[, ...]]) + The second grayscale image of the sequence. + attachment : float, optional + Attachment parameter (:math:`\lambda` in [1]_). The smaller + this parameter is, the smoother the returned result will be. + tightness : float, optional + Tightness parameter (:math:`\theta` in [1]_). It should have + a small value in order to maintain attachment and + regularization parts in correspondence. + num_warp : int, optional + Number of times moving_image is warped. + num_iter : int, optional + Number of fixed point iteration. + tol : float, optional + Tolerance used as stopping criterion based on the L² distance + between two consecutive values of (u, v). + prefilter : bool, optional + Whether to prefilter the estimated optical flow before each + image warp. When True, a median filter with window size 3 + along each axis is applied. This helps to remove potential + outliers. + dtype : dtype, optional + Output data type: must be floating point. Single precision + provides good results and saves memory usage and computation + time compared to double precision. + + Returns + ------- + flow : ndarray, shape (image0.ndim, M, N[, P[, ...]]) + The estimated optical flow components for each axis. + + Notes + ----- + Color images are not supported. + + References + ---------- + .. [1] Zach, C., Pock, T., & Bischof, H. (2007, September). A + duality based approach for realtime TV-L 1 optical flow. In Joint + pattern recognition symposium (pp. 214-223). Springer, Berlin, + Heidelberg. :DOI:`10.1007/978-3-540-74936-3_22` + .. [2] Wedel, A., Pock, T., Zach, C., Bischof, H., & Cremers, + D. (2009). An improved algorithm for TV-L 1 optical flow. In + Statistical and geometrical approaches to visual motion analysis + (pp. 23-45). Springer, Berlin, Heidelberg. + :DOI:`10.1007/978-3-642-03061-1_2` + .. [3] Pérez, J. S., Meinhardt-Llopis, E., & Facciolo, + G. (2013). TV-L1 optical flow estimation. Image Processing On + Line, 2013, 137-150. :DOI:`10.5201/ipol.2013.26` + + Examples + -------- + >>> from skimage.color import rgb2gray + >>> from skimage.data import stereo_motorcycle + >>> from skimage.registration import optical_flow_tvl1 + >>> image0, image1, disp = stereo_motorcycle() + >>> # --- Convert the images to gray level: color is not supported. + >>> image0 = rgb2gray(image0) + >>> image1 = rgb2gray(image1) + >>> flow = optical_flow_tvl1(image1, image0) + + """ + + solver = partial( + _tvl1, + attachment=attachment, + tightness=tightness, + num_warp=num_warp, + num_iter=num_iter, + tol=tol, + prefilter=prefilter, + ) + + if np.dtype(dtype) != _supported_float_type(dtype): + msg = f"dtype={dtype} is not supported. Try 'float32' or 'float64.'" + raise ValueError(msg) + + return _coarse_to_fine(reference_image, moving_image, solver, dtype=dtype) + + +def _ilk(reference_image, moving_image, flow0, radius, num_warp, gaussian, prefilter): + """Iterative Lucas-Kanade (iLK) solver for optical flow estimation. + + Parameters + ---------- + reference_image : ndarray, shape (M, N[, P[, ...]]) + The first grayscale image of the sequence. + moving_image : ndarray, shape (M, N[, P[, ...]]) + The second grayscale image of the sequence. + flow0 : ndarray, shape (reference_image.ndim, M, N[, P[, ...]]) + Initialization for the vector field. + radius : int + Radius of the window considered around each pixel. + num_warp : int + Number of times moving_image is warped. + gaussian : bool + if True, a gaussian kernel is used for the local + integration. Otherwise, a uniform kernel is used. + prefilter : bool + Whether to prefilter the estimated optical flow before each + image warp. This helps to remove potential outliers. + + Returns + ------- + flow : ndarray, shape (reference_image.ndim, M, N[, P[, ...]]) + The estimated optical flow components for each axis. + + """ + dtype = reference_image.dtype + ndim = reference_image.ndim + size = 2 * radius + 1 + + if gaussian: + sigma = ndim * (size / 4,) + filter_func = partial(gaussian_filter, sigma=sigma, mode='mirror') + else: + filter_func = partial(ndi.uniform_filter, size=ndim * (size,), mode='mirror') + + flow = flow0 + # For each pixel location (i, j), the optical flow X = flow[:, i, j] + # is the solution of the ndim x ndim linear system + # A[i, j] * X = b[i, j] + A = np.zeros(reference_image.shape + (ndim, ndim), dtype=dtype) + b = np.zeros(reference_image.shape + (ndim, 1), dtype=dtype) + + grid = np.meshgrid( + *[np.arange(n, dtype=dtype) for n in reference_image.shape], + indexing='ij', + sparse=True, + ) + + for _ in range(num_warp): + if prefilter: + flow = ndi.median_filter(flow, (1,) + ndim * (3,)) + + moving_image_warp = warp( + moving_image, _get_warp_points(grid, flow), mode='edge' + ) + grad = np.stack(np.gradient(moving_image_warp), axis=0) + error_image = (grad * flow).sum(axis=0) + reference_image - moving_image_warp + + # Local linear systems creation + for i, j in combinations_with_replacement(range(ndim), 2): + A[..., i, j] = A[..., j, i] = filter_func(grad[i] * grad[j]) + + for i in range(ndim): + b[..., i, 0] = filter_func(grad[i] * error_image) + + # Don't consider badly conditioned linear systems + idx = abs(np.linalg.det(A)) < 1e-14 + A[idx] = np.eye(ndim, dtype=dtype) + b[idx] = 0 + + # Solve the local linear systems + flow = np.moveaxis(np.linalg.solve(A, b)[..., 0], ndim, 0) + + return flow + + +def optical_flow_ilk( + reference_image, + moving_image, + *, + radius=7, + num_warp=10, + gaussian=False, + prefilter=False, + dtype=np.float32, +): + """Coarse to fine optical flow estimator. + + The iterative Lucas-Kanade (iLK) solver is applied at each level + of the image pyramid. iLK [1]_ is a fast and robust alternative to + TVL1 algorithm although less accurate for rendering flat surfaces + and object boundaries (see [2]_). + + Parameters + ---------- + reference_image : ndarray, shape (M, N[, P[, ...]]) + The first grayscale image of the sequence. + moving_image : ndarray, shape (M, N[, P[, ...]]) + The second grayscale image of the sequence. + radius : int, optional + Radius of the window considered around each pixel. + num_warp : int, optional + Number of times moving_image is warped. + gaussian : bool, optional + If True, a Gaussian kernel is used for the local + integration. Otherwise, a uniform kernel is used. + prefilter : bool, optional + Whether to prefilter the estimated optical flow before each + image warp. When True, a median filter with window size 3 + along each axis is applied. This helps to remove potential + outliers. + dtype : dtype, optional + Output data type: must be floating point. Single precision + provides good results and saves memory usage and computation + time compared to double precision. + + Returns + ------- + flow : ndarray, shape (reference_image.ndim, M, N[, P[, ...]]) + The estimated optical flow components for each axis. + + Notes + ----- + - The implemented algorithm is described in **Table2** of [1]_. + - Color images are not supported. + + References + ---------- + .. [1] Le Besnerais, G., & Champagnat, F. (2005, September). Dense + optical flow by iterative local window registration. In IEEE + International Conference on Image Processing 2005 (Vol. 1, + pp. I-137). IEEE. :DOI:`10.1109/ICIP.2005.1529706` + .. [2] Plyer, A., Le Besnerais, G., & Champagnat, + F. (2016). Massively parallel Lucas Kanade optical flow for + real-time video processing applications. Journal of Real-Time + Image Processing, 11(4), 713-730. :DOI:`10.1007/s11554-014-0423-0` + + Examples + -------- + >>> from skimage.color import rgb2gray + >>> from skimage.data import stereo_motorcycle + >>> from skimage.registration import optical_flow_ilk + >>> reference_image, moving_image, disp = stereo_motorcycle() + >>> # --- Convert the images to gray level: color is not supported. + >>> reference_image = rgb2gray(reference_image) + >>> moving_image = rgb2gray(moving_image) + >>> flow = optical_flow_ilk(moving_image, reference_image) + + """ + + solver = partial( + _ilk, radius=radius, num_warp=num_warp, gaussian=gaussian, prefilter=prefilter + ) + + if np.dtype(dtype) != _supported_float_type(dtype): + msg = f"dtype={dtype} is not supported. Try 'float32' or 'float64.'" + raise ValueError(msg) + + return _coarse_to_fine(reference_image, moving_image, solver, dtype=dtype) diff --git a/lib/python3.10/site-packages/skimage/registration/_optical_flow_utils.py b/lib/python3.10/site-packages/skimage/registration/_optical_flow_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..51821b48d4c28a54b7e13a2980a845f20d9d2cc4 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/_optical_flow_utils.py @@ -0,0 +1,150 @@ +"""Common tools to optical flow algorithms.""" + +import numpy as np +from scipy import ndimage as ndi + +from ..transform import pyramid_reduce +from ..util.dtype import _convert + + +def _get_warp_points(grid, flow): + """Compute warp point coordinates. + + Parameters + ---------- + grid : iterable + The sparse grid to be warped (obtained using + ``np.meshgrid(..., sparse=True)).``) + flow : ndarray + The warping motion field. + + Returns + ------- + out : ndarray + The warp point coordinates. + + """ + out = flow.copy() + for idx, g in enumerate(grid): + out[idx, ...] += g + return out + + +def _resize_flow(flow, shape): + """Rescale the values of the vector field (u, v) to the desired shape. + + The values of the output vector field are scaled to the new + resolution. + + Parameters + ---------- + flow : ndarray + The motion field to be processed. + shape : iterable + Couple of integers representing the output shape. + + Returns + ------- + rflow : ndarray + The resized and rescaled motion field. + + """ + + scale = [n / o for n, o in zip(shape, flow.shape[1:])] + scale_factor = np.array(scale, dtype=flow.dtype) + + for _ in shape: + scale_factor = scale_factor[..., np.newaxis] + + rflow = scale_factor * ndi.zoom( + flow, [1] + scale, order=0, mode='nearest', prefilter=False + ) + + return rflow + + +def _get_pyramid(I, downscale=2.0, nlevel=10, min_size=16): + """Construct image pyramid. + + Parameters + ---------- + I : ndarray + The image to be preprocessed (Grayscale or RGB). + downscale : float + The pyramid downscale factor. + nlevel : int + The maximum number of pyramid levels. + min_size : int + The minimum size for any dimension of the pyramid levels. + + Returns + ------- + pyramid : list[ndarray] + The coarse to fine images pyramid. + + """ + + pyramid = [I] + size = min(I.shape) + count = 1 + + while (count < nlevel) and (size > downscale * min_size): + J = pyramid_reduce(pyramid[-1], downscale, channel_axis=None) + pyramid.append(J) + size = min(J.shape) + count += 1 + + return pyramid[::-1] + + +def _coarse_to_fine( + I0, I1, solver, downscale=2, nlevel=10, min_size=16, dtype=np.float32 +): + """Generic coarse to fine solver. + + Parameters + ---------- + I0 : ndarray + The first grayscale image of the sequence. + I1 : ndarray + The second grayscale image of the sequence. + solver : callable + The solver applied at each pyramid level. + downscale : float + The pyramid downscale factor. + nlevel : int + The maximum number of pyramid levels. + min_size : int + The minimum size for any dimension of the pyramid levels. + dtype : dtype + Output data type. + + Returns + ------- + flow : ndarray + The estimated optical flow components for each axis. + + """ + + if I0.shape != I1.shape: + raise ValueError("Input images should have the same shape") + + if np.dtype(dtype).char not in 'efdg': + raise ValueError("Only floating point data type are valid" " for optical flow") + + pyramid = list( + zip( + _get_pyramid(_convert(I0, dtype), downscale, nlevel, min_size), + _get_pyramid(_convert(I1, dtype), downscale, nlevel, min_size), + ) + ) + + # Initialization to 0 at coarsest level. + flow = np.zeros((pyramid[0][0].ndim,) + pyramid[0][0].shape, dtype=dtype) + + flow = solver(pyramid[0][0], pyramid[0][1], flow) + + for J0, J1 in pyramid[1:]: + flow = solver(J0, J1, _resize_flow(flow, J0.shape)) + + return flow diff --git a/lib/python3.10/site-packages/skimage/registration/_phase_cross_correlation.py b/lib/python3.10/site-packages/skimage/registration/_phase_cross_correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..a4bf8b800433e07185f537d2e3297875cc945a0e --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/_phase_cross_correlation.py @@ -0,0 +1,420 @@ +""" +Port of Manuel Guizar's code from: +http://www.mathworks.com/matlabcentral/fileexchange/18401-efficient-subpixel-image-registration-by-cross-correlation +""" + +import itertools +import warnings + +import numpy as np +from scipy.fft import fftn, ifftn, fftfreq +from scipy import ndimage as ndi + +from ._masked_phase_cross_correlation import _masked_phase_cross_correlation + + +def _upsampled_dft(data, upsampled_region_size, upsample_factor=1, axis_offsets=None): + """ + Upsampled DFT by matrix multiplication. + + This code is intended to provide the same result as if the following + operations were performed: + - Embed the array "data" in an array that is ``upsample_factor`` times + larger in each dimension. ifftshift to bring the center of the + image to (1,1). + - Take the FFT of the larger array. + - Extract an ``[upsampled_region_size]`` region of the result, starting + with the ``[axis_offsets+1]`` element. + + It achieves this result by computing the DFT in the output array without + the need to zeropad. Much faster and memory efficient than the zero-padded + FFT approach if ``upsampled_region_size`` is much smaller than + ``data.size * upsample_factor``. + + Parameters + ---------- + data : array + The input data array (DFT of original data) to upsample. + upsampled_region_size : integer or tuple of integers, optional + The size of the region to be sampled. If one integer is provided, it + is duplicated up to the dimensionality of ``data``. + upsample_factor : integer, optional + The upsampling factor. Defaults to 1. + axis_offsets : tuple of integers, optional + The offsets of the region to be sampled. Defaults to None (uses + image center) + + Returns + ------- + output : ndarray + The upsampled DFT of the specified region. + """ + # if people pass in an integer, expand it to a list of equal-sized sections + if not hasattr(upsampled_region_size, "__iter__"): + upsampled_region_size = [ + upsampled_region_size, + ] * data.ndim + else: + if len(upsampled_region_size) != data.ndim: + raise ValueError( + "shape of upsampled region sizes must be equal " + "to input data's number of dimensions." + ) + + if axis_offsets is None: + axis_offsets = [ + 0, + ] * data.ndim + else: + if len(axis_offsets) != data.ndim: + raise ValueError( + "number of axis offsets must be equal to input " + "data's number of dimensions." + ) + + im2pi = 1j * 2 * np.pi + + dim_properties = list(zip(data.shape, upsampled_region_size, axis_offsets)) + + for n_items, ups_size, ax_offset in dim_properties[::-1]: + kernel = (np.arange(ups_size) - ax_offset)[:, None] * fftfreq( + n_items, upsample_factor + ) + kernel = np.exp(-im2pi * kernel) + # use kernel with same precision as the data + kernel = kernel.astype(data.dtype, copy=False) + + # Equivalent to: + # data[i, j, k] = kernel[i, :] @ data[j, k].T + data = np.tensordot(kernel, data, axes=(1, -1)) + return data + + +def _compute_phasediff(cross_correlation_max): + """ + Compute global phase difference between the two images (should be + zero if images are non-negative). + + Parameters + ---------- + cross_correlation_max : complex + The complex value of the cross correlation at its maximum point. + """ + return np.arctan2(cross_correlation_max.imag, cross_correlation_max.real) + + +def _compute_error(cross_correlation_max, src_amp, target_amp): + """ + Compute RMS error metric between ``src_image`` and ``target_image``. + + Parameters + ---------- + cross_correlation_max : complex + The complex value of the cross correlation at its maximum point. + src_amp : float + The normalized average image intensity of the source image + target_amp : float + The normalized average image intensity of the target image + """ + amp = src_amp * target_amp + if amp == 0: + warnings.warn( + "Could not determine RMS error between images with the normalized " + f"average intensities {src_amp!r} and {target_amp!r}. Either the " + "reference or moving image may be empty.", + UserWarning, + stacklevel=3, + ) + with np.errstate(invalid="ignore"): + error = 1.0 - cross_correlation_max * cross_correlation_max.conj() / amp + return np.sqrt(np.abs(error)) + + +def _disambiguate_shift(reference_image, moving_image, shift): + """Determine the correct real-space shift based on periodic shift. + + When determining a translation shift from phase cross-correlation in + Fourier space, the shift is only correct to within a period of the image + size along each axis, resulting in $2^n$ possible shifts, where $n$ is the + number of dimensions of the image. This function checks the + cross-correlation in real space for each of those shifts, and returns the + one with the highest cross-correlation. + + The strategy we use is to perform the shift on the moving image *using the + 'grid-wrap' mode* in `scipy.ndimage`. The moving image's original borders + then define $2^n$ quadrants, which we cross-correlate with the reference + image in turn using slicing. The entire operation is thus $O(2^n + m)$, + where $m$ is the number of pixels in the image (and typically dominates). + + Parameters + ---------- + reference_image : numpy array + The reference (non-moving) image. + moving_image : numpy array + The moving image: applying the shift to this image overlays it on the + reference image. Must be the same shape as the reference image. + shift : ndarray + The shift to apply to each axis of the moving image, *modulo* image + size. The length of ``shift`` must be equal to ``moving_image.ndim``. + + Returns + ------- + real_shift : ndarray + The shift disambiguated in real space. + """ + shape = reference_image.shape + positive_shift = [shift_i % s for shift_i, s in zip(shift, shape)] + negative_shift = [shift_i - s for shift_i, s in zip(positive_shift, shape)] + subpixel = np.any(np.array(shift) % 1 != 0) + interp_order = 3 if subpixel else 0 + shifted = ndi.shift(moving_image, shift, mode='grid-wrap', order=interp_order) + indices = np.round(positive_shift).astype(int) + splits_per_dim = [(slice(0, i), slice(i, None)) for i in indices] + max_corr = -1.0 + max_slice = None + for test_slice in itertools.product(*splits_per_dim): + reference_tile = np.reshape(reference_image[test_slice], -1) + moving_tile = np.reshape(shifted[test_slice], -1) + corr = -1.0 + if reference_tile.size > 2: + # In the case of zero std, np.corrcoef returns NaN and warns + # about division by zero. This is expected and handled below. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=RuntimeWarning) + corr = np.corrcoef(reference_tile, moving_tile)[0, 1] + if corr > max_corr: + max_corr = corr + max_slice = test_slice + if max_slice is None: + warnings.warn( + f"Could not determine real-space shift for periodic shift {shift!r} " + f"as requested by `disambiguate=True` (disambiguation is degenerate).", + stacklevel=3, + ) + return shift + real_shift_acc = [] + for sl, pos_shift, neg_shift in zip(max_slice, positive_shift, negative_shift): + real_shift_acc.append(pos_shift if sl.stop is None else neg_shift) + + return np.array(real_shift_acc) + + +def phase_cross_correlation( + reference_image, + moving_image, + *, + upsample_factor=1, + space="real", + disambiguate=False, + reference_mask=None, + moving_mask=None, + overlap_ratio=0.3, + normalization="phase", +): + """Efficient subpixel image translation registration by cross-correlation. + + This code gives the same precision as the FFT upsampled cross-correlation + in a fraction of the computation time and with reduced memory requirements. + It obtains an initial estimate of the cross-correlation peak by an FFT and + then refines the shift estimation by upsampling the DFT only in a small + neighborhood of that estimate by means of a matrix-multiply DFT [1]_. + + Parameters + ---------- + reference_image : array + Reference image. + moving_image : array + Image to register. Must be same dimensionality as + ``reference_image``. + upsample_factor : int, optional + Upsampling factor. Images will be registered to within + ``1 / upsample_factor`` of a pixel. For example + ``upsample_factor == 20`` means the images will be registered + within 1/20th of a pixel. Default is 1 (no upsampling). + Not used if any of ``reference_mask`` or ``moving_mask`` is not None. + space : string, one of "real" or "fourier", optional + Defines how the algorithm interprets input data. "real" means + data will be FFT'd to compute the correlation, while "fourier" + data will bypass FFT of input data. Case insensitive. Not + used if any of ``reference_mask`` or ``moving_mask`` is not + None. + disambiguate : bool + The shift returned by this function is only accurate *modulo* the + image shape, due to the periodic nature of the Fourier transform. If + this parameter is set to ``True``, the *real* space cross-correlation + is computed for each possible shift, and the shift with the highest + cross-correlation within the overlapping area is returned. + reference_mask : ndarray + Boolean mask for ``reference_image``. The mask should evaluate + to ``True`` (or 1) on valid pixels. ``reference_mask`` should + have the same shape as ``reference_image``. + moving_mask : ndarray or None, optional + Boolean mask for ``moving_image``. The mask should evaluate to ``True`` + (or 1) on valid pixels. ``moving_mask`` should have the same shape + as ``moving_image``. If ``None``, ``reference_mask`` will be used. + overlap_ratio : float, optional + Minimum allowed overlap ratio between images. The correlation for + translations corresponding with an overlap ratio lower than this + threshold will be ignored. A lower `overlap_ratio` leads to smaller + maximum translation, while a higher `overlap_ratio` leads to greater + robustness against spurious matches due to small overlap between + masked images. Used only if one of ``reference_mask`` or + ``moving_mask`` is not None. + normalization : {"phase", None} + The type of normalization to apply to the cross-correlation. This + parameter is unused when masks (`reference_mask` and `moving_mask`) are + supplied. + + Returns + ------- + shift : ndarray + Shift vector (in pixels) required to register ``moving_image`` + with ``reference_image``. Axis ordering is consistent with + the axis order of the input array. + error : float + Translation invariant normalized RMS error between + ``reference_image`` and ``moving_image``. For masked cross-correlation + this error is not available and NaN is returned. + phasediff : float + Global phase difference between the two images (should be + zero if images are non-negative). For masked cross-correlation + this phase difference is not available and NaN is returned. + + Notes + ----- + The use of cross-correlation to estimate image translation has a long + history dating back to at least [2]_. The "phase correlation" + method (selected by ``normalization="phase"``) was first proposed in [3]_. + Publications [1]_ and [2]_ use an unnormalized cross-correlation + (``normalization=None``). Which form of normalization is better is + application-dependent. For example, the phase correlation method works + well in registering images under different illumination, but is not very + robust to noise. In a high noise scenario, the unnormalized method may be + preferable. + + When masks are provided, a masked normalized cross-correlation algorithm is + used [5]_, [6]_. + + References + ---------- + .. [1] Manuel Guizar-Sicairos, Samuel T. Thurman, and James R. Fienup, + "Efficient subpixel image registration algorithms," + Optics Letters 33, 156-158 (2008). :DOI:`10.1364/OL.33.000156` + .. [2] P. Anuta, Spatial registration of multispectral and multitemporal + digital imagery using fast Fourier transform techniques, IEEE Trans. + Geosci. Electron., vol. 8, no. 4, pp. 353–368, Oct. 1970. + :DOI:`10.1109/TGE.1970.271435`. + .. [3] C. D. Kuglin D. C. Hines. The phase correlation image alignment + method, Proceeding of IEEE International Conference on Cybernetics + and Society, pp. 163-165, New York, NY, USA, 1975, pp. 163–165. + .. [4] James R. Fienup, "Invariant error metrics for image reconstruction" + Optics Letters 36, 8352-8357 (1997). :DOI:`10.1364/AO.36.008352` + .. [5] Dirk Padfield. Masked Object Registration in the Fourier Domain. + IEEE Transactions on Image Processing, vol. 21(5), + pp. 2706-2718 (2012). :DOI:`10.1109/TIP.2011.2181402` + .. [6] D. Padfield. "Masked FFT registration". In Proc. Computer Vision and + Pattern Recognition, pp. 2918-2925 (2010). + :DOI:`10.1109/CVPR.2010.5540032` + """ + if (reference_mask is not None) or (moving_mask is not None): + shift = _masked_phase_cross_correlation( + reference_image, moving_image, reference_mask, moving_mask, overlap_ratio + ) + return shift, np.nan, np.nan + + # images must be the same shape + if reference_image.shape != moving_image.shape: + raise ValueError("images must be same shape") + + # assume complex data is already in Fourier space + if space.lower() == 'fourier': + src_freq = reference_image + target_freq = moving_image + # real data needs to be fft'd. + elif space.lower() == 'real': + src_freq = fftn(reference_image) + target_freq = fftn(moving_image) + else: + raise ValueError('space argument must be "real" of "fourier"') + + # Whole-pixel shift - Compute cross-correlation by an IFFT + shape = src_freq.shape + image_product = src_freq * target_freq.conj() + if normalization == "phase": + eps = np.finfo(image_product.real.dtype).eps + image_product /= np.maximum(np.abs(image_product), 100 * eps) + elif normalization is not None: + raise ValueError("normalization must be either phase or None") + cross_correlation = ifftn(image_product) + + # Locate maximum + maxima = np.unravel_index( + np.argmax(np.abs(cross_correlation)), cross_correlation.shape + ) + midpoint = np.array([np.fix(axis_size / 2) for axis_size in shape]) + + float_dtype = image_product.real.dtype + + shift = np.stack(maxima).astype(float_dtype, copy=False) + shift[shift > midpoint] -= np.array(shape)[shift > midpoint] + + if upsample_factor == 1: + src_amp = np.sum(np.real(src_freq * src_freq.conj())) + src_amp /= src_freq.size + target_amp = np.sum(np.real(target_freq * target_freq.conj())) + target_amp /= target_freq.size + CCmax = cross_correlation[maxima] + # If upsampling > 1, then refine estimate with matrix multiply DFT + else: + # Initial shift estimate in upsampled grid + upsample_factor = np.array(upsample_factor, dtype=float_dtype) + shift = np.round(shift * upsample_factor) / upsample_factor + upsampled_region_size = np.ceil(upsample_factor * 1.5) + # Center of output array at dftshift + 1 + dftshift = np.fix(upsampled_region_size / 2.0) + # Matrix multiply DFT around the current shift estimate + sample_region_offset = dftshift - shift * upsample_factor + cross_correlation = _upsampled_dft( + image_product.conj(), + upsampled_region_size, + upsample_factor, + sample_region_offset, + ).conj() + # Locate maximum and map back to original pixel grid + maxima = np.unravel_index( + np.argmax(np.abs(cross_correlation)), cross_correlation.shape + ) + CCmax = cross_correlation[maxima] + + maxima = np.stack(maxima).astype(float_dtype, copy=False) + maxima -= dftshift + + shift += maxima / upsample_factor + + src_amp = np.sum(np.real(src_freq * src_freq.conj())) + target_amp = np.sum(np.real(target_freq * target_freq.conj())) + + # If its only one row or column the shift along that dimension has no + # effect. We set to zero. + for dim in range(src_freq.ndim): + if shape[dim] == 1: + shift[dim] = 0 + + if disambiguate: + if space.lower() != 'real': + reference_image = ifftn(reference_image) + moving_image = ifftn(moving_image) + shift = _disambiguate_shift(reference_image, moving_image, shift) + + # Redirect user to masked_phase_cross_correlation if NaNs are observed + if np.isnan(CCmax) or np.isnan(src_amp) or np.isnan(target_amp): + raise ValueError( + "NaN values found, please remove NaNs from your " + "input data or use the `reference_mask`/`moving_mask` " + "keywords, eg: " + "phase_cross_correlation(reference_image, moving_image, " + "reference_mask=~np.isnan(reference_image), " + "moving_mask=~np.isnan(moving_image))" + ) + + return shift, _compute_error(CCmax, src_amp, target_amp), _compute_phasediff(CCmax) diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__init__.py b/lib/python3.10/site-packages/skimage/registration/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..589ee0fd8e79dd4bc649faf26cfa3e42db3b6eee Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_ilk.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_ilk.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..087482e068037fd5421d0a2d37b6bc484ad8e35a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_ilk.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_masked_phase_cross_correlation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_masked_phase_cross_correlation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcc5f609f5753ce04c727dc5226c34394399a569 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_masked_phase_cross_correlation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_phase_cross_correlation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_phase_cross_correlation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8b8aac31105586b6a8718b28a8f0cea5543fc2e Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_phase_cross_correlation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_tvl1.cpython-310.pyc b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_tvl1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdca5f020b3048ae3a09650645c1af961a2129d0 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/registration/tests/__pycache__/test_tvl1.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/registration/tests/test_ilk.py b/lib/python3.10/site-packages/skimage/registration/tests/test_ilk.py new file mode 100644 index 0000000000000000000000000000000000000000..37a4c08925f047c68477aa8b75759be30fd8b259 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/tests/test_ilk.py @@ -0,0 +1,101 @@ +import numpy as np +import pytest + +from skimage._shared.utils import _supported_float_type +from skimage.registration import optical_flow_ilk +from .test_tvl1 import _sin_flow_gen + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +@pytest.mark.parametrize('gaussian', [True, False]) +@pytest.mark.parametrize('prefilter', [True, False]) +def test_2d_motion(dtype, gaussian, prefilter): + # Generate synthetic data + rng = np.random.default_rng(0) + image0 = rng.normal(size=(256, 256)) + gt_flow, image1 = _sin_flow_gen(image0) + image1 = image1.astype(dtype, copy=False) + float_dtype = _supported_float_type(dtype) + # Estimate the flow + flow = optical_flow_ilk( + image0, image1, gaussian=gaussian, prefilter=prefilter, dtype=float_dtype + ) + assert flow.dtype == _supported_float_type(dtype) + # Assert that the average absolute error is less then half a pixel + assert abs(flow - gt_flow).mean() < 0.5 + + if dtype != float_dtype: + with pytest.raises(ValueError): + optical_flow_ilk( + image0, image1, gaussian=gaussian, prefilter=prefilter, dtype=dtype + ) + + +@pytest.mark.parametrize('gaussian', [True, False]) +@pytest.mark.parametrize('prefilter', [True, False]) +def test_3d_motion(gaussian, prefilter): + # Generate synthetic data + rng = np.random.default_rng(123) + image0 = rng.normal(size=(50, 55, 60)) + gt_flow, image1 = _sin_flow_gen(image0, npics=3) + # Estimate the flow + flow = optical_flow_ilk( + image0, image1, radius=5, gaussian=gaussian, prefilter=prefilter + ) + + # Assert that the average absolute error is less then half a pixel + assert abs(flow - gt_flow).mean() < 0.5 + + +def test_no_motion_2d(): + rng = np.random.default_rng(0) + img = rng.normal(size=(256, 256)) + + flow = optical_flow_ilk(img, img) + + assert np.all(flow == 0) + + +def test_no_motion_3d(): + rng = np.random.default_rng(0) + img = rng.normal(size=(64, 64, 64)) + + flow = optical_flow_ilk(img, img) + + assert np.all(flow == 0) + + +def test_optical_flow_dtype(): + # Generate synthetic data + rng = np.random.default_rng(0) + image0 = rng.normal(size=(256, 256)) + gt_flow, image1 = _sin_flow_gen(image0) + # Estimate the flow at double precision + flow_f64 = optical_flow_ilk(image0, image1, dtype='float64') + + assert flow_f64.dtype == 'float64' + + # Estimate the flow at single precision + flow_f32 = optical_flow_ilk(image0, image1, dtype='float32') + + assert flow_f32.dtype == 'float32' + + # Assert that floating point precision does not affect the quality + # of the estimated flow + + assert abs(flow_f64 - flow_f32).mean() < 1e-3 + + +def test_incompatible_shapes(): + rng = np.random.default_rng(0) + I0 = rng.normal(size=(256, 256)) + I1 = rng.normal(size=(255, 256)) + with pytest.raises(ValueError): + u, v = optical_flow_ilk(I0, I1) + + +def test_wrong_dtype(): + rng = np.random.default_rng(0) + img = rng.normal(size=(256, 256)) + with pytest.raises(ValueError): + u, v = optical_flow_ilk(img, img, dtype='int') diff --git a/lib/python3.10/site-packages/skimage/registration/tests/test_masked_phase_cross_correlation.py b/lib/python3.10/site-packages/skimage/registration/tests/test_masked_phase_cross_correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..8e0fb79f1c7711a8ae2e41ce7fa6ab5b8a219fc3 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/tests/test_masked_phase_cross_correlation.py @@ -0,0 +1,278 @@ +import numpy as np +import pytest +from numpy.testing import ( + assert_almost_equal, + assert_array_almost_equal, + assert_array_equal, + assert_array_less, + assert_equal, +) +from scipy.ndimage import fourier_shift, shift as real_shift +import scipy.fft as fft + +from skimage._shared.testing import fetch +from skimage._shared.utils import _supported_float_type +from skimage.data import camera, brain + + +from skimage.io import imread +from skimage.registration._masked_phase_cross_correlation import ( + _masked_phase_cross_correlation as masked_register_translation, + cross_correlate_masked, +) +from skimage.registration import phase_cross_correlation + + +def test_masked_registration_vs_phase_cross_correlation(): + """masked_register_translation should give the same results as + phase_cross_correlation in the case of trivial masks.""" + reference_image = camera() + shift = (-7, 12) + shifted = np.real(fft.ifft2(fourier_shift(fft.fft2(reference_image), shift))) + trivial_mask = np.ones_like(reference_image) + + nonmasked_result, *_ = phase_cross_correlation(reference_image, shifted) + masked_result = masked_register_translation( + reference_image, shifted, reference_mask=trivial_mask, overlap_ratio=1 / 10 + ) + + assert_equal(nonmasked_result, masked_result) + + +def test_masked_registration_random_masks(): + """masked_register_translation should be able to register translations + between images even with random masks.""" + # See random number generator for reproducible results + np.random.seed(23) + + reference_image = camera() + shift = (-7, 12) + shifted = np.real(fft.ifft2(fourier_shift(fft.fft2(reference_image), shift))) + + # Random masks with 75% of pixels being valid + ref_mask = np.random.choice([True, False], reference_image.shape, p=[3 / 4, 1 / 4]) + shifted_mask = np.random.choice([True, False], shifted.shape, p=[3 / 4, 1 / 4]) + + measured_shift = masked_register_translation( + reference_image, shifted, reference_mask=ref_mask, moving_mask=shifted_mask + ) + assert_equal(measured_shift, -np.array(shift)) + + +def test_masked_registration_3d_contiguous_mask(): + """masked_register_translation should be able to register translations + between volumes with contiguous masks.""" + ref_vol = brain()[:, ::2, ::2] + + offset = (1, -5, 10) + + # create square mask + ref_mask = np.zeros_like(ref_vol, dtype=bool) + ref_mask[:-2, 75:100, 75:100] = True + ref_shifted = real_shift(ref_vol, offset) + + measured_offset = masked_register_translation( + ref_vol, ref_shifted, reference_mask=ref_mask, moving_mask=ref_mask + ) + + assert_equal(offset, -np.array(measured_offset)) + + +def test_masked_registration_random_masks_non_equal_sizes(): + """masked_register_translation should be able to register + translations between images that are not the same size even + with random masks.""" + # See random number generator for reproducible results + np.random.seed(23) + + reference_image = camera() + shift = (-7, 12) + shifted = np.real(fft.ifft2(fourier_shift(fft.fft2(reference_image), shift))) + + # Crop the shifted image + shifted = shifted[64:-64, 64:-64] + + # Random masks with 75% of pixels being valid + ref_mask = np.random.choice([True, False], reference_image.shape, p=[3 / 4, 1 / 4]) + shifted_mask = np.random.choice([True, False], shifted.shape, p=[3 / 4, 1 / 4]) + + measured_shift = masked_register_translation( + reference_image, + shifted, + reference_mask=np.ones_like(ref_mask), + moving_mask=np.ones_like(shifted_mask), + ) + assert_equal(measured_shift, -np.array(shift)) + + +def test_masked_registration_padfield_data(): + """Masked translation registration should behave like in the original + publication""" + # Test translated from MATLABimplementation `MaskedFFTRegistrationTest` + # file. You can find the source code here: + # http://www.dirkpadfield.com/Home/MaskedFFTRegistrationCode.zip + + shifts = [(75, 75), (-130, 130), (130, 130)] + for xi, yi in shifts: + fixed_image = imread(fetch(f'registration/tests/data/OriginalX{xi}Y{yi}.png')) + moving_image = imread( + fetch(f'registration/tests/data/TransformedX{xi}Y{yi}.png') + ) + + # Valid pixels are 1 + fixed_mask = fixed_image != 0 + moving_mask = moving_image != 0 + + # Note that shifts in x and y and shifts in cols and rows + shift_y, shift_x = masked_register_translation( + fixed_image, + moving_image, + reference_mask=fixed_mask, + moving_mask=moving_mask, + overlap_ratio=0.1, + ) + # Note: by looking at the test code from Padfield's + # MaskedFFTRegistrationCode repository, the + # shifts were not xi and yi, but xi and -yi + assert_equal((shift_x, shift_y), (-xi, yi)) + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_cross_correlate_masked_output_shape(dtype): + """Masked normalized cross-correlation should return a shape + of N + M + 1 for each transform axis.""" + shape1 = (15, 4, 5) + shape2 = (6, 12, 7) + expected_full_shape = tuple(np.array(shape1) + np.array(shape2) - 1) + expected_same_shape = shape1 + + arr1 = np.zeros(shape1, dtype=dtype) + arr2 = np.zeros(shape2, dtype=dtype) + # Trivial masks + m1 = np.ones_like(arr1) + m2 = np.ones_like(arr2) + + float_dtype = _supported_float_type(dtype) + + full_xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1, 2), mode='full') + assert_equal(full_xcorr.shape, expected_full_shape) + assert full_xcorr.dtype == float_dtype + + same_xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1, 2), mode='same') + assert_equal(same_xcorr.shape, expected_same_shape) + assert same_xcorr.dtype == float_dtype + + +def test_cross_correlate_masked_test_against_mismatched_dimensions(): + """Masked normalized cross-correlation should raise an error if array + dimensions along non-transformation axes are mismatched.""" + shape1 = (23, 1, 1) + shape2 = (6, 2, 2) + + arr1 = np.zeros(shape1) + arr2 = np.zeros(shape2) + + # Trivial masks + m1 = np.ones_like(arr1) + m2 = np.ones_like(arr2) + + with pytest.raises(ValueError): + cross_correlate_masked(arr1, arr2, m1, m2, axes=(1, 2)) + + +def test_cross_correlate_masked_output_range(): + """Masked normalized cross-correlation should return between 1 and -1.""" + # See random number generator for reproducible results + np.random.seed(23) + + # Array dimensions must match along non-transformation axes, in + # this case + # axis 0 + shape1 = (15, 4, 5) + shape2 = (15, 12, 7) + + # Initial array ranges between -5 and 5 + arr1 = 10 * np.random.random(shape1) - 5 + arr2 = 10 * np.random.random(shape2) - 5 + + # random masks + m1 = np.random.choice([True, False], arr1.shape) + m2 = np.random.choice([True, False], arr2.shape) + + xcorr = cross_correlate_masked(arr1, arr2, m1, m2, axes=(1, 2)) + + # No assert array less or equal, so we add an eps + # Also could not find an `assert_array_greater`, Use (-xcorr) instead + eps = np.finfo(float).eps + assert_array_less(xcorr, 1 + eps) + assert_array_less(-xcorr, 1 + eps) + + +def test_cross_correlate_masked_side_effects(): + """Masked normalized cross-correlation should not modify the inputs.""" + shape1 = (2, 2, 2) + shape2 = (2, 2, 2) + + arr1 = np.zeros(shape1) + arr2 = np.zeros(shape2) + + # Trivial masks + m1 = np.ones_like(arr1) + m2 = np.ones_like(arr2) + + for arr in (arr1, arr2, m1, m2): + arr.setflags(write=False) + + cross_correlate_masked(arr1, arr2, m1, m2) + + +def test_cross_correlate_masked_over_axes(): + """Masked normalized cross-correlation over axes should be + equivalent to a loop over non-transform axes.""" + # See random number generator for reproducible results + np.random.seed(23) + + arr1 = np.random.random((8, 8, 5)) + arr2 = np.random.random((8, 8, 5)) + + m1 = np.random.choice([True, False], arr1.shape) + m2 = np.random.choice([True, False], arr2.shape) + + # Loop over last axis + with_loop = np.empty_like(arr1, dtype=complex) + for index in range(arr1.shape[-1]): + with_loop[:, :, index] = cross_correlate_masked( + arr1[:, :, index], + arr2[:, :, index], + m1[:, :, index], + m2[:, :, index], + axes=(0, 1), + mode='same', + ) + + over_axes = cross_correlate_masked(arr1, arr2, m1, m2, axes=(0, 1), mode='same') + + assert_array_almost_equal(with_loop, over_axes) + + +def test_cross_correlate_masked_autocorrelation_trivial_masks(): + """Masked normalized cross-correlation between identical arrays + should reduce to an autocorrelation even with random masks.""" + # See random number generator for reproducible results + np.random.seed(23) + + arr1 = camera() + + # Random masks with 75% of pixels being valid + m1 = np.random.choice([True, False], arr1.shape, p=[3 / 4, 1 / 4]) + m2 = np.random.choice([True, False], arr1.shape, p=[3 / 4, 1 / 4]) + + xcorr = cross_correlate_masked( + arr1, arr1, m1, m2, axes=(0, 1), mode='same', overlap_ratio=0 + ).real + max_index = np.unravel_index(np.argmax(xcorr), xcorr.shape) + + # Autocorrelation should have maximum in center of array + # uint8 inputs will be processed in float32, so reduce decimal to 5 + assert_almost_equal(xcorr.max(), 1, decimal=5) + assert_array_equal(max_index, np.array(arr1.shape) / 2) diff --git a/lib/python3.10/site-packages/skimage/registration/tests/test_phase_cross_correlation.py b/lib/python3.10/site-packages/skimage/registration/tests/test_phase_cross_correlation.py new file mode 100644 index 0000000000000000000000000000000000000000..a43f14f3e9f1baae4ff754cfa5ebd399e043ae04 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/tests/test_phase_cross_correlation.py @@ -0,0 +1,251 @@ +import itertools +import warnings + +import numpy as np +import pytest +from numpy.testing import assert_allclose +from scipy.ndimage import fourier_shift +import scipy.fft as fft + +from skimage import img_as_float +from skimage._shared._warnings import expected_warnings +from skimage._shared.testing import assert_stacklevel +from skimage._shared.utils import _supported_float_type +from skimage.data import camera, binary_blobs, eagle +from skimage.registration._phase_cross_correlation import ( + phase_cross_correlation, + _upsampled_dft, +) + + +@pytest.mark.parametrize('normalization', [None, 'phase']) +def test_correlation(normalization): + reference_image = fft.fftn(camera()) + shift = (-7, 12) + shifted_image = fourier_shift(reference_image, shift) + + # pixel precision + result, _, _ = phase_cross_correlation( + reference_image, shifted_image, space="fourier", normalization=normalization + ) + assert_allclose(result[:2], -np.array(shift)) + + +@pytest.mark.parametrize('normalization', ['nonexisting']) +def test_correlation_invalid_normalization(normalization): + reference_image = fft.fftn(camera()) + shift = (-7, 12) + shifted_image = fourier_shift(reference_image, shift) + + # pixel precision + with pytest.raises(ValueError): + phase_cross_correlation( + reference_image, shifted_image, space="fourier", normalization=normalization + ) + + +@pytest.mark.parametrize('normalization', [None, 'phase']) +def test_subpixel_precision(normalization): + reference_image = fft.fftn(camera()) + subpixel_shift = (-2.4, 1.32) + shifted_image = fourier_shift(reference_image, subpixel_shift) + + # subpixel precision + result, _, _ = phase_cross_correlation( + reference_image, + shifted_image, + upsample_factor=100, + space="fourier", + normalization=normalization, + ) + assert_allclose(result[:2], -np.array(subpixel_shift), atol=0.05) + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_real_input(dtype): + reference_image = camera().astype(dtype, copy=False) + subpixel_shift = (-2.4, 1.32) + shifted_image = fourier_shift(fft.fftn(reference_image), subpixel_shift) + shifted_image = fft.ifftn(shifted_image).real.astype(dtype, copy=False) + + # subpixel precision + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, upsample_factor=100 + ) + assert result.dtype == _supported_float_type(dtype) + assert_allclose(result[:2], -np.array(subpixel_shift), atol=0.05) + + +def test_size_one_dimension_input(): + # take a strip of the input image + reference_image = fft.fftn(camera()[:, 15]).reshape((-1, 1)) + subpixel_shift = (-2.4, 4) + shifted_image = fourier_shift(reference_image, subpixel_shift) + + # subpixel precision + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, upsample_factor=20, space="fourier" + ) + assert_allclose(result[:2], -np.array((-2.4, 0)), atol=0.05) + + +def test_3d_input(): + phantom = img_as_float(binary_blobs(length=32, n_dim=3)) + reference_image = fft.fftn(phantom) + shift = (-2.0, 1.0, 5.0) + shifted_image = fourier_shift(reference_image, shift) + + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, space="fourier" + ) + assert_allclose(result, -np.array(shift), atol=0.05) + + # subpixel precision now available for 3-D data + + subpixel_shift = (-2.3, 1.7, 5.4) + shifted_image = fourier_shift(reference_image, subpixel_shift) + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, upsample_factor=100, space="fourier" + ) + assert_allclose(result, -np.array(subpixel_shift), atol=0.05) + + +def test_unknown_space_input(): + image = np.ones((5, 5)) + with pytest.raises(ValueError): + phase_cross_correlation(image, image, space="frank") + + +def test_wrong_input(): + # Dimensionality mismatch + image = np.ones((5, 5, 1)) + template = np.ones((5, 5)) + with pytest.raises(ValueError): + phase_cross_correlation(template, image) + + # Size mismatch + image = np.ones((5, 5)) + template = np.ones((4, 4)) + with pytest.raises(ValueError): + phase_cross_correlation(template, image) + + # NaN values in data + image = np.ones((5, 5)) + image[0][0] = np.nan + template = np.ones((5, 5)) + with expected_warnings( + [ + r"invalid value encountered in true_divide" + + r"|" + + r"invalid value encountered in divide" + + r"|\A\Z" + ] + ): + with pytest.raises(ValueError): + phase_cross_correlation(template, image) + + +def test_4d_input_pixel(): + phantom = img_as_float(binary_blobs(length=32, n_dim=4)) + reference_image = fft.fftn(phantom) + shift = (-2.0, 1.0, 5.0, -3) + shifted_image = fourier_shift(reference_image, shift) + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, space="fourier" + ) + assert_allclose(result, -np.array(shift), atol=0.05) + + +def test_4d_input_subpixel(): + phantom = img_as_float(binary_blobs(length=32, n_dim=4)) + reference_image = fft.fftn(phantom) + subpixel_shift = (-2.3, 1.7, 5.4, -3.2) + shifted_image = fourier_shift(reference_image, subpixel_shift) + result, error, diffphase = phase_cross_correlation( + reference_image, shifted_image, upsample_factor=10, space="fourier" + ) + assert_allclose(result, -np.array(subpixel_shift), atol=0.05) + + +def test_mismatch_upsampled_region_size(): + with pytest.raises(ValueError): + _upsampled_dft(np.ones((4, 4)), upsampled_region_size=[3, 2, 1, 4]) + + +def test_mismatch_offsets_size(): + with pytest.raises(ValueError): + _upsampled_dft(np.ones((4, 4)), 3, axis_offsets=[3, 2, 1, 4]) + + +@pytest.mark.parametrize( + ('shift0', 'shift1'), + itertools.product((100, -100, 350, -350), (100, -100, 350, -350)), +) +def test_disambiguate_2d(shift0, shift1): + image = eagle()[500:, 900:] # use a highly textured part of image + shift = (shift0, shift1) + origin0 = [] + for s in shift: + if s > 0: + origin0.append(0) + else: + origin0.append(-s) + origin1 = np.array(origin0) + shift + slice0 = tuple(slice(o, o + 450) for o in origin0) + slice1 = tuple(slice(o, o + 450) for o in origin1) + reference = image[slice0] + moving = image[slice1] + computed_shift, _, _ = phase_cross_correlation( + reference, + moving, + disambiguate=True, + ) + np.testing.assert_equal(shift, computed_shift) + + +def test_invalid_value_in_division_warnings(): + """Regression test for https://github.com/scikit-image/scikit-image/issues/7146.""" + im1 = np.zeros((100, 100)) + im1[50, 50] = 1 + im2 = np.zeros((100, 100)) + im2[60, 60] = 1 + with warnings.catch_warnings(): + warnings.simplefilter("error") + phase_cross_correlation(im1, im2, disambiguate=True) + + +@pytest.mark.parametrize('disambiguate', [True, False]) +def test_disambiguate_zero_shift(disambiguate): + """When the shift is 0, disambiguation becomes degenerate. + + Some quadrants become size 0, which prevents computation of + cross-correlation. This test ensures that nothing bad happens in that + scenario. + """ + image = camera() + computed_shift, _, _ = phase_cross_correlation( + image, + image, + disambiguate=disambiguate, + ) + assert isinstance(computed_shift, np.ndarray) + np.testing.assert_array_equal(computed_shift, np.array((0.0, 0.0))) + + +@pytest.mark.parametrize('null_images', [(1, 0), (0, 1), (0, 0)]) +def test_disambiguate_empty_image(null_images): + """When the image is empty, disambiguation becomes degenerate.""" + image = camera() + with pytest.warns(UserWarning) as records: + shift, error, phasediff = phase_cross_correlation( + image * null_images[0], image * null_images[1], disambiguate=True + ) + assert_stacklevel(records, offset=-3) + np.testing.assert_array_equal(shift, np.array([0.0, 0.0])) + assert np.isnan(error) + assert phasediff == 0.0 + + # Check warnings + assert len(records) == 2 + assert "Could not determine real-space shift" in records[0].message.args[0] + assert "Could not determine RMS error between images" in records[1].message.args[0] diff --git a/lib/python3.10/site-packages/skimage/registration/tests/test_tvl1.py b/lib/python3.10/site-packages/skimage/registration/tests/test_tvl1.py new file mode 100644 index 0000000000000000000000000000000000000000..171c8da6d35ebdbd90f3b78cb3b924d0081d6105 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/registration/tests/test_tvl1.py @@ -0,0 +1,118 @@ +import numpy as np +import pytest + +from skimage._shared.utils import _supported_float_type +from skimage.registration import optical_flow_tvl1 +from skimage.transform import warp + + +def _sin_flow_gen(image0, max_motion=4.5, npics=5): + """Generate a synthetic ground truth optical flow with a sinusoid as + first component. + + Parameters + ---------- + image0: ndarray + The base image to be warped. + max_motion: float + Maximum flow magnitude. + npics: int + Number of sinusoid pics. + + Returns + ------- + flow, image1 : ndarray + The synthetic ground truth optical flow with a sinusoid as + first component and the corresponding warped image. + + """ + grid = np.meshgrid(*[np.arange(n) for n in image0.shape], indexing='ij') + grid = np.stack(grid) + gt_flow = np.zeros_like(grid, dtype=float) + gt_flow[0, ...] = max_motion * np.sin(grid[0] / grid[0].max() * npics * np.pi) + image1 = warp(image0, grid - gt_flow, mode='edge') + return gt_flow, image1 + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_2d_motion(dtype): + # Generate synthetic data + rng = np.random.default_rng(0) + image0 = rng.normal(size=(256, 256)) + gt_flow, image1 = _sin_flow_gen(image0) + image1 = image1.astype(dtype, copy=False) + float_dtype = _supported_float_type(dtype) + # Estimate the flow + flow = optical_flow_tvl1(image0, image1, attachment=5, dtype=float_dtype) + assert flow.dtype == float_dtype + # Assert that the average absolute error is less then half a pixel + assert abs(flow - gt_flow).mean() < 0.5 + + if dtype != float_dtype: + with pytest.raises(ValueError): + optical_flow_tvl1(image0, image1, attachment=5, dtype=dtype) + + +def test_3d_motion(): + # Generate synthetic data + rng = np.random.default_rng(0) + image0 = rng.normal(size=(100, 100, 100)) + gt_flow, image1 = _sin_flow_gen(image0) + # Estimate the flow + flow = optical_flow_tvl1(image0, image1, attachment=10) + # Assert that the average absolute error is less then half a pixel + assert abs(flow - gt_flow).mean() < 0.5 + + +def test_no_motion_2d(): + rng = np.random.default_rng(0) + img = rng.normal(size=(256, 256)) + + flow = optical_flow_tvl1(img, img) + + assert np.all(flow == 0) + + +def test_no_motion_3d(): + rng = np.random.default_rng(0) + img = rng.normal(size=(64, 64, 64)) + + flow = optical_flow_tvl1(img, img) + + assert np.all(flow == 0) + + +def test_optical_flow_dtype(): + # Generate synthetic data + rng = np.random.default_rng(0) + image0 = rng.normal(size=(256, 256)) + gt_flow, image1 = _sin_flow_gen(image0) + # Estimate the flow at double precision + flow_f64 = optical_flow_tvl1(image0, image1, attachment=5, dtype=np.float64) + + assert flow_f64.dtype == np.float64 + + # Estimate the flow at single precision + flow_f32 = optical_flow_tvl1(image0, image1, attachment=5, dtype=np.float32) + + assert flow_f32.dtype == np.float32 + + # Assert that floating point precision does not affect the quality + # of the estimated flow + + assert np.abs(flow_f64 - flow_f32).mean() < 1e-3 + + +def test_incompatible_shapes(): + rng = np.random.default_rng(0) + I0 = rng.normal(size=(256, 256)) + I1 = rng.normal(size=(128, 256)) + with pytest.raises(ValueError): + u, v = optical_flow_tvl1(I0, I1) + + +def test_wrong_dtype(): + rng = np.random.default_rng(0) + img = rng.normal(size=(256, 256)) + with pytest.raises(ValueError): + u, v = optical_flow_tvl1(img, img, dtype=np.int64) diff --git a/lib/python3.10/site-packages/skimage/restoration/__init__.py b/lib/python3.10/site-packages/skimage/restoration/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..13c8e7421d0898c0da6029dc1e31c2362176eb4c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/__init__.py @@ -0,0 +1,5 @@ +"""Restoration algorithms, e.g., deconvolution algorithms, denoising, etc.""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/restoration/__init__.pyi b/lib/python3.10/site-packages/skimage/restoration/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..399036ae296af775c89461588d417ab4ca950129 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/__init__.pyi @@ -0,0 +1,38 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'wiener', + 'unsupervised_wiener', + 'richardson_lucy', + 'unwrap_phase', + 'denoise_tv_bregman', + 'denoise_tv_chambolle', + 'denoise_bilateral', + 'denoise_wavelet', + 'denoise_nl_means', + 'denoise_invariant', + 'estimate_sigma', + 'inpaint_biharmonic', + 'cycle_spin', + 'calibrate_denoiser', + 'rolling_ball', + 'ellipsoid_kernel', + 'ball_kernel', +] + +from .deconvolution import wiener, unsupervised_wiener, richardson_lucy +from .unwrap import unwrap_phase +from ._denoise import ( + denoise_tv_chambolle, + denoise_tv_bregman, + denoise_bilateral, + denoise_wavelet, + estimate_sigma, +) +from ._cycle_spin import cycle_spin +from .non_local_means import denoise_nl_means +from .inpaint import inpaint_biharmonic +from .j_invariant import calibrate_denoiser, denoise_invariant +from ._rolling_ball import rolling_ball, ball_kernel, ellipsoid_kernel diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e3b669efda13eb9b4f7083df48b58aeefbb0642 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/_cycle_spin.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_cycle_spin.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2679f97e1d0a93bb880d4f0d597e040e0a9bd86e Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_cycle_spin.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/_denoise.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_denoise.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a025fda4e21d02dc7ab28578eaf1846fda9092f6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_denoise.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/_rolling_ball.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_rolling_ball.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b7db0ce01fe1daadaa6a2e5a22fd3d0014b208a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/_rolling_ball.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/deconvolution.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/deconvolution.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..292b10a7bba99876f1d615ad2336c9231db5393b Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/deconvolution.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/inpaint.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/inpaint.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5624ce192379bc55a6e398f436ce4e3774ed7b2a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/inpaint.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/j_invariant.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/j_invariant.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eee79f46015534c5d66ed04c7232890dec519783 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/j_invariant.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/non_local_means.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/non_local_means.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e07d56f861f7b51dac0490636c2ac835a69b2d6 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/non_local_means.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/uft.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/uft.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e905aaa2d9dbe43a780449a7f7e25a078c58614d Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/uft.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/__pycache__/unwrap.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/__pycache__/unwrap.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f1dd5ad35a429a1d3db99f089c523beedaf750f Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/__pycache__/unwrap.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/_cycle_spin.py b/lib/python3.10/site-packages/skimage/restoration/_cycle_spin.py new file mode 100644 index 0000000000000000000000000000000000000000..6c1bc8ea50ff001d0112ca409958b78911781500 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/_cycle_spin.py @@ -0,0 +1,164 @@ +from itertools import product +import numpy as np +from .._shared import utils +from .._shared.utils import warn + +try: + import dask + + dask_available = True +except ImportError: + dask_available = False + + +def _generate_shifts(ndim, multichannel, max_shifts, shift_steps=1): + """Returns all combinations of shifts in n dimensions over the specified + max_shifts and step sizes. + + Examples + -------- + >>> s = list(_generate_shifts(2, False, max_shifts=(1, 2), shift_steps=1)) + >>> print(s) + [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] + """ + mc = int(multichannel) + if np.isscalar(max_shifts): + max_shifts = (max_shifts,) * (ndim - mc) + (0,) * mc + elif multichannel and len(max_shifts) == ndim - 1: + max_shifts = tuple(max_shifts) + (0,) + elif len(max_shifts) != ndim: + raise ValueError("max_shifts should have length ndim") + + if np.isscalar(shift_steps): + shift_steps = (shift_steps,) * (ndim - mc) + (1,) * mc + elif multichannel and len(shift_steps) == ndim - 1: + shift_steps = tuple(shift_steps) + (1,) + elif len(shift_steps) != ndim: + raise ValueError("max_shifts should have length ndim") + + if any(s < 1 for s in shift_steps): + raise ValueError("shift_steps must all be >= 1") + + if multichannel and max_shifts[-1] != 0: + raise ValueError( + "Multichannel cycle spinning should not have shifts along the " "last axis." + ) + + return product(*[range(0, s + 1, t) for s, t in zip(max_shifts, shift_steps)]) + + +@utils.channel_as_last_axis() +def cycle_spin( + x, + func, + max_shifts, + shift_steps=1, + num_workers=None, + func_kw=None, + *, + channel_axis=None, +): + """Cycle spinning (repeatedly apply func to shifted versions of x). + + Parameters + ---------- + x : array-like + Data for input to ``func``. + func : function + A function to apply to circularly shifted versions of ``x``. Should + take ``x`` as its first argument. Any additional arguments can be + supplied via ``func_kw``. + max_shifts : int or tuple + If an integer, shifts in ``range(0, max_shifts+1)`` will be used along + each axis of ``x``. If a tuple, ``range(0, max_shifts[i]+1)`` will be + along axis i. + shift_steps : int or tuple, optional + The step size for the shifts applied along axis, i, are:: + ``range((0, max_shifts[i]+1, shift_steps[i]))``. If an integer is + provided, the same step size is used for all axes. + num_workers : int or None, optional + The number of parallel threads to use during cycle spinning. If set to + ``None``, the full set of available cores are used. + func_kw : dict, optional + Additional keyword arguments to supply to ``func``. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + avg_y : np.ndarray + The output of ``func(x, **func_kw)`` averaged over all combinations of + the specified axis shifts. + + Notes + ----- + Cycle spinning was proposed as a way to approach shift-invariance via + performing several circular shifts of a shift-variant transform [1]_. + + For a n-level discrete wavelet transforms, one may wish to perform all + shifts up to ``max_shifts = 2**n - 1``. In practice, much of the benefit + can often be realized with only a small number of shifts per axis. + + For transforms such as the blockwise discrete cosine transform, one may + wish to evaluate shifts up to the block size used by the transform. + + References + ---------- + .. [1] R.R. Coifman and D.L. Donoho. "Translation-Invariant De-Noising". + Wavelets and Statistics, Lecture Notes in Statistics, vol.103. + Springer, New York, 1995, pp.125-150. + :DOI:`10.1007/978-1-4612-2544-7_9` + + Examples + -------- + >>> import skimage.data + >>> from skimage import img_as_float + >>> from skimage.restoration import denoise_tv_chambolle, cycle_spin + >>> img = img_as_float(skimage.data.camera()) + >>> sigma = 0.1 + >>> img = img + sigma * np.random.standard_normal(img.shape) + >>> denoised = cycle_spin(img, func=denoise_tv_chambolle, + ... max_shifts=3) # doctest: +IGNORE_WARNINGS + + """ + if func_kw is None: + func_kw = {} + + x = np.asanyarray(x) + multichannel = channel_axis is not None + all_shifts = _generate_shifts(x.ndim, multichannel, max_shifts, shift_steps) + all_shifts = list(all_shifts) + roll_axes = tuple(range(x.ndim)) + + def _run_one_shift(shift): + # shift, apply function, inverse shift + xs = np.roll(x, shift, axis=roll_axes) + tmp = func(xs, **func_kw) + return np.roll(tmp, tuple(-s for s in shift), axis=roll_axes) + + if not dask_available and (num_workers is None or num_workers > 1): + num_workers = 1 + warn( + 'The optional dask dependency is not installed. ' + 'The number of workers is set to 1. To silence ' + 'this warning, install dask or explicitly set `num_workers=1` ' + 'when calling the `cycle_spin` function' + ) + # compute a running average across the cycle shifts + if num_workers == 1: + # serial processing + mean = _run_one_shift(all_shifts[0]) + for shift in all_shifts[1:]: + mean += _run_one_shift(shift) + mean /= len(all_shifts) + else: + # multithreaded via dask + futures = [dask.delayed(_run_one_shift)(s) for s in all_shifts] + mean = sum(futures) / len(futures) + mean = mean.compute(num_workers=num_workers) + return mean diff --git a/lib/python3.10/site-packages/skimage/restoration/_denoise.py b/lib/python3.10/site-packages/skimage/restoration/_denoise.py new file mode 100644 index 0000000000000000000000000000000000000000..4f4a235ac40c7062047a6fbc01cf824b88174f4d --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/_denoise.py @@ -0,0 +1,1128 @@ +import functools +from math import ceil +import numbers + +import scipy.stats +import numpy as np + +from ..util.dtype import img_as_float +from .._shared import utils +from .._shared.utils import _supported_float_type, warn +from ._denoise_cy import _denoise_bilateral, _denoise_tv_bregman +from .. import color +from ..color.colorconv import ycbcr_from_rgb + + +def _gaussian_weight(array, sigma_squared, *, dtype=float): + """Helping function. Define a Gaussian weighting from array and + sigma_square. + + Parameters + ---------- + array : ndarray + Input array. + sigma_squared : float + The squared standard deviation used in the filter. + dtype : data type object, optional (default : float) + The type and size of the data to be returned. + + Returns + ------- + gaussian : ndarray + The input array filtered by the Gaussian. + """ + return np.exp(-0.5 * (array**2 / sigma_squared), dtype=dtype) + + +def _compute_color_lut(bins, sigma, max_value, *, dtype=float): + """Helping function. Define a lookup table containing Gaussian filter + values using the color distance sigma. + + Parameters + ---------- + bins : int + Number of discrete values for Gaussian weights of color filtering. + A larger value results in improved accuracy. + sigma : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. Note, that the image will be converted using + the `img_as_float` function and thus the standard deviation is in + respect to the range ``[0, 1]``. If the value is ``None`` the standard + deviation of the ``image`` will be used. + max_value : float + Maximum value of the input image. + dtype : data type object, optional (default : float) + The type and size of the data to be returned. + + Returns + ------- + color_lut : ndarray + Lookup table for the color distance sigma. + """ + values = np.linspace(0, max_value, bins, endpoint=False) + return _gaussian_weight(values, sigma**2, dtype=dtype) + + +def _compute_spatial_lut(win_size, sigma, *, dtype=float): + """Helping function. Define a lookup table containing Gaussian filter + values using the spatial sigma. + + Parameters + ---------- + win_size : int + Window size for filtering. + If win_size is not specified, it is calculated as + ``max(5, 2 * ceil(3 * sigma_spatial) + 1)``. + sigma : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + dtype : data type object + The type and size of the data to be returned. + + Returns + ------- + spatial_lut : ndarray + Lookup table for the spatial sigma. + """ + grid_points = np.arange(-win_size // 2, win_size // 2 + 1) + rr, cc = np.meshgrid(grid_points, grid_points, indexing='ij') + distances = np.hypot(rr, cc) + return _gaussian_weight(distances, sigma**2, dtype=dtype).ravel() + + +@utils.channel_as_last_axis() +def denoise_bilateral( + image, + win_size=None, + sigma_color=None, + sigma_spatial=1, + bins=10000, + mode='constant', + cval=0, + *, + channel_axis=None, +): + """Denoise image using bilateral filter. + + Parameters + ---------- + image : ndarray, shape (M, N[, 3]) + Input image, 2D grayscale or RGB. + win_size : int + Window size for filtering. + If win_size is not specified, it is calculated as + ``max(5, 2 * ceil(3 * sigma_spatial) + 1)``. + sigma_color : float + Standard deviation for grayvalue/color distance (radiometric + similarity). A larger value results in averaging of pixels with larger + radiometric differences. If ``None``, the standard deviation of + ``image`` will be used. + sigma_spatial : float + Standard deviation for range distance. A larger value results in + averaging of pixels with larger spatial differences. + bins : int + Number of discrete values for Gaussian weights of color filtering. + A larger value results in improved accuracy. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'} + How to handle values outside the image borders. See + `numpy.pad` for detail. + cval : int or float + Used in conjunction with mode 'constant', the value outside + the image boundaries. + channel_axis : int or None, optional + If ``None``, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + denoised : ndarray + Denoised image. + + Notes + ----- + This is an edge-preserving, denoising filter. It averages pixels based on + their spatial closeness and radiometric similarity [1]_. + + Spatial closeness is measured by the Gaussian function of the Euclidean + distance between two pixels and a certain standard deviation + (`sigma_spatial`). + + Radiometric similarity is measured by the Gaussian function of the + Euclidean distance between two color values and a certain standard + deviation (`sigma_color`). + + Note that, if the image is of any `int` dtype, ``image`` will be + converted using the `img_as_float` function and thus the standard + deviation (`sigma_color`) will be in range ``[0, 1]``. + + For more information on scikit-image's data type conversions and how + images are rescaled in these conversions, + see: https://scikit-image.org/docs/stable/user_guide/data_types.html. + + References + ---------- + .. [1] C. Tomasi and R. Manduchi. "Bilateral Filtering for Gray and Color + Images." IEEE International Conference on Computer Vision (1998) + 839-846. :DOI:`10.1109/ICCV.1998.710815` + + Examples + -------- + >>> from skimage import data, img_as_float + >>> astro = img_as_float(data.astronaut()) + >>> astro = astro[220:300, 220:320] + >>> rng = np.random.default_rng() + >>> noisy = astro + 0.6 * astro.std() * rng.random(astro.shape) + >>> noisy = np.clip(noisy, 0, 1) + >>> denoised = denoise_bilateral(noisy, sigma_color=0.05, sigma_spatial=15, + ... channel_axis=-1) + """ + if channel_axis is not None: + if image.ndim != 3: + if image.ndim == 2: + raise ValueError( + "Use ``channel_axis=None`` for 2D grayscale " + "images. The last axis of the input image " + "must be multiple color channels not another " + "spatial dimension." + ) + else: + raise ValueError( + f'Bilateral filter is only implemented for ' + f'2D grayscale images (image.ndim == 2) and ' + f'2D multichannel (image.ndim == 3) images, ' + f'but the input image has {image.ndim} dimensions.' + ) + elif image.shape[2] not in (3, 4): + if image.shape[2] > 4: + msg = ( + f'The last axis of the input image is ' + f'interpreted as channels. Input image with ' + f'shape {image.shape} has {image.shape[2]} channels ' + f'in last axis. ``denoise_bilateral``is implemented ' + f'for 2D grayscale and color images only.' + ) + warn(msg) + else: + msg = ( + f'Input image must be grayscale, RGB, or RGBA; ' + f'but has shape {image.shape}.' + ) + warn(msg) + else: + if image.ndim > 2: + raise ValueError( + f'Bilateral filter is not implemented for ' + f'grayscale images of 3 or more dimensions, ' + f'but input image has {image.shape} shape. Use ' + f'``channel_axis=-1`` for 2D RGB images.' + ) + + if win_size is None: + win_size = max(5, 2 * int(ceil(3 * sigma_spatial)) + 1) + + min_value = image.min() + max_value = image.max() + + if min_value == max_value: + return image + + # if image.max() is 0, then dist_scale can have an unverified value + # and color_lut[(dist * dist_scale)] may cause a segmentation fault + # so we verify we have a positive image and that the max is not 0.0. + + image = np.atleast_3d(img_as_float(image)) + image = np.ascontiguousarray(image) + + sigma_color = sigma_color or image.std() + + color_lut = _compute_color_lut(bins, sigma_color, max_value, dtype=image.dtype) + + range_lut = _compute_spatial_lut(win_size, sigma_spatial, dtype=image.dtype) + + out = np.empty(image.shape, dtype=image.dtype) + + dims = image.shape[2] + + # There are a number of arrays needed in the Cython function. + # It's easier to allocate them outside of Cython so that all + # arrays are in the same type, then just copy the empty array + # where needed within Cython. + empty_dims = np.empty(dims, dtype=image.dtype) + + if min_value < 0: + image = image - min_value + max_value -= min_value + _denoise_bilateral( + image, + max_value, + win_size, + sigma_color, + sigma_spatial, + bins, + mode, + cval, + color_lut, + range_lut, + empty_dims, + out, + ) + # need to drop the added channels axis for grayscale images + out = np.squeeze(out) + if min_value < 0: + out += min_value + return out + + +@utils.channel_as_last_axis() +def denoise_tv_bregman( + image, weight=5.0, max_num_iter=100, eps=1e-3, isotropic=True, *, channel_axis=None +): + r"""Perform total variation denoising using split-Bregman optimization. + + Given :math:`f`, a noisy image (input data), + total variation denoising (also known as total variation regularization) + aims to find an image :math:`u` with less total variation than :math:`f`, + under the constraint that :math:`u` remain similar to :math:`f`. + This can be expressed by the Rudin--Osher--Fatemi (ROF) minimization + problem: + + .. math:: + + \min_{u} \sum_{i=0}^{N-1} \left( \left| \nabla{u_i} \right| + \frac{\lambda}{2}(f_i - u_i)^2 \right) + + where :math:`\lambda` is a positive parameter. + The first term of this cost function is the total variation; + the second term represents data fidelity. As :math:`\lambda \to 0`, + the total variation term dominates, forcing the solution to have smaller + total variation, at the expense of looking less like the input data. + + This code is an implementation of the split Bregman algorithm of Goldstein + and Osher to solve the ROF problem ([1]_, [2]_, [3]_). + + Parameters + ---------- + image : ndarray + Input image to be denoised (converted using :func:`~.img_as_float`). + weight : float, optional + Denoising weight. It is equal to :math:`\frac{\lambda}{2}`. Therefore, + the smaller the `weight`, the more denoising (at + the expense of less similarity to `image`). + eps : float, optional + Tolerance :math:`\varepsilon > 0` for the stop criterion: + The algorithm stops when :math:`\|u_n - u_{n-1}\|_2 < \varepsilon`. + max_num_iter : int, optional + Maximal number of iterations used for the optimization. + isotropic : boolean, optional + Switch between isotropic and anisotropic TV denoising. + channel_axis : int or None, optional + If ``None``, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + u : ndarray + Denoised image. + + Notes + ----- + Ensure that `channel_axis` parameter is set appropriately for color + images. + + The principle of total variation denoising is explained in [4]_. + It is about minimizing the total variation of an image, + which can be roughly described as + the integral of the norm of the image gradient. Total variation + denoising tends to produce cartoon-like images, that is, + piecewise-constant images. + + See Also + -------- + denoise_tv_chambolle : Perform total variation denoising in nD. + + References + ---------- + .. [1] Tom Goldstein and Stanley Osher, "The Split Bregman Method For L1 + Regularized Problems", + https://ww3.math.ucla.edu/camreport/cam08-29.pdf + .. [2] Pascal Getreuer, "Rudin–Osher–Fatemi Total Variation Denoising + using Split Bregman" in Image Processing On Line on 2012–05–19, + https://www.ipol.im/pub/art/2012/g-tvd/article_lr.pdf + .. [3] https://web.math.ucsb.edu/~cgarcia/UGProjects/BregmanAlgorithms_JacquelineBush.pdf + .. [4] https://en.wikipedia.org/wiki/Total_variation_denoising + + """ + image = np.atleast_3d(img_as_float(image)) + + rows = image.shape[0] + cols = image.shape[1] + dims = image.shape[2] + + shape_ext = (rows + 2, cols + 2, dims) + + out = np.zeros(shape_ext, image.dtype) + + if channel_axis is not None: + channel_out = np.zeros(shape_ext[:2] + (1,), dtype=out.dtype) + for c in range(image.shape[-1]): + # the algorithm below expects 3 dimensions to always be present. + # slicing the array in this fashion preserves the channel dimension + # for us + channel_in = np.ascontiguousarray(image[..., c : c + 1]) + + _denoise_tv_bregman( + channel_in, + image.dtype.type(weight), + max_num_iter, + eps, + isotropic, + channel_out, + ) + + out[..., c] = channel_out[..., 0] + + else: + image = np.ascontiguousarray(image) + + _denoise_tv_bregman( + image, image.dtype.type(weight), max_num_iter, eps, isotropic, out + ) + + return np.squeeze(out[1:-1, 1:-1]) + + +def _denoise_tv_chambolle_nd(image, weight=0.1, eps=2.0e-4, max_num_iter=200): + """Perform total-variation denoising on n-dimensional images. + + Parameters + ---------- + image : ndarray + n-D input data to be denoised. + weight : float, optional + Denoising weight. The greater `weight`, the more denoising (at + the expense of fidelity to `input`). + eps : float, optional + Relative difference of the value of the cost function that determines + the stop criterion. The algorithm stops when: + + (E_(n-1) - E_n) < eps * E_0 + + max_num_iter : int, optional + Maximal number of iterations used for the optimization. + + Returns + ------- + out : ndarray + Denoised array of floats. + + Notes + ----- + Rudin, Osher and Fatemi algorithm. + """ + + ndim = image.ndim + p = np.zeros((image.ndim,) + image.shape, dtype=image.dtype) + g = np.zeros_like(p) + d = np.zeros_like(image) + i = 0 + while i < max_num_iter: + if i > 0: + # d will be the (negative) divergence of p + d = -p.sum(0) + slices_d = [ + slice(None), + ] * ndim + slices_p = [ + slice(None), + ] * (ndim + 1) + for ax in range(ndim): + slices_d[ax] = slice(1, None) + slices_p[ax + 1] = slice(0, -1) + slices_p[0] = ax + d[tuple(slices_d)] += p[tuple(slices_p)] + slices_d[ax] = slice(None) + slices_p[ax + 1] = slice(None) + out = image + d + else: + out = image + E = (d**2).sum() + + # g stores the gradients of out along each axis + # e.g. g[0] is the first order finite difference along axis 0 + slices_g = [ + slice(None), + ] * (ndim + 1) + for ax in range(ndim): + slices_g[ax + 1] = slice(0, -1) + slices_g[0] = ax + g[tuple(slices_g)] = np.diff(out, axis=ax) + slices_g[ax + 1] = slice(None) + + norm = np.sqrt((g**2).sum(axis=0))[np.newaxis, ...] + E += weight * norm.sum() + tau = 1.0 / (2.0 * ndim) + norm *= tau / weight + norm += 1.0 + p -= tau * g + p /= norm + E /= float(image.size) + if i == 0: + E_init = E + E_previous = E + else: + if np.abs(E_previous - E) < eps * E_init: + break + else: + E_previous = E + i += 1 + return out + + +def denoise_tv_chambolle( + image, weight=0.1, eps=2.0e-4, max_num_iter=200, *, channel_axis=None +): + r"""Perform total variation denoising in nD. + + Given :math:`f`, a noisy image (input data), + total variation denoising (also known as total variation regularization) + aims to find an image :math:`u` with less total variation than :math:`f`, + under the constraint that :math:`u` remain similar to :math:`f`. + This can be expressed by the Rudin--Osher--Fatemi (ROF) minimization + problem: + + .. math:: + + \min_{u} \sum_{i=0}^{N-1} \left( \left| \nabla{u_i} \right| + \frac{\lambda}{2}(f_i - u_i)^2 \right) + + where :math:`\lambda` is a positive parameter. + The first term of this cost function is the total variation; + the second term represents data fidelity. As :math:`\lambda \to 0`, + the total variation term dominates, forcing the solution to have smaller + total variation, at the expense of looking less like the input data. + + This code is an implementation of the algorithm proposed by Chambolle + in [1]_ to solve the ROF problem. + + Parameters + ---------- + image : ndarray + Input image to be denoised. If its dtype is not float, it gets + converted with :func:`~.img_as_float`. + weight : float, optional + Denoising weight. It is equal to :math:`\frac{1}{\lambda}`. Therefore, + the greater the `weight`, the more denoising (at the expense of + fidelity to `image`). + eps : float, optional + Tolerance :math:`\varepsilon > 0` for the stop criterion (compares to + absolute value of relative difference of the cost function :math:`E`): + The algorithm stops when :math:`|E_{n-1} - E_n| < \varepsilon * E_0`. + max_num_iter : int, optional + Maximal number of iterations used for the optimization. + channel_axis : int or None, optional + If ``None``, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + u : ndarray + Denoised image. + + Notes + ----- + Make sure to set the `channel_axis` parameter appropriately for color + images. + + The principle of total variation denoising is explained in [2]_. + It is about minimizing the total variation of an image, + which can be roughly described as + the integral of the norm of the image gradient. Total variation + denoising tends to produce cartoon-like images, that is, + piecewise-constant images. + + See Also + -------- + denoise_tv_bregman : Perform total variation denoising using split-Bregman + optimization. + + References + ---------- + .. [1] A. Chambolle, An algorithm for total variation minimization and + applications, Journal of Mathematical Imaging and Vision, + Springer, 2004, 20, 89-97. + .. [2] https://en.wikipedia.org/wiki/Total_variation_denoising + + Examples + -------- + 2D example on astronaut image: + + >>> from skimage import color, data + >>> img = color.rgb2gray(data.astronaut())[:50, :50] + >>> rng = np.random.default_rng() + >>> img += 0.5 * img.std() * rng.standard_normal(img.shape) + >>> denoised_img = denoise_tv_chambolle(img, weight=60) + + 3D example on synthetic data: + + >>> x, y, z = np.ogrid[0:20, 0:20, 0:20] + >>> mask = (x - 22)**2 + (y - 20)**2 + (z - 17)**2 < 8**2 + >>> mask = mask.astype(float) + >>> rng = np.random.default_rng() + >>> mask += 0.2 * rng.standard_normal(mask.shape) + >>> res = denoise_tv_chambolle(mask, weight=100) + + """ + + im_type = image.dtype + if not im_type.kind == 'f': + image = img_as_float(image) + + # enforce float16->float32 and float128->float64 + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + if channel_axis is not None: + channel_axis = channel_axis % image.ndim + _at = functools.partial(utils.slice_at_axis, axis=channel_axis) + out = np.zeros_like(image) + for c in range(image.shape[channel_axis]): + out[_at(c)] = _denoise_tv_chambolle_nd( + image[_at(c)], weight, eps, max_num_iter + ) + else: + out = _denoise_tv_chambolle_nd(image, weight, eps, max_num_iter) + return out + + +def _bayes_thresh(details, var): + """BayesShrink threshold for a zero-mean details coeff array.""" + # Equivalent to: dvar = np.var(details) for 0-mean details array + dvar = np.mean(details * details) + eps = np.finfo(details.dtype).eps + thresh = var / np.sqrt(max(dvar - var, eps)) + return thresh + + +def _universal_thresh(img, sigma): + """Universal threshold used by the VisuShrink method""" + return sigma * np.sqrt(2 * np.log(img.size)) + + +def _sigma_est_dwt(detail_coeffs, distribution='Gaussian'): + """Calculate the robust median estimator of the noise standard deviation. + + Parameters + ---------- + detail_coeffs : ndarray + The detail coefficients corresponding to the discrete wavelet + transform of an image. + distribution : str + The underlying noise distribution. + + Returns + ------- + sigma : float + The estimated noise standard deviation (see section 4.2 of [1]_). + + References + ---------- + .. [1] D. L. Donoho and I. M. Johnstone. "Ideal spatial adaptation + by wavelet shrinkage." Biometrika 81.3 (1994): 425-455. + :DOI:`10.1093/biomet/81.3.425` + """ + # Consider regions with detail coefficients exactly zero to be masked out + detail_coeffs = detail_coeffs[np.nonzero(detail_coeffs)] + + if distribution.lower() == 'gaussian': + # 75th quantile of the underlying, symmetric noise distribution + denom = scipy.stats.norm.ppf(0.75) + sigma = np.median(np.abs(detail_coeffs)) / denom + else: + raise ValueError("Only Gaussian noise estimation is currently " "supported") + return sigma + + +def _wavelet_threshold( + image, + wavelet, + method=None, + threshold=None, + sigma=None, + mode='soft', + wavelet_levels=None, +): + """Perform wavelet thresholding. + + Parameters + ---------- + image : ndarray (2d or 3d) of ints, uints or floats + Input data to be denoised. `image` can be of any numeric type, + but it is cast into an ndarray of floats for the computation + of the denoised image. + wavelet : string + The type of wavelet to perform. Can be any of the options + pywt.wavelist outputs. For example, this may be any of ``{db1, db2, + db3, db4, haar}``. + method : {'BayesShrink', 'VisuShrink'}, optional + Thresholding method to be used. The currently supported methods are + "BayesShrink" [1]_ and "VisuShrink" [2]_. If it is set to None, a + user-specified ``threshold`` must be supplied instead. + threshold : float, optional + The thresholding value to apply during wavelet coefficient + thresholding. The default value (None) uses the selected ``method`` to + estimate appropriate threshold(s) for noise removal. + sigma : float, optional + The standard deviation of the noise. The noise is estimated when sigma + is None (the default) by the method in [2]_. + mode : {'soft', 'hard'}, optional + An optional argument to choose the type of denoising performed. It + noted that choosing soft thresholding given additive noise finds the + best approximation of the original image. + wavelet_levels : int or None, optional + The number of wavelet decomposition levels to use. The default is + three less than the maximum number of possible decomposition levels + (see Notes below). + + Returns + ------- + out : ndarray + Denoised image. + + References + ---------- + .. [1] Chang, S. Grace, Bin Yu, and Martin Vetterli. "Adaptive wavelet + thresholding for image denoising and compression." Image Processing, + IEEE Transactions on 9.9 (2000): 1532-1546. + :DOI:`10.1109/83.862633` + .. [2] D. L. Donoho and I. M. Johnstone. "Ideal spatial adaptation + by wavelet shrinkage." Biometrika 81.3 (1994): 425-455. + :DOI:`10.1093/biomet/81.3.425` + """ + try: + import pywt + except ImportError: + raise ImportError( + 'PyWavelets is not installed. Please ensure it is installed in ' + 'order to use this function.' + ) + + wavelet = pywt.Wavelet(wavelet) + if not wavelet.orthogonal: + warn( + f'Wavelet thresholding was designed for ' + f'use with orthogonal wavelets. For nonorthogonal ' + f'wavelets such as {wavelet.name},results are ' + f'likely to be suboptimal.' + ) + + # original_extent is used to workaround PyWavelets issue #80 + # odd-sized input results in an image with 1 extra sample after waverecn + original_extent = tuple(slice(s) for s in image.shape) + + # Determine the number of wavelet decomposition levels + if wavelet_levels is None: + # Determine the maximum number of possible levels for image + wavelet_levels = pywt.dwtn_max_level(image.shape, wavelet) + + # Skip coarsest wavelet scales (see Notes in docstring). + wavelet_levels = max(wavelet_levels - 3, 1) + + coeffs = pywt.wavedecn(image, wavelet=wavelet, level=wavelet_levels) + # Detail coefficients at each decomposition level + dcoeffs = coeffs[1:] + + if sigma is None: + # Estimate the noise via the method in [2]_ + detail_coeffs = dcoeffs[-1]['d' * image.ndim] + sigma = _sigma_est_dwt(detail_coeffs, distribution='Gaussian') + + if method is not None and threshold is not None: + warn( + f'Thresholding method {method} selected. The ' + f'user-specified threshold will be ignored.' + ) + + if threshold is None: + var = sigma**2 + if method is None: + raise ValueError("If method is None, a threshold must be provided.") + elif method == "BayesShrink": + # The BayesShrink thresholds from [1]_ in docstring + threshold = [ + {key: _bayes_thresh(level[key], var) for key in level} + for level in dcoeffs + ] + elif method == "VisuShrink": + # The VisuShrink thresholds from [2]_ in docstring + threshold = _universal_thresh(image, sigma) + else: + raise ValueError(f'Unrecognized method: {method}') + + if np.isscalar(threshold): + # A single threshold for all coefficient arrays + denoised_detail = [ + { + key: pywt.threshold(level[key], value=threshold, mode=mode) + for key in level + } + for level in dcoeffs + ] + else: + # Dict of unique threshold coefficients for each detail coeff. array + denoised_detail = [ + { + key: pywt.threshold(level[key], value=thresh[key], mode=mode) + for key in level + } + for thresh, level in zip(threshold, dcoeffs) + ] + denoised_coeffs = [coeffs[0]] + denoised_detail + out = pywt.waverecn(denoised_coeffs, wavelet)[original_extent] + out = out.astype(image.dtype) + return out + + +def _scale_sigma_and_image_consistently(image, sigma, multichannel, rescale_sigma): + """If the ``image`` is rescaled, also rescale ``sigma`` consistently. + + Images that are not floating point will be rescaled via ``img_as_float``. + Half-precision images will be promoted to single precision. + """ + if multichannel: + if isinstance(sigma, numbers.Number) or sigma is None: + sigma = [sigma] * image.shape[-1] + elif len(sigma) != image.shape[-1]: + raise ValueError( + "When channel_axis is not None, sigma must be a scalar or have " + "length equal to the number of channels" + ) + if image.dtype.kind != 'f': + if rescale_sigma: + range_pre = image.max() - image.min() + image = img_as_float(image) + if rescale_sigma: + range_post = image.max() - image.min() + # apply the same magnitude scaling to sigma + scale_factor = range_post / range_pre + if multichannel: + sigma = [s * scale_factor if s is not None else s for s in sigma] + elif sigma is not None: + sigma *= scale_factor + elif image.dtype == np.float16: + image = image.astype(np.float32) + return image, sigma + + +def _rescale_sigma_rgb2ycbcr(sigmas): + """Convert user-provided noise standard deviations to YCbCr space. + + Notes + ----- + If R, G, B are linearly independent random variables and a1, a2, a3 are + scalars, then random variable C: + C = a1 * R + a2 * G + a3 * B + has variance, var_C, given by: + var_C = a1**2 * var_R + a2**2 * var_G + a3**2 * var_B + """ + if sigmas[0] is None: + return sigmas + sigmas = np.asarray(sigmas) + rgv_variances = sigmas * sigmas + for i in range(3): + scalars = ycbcr_from_rgb[i, :] + var_channel = np.sum(scalars * scalars * rgv_variances) + sigmas[i] = np.sqrt(var_channel) + return sigmas + + +@utils.channel_as_last_axis() +def denoise_wavelet( + image, + sigma=None, + wavelet='db1', + mode='soft', + wavelet_levels=None, + convert2ycbcr=False, + method='BayesShrink', + rescale_sigma=True, + *, + channel_axis=None, +): + """Perform wavelet denoising on an image. + + Parameters + ---------- + image : ndarray (M[, N[, ...P]][, C]) of ints, uints or floats + Input data to be denoised. `image` can be of any numeric type, + but it is cast into an ndarray of floats for the computation + of the denoised image. + sigma : float or list, optional + The noise standard deviation used when computing the wavelet detail + coefficient threshold(s). When None (default), the noise standard + deviation is estimated via the method in [2]_. + wavelet : string, optional + The type of wavelet to perform and can be any of the options + ``pywt.wavelist`` outputs. The default is `'db1'`. For example, + ``wavelet`` can be any of ``{'db2', 'haar', 'sym9'}`` and many more. + mode : {'soft', 'hard'}, optional + An optional argument to choose the type of denoising performed. It + noted that choosing soft thresholding given additive noise finds the + best approximation of the original image. + wavelet_levels : int or None, optional + The number of wavelet decomposition levels to use. The default is + three less than the maximum number of possible decomposition levels. + convert2ycbcr : bool, optional + If True and channel_axis is set, do the wavelet denoising in the YCbCr + colorspace instead of the RGB color space. This typically results in + better performance for RGB images. + method : {'BayesShrink', 'VisuShrink'}, optional + Thresholding method to be used. The currently supported methods are + "BayesShrink" [1]_ and "VisuShrink" [2]_. Defaults to "BayesShrink". + rescale_sigma : bool, optional + If False, no rescaling of the user-provided ``sigma`` will be + performed. The default of ``True`` rescales sigma appropriately if the + image is rescaled internally. + + .. versionadded:: 0.16 + ``rescale_sigma`` was introduced in 0.16 + channel_axis : int or None, optional + If ``None``, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : ndarray + Denoised image. + + Notes + ----- + The wavelet domain is a sparse representation of the image, and can be + thought of similarly to the frequency domain of the Fourier transform. + Sparse representations have most values zero or near-zero and truly random + noise is (usually) represented by many small values in the wavelet domain. + Setting all values below some threshold to 0 reduces the noise in the + image, but larger thresholds also decrease the detail present in the image. + + If the input is 3D, this function performs wavelet denoising on each color + plane separately. + + .. versionchanged:: 0.16 + For floating point inputs, the original input range is maintained and + there is no clipping applied to the output. Other input types will be + converted to a floating point value in the range [-1, 1] or [0, 1] + depending on the input image range. Unless ``rescale_sigma = False``, + any internal rescaling applied to the ``image`` will also be applied + to ``sigma`` to maintain the same relative amplitude. + + Many wavelet coefficient thresholding approaches have been proposed. By + default, ``denoise_wavelet`` applies BayesShrink, which is an adaptive + thresholding method that computes separate thresholds for each wavelet + sub-band as described in [1]_. + + If ``method == "VisuShrink"``, a single "universal threshold" is applied to + all wavelet detail coefficients as described in [2]_. This threshold + is designed to remove all Gaussian noise at a given ``sigma`` with high + probability, but tends to produce images that appear overly smooth. + + Although any of the wavelets from ``PyWavelets`` can be selected, the + thresholding methods assume an orthogonal wavelet transform and may not + choose the threshold appropriately for biorthogonal wavelets. Orthogonal + wavelets are desirable because white noise in the input remains white noise + in the subbands. Biorthogonal wavelets lead to colored noise in the + subbands. Additionally, the orthogonal wavelets in PyWavelets are + orthonormal so that noise variance in the subbands remains identical to the + noise variance of the input. Example orthogonal wavelets are the Daubechies + (e.g. 'db2') or symmlet (e.g. 'sym2') families. + + References + ---------- + .. [1] Chang, S. Grace, Bin Yu, and Martin Vetterli. "Adaptive wavelet + thresholding for image denoising and compression." Image Processing, + IEEE Transactions on 9.9 (2000): 1532-1546. + :DOI:`10.1109/83.862633` + .. [2] D. L. Donoho and I. M. Johnstone. "Ideal spatial adaptation + by wavelet shrinkage." Biometrika 81.3 (1994): 425-455. + :DOI:`10.1093/biomet/81.3.425` + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('pywt') + + >>> from skimage import color, data + >>> img = img_as_float(data.astronaut()) + >>> img = color.rgb2gray(img) + >>> rng = np.random.default_rng() + >>> img += 0.1 * rng.standard_normal(img.shape) + >>> img = np.clip(img, 0, 1) + >>> denoised_img = denoise_wavelet(img, sigma=0.1, rescale_sigma=True) + + """ + multichannel = channel_axis is not None + if method not in ["BayesShrink", "VisuShrink"]: + raise ValueError( + f'Invalid method: {method}. The currently supported ' + f'methods are "BayesShrink" and "VisuShrink".' + ) + + # floating-point inputs are not rescaled, so don't clip their output. + clip_output = image.dtype.kind != 'f' + + if convert2ycbcr and not multichannel: + raise ValueError("convert2ycbcr requires channel_axis to be set") + + image, sigma = _scale_sigma_and_image_consistently( + image, sigma, multichannel, rescale_sigma + ) + if multichannel: + if convert2ycbcr: + out = color.rgb2ycbcr(image) + # convert user-supplied sigmas to the new colorspace as well + if rescale_sigma: + sigma = _rescale_sigma_rgb2ycbcr(sigma) + for i in range(3): + # renormalizing this color channel to live in [0, 1] + _min, _max = out[..., i].min(), out[..., i].max() + scale_factor = _max - _min + if scale_factor == 0: + # skip any channel containing only zeros! + continue + channel = out[..., i] - _min + channel /= scale_factor + sigma_channel = sigma[i] + if sigma_channel is not None: + sigma_channel /= scale_factor + out[..., i] = denoise_wavelet( + channel, + wavelet=wavelet, + method=method, + sigma=sigma_channel, + mode=mode, + wavelet_levels=wavelet_levels, + rescale_sigma=rescale_sigma, + ) + out[..., i] = out[..., i] * scale_factor + out[..., i] += _min + out = color.ycbcr2rgb(out) + else: + out = np.empty_like(image) + for c in range(image.shape[-1]): + out[..., c] = _wavelet_threshold( + image[..., c], + wavelet=wavelet, + method=method, + sigma=sigma[c], + mode=mode, + wavelet_levels=wavelet_levels, + ) + else: + out = _wavelet_threshold( + image, + wavelet=wavelet, + method=method, + sigma=sigma, + mode=mode, + wavelet_levels=wavelet_levels, + ) + + if clip_output: + clip_range = (-1, 1) if image.min() < 0 else (0, 1) + out = np.clip(out, *clip_range, out=out) + return out + + +def estimate_sigma(image, average_sigmas=False, *, channel_axis=None): + """ + Robust wavelet-based estimator of the (Gaussian) noise standard deviation. + + Parameters + ---------- + image : ndarray + Image for which to estimate the noise standard deviation. + average_sigmas : bool, optional + If true, average the channel estimates of `sigma`. Otherwise return + a list of sigmas corresponding to each channel. + channel_axis : int or None, optional + If ``None``, the image is assumed to be grayscale (single-channel). + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + sigma : float or list + Estimated noise standard deviation(s). If `multichannel` is True and + `average_sigmas` is False, a separate noise estimate for each channel + is returned. Otherwise, the average of the individual channel + estimates is returned. + + Notes + ----- + This function assumes the noise follows a Gaussian distribution. The + estimation algorithm is based on the median absolute deviation of the + wavelet detail coefficients as described in section 4.2 of [1]_. + + References + ---------- + .. [1] D. L. Donoho and I. M. Johnstone. "Ideal spatial adaptation + by wavelet shrinkage." Biometrika 81.3 (1994): 425-455. + :DOI:`10.1093/biomet/81.3.425` + + Examples + -------- + .. testsetup:: + >>> import pytest; _ = pytest.importorskip('pywt') + + >>> import skimage.data + >>> from skimage import img_as_float + >>> img = img_as_float(skimage.data.camera()) + >>> sigma = 0.1 + >>> rng = np.random.default_rng() + >>> img = img + sigma * rng.standard_normal(img.shape) + >>> sigma_hat = estimate_sigma(img, channel_axis=None) + """ + try: + import pywt + except ImportError: + raise ImportError( + 'PyWavelets is not installed. Please ensure it is installed in ' + 'order to use this function.' + ) + + if channel_axis is not None: + channel_axis = channel_axis % image.ndim + _at = functools.partial(utils.slice_at_axis, axis=channel_axis) + nchannels = image.shape[channel_axis] + sigmas = [ + estimate_sigma(image[_at(c)], channel_axis=None) for c in range(nchannels) + ] + if average_sigmas: + sigmas = np.mean(sigmas) + return sigmas + elif image.shape[-1] <= 4: + msg = ( + f'image is size {image.shape[-1]} on the last axis, ' + f'but channel_axis is None. If this is a color image, ' + f'please set channel_axis=-1 for proper noise estimation.' + ) + warn(msg) + coeffs = pywt.dwtn(image, wavelet='db2') + detail_coeffs = coeffs['d' * image.ndim] + return _sigma_est_dwt(detail_coeffs, distribution='Gaussian') diff --git a/lib/python3.10/site-packages/skimage/restoration/_rolling_ball.py b/lib/python3.10/site-packages/skimage/restoration/_rolling_ball.py new file mode 100644 index 0000000000000000000000000000000000000000..709f8bd5e22d5fd83fb9dd7f7e7972d2ceb440cd --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/_rolling_ball.py @@ -0,0 +1,198 @@ +import numpy as np + +from .._shared.utils import _supported_float_type +from ._rolling_ball_cy import apply_kernel, apply_kernel_nan + + +def rolling_ball(image, *, radius=100, kernel=None, nansafe=False, num_threads=None): + """Estimate background intensity using the rolling-ball algorithm. + + This function is a generalization of the rolling-ball algorithm [1]_ to + estimate the background intensity of an n-dimensional image. This is + typically useful for background subtraction in case of uneven exposure. + Think of the image as a landscape (where altitude is determined by + intensity), under which a ball of given radius is rolled. At each + position, the ball's apex gives the resulting background intensity. + + Parameters + ---------- + image : ndarray + The image to be filtered. + radius : int, optional + Radius of the ball-shaped kernel to be rolled under the + image landscape. Used only if `kernel` is ``None``. + kernel : ndarray, optional + An alternative way to specify the rolling ball, as an arbitrary + kernel. It must have the same number of axes as `image`. + nansafe: bool, optional + If ``False`` (default), the function assumes that none of the values + in `image` are ``np.nan``, and uses a faster implementation. + num_threads: int, optional + The maximum number of threads to use. If ``None``, the function uses + the OpenMP default value; typically, it is equal to the maximum number + of virtual cores. + Note: This is an upper limit to the number of threads. The exact number + is determined by the system's OpenMP library. + + Returns + ------- + background : ndarray + The estimated background of the image. + + Notes + ----- + This implementation assumes that dark pixels correspond to the background. If + you have a bright background, invert the image before passing it to this + function, e.g., using :func:`skimage.util.invert`. + + For this method to give meaningful results, the radius of the ball (or + typical size of the kernel, in the general case) should be larger than the + typical size of the image features of interest. + + This algorithm is sensitive to noise (in particular salt-and-pepper + noise). If this is a problem in your image, you can apply mild + Gaussian smoothing before passing the image to this function. + + This algorithm's complexity is polynomial in the radius, with degree equal + to the image dimensionality (a 2D image is N^2, a 3D image is N^3, etc.), + so it can take a long time as the radius grows beyond 30 or so ([2]_, [3]_). + It is an exact N-dimensional calculation; if all you need is an + approximation, faster options to consider are top-hat filtering [4]_ or + downscaling-then-upscaling to reduce the size of the input processed. + + References + ---------- + .. [1] Sternberg, Stanley R. "Biomedical image processing." Computer 1 + (1983): 22-34. :DOI:`10.1109/MC.1983.1654163` + .. [2] https://github.com/scikit-image/scikit-image/issues/5193 + .. [3] https://github.com/scikit-image/scikit-image/issues/7423 + .. [4] https://forum.image.sc/t/59267/7 + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + >>> image = ski.data.coins() + >>> background = ski.restoration.rolling_ball(image) + >>> filtered_image = image - background + + >>> import numpy as np + >>> import skimage as ski + >>> image = ski.data.coins() + >>> kernel = ski.restoration.ellipsoid_kernel((101, 101), 75) + >>> background = ski.restoration.rolling_ball(image, kernel=kernel) + >>> filtered_image = image - background + """ + + image = np.asarray(image) + float_type = _supported_float_type(image.dtype) + img = image.astype(float_type, copy=False) + + if num_threads is None: + num_threads = 0 + + if kernel is None: + kernel = ball_kernel(radius, image.ndim) + + kernel = kernel.astype(float_type) + kernel_shape = np.asarray(kernel.shape) + kernel_center = kernel_shape // 2 + center_intensity = kernel[tuple(kernel_center)] + + intensity_difference = center_intensity - kernel + intensity_difference[kernel == np.inf] = np.inf + intensity_difference = intensity_difference.astype(img.dtype) + intensity_difference = intensity_difference.reshape(-1) + + img = np.pad( + img, kernel_center[:, np.newaxis], constant_values=np.inf, mode="constant" + ) + + func = apply_kernel_nan if nansafe else apply_kernel + background = func( + img.reshape(-1), + intensity_difference, + np.zeros_like(image, dtype=img.dtype).reshape(-1), + np.array(image.shape, dtype=np.intp), + np.array(img.shape, dtype=np.intp), + kernel_shape.astype(np.intp), + num_threads, + ) + + background = background.astype(image.dtype, copy=False) + + return background + + +def ball_kernel(radius, ndim): + """Create a ball kernel for restoration.rolling_ball. + + Parameters + ---------- + radius : int + Radius of the ball. + ndim : int + Number of dimensions of the ball. ``ndim`` should match the + dimensionality of the image the kernel will be applied to. + + Returns + ------- + kernel : ndarray + The kernel containing the surface intensity of the top half + of the ellipsoid. + + See Also + -------- + rolling_ball + """ + + kernel_coords = np.stack( + np.meshgrid( + *[np.arange(-x, x + 1) for x in [np.ceil(radius)] * ndim], indexing='ij' + ), + axis=-1, + ) + + sum_of_squares = np.sum(kernel_coords**2, axis=-1) + distance_from_center = np.sqrt(sum_of_squares) + kernel = np.sqrt(np.clip(radius**2 - sum_of_squares, 0, None)) + kernel[distance_from_center > radius] = np.inf + + return kernel + + +def ellipsoid_kernel(shape, intensity): + """Create an ellipoid kernel for restoration.rolling_ball. + + Parameters + ---------- + shape : array-like + Length of the principal axis of the ellipsoid (excluding + the intensity axis). The kernel needs to have the same + dimensionality as the image it will be applied to. + intensity : int + Length of the intensity axis of the ellipsoid. + + Returns + ------- + kernel : ndarray + The kernel containing the surface intensity of the top half + of the ellipsoid. + + See Also + -------- + rolling_ball + """ + + shape = np.asarray(shape) + semi_axis = np.clip(shape // 2, 1, None) + + kernel_coords = np.stack( + np.meshgrid(*[np.arange(-x, x + 1) for x in semi_axis], indexing='ij'), axis=-1 + ) + + intensity_scaling = 1 - np.sum((kernel_coords / semi_axis) ** 2, axis=-1) + kernel = intensity * np.sqrt(np.clip(intensity_scaling, 0, None)) + kernel[intensity_scaling < 0] = np.inf + + return kernel diff --git a/lib/python3.10/site-packages/skimage/restoration/deconvolution.py b/lib/python3.10/site-packages/skimage/restoration/deconvolution.py new file mode 100644 index 0000000000000000000000000000000000000000..a03b9209e00df06f44c99121e4c74f64f3dcd8b8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/deconvolution.py @@ -0,0 +1,424 @@ +"""Implementations restoration functions""" + +import numpy as np +from scipy.signal import convolve + +from .._shared.utils import _supported_float_type +from . import uft + + +def wiener(image, psf, balance, reg=None, is_real=True, clip=True): + r"""Wiener-Hunt deconvolution + + Return the deconvolution with a Wiener-Hunt approach (i.e. with + Fourier diagonalisation). + + Parameters + ---------- + image : ndarray + Input degraded image (can be n-dimensional). + psf : ndarray + Point Spread Function. This is assumed to be the impulse + response (input image space) if the data-type is real, or the + transfer function (Fourier space) if the data-type is + complex. There is no constraints on the shape of the impulse + response. The transfer function must be of shape + `(N1, N2, ..., ND)` if `is_real is True`, + `(N1, N2, ..., ND // 2 + 1)` otherwise (see `np.fft.rfftn`). + balance : float + The regularisation parameter value that tunes the balance + between the data adequacy that improve frequency restoration + and the prior adequacy that reduce frequency restoration (to + avoid noise artifacts). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the + psf. Shape constraint is the same as for the `psf` parameter. + is_real : boolean, optional + True by default. Specify if ``psf`` and ``reg`` are provided + with hermitian hypothesis, that is only half of the frequency + plane is provided (due to the redundancy of Fourier transform + of real signal). It's apply only if ``psf`` and/or ``reg`` are + provided as transfer function. For the hermitian property see + ``uft`` module or ``np.fft.rfftn``. + clip : boolean, optional + True by default. If True, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. + + Returns + ------- + im_deconv : (M, N) ndarray + The deconvolved image. + + Examples + -------- + >>> from skimage import color, data, restoration + >>> img = color.rgb2gray(data.astronaut()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> img = convolve2d(img, psf, 'same') + >>> rng = np.random.default_rng() + >>> img += 0.1 * img.std() * rng.standard_normal(img.shape) + >>> deconvolved_img = restoration.wiener(img, psf, 0.1) + + Notes + ----- + This function applies the Wiener filter to a noisy and degraded + image by an impulse response (or PSF). If the data model is + + .. math:: y = Hx + n + + where :math:`n` is noise, :math:`H` the PSF and :math:`x` the + unknown original image, the Wiener filter is + + .. math:: + \hat x = F^\dagger \left( |\Lambda_H|^2 + \lambda |\Lambda_D|^2 \right)^{-1} + \Lambda_H^\dagger F y + + where :math:`F` and :math:`F^\dagger` are the Fourier and inverse + Fourier transforms respectively, :math:`\Lambda_H` the transfer + function (or the Fourier transform of the PSF, see [Hunt] below) + and :math:`\Lambda_D` the filter to penalize the restored image + frequencies (Laplacian by default, that is penalization of high + frequency). The parameter :math:`\lambda` tunes the balance + between the data (that tends to increase high frequency, even + those coming from noise), and the regularization. + + These methods are then specific to a prior model. Consequently, + the application or the true image nature must correspond to the + prior model. By default, the prior model (Laplacian) introduce + image smoothness or pixel correlation. It can also be interpreted + as high-frequency penalization to compensate the instability of + the solution with respect to the data (sometimes called noise + amplification or "explosive" solution). + + Finally, the use of Fourier space implies a circulant property of + :math:`H`, see [2]_. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + https://www.osapublishing.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + https://hal.archives-ouvertes.fr/hal-00674508 + + .. [2] B. R. Hunt "A matrix theory proof of the discrete + convolution theorem", IEEE Trans. on Audio and + Electroacoustics, vol. au-19, no. 4, pp. 285-288, dec. 1971 + """ + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + float_type = _supported_float_type(image.dtype) + image = image.astype(float_type, copy=False) + psf = psf.real.astype(float_type, copy=False) + reg = reg.real.astype(float_type, copy=False) + + if psf.shape != reg.shape: + trans_func = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_func = psf + + wiener_filter = np.conj(trans_func) / ( + np.abs(trans_func) ** 2 + balance * np.abs(reg) ** 2 + ) + if is_real: + deconv = uft.uirfftn(wiener_filter * uft.urfftn(image), shape=image.shape) + else: + deconv = uft.uifftn(wiener_filter * uft.ufftn(image)) + + if clip: + deconv[deconv > 1] = 1 + deconv[deconv < -1] = -1 + + return deconv + + +def unsupervised_wiener( + image, psf, reg=None, user_params=None, is_real=True, clip=True, *, rng=None +): + """Unsupervised Wiener-Hunt deconvolution. + + Return the deconvolution with a Wiener-Hunt approach, where the + hyperparameters are automatically estimated. The algorithm is a + stochastic iterative process (Gibbs sampler) described in the + reference below. See also ``wiener`` function. + + Parameters + ---------- + image : (M, N) ndarray + The input degraded image. + psf : ndarray + The impulse response (input image's space) or the transfer + function (Fourier space). Both are accepted. The transfer + function is automatically recognized as being complex + (``np.iscomplexobj(psf)``). + reg : ndarray, optional + The regularisation operator. The Laplacian by default. It can + be an impulse response or a transfer function, as for the psf. + user_params : dict, optional + Dictionary of parameters for the Gibbs sampler. See below. + clip : boolean, optional + True by default. If true, pixel values of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + .. versionadded:: 0.19 + + Returns + ------- + x_postmean : (M, N) ndarray + The deconvolved image (the posterior mean). + chains : dict + The keys ``noise`` and ``prior`` contain the chain list of + noise and prior precision respectively. + + Other parameters + ---------------- + The keys of ``user_params`` are: + + threshold : float + The stopping criterion: the norm of the difference between to + successive approximated solution (empirical mean of object + samples, see Notes section). 1e-4 by default. + burnin : int + The number of sample to ignore to start computation of the + mean. 15 by default. + min_num_iter : int + The minimum number of iterations. 30 by default. + max_num_iter : int + The maximum number of iterations if ``threshold`` is not + satisfied. 200 by default. + callback : callable (None by default) + A user provided callable to which is passed, if the function + exists, the current image sample for whatever purpose. The user + can store the sample, or compute other moments than the + mean. It has no influence on the algorithm execution and is + only for inspection. + + Examples + -------- + >>> from skimage import color, data, restoration + >>> img = color.rgb2gray(data.astronaut()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> img = convolve2d(img, psf, 'same') + >>> rng = np.random.default_rng() + >>> img += 0.1 * img.std() * rng.standard_normal(img.shape) + >>> deconvolved_img = restoration.unsupervised_wiener(img, psf) + + Notes + ----- + The estimated image is design as the posterior mean of a + probability law (from a Bayesian analysis). The mean is defined as + a sum over all the possible images weighted by their respective + probability. Given the size of the problem, the exact sum is not + tractable. This algorithm use of MCMC to draw image under the + posterior law. The practical idea is to only draw highly probable + images since they have the biggest contribution to the mean. At the + opposite, the less probable images are drawn less often since + their contribution is low. Finally, the empirical mean of these + samples give us an estimation of the mean, and an exact + computation with an infinite sample set. + + References + ---------- + .. [1] François Orieux, Jean-François Giovannelli, and Thomas + Rodet, "Bayesian estimation of regularization and point + spread function parameters for Wiener-Hunt deconvolution", + J. Opt. Soc. Am. A 27, 1593-1607 (2010) + + https://www.osapublishing.org/josaa/abstract.cfm?URI=josaa-27-7-1593 + + https://hal.archives-ouvertes.fr/hal-00674508 + """ + params = { + 'threshold': 1e-4, + 'max_num_iter': 200, + 'min_num_iter': 30, + 'burnin': 15, + 'callback': None, + } + params.update(user_params or {}) + + if reg is None: + reg, _ = uft.laplacian(image.ndim, image.shape, is_real=is_real) + if not np.iscomplexobj(reg): + reg = uft.ir2tf(reg, image.shape, is_real=is_real) + float_type = _supported_float_type(image.dtype) + image = image.astype(float_type, copy=False) + psf = psf.real.astype(float_type, copy=False) + reg = reg.real.astype(float_type, copy=False) + + if psf.shape != reg.shape: + trans_fct = uft.ir2tf(psf, image.shape, is_real=is_real) + else: + trans_fct = psf + + # The mean of the object + x_postmean = np.zeros(trans_fct.shape, dtype=float_type) + # The previous computed mean in the iterative loop + prev_x_postmean = np.zeros(trans_fct.shape, dtype=float_type) + + # Difference between two successive mean + delta = np.nan + + # Initial state of the chain + gn_chain, gx_chain = [1], [1] + + # The correlation of the object in Fourier space (if size is big, + # this can reduce computation time in the loop) + areg2 = np.abs(reg) ** 2 + atf2 = np.abs(trans_fct) ** 2 + + # The Fourier transform may change the image.size attribute, so we + # store it. + if is_real: + data_spectrum = uft.urfft2(image) + else: + data_spectrum = uft.ufft2(image) + + rng = np.random.default_rng(rng) + + # Gibbs sampling + for iteration in range(params['max_num_iter']): + # Sample of Eq. 27 p(circX^k | gn^k-1, gx^k-1, y). + + # weighting (correlation in direct space) + precision = gn_chain[-1] * atf2 + gx_chain[-1] * areg2 # Eq. 29 + # Note: Use astype instead of dtype argument to standard_normal to get + # similar random values across precisions, as needed for + # reference data used by test_unsupervised_wiener. + _rand1 = rng.standard_normal(data_spectrum.shape) + _rand1 = _rand1.astype(float_type, copy=False) + _rand2 = rng.standard_normal(data_spectrum.shape) + _rand2 = _rand2.astype(float_type, copy=False) + excursion = np.sqrt(0.5 / precision) * (_rand1 + 1j * _rand2) + + # mean Eq. 30 (RLS for fixed gn, gamma0 and gamma1 ...) + wiener_filter = gn_chain[-1] * np.conj(trans_fct) / precision + + # sample of X in Fourier space + x_sample = wiener_filter * data_spectrum + excursion + if params['callback']: + params['callback'](x_sample) + + # sample of Eq. 31 p(gn | x^k, gx^k, y) + gn_chain.append( + rng.gamma( + image.size / 2, + 2 / uft.image_quad_norm(data_spectrum - x_sample * trans_fct), + ) + ) + + # sample of Eq. 31 p(gx | x^k, gn^k-1, y) + gx_chain.append( + rng.gamma((image.size - 1) / 2, 2 / uft.image_quad_norm(x_sample * reg)) + ) + + # current empirical average + if iteration > params['burnin']: + x_postmean = prev_x_postmean + x_sample + + if iteration > (params['burnin'] + 1): + current = x_postmean / (iteration - params['burnin']) + previous = prev_x_postmean / (iteration - params['burnin'] - 1) + + delta = ( + np.sum(np.abs(current - previous)) + / np.sum(np.abs(x_postmean)) + / (iteration - params['burnin']) + ) + + prev_x_postmean = x_postmean + + # stop of the algorithm + if (iteration > params['min_num_iter']) and (delta < params['threshold']): + break + + # Empirical average \approx POSTMEAN Eq. 44 + x_postmean = x_postmean / (iteration - params['burnin']) + if is_real: + x_postmean = uft.uirfft2(x_postmean, shape=image.shape) + else: + x_postmean = uft.uifft2(x_postmean) + + if clip: + x_postmean[x_postmean > 1] = 1 + x_postmean[x_postmean < -1] = -1 + + return (x_postmean, {'noise': gn_chain, 'prior': gx_chain}) + + +def richardson_lucy(image, psf, num_iter=50, clip=True, filter_epsilon=None): + """Richardson-Lucy deconvolution. + + Parameters + ---------- + image : ([P, ]M, N) ndarray + Input degraded image (can be n-dimensional). If you keep the + default `clip=True` parameter, you may want to normalize + the image so that its values fall in the [-1, 1] interval to avoid + information loss. + psf : ndarray + The point spread function. + num_iter : int, optional + Number of iterations. This parameter plays the role of + regularisation. + clip : boolean, optional + True by default. If true, pixel value of the result above 1 or + under -1 are thresholded for skimage pipeline compatibility. + filter_epsilon: float, optional + Value below which intermediate results become 0 to avoid division + by small numbers. + + Returns + ------- + im_deconv : ndarray + The deconvolved image. + + Examples + -------- + >>> from skimage import img_as_float, data, restoration + >>> camera = img_as_float(data.camera()) + >>> from scipy.signal import convolve2d + >>> psf = np.ones((5, 5)) / 25 + >>> camera = convolve2d(camera, psf, 'same') + >>> rng = np.random.default_rng() + >>> camera += 0.1 * camera.std() * rng.standard_normal(camera.shape) + >>> deconvolved = restoration.richardson_lucy(camera, psf, 5) + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Richardson%E2%80%93Lucy_deconvolution + """ + float_type = _supported_float_type(image.dtype) + image = image.astype(float_type, copy=False) + psf = psf.astype(float_type, copy=False) + im_deconv = np.full(image.shape, 0.5, dtype=float_type) + psf_mirror = np.flip(psf) + + # Small regularization parameter used to avoid 0 divisions + eps = 1e-12 + + for _ in range(num_iter): + conv = convolve(im_deconv, psf, mode='same') + eps + if filter_epsilon: + relative_blur = np.where(conv < filter_epsilon, 0, image / conv) + else: + relative_blur = image / conv + im_deconv *= convolve(relative_blur, psf_mirror, mode='same') + + if clip: + im_deconv[im_deconv > 1] = 1 + im_deconv[im_deconv < -1] = -1 + + return im_deconv diff --git a/lib/python3.10/site-packages/skimage/restoration/inpaint.py b/lib/python3.10/site-packages/skimage/restoration/inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..c333c4c9163c2a73740b502393146226b42880f7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/inpaint.py @@ -0,0 +1,340 @@ +import numpy as np +from scipy import sparse +from scipy.sparse.linalg import spsolve +import scipy.ndimage as ndi +from scipy.ndimage import laplace + +import skimage +from .._shared import utils +from ..measure import label +from ._inpaint import _build_matrix_inner + + +def _get_neighborhood(nd_idx, radius, nd_shape): + bounds_lo = np.maximum(nd_idx - radius, 0) + bounds_hi = np.minimum(nd_idx + radius + 1, nd_shape) + return bounds_lo, bounds_hi + + +def _get_neigh_coef(shape, center, dtype=float): + # Create biharmonic coefficients ndarray + neigh_coef = np.zeros(shape, dtype=dtype) + neigh_coef[center] = 1 + neigh_coef = laplace(laplace(neigh_coef)) + + # extract non-zero locations and values + coef_idx = np.where(neigh_coef) + coef_vals = neigh_coef[coef_idx] + + coef_idx = np.stack(coef_idx, axis=0) + return neigh_coef, coef_idx, coef_vals + + +def _inpaint_biharmonic_single_region( + image, mask, out, neigh_coef_full, coef_vals, raveled_offsets +): + """Solve a (sparse) linear system corresponding to biharmonic inpainting. + + This function creates a linear system of the form: + + ``A @ u = b`` + + where ``A`` is a sparse matrix, ``b`` is a vector enforcing smoothness and + boundary constraints and ``u`` is the vector of inpainted values to be + (uniquely) determined by solving the linear system. + + ``A`` is a sparse matrix of shape (n_mask, n_mask) where ``n_mask`` + corresponds to the number of non-zero values in ``mask`` (i.e. the number + of pixels to be inpainted). Each row in A will have a number of non-zero + values equal to the number of non-zero values in the biharmonic kernel, + ``neigh_coef_full``. In practice, biharmonic kernels with reduced extent + are used at the image borders. This matrix, ``A`` is the same for all + image channels (since the same inpainting mask is currently used for all + channels). + + ``u`` is a dense matrix of shape ``(n_mask, n_channels)`` and represents + the vector of unknown values for each channel. + + ``b`` is a dense matrix of shape ``(n_mask, n_channels)`` and represents + the desired output of convolving the solution with the biharmonic kernel. + At mask locations where there is no overlap with known values, ``b`` will + have a value of 0. This enforces the biharmonic smoothness constraint in + the interior of inpainting regions. For regions near the boundary that + overlap with known values, the entries in ``b`` enforce boundary conditions + designed to avoid discontinuity with the known values. + """ + + n_channels = out.shape[-1] + radius = neigh_coef_full.shape[0] // 2 + + edge_mask = np.ones(mask.shape, dtype=bool) + edge_mask[(slice(radius, -radius),) * mask.ndim] = 0 + boundary_mask = edge_mask * mask + center_mask = ~edge_mask * mask + + boundary_pts = np.where(boundary_mask) + boundary_i = np.flatnonzero(boundary_mask) + center_i = np.flatnonzero(center_mask) + mask_i = np.concatenate((boundary_i, center_i)) + + center_pts = np.where(center_mask) + mask_pts = tuple([np.concatenate((b, c)) for b, c in zip(boundary_pts, center_pts)]) + + # Use convolution to predetermine the number of non-zero entries in the + # sparse system matrix. + structure = neigh_coef_full != 0 + tmp = ndi.convolve(mask, structure, output=np.uint8, mode='constant') + nnz_matrix = tmp[mask].sum() + + # Need to estimate the number of zeros for the right hand side vector. + # The computation below will slightly overestimate the true number of zeros + # due to edge effects (the kernel itself gets shrunk in size near the + # edges, but that isn't accounted for here). We can trim any excess entries + # later. + n_mask = np.count_nonzero(mask) + n_struct = np.count_nonzero(structure) + nnz_rhs_vector_max = n_mask - np.count_nonzero(tmp == n_struct) + + # pre-allocate arrays storing sparse matrix indices and values + row_idx_known = np.empty(nnz_rhs_vector_max, dtype=np.intp) + data_known = np.zeros((nnz_rhs_vector_max, n_channels), dtype=out.dtype) + row_idx_unknown = np.empty(nnz_matrix, dtype=np.intp) + col_idx_unknown = np.empty(nnz_matrix, dtype=np.intp) + data_unknown = np.empty(nnz_matrix, dtype=out.dtype) + + # cache the various small, non-square Laplacians used near the boundary + coef_cache = {} + + # Iterate over masked points near the boundary + mask_flat = mask.reshape(-1) + out_flat = np.ascontiguousarray(out.reshape((-1, n_channels))) + idx_known = 0 + idx_unknown = 0 + mask_pt_n = -1 + boundary_pts = np.stack(boundary_pts, axis=1) + for mask_pt_n, nd_idx in enumerate(boundary_pts): + # Get bounded neighborhood of selected radius + b_lo, b_hi = _get_neighborhood(nd_idx, radius, mask.shape) + + # Create (truncated) biharmonic coefficients ndarray + coef_shape = tuple(b_hi - b_lo) + coef_center = tuple(nd_idx - b_lo) + coef_idx, coefs = coef_cache.get((coef_shape, coef_center), (None, None)) + if coef_idx is None: + _, coef_idx, coefs = _get_neigh_coef( + coef_shape, coef_center, dtype=out.dtype + ) + coef_cache[(coef_shape, coef_center)] = (coef_idx, coefs) + + # compute corresponding 1d indices into the mask + coef_idx = coef_idx + b_lo[:, np.newaxis] + index1d = np.ravel_multi_index(coef_idx, mask.shape) + + # Iterate over masked point's neighborhood + nvals = 0 + for coef, i in zip(coefs, index1d): + if mask_flat[i]: + row_idx_unknown[idx_unknown] = mask_pt_n + col_idx_unknown[idx_unknown] = i + data_unknown[idx_unknown] = coef + idx_unknown += 1 + else: + data_known[idx_known, :] -= coef * out_flat[i, :] + nvals += 1 + if nvals: + row_idx_known[idx_known] = mask_pt_n + idx_known += 1 + + # Call an efficient Cython-based implementation for all interior points + row_start = mask_pt_n + 1 + known_start_idx = idx_known + unknown_start_idx = idx_unknown + nnz_rhs = _build_matrix_inner( + # starting indices + row_start, + known_start_idx, + unknown_start_idx, + # input arrays + center_i, + raveled_offsets, + coef_vals, + mask_flat, + out_flat, + # output arrays + row_idx_known, + data_known, + row_idx_unknown, + col_idx_unknown, + data_unknown, + ) + + # trim RHS vector values and indices to the exact length + row_idx_known = row_idx_known[:nnz_rhs] + data_known = data_known[:nnz_rhs, :] + + # Form sparse matrix of unknown values + sp_shape = (n_mask, out.size) + matrix_unknown = sparse.csr_array( + (data_unknown, (row_idx_unknown, col_idx_unknown)), shape=sp_shape + ) + + # Solve linear system for masked points + matrix_unknown = matrix_unknown[:, mask_i] + + # dense vectors representing the right hand side for each channel + rhs = np.zeros((n_mask, n_channels), dtype=out.dtype) + rhs[row_idx_known, :] = data_known + + # set use_umfpack to False so float32 data is supported + result = spsolve(matrix_unknown, rhs, use_umfpack=False, permc_spec='MMD_ATA') + if result.ndim == 1: + result = result[:, np.newaxis] + + out[mask_pts] = result + return out + + +@utils.channel_as_last_axis() +def inpaint_biharmonic(image, mask, *, split_into_regions=False, channel_axis=None): + """Inpaint masked points in image with biharmonic equations. + + Parameters + ---------- + image : (M[, N[, ..., P]][, C]) ndarray + Input image. + mask : (M[, N[, ..., P]]) ndarray + Array of pixels to be inpainted. Have to be the same shape as one + of the 'image' channels. Unknown pixels have to be represented with 1, + known pixels - with 0. + split_into_regions : boolean, optional + If True, inpainting is performed on a region-by-region basis. This is + likely to be slower, but will have reduced memory requirements. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : (M[, N[, ..., P]][, C]) ndarray + Input image with masked pixels inpainted. + + References + ---------- + .. [1] S.B.Damelin and N.S.Hoang. "On Surface Completion and Image + Inpainting by Biharmonic Functions: Numerical Aspects", + International Journal of Mathematics and Mathematical Sciences, + Vol. 2018, Article ID 3950312 + :DOI:`10.1155/2018/3950312` + .. [2] C. K. Chui and H. N. Mhaskar, MRA Contextual-Recovery Extension of + Smooth Functions on Manifolds, Appl. and Comp. Harmonic Anal., + 28 (2010), 104-113, + :DOI:`10.1016/j.acha.2009.04.004` + + Examples + -------- + >>> img = np.tile(np.square(np.linspace(0, 1, 5)), (5, 1)) + >>> mask = np.zeros_like(img) + >>> mask[2, 2:] = 1 + >>> mask[1, 3:] = 1 + >>> mask[0, 4:] = 1 + >>> out = inpaint_biharmonic(img, mask) + """ + + if image.ndim < 1: + raise ValueError('Input array has to be at least 1D') + + multichannel = channel_axis is not None + img_baseshape = image.shape[:-1] if multichannel else image.shape + if img_baseshape != mask.shape: + raise ValueError('Input arrays have to be the same shape') + + if np.ma.isMaskedArray(image): + raise TypeError('Masked arrays are not supported') + + image = skimage.img_as_float(image) + + # float16->float32 and float128->float64 + float_dtype = utils._supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + mask = mask.astype(bool, copy=False) + if not multichannel: + image = image[..., np.newaxis] + out = np.copy(image, order='C') + + # Create biharmonic coefficients ndarray + radius = 2 + coef_shape = (2 * radius + 1,) * mask.ndim + coef_center = (radius,) * mask.ndim + neigh_coef_full, coef_idx, coef_vals = _get_neigh_coef( + coef_shape, coef_center, dtype=out.dtype + ) + + # stride for the last spatial dimension + channel_stride_bytes = out.strides[-2] + + # offsets to all neighboring non-zero elements in the footprint + offsets = coef_idx - radius + + # determine per-channel intensity limits + known_points = image[~mask] + limits = (known_points.min(axis=0), known_points.max(axis=0)) + + if split_into_regions: + # Split inpainting mask into independent regions + kernel = ndi.generate_binary_structure(mask.ndim, 1) + mask_dilated = ndi.binary_dilation(mask, structure=kernel) + mask_labeled = label(mask_dilated) + mask_labeled *= mask + + bbox_slices = ndi.find_objects(mask_labeled) + + for idx_region, bb_slice in enumerate(bbox_slices, 1): + # expand object bounding boxes by the biharmonic kernel radius + roi_sl = tuple( + slice(max(sl.start - radius, 0), min(sl.stop + radius, size)) + for sl, size in zip(bb_slice, mask_labeled.shape) + ) + # extract only the region surrounding the label of interest + mask_region = mask_labeled[roi_sl] == idx_region + # add slice for axes + roi_sl += (slice(None),) + # copy for contiguity and to account for possible ROI overlap + otmp = out[roi_sl].copy() + + # compute raveled offsets for the ROI + ostrides = np.array( + [s // channel_stride_bytes for s in otmp[..., 0].strides] + ) + raveled_offsets = np.sum(offsets * ostrides[..., np.newaxis], axis=0) + + _inpaint_biharmonic_single_region( + image[roi_sl], + mask_region, + otmp, + neigh_coef_full, + coef_vals, + raveled_offsets, + ) + # assign output to the + out[roi_sl] = otmp + else: + # compute raveled offsets for output image + ostrides = np.array([s // channel_stride_bytes for s in out[..., 0].strides]) + raveled_offsets = np.sum(offsets * ostrides[..., np.newaxis], axis=0) + + _inpaint_biharmonic_single_region( + image, mask, out, neigh_coef_full, coef_vals, raveled_offsets + ) + + # Handle enormous values on a per-channel basis + np.clip(out, a_min=limits[0], a_max=limits[1], out=out) + + if not multichannel: + out = out[..., 0] + + return out diff --git a/lib/python3.10/site-packages/skimage/restoration/j_invariant.py b/lib/python3.10/site-packages/skimage/restoration/j_invariant.py new file mode 100644 index 0000000000000000000000000000000000000000..5de336b3abe42b5d08227055ed3e10f7a6d5fa2b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/j_invariant.py @@ -0,0 +1,358 @@ +import itertools +import functools + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import _supported_float_type +from ..metrics import mean_squared_error +from ..util import img_as_float + + +def _interpolate_image(image, *, multichannel=False): + """Replacing each pixel in ``image`` with the average of its neighbors. + + Parameters + ---------- + image : ndarray + Input data to be interpolated. + multichannel : bool, optional + Whether the last axis of the image is to be interpreted as multiple + channels or another spatial dimension. + + Returns + ------- + interp : ndarray + Interpolated version of `image`. + """ + spatialdims = image.ndim if not multichannel else image.ndim - 1 + conv_filter = ndi.generate_binary_structure(spatialdims, 1).astype(image.dtype) + conv_filter.ravel()[conv_filter.size // 2] = 0 + conv_filter /= conv_filter.sum() + + if multichannel: + interp = np.zeros_like(image) + for i in range(image.shape[-1]): + interp[..., i] = ndi.convolve(image[..., i], conv_filter, mode='mirror') + else: + interp = ndi.convolve(image, conv_filter, mode='mirror') + return interp + + +def _generate_grid_slice(shape, *, offset, stride=3): + """Generate slices of uniformly-spaced points in an array. + + Parameters + ---------- + shape : tuple of int + Shape of the mask. + offset : int + The offset of the grid of ones. Iterating over ``offset`` will cover + the entire array. It should be between 0 and ``stride ** ndim``, not + inclusive, where ``ndim = len(shape)``. + stride : int, optional + The spacing between ones, used in each dimension. + + Returns + ------- + mask : ndarray + The mask. + + Examples + -------- + >>> shape = (4, 4) + >>> array = np.zeros(shape, dtype=int) + >>> grid_slice = _generate_grid_slice(shape, offset=0, stride=2) + >>> array[grid_slice] = 1 + >>> print(array) + [[1 0 1 0] + [0 0 0 0] + [1 0 1 0] + [0 0 0 0]] + + Changing the offset moves the location of the 1s: + + >>> array = np.zeros(shape, dtype=int) + >>> grid_slice = _generate_grid_slice(shape, offset=3, stride=2) + >>> array[grid_slice] = 1 + >>> print(array) + [[0 0 0 0] + [0 1 0 1] + [0 0 0 0] + [0 1 0 1]] + """ + phases = np.unravel_index(offset, (stride,) * len(shape)) + mask = tuple(slice(p, None, stride) for p in phases) + + return mask + + +def denoise_invariant( + image, denoise_function, *, stride=4, masks=None, denoiser_kwargs=None +): + """Apply a J-invariant version of a denoising function. + + Parameters + ---------- + image : ndarray (M[, N[, ...]][, C]) of ints, uints or floats + Input data to be denoised. `image` can be of any numeric type, + but it is cast into a ndarray of floats (using `img_as_float`) for the + computation of the denoised image. + denoise_function : function + Original denoising function. + stride : int, optional + Stride used in masking procedure that converts `denoise_function` + to J-invariance. + masks : list of ndarray, optional + Set of masks to use for computing J-invariant output. If `None`, + a full set of masks covering the image will be used. + denoiser_kwargs: + Keyword arguments passed to `denoise_function`. + + Returns + ------- + output : ndarray + Denoised image, of same shape as `image`. + + Notes + ----- + A denoising function is J-invariant if the prediction it makes for each + pixel does not depend on the value of that pixel in the original image. + The prediction for each pixel may instead use all the relevant information + contained in the rest of the image, which is typically quite significant. + Any function can be converted into a J-invariant one using a simple masking + procedure, as described in [1]. + + The pixel-wise error of a J-invariant denoiser is uncorrelated to the noise, + so long as the noise in each pixel is independent. Consequently, the average + difference between the denoised image and the oisy image, the + *self-supervised loss*, is the same as the difference between the denoised + image and the original clean image, the *ground-truth loss* (up to a + constant). + + This means that the best J-invariant denoiser for a given image can be found + using the noisy data alone, by selecting the denoiser minimizing the self- + supervised loss. + + References + ---------- + .. [1] J. Batson & L. Royer. Noise2Self: Blind Denoising by Self-Supervision, + International Conference on Machine Learning, p. 524-533 (2019). + + Examples + -------- + >>> import skimage + >>> from skimage.restoration import denoise_invariant, denoise_tv_chambolle + >>> image = skimage.util.img_as_float(skimage.data.chelsea()) + >>> noisy = skimage.util.random_noise(image, var=0.2 ** 2) + >>> denoised = denoise_invariant(noisy, denoise_function=denoise_tv_chambolle) + """ + image = img_as_float(image) + + # promote float16->float32 if needed + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + if denoiser_kwargs is None: + denoiser_kwargs = {} + + multichannel = denoiser_kwargs.get('channel_axis', None) is not None + interp = _interpolate_image(image, multichannel=multichannel) + output = np.zeros_like(image) + + if masks is None: + spatialdims = image.ndim if not multichannel else image.ndim - 1 + n_masks = stride**spatialdims + masks = ( + _generate_grid_slice(image.shape[:spatialdims], offset=idx, stride=stride) + for idx in range(n_masks) + ) + + for mask in masks: + input_image = image.copy() + input_image[mask] = interp[mask] + output[mask] = denoise_function(input_image, **denoiser_kwargs)[mask] + return output + + +def _product_from_dict(dictionary): + """Utility function to convert parameter ranges to parameter combinations. + + Converts a dict of lists into a list of dicts whose values consist of the + cartesian product of the values in the original dict. + + Parameters + ---------- + dictionary : dict of lists + Dictionary of lists to be multiplied. + + Yields + ------ + selections : dicts of values + Dicts containing individual combinations of the values in the input + dict. + """ + keys = dictionary.keys() + for element in itertools.product(*dictionary.values()): + yield dict(zip(keys, element)) + + +def calibrate_denoiser( + image, + denoise_function, + denoise_parameters, + *, + stride=4, + approximate_loss=True, + extra_output=False, +): + """Calibrate a denoising function and return optimal J-invariant version. + + The returned function is partially evaluated with optimal parameter values + set for denoising the input image. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using `img_as_float`). + denoise_function : function + Denoising function to be calibrated. + denoise_parameters : dict of list + Ranges of parameters for `denoise_function` to be calibrated over. + stride : int, optional + Stride used in masking procedure that converts `denoise_function` + to J-invariance. + approximate_loss : bool, optional + Whether to approximate the self-supervised loss used to evaluate the + denoiser by only computing it on one masked version of the image. + If False, the runtime will be a factor of `stride**image.ndim` longer. + extra_output : bool, optional + If True, return parameters and losses in addition to the calibrated + denoising function + + Returns + ------- + best_denoise_function : function + The optimal J-invariant version of `denoise_function`. + + If `extra_output` is True, the following tuple is also returned: + + (parameters_tested, losses) : tuple (list of dict, list of int) + List of parameters tested for `denoise_function`, as a dictionary of + kwargs + Self-supervised loss for each set of parameters in `parameters_tested`. + + + Notes + ----- + + The calibration procedure uses a self-supervised mean-square-error loss + to evaluate the performance of J-invariant versions of `denoise_function`. + The minimizer of the self-supervised loss is also the minimizer of the + ground-truth loss (i.e., the true MSE error) [1]. The returned function + can be used on the original noisy image, or other images with similar + characteristics. + + Increasing the stride increases the performance of `best_denoise_function` + at the expense of increasing its runtime. It has no effect on the runtime + of the calibration. + + References + ---------- + .. [1] J. Batson & L. Royer. Noise2Self: Blind Denoising by Self-Supervision, + International Conference on Machine Learning, p. 524-533 (2019). + + Examples + -------- + >>> from skimage import color, data + >>> from skimage.restoration import denoise_tv_chambolle + >>> import numpy as np + >>> img = color.rgb2gray(data.astronaut()[:50, :50]) + >>> rng = np.random.default_rng() + >>> noisy = img + 0.5 * img.std() * rng.standard_normal(img.shape) + >>> parameters = {'weight': np.arange(0.01, 0.3, 0.02)} + >>> denoising_function = calibrate_denoiser(noisy, denoise_tv_chambolle, + ... denoise_parameters=parameters) + >>> denoised_img = denoising_function(img) + + """ + parameters_tested, losses = _calibrate_denoiser_search( + image, + denoise_function, + denoise_parameters=denoise_parameters, + stride=stride, + approximate_loss=approximate_loss, + ) + + idx = np.argmin(losses) + best_parameters = parameters_tested[idx] + + best_denoise_function = functools.partial( + denoise_invariant, + denoise_function=denoise_function, + stride=stride, + denoiser_kwargs=best_parameters, + ) + + if extra_output: + return best_denoise_function, (parameters_tested, losses) + else: + return best_denoise_function + + +def _calibrate_denoiser_search( + image, denoise_function, denoise_parameters, *, stride=4, approximate_loss=True +): + """Return a parameter search history with losses for a denoise function. + + Parameters + ---------- + image : ndarray + Input data to be denoised (converted using `img_as_float`). + denoise_function : function + Denoising function to be calibrated. + denoise_parameters : dict of list + Ranges of parameters for `denoise_function` to be calibrated over. + stride : int, optional + Stride used in masking procedure that converts `denoise_function` + to J-invariance. + approximate_loss : bool, optional + Whether to approximate the self-supervised loss used to evaluate the + denoiser by only computing it on one masked version of the image. + If False, the runtime will be a factor of `stride**image.ndim` longer. + + Returns + ------- + parameters_tested : list of dict + List of parameters tested for `denoise_function`, as a dictionary of + kwargs. + losses : list of int + Self-supervised loss for each set of parameters in `parameters_tested`. + """ + image = img_as_float(image) + parameters_tested = list(_product_from_dict(denoise_parameters)) + losses = [] + + for denoiser_kwargs in parameters_tested: + multichannel = denoiser_kwargs.get('channel_axis', None) is not None + if not approximate_loss: + denoised = denoise_invariant( + image, denoise_function, stride=stride, denoiser_kwargs=denoiser_kwargs + ) + loss = mean_squared_error(image, denoised) + else: + spatialdims = image.ndim if not multichannel else image.ndim - 1 + n_masks = stride**spatialdims + mask = _generate_grid_slice( + image.shape[:spatialdims], offset=n_masks // 2, stride=stride + ) + + masked_denoised = denoise_invariant( + image, denoise_function, masks=[mask], denoiser_kwargs=denoiser_kwargs + ) + + loss = mean_squared_error(image[mask], masked_denoised[mask]) + + losses.append(loss) + + return parameters_tested, losses diff --git a/lib/python3.10/site-packages/skimage/restoration/non_local_means.py b/lib/python3.10/site-packages/skimage/restoration/non_local_means.py new file mode 100644 index 0000000000000000000000000000000000000000..a6f15df00cb831ff23abd71de2269dc070ec6668 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/non_local_means.py @@ -0,0 +1,187 @@ +import numpy as np + +from .._shared import utils +from .._shared.utils import convert_to_float +from ._nl_means_denoising import ( + _nl_means_denoising_2d, + _nl_means_denoising_3d, + _fast_nl_means_denoising_2d, + _fast_nl_means_denoising_3d, + _fast_nl_means_denoising_4d, +) + + +@utils.channel_as_last_axis() +def denoise_nl_means( + image, + patch_size=7, + patch_distance=11, + h=0.1, + fast_mode=True, + sigma=0.0, + *, + preserve_range=False, + channel_axis=None, +): + """Perform non-local means denoising on 2D-4D grayscale or RGB images. + + Parameters + ---------- + image : 2D or 3D ndarray + Input image to be denoised, which can be 2D or 3D, and grayscale + or RGB (for 2D images only, see ``channel_axis`` parameter). There can + be any number of channels (does not strictly have to be RGB). + patch_size : int, optional + Size of patches used for denoising. + patch_distance : int, optional + Maximal distance in pixels where to search patches used for denoising. + h : float, optional + Cut-off distance (in gray levels). The higher h, the more permissive + one is in accepting patches. A higher h results in a smoother image, + at the expense of blurring features. For a Gaussian noise of standard + deviation sigma, a rule of thumb is to choose the value of h to be + sigma of slightly less. + fast_mode : bool, optional + If True (default value), a fast version of the non-local means + algorithm is used. If False, the original version of non-local means is + used. See the Notes section for more details about the algorithms. + sigma : float, optional + The standard deviation of the (Gaussian) noise. If provided, a more + robust computation of patch weights is computed that takes the expected + noise variance into account (see Notes below). + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + result : ndarray + Denoised image, of same shape as `image`. + + Notes + ----- + + The non-local means algorithm is well suited for denoising images with + specific textures. The principle of the algorithm is to average the value + of a given pixel with values of other pixels in a limited neighborhood, + provided that the *patches* centered on the other pixels are similar enough + to the patch centered on the pixel of interest. + + In the original version of the algorithm [1]_, corresponding to + ``fast=False``, the computational complexity is:: + + image.size * patch_size ** image.ndim * patch_distance ** image.ndim + + Hence, changing the size of patches or their maximal distance has a + strong effect on computing times, especially for 3-D images. + + However, the default behavior corresponds to ``fast_mode=True``, for which + another version of non-local means [2]_ is used, corresponding to a + complexity of:: + + image.size * patch_distance ** image.ndim + + The computing time depends only weakly on the patch size, thanks to + the computation of the integral of patches distances for a given + shift, that reduces the number of operations [1]_. Therefore, this + algorithm executes faster than the classic algorithm + (``fast_mode=False``), at the expense of using twice as much memory. + This implementation has been proven to be more efficient compared to + other alternatives, see e.g. [3]_. + + Compared to the classic algorithm, all pixels of a patch contribute + to the distance to another patch with the same weight, no matter + their distance to the center of the patch. This coarser computation + of the distance can result in a slightly poorer denoising + performance. Moreover, for small images (images with a linear size + that is only a few times the patch size), the classic algorithm can + be faster due to boundary effects. + + The image is padded using the `reflect` mode of `skimage.util.pad` + before denoising. + + If the noise standard deviation, `sigma`, is provided a more robust + computation of patch weights is used. Subtracting the known noise variance + from the computed patch distances improves the estimates of patch + similarity, giving a moderate improvement to denoising performance [4]_. + It was also mentioned as an option for the fast variant of the algorithm in + [3]_. + + When `sigma` is provided, a smaller `h` should typically be used to + avoid oversmoothing. The optimal value for `h` depends on the image + content and noise level, but a reasonable starting point is + ``h = 0.8 * sigma`` when `fast_mode` is `True`, or ``h = 0.6 * sigma`` when + `fast_mode` is `False`. + + References + ---------- + .. [1] A. Buades, B. Coll, & J-M. Morel. A non-local algorithm for image + denoising. In CVPR 2005, Vol. 2, pp. 60-65, IEEE. + :DOI:`10.1109/CVPR.2005.38` + + .. [2] J. Darbon, A. Cunha, T.F. Chan, S. Osher, and G.J. Jensen, Fast + nonlocal filtering applied to electron cryomicroscopy, in 5th IEEE + International Symposium on Biomedical Imaging: From Nano to Macro, + 2008, pp. 1331-1334. + :DOI:`10.1109/ISBI.2008.4541250` + + .. [3] Jacques Froment. Parameter-Free Fast Pixelwise Non-Local Means + Denoising. Image Processing On Line, 2014, vol. 4, pp. 300-326. + :DOI:`10.5201/ipol.2014.120` + + .. [4] A. Buades, B. Coll, & J-M. Morel. Non-Local Means Denoising. + Image Processing On Line, 2011, vol. 1, pp. 208-212. + :DOI:`10.5201/ipol.2011.bcm_nlm` + + Examples + -------- + >>> a = np.zeros((40, 40)) + >>> a[10:-10, 10:-10] = 1. + >>> rng = np.random.default_rng() + >>> a += 0.3 * rng.standard_normal(a.shape) + >>> denoised_a = denoise_nl_means(a, 7, 5, 0.1) + """ + if channel_axis is None: + multichannel = False + image = image[..., np.newaxis] + else: + multichannel = True + + ndim_no_channel = image.ndim - 1 + if (ndim_no_channel < 2) or (ndim_no_channel > 4): + raise NotImplementedError( + "Non-local means denoising is only implemented for 2D, " + "3D or 4D grayscale or multichannel images." + ) + + image = convert_to_float(image, preserve_range) + if not image.flags.c_contiguous: + image = np.ascontiguousarray(image) + + kwargs = dict(s=patch_size, d=patch_distance, h=h, var=sigma * sigma) + if ndim_no_channel == 2: + nlm_func = _fast_nl_means_denoising_2d if fast_mode else _nl_means_denoising_2d + elif ndim_no_channel == 3: + if multichannel and not fast_mode: + raise NotImplementedError("Multichannel 3D requires fast_mode to be True.") + if fast_mode: + nlm_func = _fast_nl_means_denoising_3d + else: + # have to drop the size 1 channel axis for slow mode + image = image[..., 0] + nlm_func = _nl_means_denoising_3d + elif ndim_no_channel == 4: + if fast_mode: + nlm_func = _fast_nl_means_denoising_4d + else: + raise NotImplementedError("4D requires fast_mode to be True.") + dn = np.asarray(nlm_func(image, **kwargs)) + return dn diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__init__.py b/lib/python3.10/site-packages/skimage/restoration/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29eb4a658a34878353cea112597bfda24fa598d8 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_denoise.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_denoise.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2867b8dd167ab71cbd21717a46e52872aba5b815 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_denoise.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_inpaint.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_inpaint.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbb3d471221b6d7ce5a549d275a0678d25addeed Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_inpaint.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_j_invariant.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_j_invariant.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71f40acac027d606f83a5f1843ac0997e2a709ec Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_j_invariant.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_restoration.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_restoration.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc7806d80c9ce20a621baa608b5d4a981bd0149f Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_restoration.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_rolling_ball.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_rolling_ball.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96aef684372e1e31a09734417764bf222f2b77d1 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_rolling_ball.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_unwrap.cpython-310.pyc b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_unwrap.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..203f0cde09a81ed2dc072353c7dbbcbace4e3200 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/restoration/tests/__pycache__/test_unwrap.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_denoise.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_denoise.py new file mode 100644 index 0000000000000000000000000000000000000000..7dcbcfc7ac02c141be4d3a7f84f57ca16bd7048b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_denoise.py @@ -0,0 +1,1188 @@ +import functools +import itertools + +import numpy as np +import pytest +from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_warns + +from skimage import color, data, img_as_float, restoration +from skimage._shared._warnings import expected_warnings +from skimage._shared.utils import _supported_float_type, slice_at_axis +from skimage.metrics import peak_signal_noise_ratio, structural_similarity +from skimage.restoration._denoise import _wavelet_threshold + +try: + import pywt +except ImportError: + PYWT_NOT_INSTALLED = True +else: + PYWT_NOT_INSTALLED = False + +xfail_without_pywt = pytest.mark.xfail( + condition=PYWT_NOT_INSTALLED, + reason="optional dependency PyWavelets is not installed", + raises=ImportError, +) + + +try: + import dask # noqa +except ImportError: + DASK_NOT_INSTALLED_WARNING = 'The optional dask dependency is not installed' +else: + DASK_NOT_INSTALLED_WARNING = None + + +np.random.seed(1234) + + +astro = img_as_float(data.astronaut()[:128, :128]) +astro_gray = color.rgb2gray(astro) +# Make sure that all tests below that rely on 0-1 range are valid: +assert np.max(astro_gray) <= 1.0 + +checkerboard_gray = img_as_float(data.checkerboard()) +checkerboard = color.gray2rgb(checkerboard_gray) +assert np.max(checkerboard_gray) <= 1.0 + +# versions with one odd-sized dimension +astro_gray_odd = astro_gray[:, :-1] +astro_odd = astro[:, :-1] + + +float_dtypes = [np.float16, np.float32, np.float64] +try: + float_dtypes += [np.float128] +except AttributeError: + pass + + +@pytest.mark.parametrize('dtype', float_dtypes) +def test_denoise_tv_chambolle_2d(dtype): + # astronaut image + img = astro_gray.astype(dtype, copy=True) + # add noise to astronaut + img += 0.5 * img.std() * np.random.rand(*img.shape) + # clip noise so that it does not exceed allowed range for float images. + img = np.clip(img, 0, 1) + # denoise + denoised_astro = restoration.denoise_tv_chambolle(img, weight=0.1) + assert denoised_astro.dtype == _supported_float_type(img.dtype) + + from scipy import ndimage as ndi + + # Convert to a floating point type supported by scipy.ndimage + float_dtype = _supported_float_type(img.dtype) + img = img.astype(float_dtype, copy=False) + + grad = ndi.morphological_gradient(img, size=((3, 3))) + grad_denoised = ndi.morphological_gradient(denoised_astro, size=((3, 3))) + # test if the total variation has decreased + assert grad_denoised.dtype == float_dtype + assert np.sqrt((grad_denoised**2).sum()) < np.sqrt((grad**2).sum()) + + +@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1]) +def test_denoise_tv_chambolle_multichannel(channel_axis): + denoised0 = restoration.denoise_tv_chambolle(astro[..., 0], weight=0.1) + + img = np.moveaxis(astro, -1, channel_axis) + denoised = restoration.denoise_tv_chambolle( + img, weight=0.1, channel_axis=channel_axis + ) + _at = functools.partial(slice_at_axis, axis=channel_axis % img.ndim) + assert_array_equal(denoised[_at(0)], denoised0) + + # tile astronaut subset to generate 3D+channels data + astro3 = np.tile(astro[:64, :64, np.newaxis, :], [1, 1, 2, 1]) + # modify along tiled dimension to give non-zero gradient on 3rd axis + astro3[:, :, 0, :] = 2 * astro3[:, :, 0, :] + denoised0 = restoration.denoise_tv_chambolle(astro3[..., 0], weight=0.1) + + astro3 = np.moveaxis(astro3, -1, channel_axis) + denoised = restoration.denoise_tv_chambolle( + astro3, weight=0.1, channel_axis=channel_axis + ) + _at = functools.partial(slice_at_axis, axis=channel_axis % astro3.ndim) + assert_array_equal(denoised[_at(0)], denoised0) + + +def test_denoise_tv_chambolle_float_result_range(): + # astronaut image + img = astro_gray + int_astro = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_astro) > 1 + denoised_int_astro = restoration.denoise_tv_chambolle(int_astro, weight=0.1) + # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_astro.dtype == float + assert np.max(denoised_int_astro) <= 1.0 + assert np.min(denoised_int_astro) >= 0.0 + + +def test_denoise_tv_chambolle_3d(): + """Apply the TV denoising algorithm on a 3D image representing a sphere.""" + x, y, z = np.ogrid[0:40, 0:40, 0:40] + mask = (x - 22) ** 2 + (y - 20) ** 2 + (z - 17) ** 2 < 8**2 + mask = 100 * mask.astype(float) + mask += 60 + mask += 20 * np.random.rand(*mask.shape) + mask[mask < 0] = 0 + mask[mask > 255] = 255 + res = restoration.denoise_tv_chambolle(mask.astype(np.uint8), weight=0.1) + assert res.dtype == float + assert res.std() * 255 < mask.std() + + +def test_denoise_tv_chambolle_1d(): + """Apply the TV denoising algorithm on a 1D sinusoid.""" + x = 125 + 100 * np.sin(np.linspace(0, 8 * np.pi, 1000)) + x += 20 * np.random.rand(x.size) + x = np.clip(x, 0, 255) + res = restoration.denoise_tv_chambolle(x.astype(np.uint8), weight=0.1) + assert res.dtype == float + assert res.std() * 255 < x.std() + + +def test_denoise_tv_chambolle_4d(): + """TV denoising for a 4D input.""" + im = 255 * np.random.rand(8, 8, 8, 8) + res = restoration.denoise_tv_chambolle(im.astype(np.uint8), weight=0.1) + assert res.dtype == float + assert res.std() * 255 < im.std() + + +def test_denoise_tv_chambolle_weighting(): + # make sure a specified weight gives consistent results regardless of + # the number of input image dimensions + rstate = np.random.default_rng(1234) + img2d = astro_gray.copy() + img2d += 0.15 * rstate.standard_normal(img2d.shape) + img2d = np.clip(img2d, 0, 1) + + ssim_noisy = structural_similarity(astro_gray, img2d, data_range=1.0) + + # generate 4D image by tiling + img4d = np.tile(img2d[..., None, None], (1, 1, 2, 2)) + + w = 0.2 + denoised_2d = restoration.denoise_tv_chambolle(img2d, weight=w) + denoised_4d = restoration.denoise_tv_chambolle(img4d, weight=w) + assert denoised_2d.dtype == np.float64 + assert denoised_4d.dtype == np.float64 + ssim_2d = structural_similarity(denoised_2d, astro_gray, data_range=1.0) + ssim = structural_similarity(denoised_2d, denoised_4d[:, :, 0, 0], data_range=1.0) + assert ssim > 0.98 # clsoe to 1.0 + assert ssim_2d > ssim_noisy # quality must increase after denoising + + +def test_denoise_tv_bregman_2d(): + img = checkerboard_gray.copy() + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1) + + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() + + +def test_denoise_tv_bregman_float_result_range(): + # astronaut image + img = astro_gray.copy() + int_astro = np.multiply(img, 255).astype(np.uint8) + assert np.max(int_astro) > 1 + denoised_int_astro = restoration.denoise_tv_bregman(int_astro, weight=60.0) + # test if the value range of output float data is within [0.0:1.0] + assert denoised_int_astro.dtype == float + assert np.max(denoised_int_astro) <= 1.0 + assert np.min(denoised_int_astro) >= 0.0 + + +def test_denoise_tv_bregman_3d(): + img = checkerboard.copy() + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1) + + out1 = restoration.denoise_tv_bregman(img, weight=10) + out2 = restoration.denoise_tv_bregman(img, weight=5) + + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() + + +@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1]) +def test_denoise_tv_bregman_3d_multichannel(channel_axis): + img_astro = astro.copy() + denoised0 = restoration.denoise_tv_bregman(img_astro[..., 0], weight=60.0) + img_astro = np.moveaxis(img_astro, -1, channel_axis) + denoised = restoration.denoise_tv_bregman( + img_astro, weight=60.0, channel_axis=channel_axis + ) + _at = functools.partial(slice_at_axis, axis=channel_axis % img_astro.ndim) + assert_array_equal(denoised0, denoised[_at(0)]) + + +def test_denoise_tv_bregman_multichannel(): + img = checkerboard_gray.copy()[:50, :50] + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1) + + out1 = restoration.denoise_tv_bregman(img, weight=60.0) + out2 = restoration.denoise_tv_bregman(img, weight=60.0, channel_axis=-1) + + assert_array_equal(out1, out2) + + +def test_denoise_bilateral_null(): + img = np.zeros((50, 50)) + out = restoration.denoise_bilateral(img) + + # image full of zeros should return identity + assert_array_equal(out, img) + + +def test_denoise_bilateral_negative(): + img = -np.ones((50, 50)) + out = restoration.denoise_bilateral(img) + + # image with only negative values should be ok + assert_array_equal(out, img) + + +def test_denoise_bilateral_negative2(): + img = np.ones((50, 50)) + img[2, 2] = 2 + + out1 = restoration.denoise_bilateral(img) + out2 = restoration.denoise_bilateral(img - 10) # contains negative values + + # 2 images with a given offset should give the same result (with the same + # offset) + assert_array_almost_equal(out1, out2 + 10) + + +def test_denoise_bilateral_2d(): + img = checkerboard_gray.copy()[:50, :50] + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1) + + out1 = restoration.denoise_bilateral( + img, sigma_color=0.1, sigma_spatial=10, channel_axis=None + ) + out2 = restoration.denoise_bilateral( + img, sigma_color=0.2, sigma_spatial=20, channel_axis=None + ) + + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() + + +def test_denoise_bilateral_pad(): + """This test checks if the bilateral filter is returning an image + correctly padded.""" + img = img_as_float(data.chelsea())[100:200, 100:200] + img_bil = restoration.denoise_bilateral( + img, sigma_color=0.1, sigma_spatial=10, channel_axis=-1 + ) + condition_padding = np.count_nonzero(np.isclose(img_bil, 0, atol=0.001)) + assert_array_equal(condition_padding, 0) + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_denoise_bilateral_types(dtype): + img = checkerboard_gray.copy()[:50, :50] + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1).astype(dtype) + + # check that we can process multiple float types + restoration.denoise_bilateral( + img, sigma_color=0.1, sigma_spatial=10, channel_axis=None + ) + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_denoise_bregman_types(dtype): + img = checkerboard_gray.copy()[:50, :50] + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1).astype(dtype) + + # check that we can process multiple float types + restoration.denoise_tv_bregman(img, weight=5) + + +def test_denoise_bilateral_zeros(): + img = np.zeros((10, 10)) + assert_array_equal(img, restoration.denoise_bilateral(img, channel_axis=None)) + + +def test_denoise_bilateral_constant(): + img = np.ones((10, 10)) * 5 + assert_array_equal(img, restoration.denoise_bilateral(img, channel_axis=None)) + + +@pytest.mark.parametrize('channel_axis', [0, 1, -1]) +def test_denoise_bilateral_color(channel_axis): + img = checkerboard.copy()[:50, :50] + # add some random noise + img += 0.5 * img.std() * np.random.rand(*img.shape) + img = np.clip(img, 0, 1) + + img = np.moveaxis(img, -1, channel_axis) + out1 = restoration.denoise_bilateral( + img, sigma_color=0.1, sigma_spatial=10, channel_axis=channel_axis + ) + out2 = restoration.denoise_bilateral( + img, sigma_color=0.2, sigma_spatial=20, channel_axis=channel_axis + ) + img = np.moveaxis(img, channel_axis, -1) + out1 = np.moveaxis(out1, channel_axis, -1) + out2 = np.moveaxis(out2, channel_axis, -1) + + # make sure noise is reduced in the checkerboard cells + assert img[30:45, 5:15].std() > out1[30:45, 5:15].std() + assert out1[30:45, 5:15].std() > out2[30:45, 5:15].std() + + +def test_denoise_bilateral_3d_grayscale(): + img = np.ones((50, 50, 3)) + with pytest.raises(ValueError): + restoration.denoise_bilateral(img, channel_axis=None) + + +def test_denoise_bilateral_3d_multichannel(): + img = np.ones((50, 50, 50)) + with expected_warnings(["grayscale"]): + result = restoration.denoise_bilateral(img, channel_axis=-1) + + assert_array_equal(result, img) + + +def test_denoise_bilateral_multidimensional(): + img = np.ones((10, 10, 10, 10)) + with pytest.raises(ValueError): + restoration.denoise_bilateral(img, channel_axis=None) + with pytest.raises(ValueError): + restoration.denoise_bilateral(img, channel_axis=-1) + + +def test_denoise_bilateral_nan(): + img = np.full((50, 50), np.nan) + # This is in fact an optional warning for our test suite. + # Python 3.5 will not trigger a warning. + with expected_warnings([r'invalid|\A\Z']): + out = restoration.denoise_bilateral(img, channel_axis=None) + assert_array_equal(img, out) + + +@pytest.mark.parametrize('fast_mode', [False, True]) +def test_denoise_nl_means_2d(fast_mode): + img = np.zeros((40, 40)) + img[10:-10, 10:-10] = 1.0 + sigma = 0.3 + img += sigma * np.random.standard_normal(img.shape) + img_f32 = img.astype('float32') + for s in [sigma, 0]: + denoised = restoration.denoise_nl_means( + img, 7, 5, 0.2, fast_mode=fast_mode, channel_axis=None, sigma=s + ) + # make sure noise is reduced + assert img.std() > denoised.std() + + denoised_f32 = restoration.denoise_nl_means( + img_f32, 7, 5, 0.2, fast_mode=fast_mode, channel_axis=None, sigma=s + ) + # make sure noise is reduced + assert img.std() > denoised_f32.std() + + # Check single precision result + assert np.allclose(denoised_f32, denoised, atol=1e-2) + + +@pytest.mark.parametrize('fast_mode', [False, True]) +@pytest.mark.parametrize('n_channels', [2, 3, 6]) +@pytest.mark.parametrize('dtype', ['float64', 'float32']) +def test_denoise_nl_means_2d_multichannel(fast_mode, n_channels, dtype): + # reduce image size because nl means is slow + img = np.copy(astro[:50, :50]) + img = np.concatenate( + (img,) * 2, + ) # 6 channels + img = img.astype(dtype) + + # add some random noise + sigma = 0.1 + imgn = img + sigma * np.random.standard_normal(img.shape) + imgn = np.clip(imgn, 0, 1) + imgn = imgn.astype(dtype) + + for s in [sigma, 0]: + psnr_noisy = peak_signal_noise_ratio( + img[..., :n_channels], imgn[..., :n_channels] + ) + denoised = restoration.denoise_nl_means( + imgn[..., :n_channels], + 3, + 5, + h=0.75 * sigma, + fast_mode=fast_mode, + channel_axis=-1, + sigma=s, + ) + psnr_denoised = peak_signal_noise_ratio( + denoised[..., :n_channels], img[..., :n_channels] + ) + + # make sure noise is reduced + assert psnr_denoised > psnr_noisy + + +@pytest.mark.parametrize('fast_mode', [False, True]) +@pytest.mark.parametrize('dtype', ['float64', 'float32']) +def test_denoise_nl_means_3d(fast_mode, dtype): + img = np.zeros((12, 12, 8), dtype=dtype) + img[5:-5, 5:-5, 2:-2] = 1.0 + sigma = 0.3 + imgn = img + sigma * np.random.standard_normal(img.shape) + imgn = imgn.astype(dtype) + psnr_noisy = peak_signal_noise_ratio(img, imgn) + for s in [sigma, 0]: + denoised = restoration.denoise_nl_means( + imgn, 3, 4, h=0.75 * sigma, fast_mode=fast_mode, channel_axis=None, sigma=s + ) + # make sure noise is reduced + assert peak_signal_noise_ratio(img, denoised) > psnr_noisy + + +@pytest.mark.parametrize('fast_mode', [False, True]) +@pytest.mark.parametrize('dtype', ['float64', 'float32', 'float16']) +@pytest.mark.parametrize('channel_axis', [0, -1]) +def test_denoise_nl_means_multichannel(fast_mode, dtype, channel_axis): + # for true 3D data, 3D denoising is better than denoising as 2D+channels + + # synthetic 3d volume + img = data.binary_blobs(length=32, n_dim=3, rng=5) + img = img[:, :24, :16].astype(dtype, copy=False) + + sigma = 0.2 + rng = np.random.default_rng(5) + imgn = img + sigma * rng.standard_normal(img.shape) + imgn = imgn.astype(dtype) + + # test 3D denoising (channel_axis = None) + denoised_ok_multichannel = restoration.denoise_nl_means( + imgn.copy(), + 3, + 2, + h=0.6 * sigma, + sigma=sigma, + fast_mode=fast_mode, + channel_axis=None, + ) + + # set a channel axis: one dimension is (incorrectly) considered "channels" + imgn = np.moveaxis(imgn, -1, channel_axis) + denoised_wrong_multichannel = restoration.denoise_nl_means( + imgn.copy(), + 3, + 2, + h=0.6 * sigma, + sigma=sigma, + fast_mode=fast_mode, + channel_axis=channel_axis, + ) + denoised_wrong_multichannel = np.moveaxis( + denoised_wrong_multichannel, channel_axis, -1 + ) + + img = img.astype(denoised_wrong_multichannel.dtype) + psnr_wrong = peak_signal_noise_ratio(img, denoised_wrong_multichannel) + psnr_ok = peak_signal_noise_ratio(img, denoised_ok_multichannel) + assert psnr_ok > psnr_wrong + + +def test_denoise_nl_means_4d(): + rng = np.random.default_rng(5) + img = np.zeros((10, 10, 8, 5)) + img[2:-2, 2:-2, 2:-2, :2] = 0.5 + img[2:-2, 2:-2, 2:-2, 2:] = 1.0 + sigma = 0.3 + imgn = img + sigma * rng.standard_normal(img.shape) + + nlmeans_kwargs = dict( + patch_size=3, patch_distance=2, h=0.3 * sigma, sigma=sigma, fast_mode=True + ) + + psnr_noisy = peak_signal_noise_ratio(img, imgn, data_range=1.0) + + # denoise by looping over 3D slices + denoised_3d = np.zeros_like(imgn) + for ch in range(img.shape[-1]): + denoised_3d[..., ch] = restoration.denoise_nl_means( + imgn[..., ch], channel_axis=None, **nlmeans_kwargs + ) + psnr_3d = peak_signal_noise_ratio(img, denoised_3d, data_range=1.0) + assert psnr_3d > psnr_noisy + + # denoise as 4D + denoised_4d = restoration.denoise_nl_means( + imgn, channel_axis=None, **nlmeans_kwargs + ) + psnr_4d = peak_signal_noise_ratio(img, denoised_4d, data_range=1.0) + assert psnr_4d > psnr_3d + + # denoise as 3D + channels instead + denoised_3dmc = restoration.denoise_nl_means( + imgn, channel_axis=-1, **nlmeans_kwargs + ) + psnr_3dmc = peak_signal_noise_ratio(img, denoised_3dmc, data_range=1.0) + assert psnr_3dmc > psnr_3d + + +def test_denoise_nl_means_4d_multichannel(): + img = np.zeros((8, 8, 8, 4, 4)) + img[2:-2, 2:-2, 2:-2, 1:-1, :] = 1.0 + sigma = 0.3 + imgn = img + sigma * np.random.randn(*img.shape) + + psnr_noisy = peak_signal_noise_ratio(img, imgn, data_range=1.0) + + denoised_4dmc = restoration.denoise_nl_means( + imgn, 3, 3, h=0.35 * sigma, fast_mode=True, channel_axis=-1, sigma=sigma + ) + psnr_4dmc = peak_signal_noise_ratio(img, denoised_4dmc, data_range=1.0) + assert psnr_4dmc > psnr_noisy + + +def test_denoise_nl_means_wrong_dimension(): + # 1D not implemented + img = np.zeros((5,)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=None) + + img = np.zeros((5, 3)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=-1) + + # 3D + channels only for fast mode + img = np.zeros((5, 5, 5, 5)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=-1, fast_mode=False) + + # 4D only for fast mode + img = np.zeros((5, 5, 5, 5)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=None, fast_mode=False) + + # 4D + channels only for fast mode + img = np.zeros((5, 5, 5, 5, 5)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=-1, fast_mode=False) + + # 5D not implemented + img = np.zeros((5, 5, 5, 5, 5)) + with pytest.raises(NotImplementedError): + restoration.denoise_nl_means(img, channel_axis=None) + + +@pytest.mark.parametrize('fast_mode', [False, True]) +@pytest.mark.parametrize('dtype', ['float64', 'float32']) +def test_no_denoising_for_small_h(fast_mode, dtype): + img = np.zeros((40, 40)) + img[10:-10, 10:-10] = 1.0 + img += 0.3 * np.random.standard_normal(img.shape) + img = img.astype(dtype) + # very small h should result in no averaging with other patches + denoised = restoration.denoise_nl_means( + img, 7, 5, 0.01, fast_mode=fast_mode, channel_axis=None + ) + assert np.allclose(denoised, img) + denoised = restoration.denoise_nl_means( + img, 7, 5, 0.01, fast_mode=fast_mode, channel_axis=None + ) + assert np.allclose(denoised, img) + + +@pytest.mark.parametrize('fast_mode', [False, True]) +def test_denoise_nl_means_2d_dtype(fast_mode): + img = np.zeros((40, 40), dtype=int) + img_f32 = img.astype('float32') + img_f64 = img.astype('float64') + + assert restoration.denoise_nl_means(img, fast_mode=fast_mode).dtype == 'float64' + + assert ( + restoration.denoise_nl_means(img_f32, fast_mode=fast_mode).dtype + == img_f32.dtype + ) + + assert ( + restoration.denoise_nl_means(img_f64, fast_mode=fast_mode).dtype + == img_f64.dtype + ) + + +@pytest.mark.parametrize('fast_mode', [False, True]) +def test_denoise_nl_means_3d_dtype(fast_mode): + img = np.zeros((12, 12, 8), dtype=int) + img_f32 = img.astype('float32') + img_f64 = img.astype('float64') + + assert ( + restoration.denoise_nl_means(img, patch_distance=2, fast_mode=fast_mode).dtype + == 'float64' + ) + + assert ( + restoration.denoise_nl_means( + img_f32, patch_distance=2, fast_mode=fast_mode + ).dtype + == img_f32.dtype + ) + + assert ( + restoration.denoise_nl_means( + img_f64, patch_distance=2, fast_mode=fast_mode + ).dtype + == img_f64.dtype + ) + + +@xfail_without_pywt +@pytest.mark.parametrize( + 'img, channel_axis, convert2ycbcr', + [ + (astro_gray, None, False), + (astro_gray_odd, None, False), + (astro_odd, -1, False), + (astro_odd, -1, True), + ], +) +def test_wavelet_denoising(img, channel_axis, convert2ycbcr): + rstate = np.random.default_rng(1234) + sigma = 0.1 + noisy = img + sigma * rstate.standard_normal(img.shape) + noisy = np.clip(noisy, 0, 1) + + # Verify that SNR is improved when true sigma is used + denoised = restoration.denoise_wavelet( + noisy, + sigma=sigma, + channel_axis=channel_axis, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + assert psnr_denoised > psnr_noisy + + # Verify that SNR is improved with internally estimated sigma + denoised = restoration.denoise_wavelet( + noisy, + channel_axis=channel_axis, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + assert psnr_denoised > psnr_noisy + + # SNR is improved less with 1 wavelet level than with the default. + denoised_1 = restoration.denoise_wavelet( + noisy, + channel_axis=channel_axis, + wavelet_levels=1, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + psnr_denoised_1 = peak_signal_noise_ratio(img, denoised_1) + assert psnr_denoised > psnr_denoised_1 + assert psnr_denoised_1 > psnr_noisy + + # Test changing noise_std (higher threshold, so less energy in signal) + res1 = restoration.denoise_wavelet( + noisy, sigma=2 * sigma, channel_axis=channel_axis, rescale_sigma=True + ) + res2 = restoration.denoise_wavelet( + noisy, sigma=sigma, channel_axis=channel_axis, rescale_sigma=True + ) + assert np.sum(res1**2) <= np.sum(res2**2) + + +@xfail_without_pywt +@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1]) +@pytest.mark.parametrize('convert2ycbcr', [False, True]) +def test_wavelet_denoising_channel_axis(channel_axis, convert2ycbcr): + rstate = np.random.default_rng(1234) + sigma = 0.1 + img = astro_odd + noisy = img + sigma * rstate.standard_normal(img.shape) + noisy = np.clip(noisy, 0, 1) + + img = np.moveaxis(img, -1, channel_axis) + noisy = np.moveaxis(noisy, -1, channel_axis) + + # Verify that SNR is improved when true sigma is used + denoised = restoration.denoise_wavelet( + noisy, + sigma=sigma, + channel_axis=channel_axis, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + assert psnr_denoised > psnr_noisy + + +@pytest.mark.parametrize( + "case", ["1d", pytest.param("2d multichannel", marks=xfail_without_pywt)] +) +@pytest.mark.parametrize( + "dtype", + [np.float16, np.float32, np.float64, np.int16, np.uint8], +) +@pytest.mark.parametrize( + "convert2ycbcr", [True, pytest.param(False, marks=xfail_without_pywt)] +) +@pytest.mark.parametrize( + "estimate_sigma", [pytest.param(True, marks=xfail_without_pywt), False] +) +def test_wavelet_denoising_scaling(case, dtype, convert2ycbcr, estimate_sigma): + """Test cases for images without prescaling via img_as_float.""" + rstate = np.random.default_rng(1234) + + if case == '1d': + # 1D single-channel in range [0, 255] + x = np.linspace(0, 255, 1024) + elif case == '2d multichannel': + # 2D multichannel in range [0, 255] + x = data.astronaut()[:64, :64] + x = x.astype(dtype) + + # add noise and clip to original signal range + sigma = 25.0 + noisy = x + sigma * rstate.standard_normal(x.shape) + noisy = np.clip(noisy, x.min(), x.max()) + noisy = noisy.astype(x.dtype) + + channel_axis = -1 if x.shape[-1] == 3 else None + + if estimate_sigma: + sigma_est = restoration.estimate_sigma(noisy, channel_axis=channel_axis) + else: + sigma_est = None + + if convert2ycbcr and channel_axis is None: + # YCbCr requires multichannel == True + with pytest.raises(ValueError): + denoised = restoration.denoise_wavelet( + noisy, + sigma=sigma_est, + wavelet='sym4', + channel_axis=channel_axis, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + return + + denoised = restoration.denoise_wavelet( + noisy, + sigma=sigma_est, + wavelet='sym4', + channel_axis=channel_axis, + convert2ycbcr=convert2ycbcr, + rescale_sigma=True, + ) + assert denoised.dtype == _supported_float_type(noisy.dtype) + + data_range = x.max() - x.min() + psnr_noisy = peak_signal_noise_ratio(x, noisy, data_range=data_range) + clipped = np.dtype(dtype).kind != 'f' + if not clipped: + psnr_denoised = peak_signal_noise_ratio(x, denoised, data_range=data_range) + + # output's max value is not substantially smaller than x's + assert denoised.max() > 0.9 * x.max() + else: + # have to compare to x_as_float in integer input cases + x_as_float = img_as_float(x) + f_data_range = x_as_float.max() - x_as_float.min() + psnr_denoised = peak_signal_noise_ratio( + x_as_float, denoised, data_range=f_data_range + ) + + # output has been clipped to expected range + assert denoised.max() <= 1.0 + if np.dtype(dtype).kind == 'u': + assert denoised.min() >= 0 + else: + assert denoised.min() >= -1 + + assert psnr_denoised > psnr_noisy + + +@xfail_without_pywt +def test_wavelet_threshold(): + rstate = np.random.default_rng(1234) + + img = astro_gray + sigma = 0.1 + noisy = img + sigma * rstate.standard_normal(img.shape) + noisy = np.clip(noisy, 0, 1) + + # employ a single, user-specified threshold instead of BayesShrink sigmas + denoised = _wavelet_threshold(noisy, wavelet='db1', method=None, threshold=sigma) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + assert psnr_denoised > psnr_noisy + + # either method or threshold must be defined + with pytest.raises(ValueError): + _wavelet_threshold(noisy, wavelet='db1', method=None, threshold=None) + + # warns if a threshold is provided in a case where it would be ignored + with expected_warnings(["Thresholding method "]): + _wavelet_threshold(noisy, wavelet='db1', method='BayesShrink', threshold=sigma) + + +@xfail_without_pywt +@pytest.mark.parametrize( + 'rescale_sigma, method, ndim', + itertools.product([True, False], ['VisuShrink', 'BayesShrink'], range(1, 5)), +) +def test_wavelet_denoising_nd(rescale_sigma, method, ndim): + rstate = np.random.default_rng(1234) + # Generate a very simple test image + if ndim < 3: + img = 0.2 * np.ones((128,) * ndim) + else: + img = 0.2 * np.ones((16,) * ndim) + img[(slice(5, 13),) * ndim] = 0.8 + + sigma = 0.1 + noisy = img + sigma * rstate.standard_normal(img.shape) + noisy = np.clip(noisy, 0, 1) + + # Mark H. 2018.08: + # The issue arises because when ndim in [1, 2] + # ``waverecn`` calls ``_match_coeff_dims`` + # Which includes a numpy 1.15 deprecation. + # for larger number of dimensions _match_coeff_dims isn't called + # for some reason. + # Verify that SNR is improved with internally estimated sigma + denoised = restoration.denoise_wavelet( + noisy, method=method, rescale_sigma=rescale_sigma + ) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + assert psnr_denoised > psnr_noisy + + +def test_wavelet_invalid_method(): + with pytest.raises(ValueError): + restoration.denoise_wavelet( + np.ones(16), method='Unimplemented', rescale_sigma=True + ) + + +@xfail_without_pywt +@pytest.mark.parametrize('rescale_sigma', [True, False]) +def test_wavelet_denoising_levels(rescale_sigma): + rstate = np.random.default_rng(1234) + ndim = 2 + N = 256 + wavelet = 'db1' + # Generate a very simple test image + img = 0.2 * np.ones((N,) * ndim) + img[(slice(5, 13),) * ndim] = 0.8 + + sigma = 0.1 + noisy = img + sigma * rstate.standard_normal(img.shape) + noisy = np.clip(noisy, 0, 1) + + denoised = restoration.denoise_wavelet( + noisy, wavelet=wavelet, rescale_sigma=rescale_sigma + ) + denoised_1 = restoration.denoise_wavelet( + noisy, wavelet=wavelet, wavelet_levels=1, rescale_sigma=rescale_sigma + ) + psnr_noisy = peak_signal_noise_ratio(img, noisy) + psnr_denoised = peak_signal_noise_ratio(img, denoised) + psnr_denoised_1 = peak_signal_noise_ratio(img, denoised_1) + + # multi-level case should outperform single level case + assert psnr_denoised > psnr_denoised_1 > psnr_noisy + + # invalid number of wavelet levels results in a ValueError or UserWarning + max_level = pywt.dwt_max_level(np.min(img.shape), pywt.Wavelet(wavelet).dec_len) + # exceeding max_level raises a UserWarning in PyWavelets >= 1.0.0 + with expected_warnings(['all coefficients will experience boundary effects']): + restoration.denoise_wavelet( + noisy, + wavelet=wavelet, + wavelet_levels=max_level + 1, + rescale_sigma=rescale_sigma, + ) + + with pytest.raises(ValueError): + restoration.denoise_wavelet( + noisy, wavelet=wavelet, wavelet_levels=-1, rescale_sigma=rescale_sigma + ) + + +@xfail_without_pywt +def test_estimate_sigma_gray(): + rstate = np.random.default_rng(1234) + # astronaut image + img = astro_gray.copy() + sigma = 0.1 + # add noise to astronaut + img += sigma * rstate.standard_normal(img.shape) + + sigma_est = restoration.estimate_sigma(img, channel_axis=None) + assert_array_almost_equal(sigma, sigma_est, decimal=2) + + +@xfail_without_pywt +def test_estimate_sigma_masked_image(): + # Verify computation on an image with a large, noise-free border. + # (zero regions will be masked out by _sigma_est_dwt to avoid returning + # sigma = 0) + rstate = np.random.default_rng(1234) + # uniform image + img = np.zeros((128, 128)) + center_roi = (slice(32, 96), slice(32, 96)) + img[center_roi] = 0.8 + sigma = 0.1 + + img[center_roi] = sigma * rstate.standard_normal(img[center_roi].shape) + + sigma_est = restoration.estimate_sigma(img, channel_axis=None) + assert_array_almost_equal(sigma, sigma_est, decimal=1) + + +@xfail_without_pywt +@pytest.mark.parametrize('channel_axis', [0, 1, 2, -1]) +def test_estimate_sigma_color(channel_axis): + rstate = np.random.default_rng(1234) + # astronaut image + img = astro.copy() + sigma = 0.1 + # add noise to astronaut + img += sigma * rstate.standard_normal(img.shape) + img = np.moveaxis(img, -1, channel_axis) + + sigma_est = restoration.estimate_sigma( + img, channel_axis=channel_axis, average_sigmas=True + ) + assert_array_almost_equal(sigma, sigma_est, decimal=2) + + sigma_list = restoration.estimate_sigma( + img, channel_axis=channel_axis, average_sigmas=False + ) + assert_array_equal(len(sigma_list), img.shape[channel_axis]) + assert_array_almost_equal(sigma_list[0], sigma_est, decimal=2) + + if channel_axis % img.ndim == 2: + # default channel_axis=None should raise a warning about last axis size + assert_warns(UserWarning, restoration.estimate_sigma, img) + + +@xfail_without_pywt +@pytest.mark.parametrize('rescale_sigma', [True, False]) +def test_wavelet_denoising_args(rescale_sigma): + """ + Some of the functions inside wavelet denoising throw an error the wrong + arguments are passed. This protects against that and verifies that all + arguments can be passed. + """ + img = astro + noisy = img.copy() + 0.1 * np.random.standard_normal(img.shape) + + for convert2ycbcr in [True, False]: + for multichannel in [True, False]: + channel_axis = -1 if multichannel else None + if convert2ycbcr and not multichannel: + with pytest.raises(ValueError): + restoration.denoise_wavelet( + noisy, + convert2ycbcr=convert2ycbcr, + channel_axis=channel_axis, + rescale_sigma=rescale_sigma, + ) + continue + for sigma in [0.1, [0.1, 0.1, 0.1], None]: + if (not multichannel and not convert2ycbcr) or ( + isinstance(sigma, list) and not multichannel + ): + continue + restoration.denoise_wavelet( + noisy, + sigma=sigma, + convert2ycbcr=convert2ycbcr, + channel_axis=channel_axis, + rescale_sigma=rescale_sigma, + ) + + +@xfail_without_pywt +@pytest.mark.parametrize('rescale_sigma', [True, False]) +def test_denoise_wavelet_biorthogonal(rescale_sigma): + """Biorthogonal wavelets should raise a warning during thresholding.""" + img = astro_gray + assert_warns( + UserWarning, + restoration.denoise_wavelet, + img, + wavelet='bior2.2', + channel_axis=None, + rescale_sigma=rescale_sigma, + ) + + +@xfail_without_pywt +@pytest.mark.parametrize('channel_axis', [-1, None]) +@pytest.mark.parametrize('rescale_sigma', [True, False]) +def test_cycle_spinning_multichannel(rescale_sigma, channel_axis): + sigma = 0.1 + rstate = np.random.default_rng(1234) + + if channel_axis is not None: + img = astro + # can either omit or be 0 along the channels axis + valid_shifts = [1, (0, 1), (1, 0), (1, 1), (1, 1, 0)] + # can either omit or be 1 on channels axis. + valid_steps = [1, 2, (1, 2), (1, 2, 1)] + # too few or too many shifts or non-zero shift on channels + invalid_shifts = [(1, 1, 2), (1,), (1, 1, 0, 1)] + # too few or too many shifts or any shifts <= 0 + invalid_steps = [(1,), (1, 1, 1, 1), (0, 1), (-1, -1)] + else: + img = astro_gray + valid_shifts = [1, (0, 1), (1, 0), (1, 1)] + valid_steps = [1, 2, (1, 2)] + invalid_shifts = [(1, 1, 2), (1,)] + invalid_steps = [(1,), (1, 1, 1), (0, 1), (-1, -1)] + + noisy = img.copy() + 0.1 * rstate.standard_normal(img.shape) + + denoise_func = restoration.denoise_wavelet + func_kw = dict(sigma=sigma, channel_axis=channel_axis, rescale_sigma=rescale_sigma) + + # max_shifts=0 is equivalent to just calling denoise_func + with expected_warnings([DASK_NOT_INSTALLED_WARNING]): + dn_cc = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=0, + func_kw=func_kw, + channel_axis=channel_axis, + ) + dn = denoise_func(noisy, **func_kw) + assert_array_equal(dn, dn_cc) + + # denoising with cycle spinning will give better PSNR than without + for max_shifts in valid_shifts: + with expected_warnings([DASK_NOT_INSTALLED_WARNING]): + dn_cc = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=max_shifts, + func_kw=func_kw, + channel_axis=channel_axis, + ) + psnr = peak_signal_noise_ratio(img, dn) + psnr_cc = peak_signal_noise_ratio(img, dn_cc) + assert psnr_cc > psnr + + for shift_steps in valid_steps: + with expected_warnings([DASK_NOT_INSTALLED_WARNING]): + dn_cc = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=2, + shift_steps=shift_steps, + func_kw=func_kw, + channel_axis=channel_axis, + ) + psnr = peak_signal_noise_ratio(img, dn) + psnr_cc = peak_signal_noise_ratio(img, dn_cc) + assert psnr_cc > psnr + + for max_shifts in invalid_shifts: + with pytest.raises(ValueError): + dn_cc = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=max_shifts, + func_kw=func_kw, + channel_axis=channel_axis, + ) + for shift_steps in invalid_steps: + with pytest.raises(ValueError): + dn_cc = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=2, + shift_steps=shift_steps, + func_kw=func_kw, + channel_axis=channel_axis, + ) + + +@xfail_without_pywt +def test_cycle_spinning_num_workers(): + img = astro_gray + sigma = 0.1 + rstate = np.random.default_rng(1234) + noisy = img.copy() + 0.1 * rstate.standard_normal(img.shape) + + denoise_func = restoration.denoise_wavelet + func_kw = dict(sigma=sigma, channel_axis=-1, rescale_sigma=True) + + # same results are expected whether using 1 worker or multiple workers + dn_cc1 = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=1, + func_kw=func_kw, + channel_axis=None, + num_workers=1, + ) + + # Repeat dn_cc1 computation, but without channel_axis specified to + # verify that the default behavior is channel_axis=None + dn_cc1_ = restoration.cycle_spin( + noisy, denoise_func, max_shifts=1, func_kw=func_kw, num_workers=1 + ) + assert_array_equal(dn_cc1, dn_cc1_) + + with expected_warnings([DASK_NOT_INSTALLED_WARNING]): + dn_cc2 = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=1, + func_kw=func_kw, + channel_axis=None, + num_workers=4, + ) + dn_cc3 = restoration.cycle_spin( + noisy, + denoise_func, + max_shifts=1, + func_kw=func_kw, + channel_axis=None, + num_workers=None, + ) + assert_array_almost_equal(dn_cc1, dn_cc2) + assert_array_almost_equal(dn_cc1, dn_cc3) diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_inpaint.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..bb8ee38b2dc67199d2fcd513fa7f67f543093db7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_inpaint.py @@ -0,0 +1,186 @@ +import numpy as np + +from skimage import data, img_as_float +from skimage._shared import testing +from skimage._shared.testing import assert_allclose +from skimage._shared.utils import _supported_float_type +from skimage.color import rgb2gray +from skimage.metrics import mean_squared_error, normalized_root_mse +from skimage.morphology import binary_dilation, disk +from skimage.restoration import inpaint + + +@testing.parametrize('dtype', [np.float16, np.float32, np.float64]) +@testing.parametrize('split_into_regions', [False, True]) +def test_inpaint_biharmonic_2d(dtype, split_into_regions): + img = np.tile(np.square(np.linspace(0, 1, 5, dtype=dtype)), (5, 1)) + mask = np.zeros_like(img) + mask[2, 2:] = 1 + mask[1, 3:] = 1 + mask[0, 4:] = 1 + img[np.where(mask)] = 0 + out = inpaint.inpaint_biharmonic(img, mask, split_into_regions=split_into_regions) + assert out.dtype == _supported_float_type(img.dtype) + + ref = np.array( + [ + [0.0, 0.0625, 0.25000000, 0.5625000, 0.73925058], + [0.0, 0.0625, 0.25000000, 0.5478048, 0.76557821], + [0.0, 0.0625, 0.25842878, 0.5623079, 0.85927796], + [0.0, 0.0625, 0.25000000, 0.5625000, 1.00000000], + [0.0, 0.0625, 0.25000000, 0.5625000, 1.00000000], + ] + ) + rtol = 1e-7 if dtype == np.float64 else 1e-6 + assert_allclose(ref, out, rtol=rtol) + + +@testing.parametrize('channel_axis', [0, 1, -1]) +def test_inpaint_biharmonic_2d_color(channel_axis): + img = img_as_float(data.astronaut()[:64, :64]) + + mask = np.zeros(img.shape[:2], dtype=bool) + mask[8:16, :16] = 1 + img_defect = img * ~mask[..., np.newaxis] + mse_defect = mean_squared_error(img, img_defect) + + img_defect = np.moveaxis(img_defect, -1, channel_axis) + img_restored = inpaint.inpaint_biharmonic( + img_defect, mask, channel_axis=channel_axis + ) + img_restored = np.moveaxis(img_restored, channel_axis, -1) + mse_restored = mean_squared_error(img, img_restored) + + assert mse_restored < 0.01 * mse_defect + + +@testing.parametrize('dtype', [np.float32, np.float64]) +def test_inpaint_biharmonic_2d_float_dtypes(dtype): + img = np.tile(np.square(np.linspace(0, 1, 5)), (5, 1)) + mask = np.zeros_like(img) + mask[2, 2:] = 1 + mask[1, 3:] = 1 + mask[0, 4:] = 1 + img[np.where(mask)] = 0 + img = img.astype(dtype, copy=False) + out = inpaint.inpaint_biharmonic(img, mask) + assert out.dtype == img.dtype + ref = np.array( + [ + [0.0, 0.0625, 0.25000000, 0.5625000, 0.73925058], + [0.0, 0.0625, 0.25000000, 0.5478048, 0.76557821], + [0.0, 0.0625, 0.25842878, 0.5623079, 0.85927796], + [0.0, 0.0625, 0.25000000, 0.5625000, 1.00000000], + [0.0, 0.0625, 0.25000000, 0.5625000, 1.00000000], + ] + ) + assert_allclose(ref, out, rtol=1e-5) + + +@testing.parametrize('split_into_regions', [False, True]) +def test_inpaint_biharmonic_3d(split_into_regions): + img = np.tile(np.square(np.linspace(0, 1, 5)), (5, 1)) + img = np.dstack((img, img.T)) + mask = np.zeros_like(img) + mask[2, 2:, :] = 1 + mask[1, 3:, :] = 1 + mask[0, 4:, :] = 1 + img[np.where(mask)] = 0 + out = inpaint.inpaint_biharmonic(img, mask, split_into_regions=split_into_regions) + ref = np.dstack( + ( + np.array( + [ + [0.0000, 0.0625, 0.25000000, 0.56250000, 0.53752796], + [0.0000, 0.0625, 0.25000000, 0.44443780, 0.53762210], + [0.0000, 0.0625, 0.23693666, 0.46621112, 0.68615592], + [0.0000, 0.0625, 0.25000000, 0.56250000, 1.00000000], + [0.0000, 0.0625, 0.25000000, 0.56250000, 1.00000000], + ] + ), + np.array( + [ + [0.0000, 0.0000, 0.00000000, 0.00000000, 0.19621902], + [0.0625, 0.0625, 0.06250000, 0.17470756, 0.30140091], + [0.2500, 0.2500, 0.27241289, 0.35155440, 0.43068654], + [0.5625, 0.5625, 0.56250000, 0.56250000, 0.56250000], + [1.0000, 1.0000, 1.00000000, 1.00000000, 1.00000000], + ] + ), + ) + ) + assert_allclose(ref, out) + + +def test_invalid_input(): + img, mask = np.zeros([]), np.zeros([]) + with testing.raises(ValueError): + inpaint.inpaint_biharmonic(img, mask) + + img, mask = np.zeros((2, 2)), np.zeros((4, 1)) + with testing.raises(ValueError): + inpaint.inpaint_biharmonic(img, mask) + + img = np.ma.array(np.zeros((2, 2)), mask=[[0, 0], [0, 0]]) + mask = np.zeros((2, 2)) + with testing.raises(TypeError): + inpaint.inpaint_biharmonic(img, mask) + + +@testing.parametrize('dtype', [np.uint8, np.float32, np.float64]) +@testing.parametrize('order', ['C', 'F']) +@testing.parametrize('channel_axis', [None, -1]) +@testing.parametrize('split_into_regions', [False, True]) +def test_inpaint_nrmse(dtype, order, channel_axis, split_into_regions): + image_orig = data.astronaut()[:, :200] + float_dtype = np.float32 if dtype == np.float32 else np.float64 + image_orig = image_orig.astype(float_dtype, copy=False) + + # Create mask with six block defect regions + mask = np.zeros(image_orig.shape[:-1], dtype=bool) + mask[20:50, 3:20] = 1 + mask[165:180, 90:155] = 1 + mask[40:60, 170:195] = 1 + mask[-60:-40, 170:195] = 1 + mask[-180:-165, 90:155] = 1 + mask[-50:-20, :20] = 1 + + # add a few long, narrow defects + mask[200:205, -200:] = 1 + mask[150:255, 20:22] = 1 + mask[365:368, 60:130] = 1 + + # add randomly positioned small point-like defects + rstate = np.random.default_rng(0) + for radius in [0, 2, 4]: + # larger defects are less common + thresh = 3.25 + 0.25 * radius # larger defects less common + tmp_mask = rstate.standard_normal(image_orig.shape[:-1]) > thresh + if radius > 0: + tmp_mask = binary_dilation(tmp_mask, disk(radius, dtype=bool)) + mask[tmp_mask] = 1 + + # Defect image over the same region in each color channel + image_defect = image_orig.copy() + for layer in range(image_defect.shape[-1]): + image_defect[np.where(mask)] = 0 + + if channel_axis is None: + image_orig = rgb2gray(image_orig) + image_defect = rgb2gray(image_defect) + + image_orig = image_orig.astype(dtype, copy=False) + image_defect = image_defect.astype(dtype, copy=False) + + image_defect = np.asarray(image_defect, order=order) + image_result = inpaint.inpaint_biharmonic( + image_defect, + mask, + channel_axis=channel_axis, + split_into_regions=split_into_regions, + ) + assert image_result.dtype == float_dtype + + nrmse_defect = normalized_root_mse(image_orig, image_defect) + nrmse_result = normalized_root_mse(img_as_float(image_orig), image_result) + assert nrmse_result < 0.2 * nrmse_defect diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_j_invariant.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_j_invariant.py new file mode 100644 index 0000000000000000000000000000000000000000..61911d8ead21e247b66606eb06994ca173ba1793 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_j_invariant.py @@ -0,0 +1,98 @@ +import functools +import numpy as np +import pytest + +from skimage._shared.testing import assert_ +from skimage._shared.utils import _supported_float_type +from skimage.data import binary_blobs +from skimage.data import camera, chelsea +from skimage.metrics import mean_squared_error as mse +from skimage.restoration import calibrate_denoiser, denoise_wavelet +from skimage.restoration.j_invariant import denoise_invariant +from skimage.util import img_as_float, random_noise +from skimage.restoration.tests.test_denoise import xfail_without_pywt + +test_img = img_as_float(camera()) +test_img_color = img_as_float(chelsea()) +test_img_3d = img_as_float(binary_blobs(64, n_dim=3)) / 2 +noisy_img = random_noise(test_img, mode='gaussian', var=0.01) +noisy_img_color = random_noise(test_img_color, mode='gaussian', var=0.01) +noisy_img_3d = random_noise(test_img_3d, mode='gaussian', var=0.1) + +_denoise_wavelet = functools.partial(denoise_wavelet, rescale_sigma=True) + + +@xfail_without_pywt +def test_invariant_denoise(): + denoised_img = denoise_invariant(noisy_img, _denoise_wavelet) + + denoised_mse = mse(denoised_img, test_img) + original_mse = mse(noisy_img, test_img) + assert_(denoised_mse < original_mse) + + +@xfail_without_pywt +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_invariant_denoise_color(dtype): + denoised_img_color = denoise_invariant( + noisy_img_color.astype(dtype), + _denoise_wavelet, + denoiser_kwargs=dict(channel_axis=-1), + ) + denoised_mse = mse(denoised_img_color, test_img_color) + original_mse = mse(noisy_img_color, test_img_color) + assert denoised_mse < original_mse + assert denoised_img_color.dtype == _supported_float_type(dtype) + + +@xfail_without_pywt +def test_invariant_denoise_3d(): + denoised_img_3d = denoise_invariant(noisy_img_3d, _denoise_wavelet) + + denoised_mse = mse(denoised_img_3d, test_img_3d) + original_mse = mse(noisy_img_3d, test_img_3d) + assert_(denoised_mse < original_mse) + + +@xfail_without_pywt +def test_calibrate_denoiser_extra_output(): + parameter_ranges = {'sigma': np.linspace(0.1, 1, 5) / 2} + _, (parameters_tested, losses) = calibrate_denoiser( + noisy_img, + _denoise_wavelet, + denoise_parameters=parameter_ranges, + extra_output=True, + ) + + all_denoised = [ + denoise_invariant(noisy_img, _denoise_wavelet, denoiser_kwargs=denoiser_kwargs) + for denoiser_kwargs in parameters_tested + ] + + ground_truth_losses = [mse(img, test_img) for img in all_denoised] + assert_(np.argmin(losses) == np.argmin(ground_truth_losses)) + + +@xfail_without_pywt +def test_calibrate_denoiser(): + parameter_ranges = {'sigma': np.linspace(0.1, 1, 5) / 2} + + denoiser = calibrate_denoiser( + noisy_img, _denoise_wavelet, denoise_parameters=parameter_ranges + ) + + denoised_mse = mse(denoiser(noisy_img), test_img) + original_mse = mse(noisy_img, test_img) + assert_(denoised_mse < original_mse) + + +@xfail_without_pywt +def test_input_image_not_modified(): + input_image = noisy_img.copy() + + parameter_ranges = {'sigma': np.random.random(5) / 2} + calibrate_denoiser( + input_image, _denoise_wavelet, denoise_parameters=parameter_ranges + ) + + assert_(np.all(noisy_img == input_image)) diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_restoration.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_restoration.py new file mode 100644 index 0000000000000000000000000000000000000000..6b7de49cde8391d38c813832eb3bd8c78b4553d6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_restoration.py @@ -0,0 +1,182 @@ +import numpy as np +import pytest +from scipy import ndimage as ndi +from scipy.signal import convolve2d, convolve + +from skimage import restoration, util +from skimage._shared import filters +from skimage._shared.testing import fetch +from skimage._shared.utils import _supported_float_type +from skimage.color import rgb2gray +from skimage.data import astronaut, camera +from skimage.restoration import uft + + +test_img = util.img_as_float(camera()) + + +def _get_rtol_atol(dtype): + rtol = 1e-3 + atol = 0 + if dtype == np.float16: + rtol = 1e-2 + atol = 1e-3 + elif dtype == np.float32: + atol = 1e-5 + return rtol, atol + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_wiener(dtype, ndim): + """ + currently only performs pixelwise comparison to + precomputed result in 2d case. + """ + + rng = np.random.RandomState(0) + psf = np.ones([5] * ndim, dtype=dtype) / 5**ndim + + # for ndim == 2 use camera (to compare to presaved result) + if ndim != 2: + test_img = rng.randint(0, 100, [50] * ndim) + else: + test_img = util.img_as_float(camera()) + + data = convolve(test_img, psf, 'same') + data += 0.1 * data.std() * rng.standard_normal(data.shape) + data = data.astype(dtype, copy=False) + deconvolved = restoration.wiener(data, psf, 0.05) + assert deconvolved.dtype == _supported_float_type(dtype) + + if ndim == 2: + rtol, atol = _get_rtol_atol(dtype) + path = fetch('restoration/tests/camera_wiener.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=rtol, atol=atol) + + _, laplacian = uft.laplacian(ndim, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + assert otf.real.dtype == _supported_float_type(dtype) + deconvolved = restoration.wiener(data, otf, 0.05, reg=laplacian, is_real=False) + assert deconvolved.real.dtype == _supported_float_type(dtype) + if ndim == 2: + np.testing.assert_allclose( + np.real(deconvolved), np.load(path), rtol=rtol, atol=atol + ) + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_unsupervised_wiener(dtype): + psf = np.ones((5, 5), dtype=dtype) / 25 + data = convolve2d(test_img, psf, 'same') + seed = 16829302 + # keep old-style RandomState here for compatibility with previously stored + # reference data in camera_unsup.npy and camera_unsup2.npy + rng = np.random.RandomState(seed) + data += 0.1 * data.std() * rng.standard_normal(data.shape) + data = data.astype(dtype, copy=False) + deconvolved, _ = restoration.unsupervised_wiener(data, psf, rng=seed) + restoration.unsupervised_wiener(data, psf, rng=seed) + float_type = _supported_float_type(dtype) + assert deconvolved.dtype == float_type + + rtol, atol = _get_rtol_atol(dtype) + path = fetch('restoration/tests/camera_unsup.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=rtol, atol=atol) + + _, laplacian = uft.laplacian(2, data.shape) + otf = uft.ir2tf(psf, data.shape, is_real=False) + assert otf.real.dtype == _supported_float_type(dtype) + deconvolved2 = restoration.unsupervised_wiener( + data, + otf, + reg=laplacian, + is_real=False, + user_params={ + "callback": lambda x: None, + "max_num_iter": 200, + "min_num_iter": 30, + }, + rng=seed, + )[0] + assert deconvolved2.real.dtype == float_type + path = fetch('restoration/tests/camera_unsup2.npy') + np.testing.assert_allclose( + np.real(deconvolved2), np.load(path), rtol=rtol, atol=atol + ) + + +def test_unsupervised_wiener_deprecated_user_param(): + psf = np.ones((5, 5), dtype=float) / 25 + data = convolve2d(test_img, psf, 'same') + otf = uft.ir2tf(psf, data.shape, is_real=False) + _, laplacian = uft.laplacian(2, data.shape) + restoration.unsupervised_wiener( + data, + otf, + reg=laplacian, + is_real=False, + user_params={"max_num_iter": 300, "min_num_iter": 30}, + rng=5, + ) + + +def test_image_shape(): + """Test that shape of output image in deconvolution is same as input. + + This addresses issue #1172. + """ + point = np.zeros((5, 5), float) + point[2, 2] = 1.0 + psf = filters.gaussian(point, sigma=1.0, mode='reflect') + # image shape: (45, 45), as reported in #1172 + image = util.img_as_float(camera()[65:165, 215:315]) # just the face + image_conv = ndi.convolve(image, psf) + deconv_sup = restoration.wiener(image_conv, psf, 1) + deconv_un = restoration.unsupervised_wiener(image_conv, psf)[0] + # test the shape + np.testing.assert_equal(image.shape, deconv_sup.shape) + np.testing.assert_equal(image.shape, deconv_un.shape) + # test the reconstruction error + sup_relative_error = np.abs(deconv_sup - image) / image + un_relative_error = np.abs(deconv_un - image) / image + np.testing.assert_array_less(np.median(sup_relative_error), 0.1) + np.testing.assert_array_less(np.median(un_relative_error), 0.1) + + +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_richardson_lucy(ndim): + psf = np.ones([5] * ndim, dtype=float) / 5**ndim + if ndim != 2: + test_img = np.random.randint(0, 100, [30] * ndim) + else: + test_img = util.img_as_float(camera()) + data = convolve(test_img, psf, 'same') + + rng = np.random.RandomState(0) + data += 0.1 * data.std() * rng.standard_normal(data.shape) + deconvolved = restoration.richardson_lucy(data, psf, num_iter=5) + + if ndim == 2: + path = fetch('restoration/tests/camera_rl.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3) + + +@pytest.mark.parametrize('dtype_image', [np.float16, np.float32, np.float64]) +@pytest.mark.parametrize('dtype_psf', [np.float32, np.float64]) +def test_richardson_lucy_filtered(dtype_image, dtype_psf): + if dtype_image == np.float64: + atol = 1e-8 + else: + atol = 1e-5 + test_img_astro = rgb2gray(astronaut()) + + psf = np.ones((5, 5), dtype=dtype_psf) / 25 + data = convolve2d(test_img_astro, psf, 'same') + data = data.astype(dtype_image, copy=False) + + deconvolved = restoration.richardson_lucy(data, psf, 5, filter_epsilon=1e-6) + assert deconvolved.dtype == _supported_float_type(data.dtype) + + path = fetch('restoration/tests/astronaut_rl.npy') + np.testing.assert_allclose(deconvolved, np.load(path), rtol=1e-3, atol=atol) diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_rolling_ball.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_rolling_ball.py new file mode 100644 index 0000000000000000000000000000000000000000..42f4e57c4526232fbbb92107df95ac454f9601f6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_rolling_ball.py @@ -0,0 +1,96 @@ +""" +Tests for Rolling Ball Filter +(skimage.restoration.rolling_ball) +""" + +import numpy as np +import pytest + +from skimage import data +from skimage.restoration._rolling_ball import rolling_ball +from skimage.restoration._rolling_ball import ellipsoid_kernel + + +@pytest.mark.parametrize( + 'dtype', [np.uint8, np.int32, np.float16, np.float32, np.float64] +) +def test_ellipsoid_const(dtype): + img = 155 * np.ones((100, 100), dtype=dtype) + kernel = ellipsoid_kernel((25, 53), 50) + background = rolling_ball(img, kernel=kernel) + assert np.allclose(img - background, np.zeros_like(img)) + assert background.dtype == img.dtype + + +def test_nan_const(): + img = 123 * np.ones((100, 100), dtype=float) + img[20, 20] = np.nan + img[50, 53] = np.nan + + kernel_shape = (10, 10) + x = np.arange(-kernel_shape[1] // 2, kernel_shape[1] // 2 + 1)[np.newaxis, :] + y = np.arange(-kernel_shape[0] // 2, kernel_shape[0] // 2 + 1)[:, np.newaxis] + expected_img = np.zeros_like(img) + expected_img[y + 20, x + 20] = np.nan + expected_img[y + 50, x + 53] = np.nan + kernel = ellipsoid_kernel(kernel_shape, 100) + background = rolling_ball(img, kernel=kernel, nansafe=True) + assert np.allclose(img - background, expected_img, equal_nan=True) + + +@pytest.mark.parametrize("radius", [1, 2.5, 10.346, 50]) +def test_const_image(radius): + # infinite plane light source at top left corner + img = 23 * np.ones((100, 100), dtype=np.uint8) + background = rolling_ball(img, radius=radius) + assert np.allclose(img - background, np.zeros_like(img)) + + +def test_radial_gradient(): + # spot light source at top left corner + spot_radius = 50 + x, y = np.meshgrid(range(5), range(5)) + img = np.sqrt(np.clip(spot_radius**2 - y**2 - x**2, 0, None)) + + background = rolling_ball(img, radius=5) + assert np.allclose(img - background, np.zeros_like(img)) + + +def test_linear_gradient(): + # linear light source centered at top left corner + x, y = np.meshgrid(range(100), range(100)) + img = y * 20 + x * 20 + + expected_img = 19 * np.ones_like(img) + expected_img[0, 0] = 0 + + background = rolling_ball(img, radius=1) + assert np.allclose(img - background, expected_img) + + +@pytest.mark.parametrize("radius", [2, 10, 12.5, 50]) +def test_preserve_peaks(radius): + x, y = np.meshgrid(range(100), range(100)) + img = 0 * x + 0 * y + 10 + img[10, 10] = 20 + img[20, 20] = 35 + img[45, 26] = 156 + + expected_img = img - 10 + background = rolling_ball(img, radius=radius) + assert np.allclose(img - background, expected_img) + + +@pytest.mark.parametrize("num_threads", [None, 1, 2]) +def test_threads(num_threads): + # not testing if we use multiple threads + # just checking if the API throws an exception + img = 23 * np.ones((100, 100), dtype=np.uint8) + rolling_ball(img, radius=10, num_threads=num_threads) + rolling_ball(img, radius=10, nansafe=True, num_threads=num_threads) + + +def test_ndim(): + image = data.cells3d()[:5, 1, ...] + kernel = ellipsoid_kernel((3, 100, 100), 100) + rolling_ball(image, kernel=kernel) diff --git a/lib/python3.10/site-packages/skimage/restoration/tests/test_unwrap.py b/lib/python3.10/site-packages/skimage/restoration/tests/test_unwrap.py new file mode 100644 index 0000000000000000000000000000000000000000..0039fc6617fc0a4343d83bc01890feb3b3ce1c25 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/tests/test_unwrap.py @@ -0,0 +1,236 @@ +import numpy as np +from skimage.restoration import unwrap_phase +import sys + +from skimage._shared import testing +from skimage._shared.testing import ( + assert_array_almost_equal_nulp, + assert_almost_equal, + assert_array_equal, + assert_, + skipif, +) +from skimage._shared._warnings import expected_warnings + + +def assert_phase_almost_equal(a, b, *args, **kwargs): + """An assert_almost_equal insensitive to phase shifts of n*2*pi.""" + shift = 2 * np.pi * np.round((b.mean() - a.mean()) / (2 * np.pi)) + with expected_warnings( + [r'invalid value encountered|\A\Z', r'divide by zero encountered|\A\Z'] + ): + print('assert_phase_allclose, abs', np.max(np.abs(a - (b - shift)))) + print('assert_phase_allclose, rel', np.max(np.abs((a - (b - shift)) / a))) + if np.ma.isMaskedArray(a): + assert_(np.ma.isMaskedArray(b)) + assert_array_equal(a.mask, b.mask) + assert_(a.fill_value == b.fill_value) + au = np.asarray(a) + bu = np.asarray(b) + with expected_warnings( + [r'invalid value encountered|\A\Z', r'divide by zero encountered|\A\Z'] + ): + print( + 'assert_phase_allclose, no mask, abs', np.max(np.abs(au - (bu - shift))) + ) + print( + 'assert_phase_allclose, no mask, rel', + np.max(np.abs((au - (bu - shift)) / au)), + ) + assert_array_almost_equal_nulp(a + shift, b, *args, **kwargs) + + +def check_unwrap(image, mask=None): + image_wrapped = np.angle(np.exp(1j * image)) + if mask is not None: + print('Testing a masked image') + image = np.ma.array(image, mask=mask, fill_value=0.5) + image_wrapped = np.ma.array(image_wrapped, mask=mask, fill_value=0.5) + image_unwrapped = unwrap_phase(image_wrapped, rng=0) + assert_phase_almost_equal(image_unwrapped, image) + + +def test_unwrap_1d(): + image = np.linspace(0, 10 * np.pi, 100) + check_unwrap(image) + # Masked arrays are not allowed in 1D + with testing.raises(ValueError): + check_unwrap(image, True) + # wrap_around is not allowed in 1D + with testing.raises(ValueError): + unwrap_phase(image, True, rng=0) + + +@testing.parametrize("check_with_mask", (False, True)) +def test_unwrap_2d(check_with_mask): + mask = None + x, y = np.ogrid[:8, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1) + if check_with_mask: + mask = np.zeros(image.shape, dtype=bool) + mask[4:6, 4:8] = True + check_unwrap(image, mask) + + +@testing.parametrize("check_with_mask", (False, True)) +def test_unwrap_3d(check_with_mask): + mask = None + x, y, z = np.ogrid[:8, :12, :16] + image = 2 * np.pi * (x * 0.2 + y * 0.1 + z * 0.05) + if check_with_mask: + mask = np.zeros(image.shape, dtype=bool) + mask[4:6, 4:6, 1:3] = True + check_unwrap(image, mask) + + +def check_wrap_around(ndim, axis): + # create a ramp, but with the last pixel along axis equalling the first + elements = 100 + ramp = np.linspace(0, 12 * np.pi, elements) + ramp[-1] = ramp[0] + image = ramp.reshape(tuple([elements if n == axis else 1 for n in range(ndim)])) + image_wrapped = np.angle(np.exp(1j * image)) + + index_first = tuple([0] * ndim) + index_last = tuple([-1 if n == axis else 0 for n in range(ndim)]) + # unwrap the image without wrap around + # We do not want warnings about length 1 dimensions + with expected_warnings([r'Image has a length 1 dimension|\A\Z']): + image_unwrap_no_wrap_around = unwrap_phase(image_wrapped, rng=0) + print( + 'endpoints without wrap_around:', + image_unwrap_no_wrap_around[index_first], + image_unwrap_no_wrap_around[index_last], + ) + # without wrap around, the endpoints of the image should differ + assert_( + abs( + image_unwrap_no_wrap_around[index_first] + - image_unwrap_no_wrap_around[index_last] + ) + > np.pi + ) + # unwrap the image with wrap around + wrap_around = [n == axis for n in range(ndim)] + # We do not want warnings about length 1 dimensions + with expected_warnings([r'Image has a length 1 dimension.|\A\Z']): + image_unwrap_wrap_around = unwrap_phase(image_wrapped, wrap_around, rng=0) + print( + 'endpoints with wrap_around:', + image_unwrap_wrap_around[index_first], + image_unwrap_wrap_around[index_last], + ) + # with wrap around, the endpoints of the image should be equal + assert_almost_equal( + image_unwrap_wrap_around[index_first], image_unwrap_wrap_around[index_last] + ) + + +dim_axis = [(ndim, axis) for ndim in (2, 3) for axis in range(ndim)] + + +@skipif( + sys.version_info[:2] == (3, 4), + reason="Doesn't work with python 3.4. See issue #3079", +) +@testing.parametrize("ndim, axis", dim_axis) +def test_wrap_around(ndim, axis): + check_wrap_around(ndim, axis) + + +def test_mask(): + length = 100 + ramps = [ + np.linspace(0, 4 * np.pi, length), + np.linspace(0, 8 * np.pi, length), + np.linspace(0, 6 * np.pi, length), + ] + image = np.vstack(ramps) + mask_1d = np.ones((length,), dtype=bool) + mask_1d[0] = mask_1d[-1] = False + for i in range(len(ramps)): + # mask all ramps but the i'th one + mask = np.zeros(image.shape, dtype=bool) + mask |= mask_1d.reshape(1, -1) + mask[i, :] = False # unmask i'th ramp + image_wrapped = np.ma.array(np.angle(np.exp(1j * image)), mask=mask) + image_unwrapped = unwrap_phase(image_wrapped) + image_unwrapped -= image_unwrapped[0, 0] # remove phase shift + # The end of the unwrapped array should have value equal to the + # endpoint of the unmasked ramp + assert_array_almost_equal_nulp(image_unwrapped[:, -1], image[i, -1]) + assert_(np.ma.isMaskedArray(image_unwrapped)) + + # Same tests, but forcing use of the 3D unwrapper by reshaping + with expected_warnings(['length 1 dimension']): + shape = (1,) + image_wrapped.shape + image_wrapped_3d = image_wrapped.reshape(shape) + image_unwrapped_3d = unwrap_phase(image_wrapped_3d) + # remove phase shift + image_unwrapped_3d -= image_unwrapped_3d[0, 0, 0] + assert_array_almost_equal_nulp(image_unwrapped_3d[:, :, -1], image[i, -1]) + + +def test_invalid_input(): + with testing.raises(ValueError): + unwrap_phase(np.zeros([])) + with testing.raises(ValueError): + unwrap_phase(np.zeros((1, 1, 1, 1))) + with testing.raises(ValueError): + unwrap_phase(np.zeros((1, 1)), 3 * [False]) + with testing.raises(ValueError): + unwrap_phase(np.zeros((1, 1)), 'False') + + +def test_unwrap_3d_middle_wrap_around(): + # Segmentation fault in 3D unwrap phase with middle dimension connected + # GitHub issue #1171 + image = np.zeros((20, 30, 40), dtype=np.float32) + unwrap = unwrap_phase(image, wrap_around=[False, True, False]) + assert_(np.all(unwrap == 0)) + + +def test_unwrap_2d_compressed_mask(): + # ValueError when image is masked array with a compressed mask (no masked + # elements). GitHub issue #1346 + image = np.ma.zeros((10, 10)) + unwrap = unwrap_phase(image) + assert_(np.all(unwrap == 0)) + + +def test_unwrap_2d_all_masked(): + # Segmentation fault when image is masked array with a all elements masked + # GitHub issue #1347 + # all elements masked + image = np.ma.zeros((10, 10)) + image[:] = np.ma.masked + unwrap = unwrap_phase(image) + assert_(np.ma.isMaskedArray(unwrap)) + assert_(np.all(unwrap.mask)) + + # 1 unmasked element, still zero edges + image = np.ma.zeros((10, 10)) + image[:] = np.ma.masked + image[0, 0] = 0 + unwrap = unwrap_phase(image) + assert_(np.ma.isMaskedArray(unwrap)) + assert_(np.sum(unwrap.mask) == 99) # all but one masked + assert_(unwrap[0, 0] == 0) + + +def test_unwrap_3d_all_masked(): + # all elements masked + image = np.ma.zeros((10, 10, 10)) + image[:] = np.ma.masked + unwrap = unwrap_phase(image) + assert_(np.ma.isMaskedArray(unwrap)) + assert_(np.all(unwrap.mask)) + + # 1 unmasked element, still zero edges + image = np.ma.zeros((10, 10, 10)) + image[:] = np.ma.masked + image[0, 0, 0] = 0 + unwrap = unwrap_phase(image) + assert_(np.ma.isMaskedArray(unwrap)) + assert_(np.sum(unwrap.mask) == 999) # all but one masked + assert_(unwrap[0, 0, 0] == 0) diff --git a/lib/python3.10/site-packages/skimage/restoration/uft.py b/lib/python3.10/site-packages/skimage/restoration/uft.py new file mode 100644 index 0000000000000000000000000000000000000000..db4bc16ad158070591a1ea26a940b874d79e097c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/uft.py @@ -0,0 +1,451 @@ +r"""Function of unitary fourier transform (uft) and utilities + +This module implements the unitary fourier transform, also known as +the ortho-normal transform. It is especially useful for convolution +[1], as it respects the Parseval equality. The value of the null +frequency is equal to + +.. math:: \frac{1}{\sqrt{n}} \sum_i x_i + +so the Fourier transform has the same energy as the original image +(see ``image_quad_norm`` function). The transform is applied from the +last axis for performance (assuming a C-order array input). + +References +---------- +.. [1] B. R. Hunt "A matrix theory proof of the discrete convolution + theorem", IEEE Trans. on Audio and Electroacoustics, + vol. au-19, no. 4, pp. 285-288, dec. 1971 + +""" + +import numpy as np +import scipy.fft as fft + +from .._shared.utils import _supported_float_type + + +def ufftn(inarray, dim=None): + """N-dimensional unitary Fourier transform. + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The last axis along which to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray (same shape than inarray) + The unitary N-D Fourier transform of ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = ufftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = fft.fftn(inarray, axes=range(-dim, 0), norm='ortho') + return outarray + + +def uifftn(inarray, dim=None): + """N-dimensional unitary inverse Fourier transform. + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The last axis along which to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray + The unitary inverse nD Fourier transform of ``inarray``. Has the same shape as + ``inarray``. + + Examples + -------- + >>> input = np.ones((3, 3, 3)) + >>> output = uifftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (3, 3, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = fft.ifftn(inarray, axes=range(-dim, 0), norm='ortho') + return outarray + + +def urfftn(inarray, dim=None): + """N-dimensional real unitary Fourier transform. + + This transform considers the Hermitian property of the transform on + real-valued input. + + Parameters + ---------- + inarray : ndarray, shape (M[, ...], P) + The array to transform. + dim : int, optional + The last axis along which to compute the transform. All + axes by default. + + Returns + ------- + outarray : ndarray, shape (M[, ...], P / 2 + 1) + The unitary N-D real Fourier transform of ``inarray``. + + Notes + ----- + The ``urfft`` functions assume an input array of real + values. Consequently, the output has a Hermitian property and + redundant values are not computed or returned. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = urfftn(input) + >>> np.allclose(np.sum(input) / np.sqrt(input.size), output[0, 0, 0]) + True + >>> output.shape + (5, 5, 3) + """ + if dim is None: + dim = inarray.ndim + outarray = fft.rfftn(inarray, axes=range(-dim, 0), norm='ortho') + return outarray + + +def uirfftn(inarray, dim=None, shape=None): + """N-dimensional inverse real unitary Fourier transform. + + This transform considers the Hermitian property of the transform + from complex to real input. + + Parameters + ---------- + inarray : ndarray + The array to transform. + dim : int, optional + The last axis along which to compute the transform. All + axes by default. + shape : tuple of int, optional + The shape of the output. The shape of ``rfft`` is ambiguous in + case of odd-valued input shape. In this case, this parameter + should be provided. See ``np.fft.irfftn``. + + Returns + ------- + outarray : ndarray + The unitary N-D inverse real Fourier transform of ``inarray``. + + Notes + ----- + The ``uirfft`` function assumes that the output array is + real-valued. Consequently, the input is assumed to have a Hermitian + property and redundant values are implicit. + + Examples + -------- + >>> input = np.ones((5, 5, 5)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (5, 5, 5) + """ + if dim is None: + dim = inarray.ndim + outarray = fft.irfftn(inarray, shape, axes=range(-dim, 0), norm='ortho') + return outarray + + +def ufft2(inarray): + """2-dimensional unitary Fourier transform. + + Compute the Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape as inarray) + The unitary 2-D Fourier transform of ``inarray``. + + See Also + -------- + uifft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = ufft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[1, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return ufftn(inarray, 2) + + +def uifft2(inarray): + """2-dimensional inverse unitary Fourier transform. + + Compute the inverse Fourier transform on the last 2 axes. + + Parameters + ---------- + inarray : ndarray + The array to transform. + + Returns + ------- + outarray : ndarray (same shape as inarray) + The unitary 2-D inverse Fourier transform of ``inarray``. + + See Also + -------- + uifft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uifft2(input) + >>> np.allclose(np.sum(input[1, ...]) / np.sqrt(input[1, ...].size), + ... output[0, 0, 0]) + True + >>> output.shape + (10, 128, 128) + """ + return uifftn(inarray, 2) + + +def urfft2(inarray): + """2-dimensional real unitary Fourier transform + + Compute the real Fourier transform on the last 2 axes. This + transform considers the Hermitian property of the transform from + complex to real-valued input. + + Parameters + ---------- + inarray : ndarray, shape (M[, ...], P) + The array to transform. + + Returns + ------- + outarray : ndarray, shape (M[, ...], 2 * (P - 1)) + The unitary 2-D real Fourier transform of ``inarray``. + + See Also + -------- + ufft2, ufftn, urfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = urfft2(input) + >>> np.allclose(np.sum(input[1,...]) / np.sqrt(input[1,...].size), + ... output[1, 0, 0]) + True + >>> output.shape + (10, 128, 65) + """ + return urfftn(inarray, 2) + + +def uirfft2(inarray, shape=None): + """2-dimensional inverse real unitary Fourier transform. + + Compute the real inverse Fourier transform on the last 2 axes. + This transform considers the Hermitian property of the transform + from complex to real-valued input. + + Parameters + ---------- + inarray : ndarray, shape (M[, ...], P) + The array to transform. + shape : tuple of int, optional + The shape of the output. The shape of ``rfft`` is ambiguous in + case of odd-valued input shape. In this case, this parameter + should be provided. See ``np.fft.irfftn``. + + Returns + ------- + outarray : ndarray, shape (M[, ...], 2 * (P - 1)) + The unitary 2-D inverse real Fourier transform of ``inarray``. + + See Also + -------- + urfft2, uifftn, uirfftn + + Examples + -------- + >>> input = np.ones((10, 128, 128)) + >>> output = uirfftn(urfftn(input), shape=input.shape) + >>> np.allclose(input, output) + True + >>> output.shape + (10, 128, 128) + """ + return uirfftn(inarray, 2, shape=shape) + + +def image_quad_norm(inarray): + """Return the quadratic norm of images in Fourier space. + + This function detects whether the input image satisfies the + Hermitian property. + + Parameters + ---------- + inarray : ndarray + Input image. The image data should reside in the final two + axes. + + Returns + ------- + norm : float + The quadratic norm of ``inarray``. + + Examples + -------- + >>> input = np.ones((5, 5)) + >>> image_quad_norm(ufft2(input)) == np.sum(np.abs(input)**2) + True + >>> image_quad_norm(ufft2(input)) == image_quad_norm(urfft2(input)) + True + """ + # If there is a Hermitian symmetry + if inarray.shape[-1] != inarray.shape[-2]: + return 2 * np.sum(np.sum(np.abs(inarray) ** 2, axis=-1), axis=-1) - np.sum( + np.abs(inarray[..., 0]) ** 2, axis=-1 + ) + else: + return np.sum(np.sum(np.abs(inarray) ** 2, axis=-1), axis=-1) + + +def ir2tf(imp_resp, shape, dim=None, is_real=True): + """Compute the transfer function of an impulse response (IR). + + This function makes the necessary correct zero-padding, zero + convention, correct fft2, etc... to compute the transfer function + of IR. To use with unitary Fourier transform for the signal (ufftn + or equivalent). + + Parameters + ---------- + imp_resp : ndarray + The impulse responses. + shape : tuple of int + A tuple of integer corresponding to the target shape of the + transfer function. + dim : int, optional + The last axis along which to compute the transform. All + axes by default. + is_real : boolean, optional + If True (default), imp_resp is supposed real and the Hermitian property + is used with rfftn Fourier transform. + + Returns + ------- + y : complex ndarray + The transfer function of shape ``shape``. + + See Also + -------- + ufftn, uifftn, urfftn, uirfftn + + Examples + -------- + >>> np.all(np.array([[4, 0], [0, 0]]) == ir2tf(np.ones((2, 2)), (2, 2))) + True + >>> ir2tf(np.ones((2, 2)), (512, 512)).shape == (512, 257) + True + >>> ir2tf(np.ones((2, 2)), (512, 512), is_real=False).shape == (512, 512) + True + + Notes + ----- + The input array can be composed of multiple-dimensional IR with + an arbitrary number of IR. The individual IR must be accessed + through the first axes. The last ``dim`` axes contain the space + definition. + """ + if not dim: + dim = imp_resp.ndim + # Zero padding and fill + irpadded_dtype = _supported_float_type(imp_resp.dtype) + irpadded = np.zeros(shape, dtype=irpadded_dtype) + irpadded[tuple([slice(0, s) for s in imp_resp.shape])] = imp_resp + # Roll for zero convention of the fft to avoid the phase + # problem. Work with odd and even size. + for axis, axis_size in enumerate(imp_resp.shape): + if axis >= imp_resp.ndim - dim: + irpadded = np.roll(irpadded, shift=-int(np.floor(axis_size / 2)), axis=axis) + + func = fft.rfftn if is_real else fft.fftn + out = func(irpadded, axes=(range(-dim, 0))) + + # TODO: remove .astype call once SciPy >= 1.4 is required + cplx_dtype = np.promote_types(irpadded_dtype, np.complex64) + return out.astype(cplx_dtype, copy=False) + + +def laplacian(ndim, shape, is_real=True): + """Return the transfer function of the Laplacian. + + Laplacian is the second order difference, on row and column. + + Parameters + ---------- + ndim : int + The dimension of the Laplacian. + shape : tuple + The support on which to compute the transfer function. + is_real : boolean, optional + If True (default), imp_resp is assumed to be real-valued and + the Hermitian property is used with rfftn Fourier transform + to return the transfer function. + + Returns + ------- + tf : array_like, complex + The transfer function. + impr : array_like, real + The Laplacian. + + Examples + -------- + >>> tf, ir = laplacian(2, (32, 32)) + >>> np.all(ir == np.array([[0, -1, 0], [-1, 4, -1], [0, -1, 0]])) + True + >>> np.all(tf == ir2tf(ir, (32, 32))) + True + """ + impr = np.zeros([3] * ndim) + for dim in range(ndim): + idx = tuple( + [slice(1, 2)] * dim + [slice(None)] + [slice(1, 2)] * (ndim - dim - 1) + ) + impr[idx] = np.array([-1.0, 0.0, -1.0]).reshape( + [-1 if i == dim else 1 for i in range(ndim)] + ) + impr[(slice(1, 2),) * ndim] = 2.0 * ndim + return ir2tf(impr, shape, is_real=is_real), impr diff --git a/lib/python3.10/site-packages/skimage/restoration/unwrap.py b/lib/python3.10/site-packages/skimage/restoration/unwrap.py new file mode 100644 index 0000000000000000000000000000000000000000..cb075d690a1178c9022700df33ac5674c04b3fee --- /dev/null +++ b/lib/python3.10/site-packages/skimage/restoration/unwrap.py @@ -0,0 +1,116 @@ +import numpy as np + +from .._shared.utils import warn + +from ._unwrap_1d import unwrap_1d +from ._unwrap_2d import unwrap_2d +from ._unwrap_3d import unwrap_3d + + +def unwrap_phase(image, wrap_around=False, rng=None): + '''Recover the original from a wrapped phase image. + + From an image wrapped to lie in the interval [-pi, pi), recover the + original, unwrapped image. + + Parameters + ---------- + image : (M[, N[, P]]) ndarray or masked array of floats + The values should be in the range [-pi, pi). If a masked array is + provided, the masked entries will not be changed, and their values + will not be used to guide the unwrapping of neighboring, unmasked + values. Masked 1D arrays are not allowed, and will raise a + `ValueError`. + wrap_around : bool or sequence of bool, optional + When an element of the sequence is `True`, the unwrapping process + will regard the edges along the corresponding axis of the image to be + connected and use this connectivity to guide the phase unwrapping + process. If only a single boolean is given, it will apply to all axes. + Wrap around is not supported for 1D arrays. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + Unwrapping relies on a random initialization. This sets the + PRNG to use to achieve deterministic behavior. + + Returns + ------- + image_unwrapped : array_like, double + Unwrapped image of the same shape as the input. If the input `image` + was a masked array, the mask will be preserved. + + Raises + ------ + ValueError + If called with a masked 1D array or called with a 1D array and + ``wrap_around=True``. + + Examples + -------- + >>> c0, c1 = np.ogrid[-1:1:128j, -1:1:128j] + >>> image = 12 * np.pi * np.exp(-(c0**2 + c1**2)) + >>> image_wrapped = np.angle(np.exp(1j * image)) + >>> image_unwrapped = unwrap_phase(image_wrapped) + >>> np.std(image_unwrapped - image) < 1e-6 # A constant offset is normal + True + + References + ---------- + .. [1] Miguel Arevallilo Herraez, David R. Burton, Michael J. Lalor, + and Munther A. Gdeisat, "Fast two-dimensional phase-unwrapping + algorithm based on sorting by reliability following a noncontinuous + path", Journal Applied Optics, Vol. 41, No. 35 (2002) 7437, + .. [2] Abdul-Rahman, H., Gdeisat, M., Burton, D., & Lalor, M., "Fast + three-dimensional phase-unwrapping algorithm based on sorting by + reliability following a non-continuous path. In W. Osten, + C. Gorecki, & E. L. Novak (Eds.), Optical Metrology (2005) 32--40, + International Society for Optics and Photonics. + ''' + if image.ndim not in (1, 2, 3): + raise ValueError('Image must be 1, 2, or 3 dimensional') + if isinstance(wrap_around, bool): + wrap_around = [wrap_around] * image.ndim + elif hasattr(wrap_around, '__getitem__') and not isinstance(wrap_around, str): + if len(wrap_around) != image.ndim: + raise ValueError( + 'Length of `wrap_around` must equal the ' 'dimensionality of image' + ) + wrap_around = [bool(wa) for wa in wrap_around] + else: + raise ValueError( + '`wrap_around` must be a bool or a sequence with ' + 'length equal to the dimensionality of image' + ) + if image.ndim == 1: + if np.ma.isMaskedArray(image): + raise ValueError('1D masked images cannot be unwrapped') + if wrap_around[0]: + raise ValueError('`wrap_around` is not supported for 1D images') + if image.ndim in (2, 3) and 1 in image.shape: + warn( + 'Image has a length 1 dimension. Consider using an ' + 'array of lower dimensionality to use a more efficient ' + 'algorithm' + ) + + if np.ma.isMaskedArray(image): + mask = np.require(np.ma.getmaskarray(image), np.uint8, ['C']) + else: + mask = np.zeros_like(image, dtype=np.uint8, order='C') + + image_not_masked = np.asarray(np.ma.getdata(image), dtype=np.float64, order='C') + image_unwrapped = np.empty_like(image, dtype=np.float64, order='C', subok=False) + + if image.ndim == 1: + unwrap_1d(image_not_masked, image_unwrapped) + elif image.ndim == 2: + unwrap_2d(image_not_masked, mask, image_unwrapped, wrap_around, rng) + elif image.ndim == 3: + unwrap_3d(image_not_masked, mask, image_unwrapped, wrap_around, rng) + + if np.ma.isMaskedArray(image): + return np.ma.array(image_unwrapped, mask=mask, fill_value=image.fill_value) + else: + return image_unwrapped diff --git a/lib/python3.10/site-packages/skimage/segmentation/__init__.py b/lib/python3.10/site-packages/skimage/segmentation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..319b6bde192c49f25e426e05ab2ccba97bb0b631 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/__init__.py @@ -0,0 +1,45 @@ +"""Algorithms to partition images into meaningful regions or boundaries.""" + +from ._expand_labels import expand_labels +from .random_walker_segmentation import random_walker +from .active_contour_model import active_contour +from ._felzenszwalb import felzenszwalb +from .slic_superpixels import slic +from ._quickshift import quickshift +from .boundaries import find_boundaries, mark_boundaries +from ._clear_border import clear_border +from ._join import join_segmentations, relabel_sequential +from ._watershed import watershed +from ._chan_vese import chan_vese +from .morphsnakes import ( + morphological_geodesic_active_contour, + morphological_chan_vese, + inverse_gaussian_gradient, + disk_level_set, + checkerboard_level_set, +) +from ..morphology import flood, flood_fill + + +__all__ = [ + 'expand_labels', + 'random_walker', + 'active_contour', + 'felzenszwalb', + 'slic', + 'quickshift', + 'find_boundaries', + 'mark_boundaries', + 'clear_border', + 'join_segmentations', + 'relabel_sequential', + 'watershed', + 'chan_vese', + 'morphological_geodesic_active_contour', + 'morphological_chan_vese', + 'inverse_gaussian_gradient', + 'disk_level_set', + 'checkerboard_level_set', + 'flood', + 'flood_fill', +] diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ebb75d9fe1ee7b656da13ffa9f70161beba7895 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_chan_vese.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_chan_vese.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9cd2d5b7399792cd8106ef0f6f1dc5f26f4bd739 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_chan_vese.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_clear_border.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_clear_border.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe3452c45776ed13a4e42188620a916f55ca49c1 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_clear_border.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_expand_labels.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_expand_labels.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0dc09ba334565c3bef979d4383515b3251157376 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_expand_labels.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_felzenszwalb.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_felzenszwalb.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b692d8922c314b1a22836698b1e99363e8cbb3a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_felzenszwalb.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_join.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_join.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e970b31dda984742e21f5316cfed3b5fab8260b0 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_join.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_quickshift.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_quickshift.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bf7eae1033112e60a5f428d068aef3c878ea822 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_quickshift.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_watershed.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_watershed.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a4eaa79f08a77211db757ce8fecbeea3e87376c Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/_watershed.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/active_contour_model.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/active_contour_model.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37cf76c3122b2dec0f0a654cf85fcb09fae8b736 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/active_contour_model.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/boundaries.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/boundaries.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98663813173f7d9e98a2581b8b833f9673beb53c Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/boundaries.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/morphsnakes.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/morphsnakes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3a1f0b2f28a6cb93729636770cc301f4c13f69e Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/morphsnakes.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/random_walker_segmentation.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/random_walker_segmentation.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68f3956323ebc9b7c3057c5b4316350cf18d014b Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/random_walker_segmentation.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/__pycache__/slic_superpixels.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/slic_superpixels.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b770dc1863bda13eaddb7ec019cc3c0c535a3486 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/__pycache__/slic_superpixels.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/_chan_vese.py b/lib/python3.10/site-packages/skimage/segmentation/_chan_vese.py new file mode 100644 index 0000000000000000000000000000000000000000..31c3f1e72af455fd3ba5405d768075b02212e4ff --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_chan_vese.py @@ -0,0 +1,365 @@ +import numpy as np +from scipy.ndimage import distance_transform_edt as distance + +from .._shared.utils import _supported_float_type + + +def _cv_calculate_variation(image, phi, mu, lambda1, lambda2, dt): + """Returns the variation of level set 'phi' based on algorithm parameters. + + This corresponds to equation (22) of the paper by Pascal Getreuer, + which computes the next iteration of the level set based on a current + level set. + + A full explanation regarding all the terms is beyond the scope of the + present description, but there is one difference of particular import. + In the original algorithm, convergence is accelerated, and required + memory is reduced, by using a single array. This array, therefore, is a + combination of non-updated and updated values. If this were to be + implemented in python, this would require a double loop, where the + benefits of having fewer iterations would be outweided by massively + increasing the time required to perform each individual iteration. A + similar approach is used by Rami Cohen, and it is from there that the + C1-4 notation is taken. + """ + eta = 1e-16 + P = np.pad(phi, 1, mode='edge') + + phixp = P[1:-1, 2:] - P[1:-1, 1:-1] + phixn = P[1:-1, 1:-1] - P[1:-1, :-2] + phix0 = (P[1:-1, 2:] - P[1:-1, :-2]) / 2.0 + + phiyp = P[2:, 1:-1] - P[1:-1, 1:-1] + phiyn = P[1:-1, 1:-1] - P[:-2, 1:-1] + phiy0 = (P[2:, 1:-1] - P[:-2, 1:-1]) / 2.0 + + C1 = 1.0 / np.sqrt(eta + phixp**2 + phiy0**2) + C2 = 1.0 / np.sqrt(eta + phixn**2 + phiy0**2) + C3 = 1.0 / np.sqrt(eta + phix0**2 + phiyp**2) + C4 = 1.0 / np.sqrt(eta + phix0**2 + phiyn**2) + + K = P[1:-1, 2:] * C1 + P[1:-1, :-2] * C2 + P[2:, 1:-1] * C3 + P[:-2, 1:-1] * C4 + + Hphi = (phi > 0).astype(image.dtype) + (c1, c2) = _cv_calculate_averages(image, Hphi) + + difference_from_average_term = ( + -lambda1 * (image - c1) ** 2 + lambda2 * (image - c2) ** 2 + ) + new_phi = phi + (dt * _cv_delta(phi)) * (mu * K + difference_from_average_term) + return new_phi / (1 + mu * dt * _cv_delta(phi) * (C1 + C2 + C3 + C4)) + + +def _cv_heavyside(x, eps=1.0): + """Returns the result of a regularised heavyside function of the + input value(s). + """ + return 0.5 * (1.0 + (2.0 / np.pi) * np.arctan(x / eps)) + + +def _cv_delta(x, eps=1.0): + """Returns the result of a regularised dirac function of the + input value(s). + """ + return eps / (eps**2 + x**2) + + +def _cv_calculate_averages(image, Hphi): + """Returns the average values 'inside' and 'outside'.""" + H = Hphi + Hinv = 1.0 - H + Hsum = np.sum(H) + Hinvsum = np.sum(Hinv) + avg_inside = np.sum(image * H) + avg_oustide = np.sum(image * Hinv) + if Hsum != 0: + avg_inside /= Hsum + if Hinvsum != 0: + avg_oustide /= Hinvsum + return (avg_inside, avg_oustide) + + +def _cv_difference_from_average_term(image, Hphi, lambda_pos, lambda_neg): + """Returns the 'energy' contribution due to the difference from + the average value within a region at each point. + """ + (c1, c2) = _cv_calculate_averages(image, Hphi) + Hinv = 1.0 - Hphi + return lambda_pos * (image - c1) ** 2 * Hphi + lambda_neg * (image - c2) ** 2 * Hinv + + +def _cv_edge_length_term(phi, mu): + """Returns the 'energy' contribution due to the length of the + edge between regions at each point, multiplied by a factor 'mu'. + """ + P = np.pad(phi, 1, mode='edge') + fy = (P[2:, 1:-1] - P[:-2, 1:-1]) / 2.0 + fx = (P[1:-1, 2:] - P[1:-1, :-2]) / 2.0 + return mu * _cv_delta(phi) * np.sqrt(fx**2 + fy**2) + + +def _cv_energy(image, phi, mu, lambda1, lambda2): + """Returns the total 'energy' of the current level set function. + + This corresponds to equation (7) of the paper by Pascal Getreuer, + which is the weighted sum of the following: + (A) the length of the contour produced by the zero values of the + level set, + (B) the area of the "foreground" (area of the image where the + level set is positive), + (C) the variance of the image inside the foreground, + (D) the variance of the image outside of the foreground + + Each value is computed for each pixel, and then summed. The weight + of (B) is set to 0 in this implementation. + """ + H = _cv_heavyside(phi) + avgenergy = _cv_difference_from_average_term(image, H, lambda1, lambda2) + lenenergy = _cv_edge_length_term(phi, mu) + return np.sum(avgenergy) + np.sum(lenenergy) + + +def _cv_reset_level_set(phi): + """This is a placeholder function as resetting the level set is not + strictly necessary, and has not been done for this implementation. + """ + return phi + + +def _cv_checkerboard(image_size, square_size, dtype=np.float64): + """Generates a checkerboard level set function. + + According to Pascal Getreuer, such a level set function has fast + convergence. + """ + yv = np.arange(image_size[0], dtype=dtype).reshape(image_size[0], 1) + xv = np.arange(image_size[1], dtype=dtype) + sf = np.pi / square_size + xv *= sf + yv *= sf + return np.sin(yv) * np.sin(xv) + + +def _cv_large_disk(image_size): + """Generates a disk level set function. + + The disk covers the whole image along its smallest dimension. + """ + res = np.ones(image_size) + centerY = int((image_size[0] - 1) / 2) + centerX = int((image_size[1] - 1) / 2) + res[centerY, centerX] = 0.0 + radius = float(min(centerX, centerY)) + return (radius - distance(res)) / radius + + +def _cv_small_disk(image_size): + """Generates a disk level set function. + + The disk covers half of the image along its smallest dimension. + """ + res = np.ones(image_size) + centerY = int((image_size[0] - 1) / 2) + centerX = int((image_size[1] - 1) / 2) + res[centerY, centerX] = 0.0 + radius = float(min(centerX, centerY)) / 2.0 + return (radius - distance(res)) / (radius * 3) + + +def _cv_init_level_set(init_level_set, image_shape, dtype=np.float64): + """Generates an initial level set function conditional on input arguments.""" + if isinstance(init_level_set, str): + if init_level_set == 'checkerboard': + res = _cv_checkerboard(image_shape, 5, dtype) + elif init_level_set == 'disk': + res = _cv_large_disk(image_shape) + elif init_level_set == 'small disk': + res = _cv_small_disk(image_shape) + else: + raise ValueError("Incorrect name for starting level set preset.") + else: + res = init_level_set + return res.astype(dtype, copy=False) + + +def chan_vese( + image, + mu=0.25, + lambda1=1.0, + lambda2=1.0, + tol=1e-3, + max_num_iter=500, + dt=0.5, + init_level_set='checkerboard', + extended_output=False, +): + """Chan-Vese segmentation algorithm. + + Active contour model by evolving a level set. Can be used to + segment objects without clearly defined boundaries. + + Parameters + ---------- + image : (M, N) ndarray + Grayscale image to be segmented. + mu : float, optional + 'edge length' weight parameter. Higher `mu` values will + produce a 'round' edge, while values closer to zero will + detect smaller objects. + lambda1 : float, optional + 'difference from average' weight parameter for the output + region with value 'True'. If it is lower than `lambda2`, this + region will have a larger range of values than the other. + lambda2 : float, optional + 'difference from average' weight parameter for the output + region with value 'False'. If it is lower than `lambda1`, this + region will have a larger range of values than the other. + tol : float, positive, optional + Level set variation tolerance between iterations. If the + L2 norm difference between the level sets of successive + iterations normalized by the area of the image is below this + value, the algorithm will assume that the solution was + reached. + max_num_iter : uint, optional + Maximum number of iterations allowed before the algorithm + interrupts itself. + dt : float, optional + A multiplication factor applied at calculations for each step, + serves to accelerate the algorithm. While higher values may + speed up the algorithm, they may also lead to convergence + problems. + init_level_set : str or (M, N) ndarray, optional + Defines the starting level set used by the algorithm. + If a string is inputted, a level set that matches the image + size will automatically be generated. Alternatively, it is + possible to define a custom level set, which should be an + array of float values, with the same shape as 'image'. + Accepted string values are as follows. + + 'checkerboard' + the starting level set is defined as + sin(x/5*pi)*sin(y/5*pi), where x and y are pixel + coordinates. This level set has fast convergence, but may + fail to detect implicit edges. + 'disk' + the starting level set is defined as the opposite + of the distance from the center of the image minus half of + the minimum value between image width and image height. + This is somewhat slower, but is more likely to properly + detect implicit edges. + 'small disk' + the starting level set is defined as the + opposite of the distance from the center of the image + minus a quarter of the minimum value between image width + and image height. + extended_output : bool, optional + If set to True, the return value will be a tuple containing + the three return values (see below). If set to False which + is the default value, only the 'segmentation' array will be + returned. + + Returns + ------- + segmentation : (M, N) ndarray, bool + Segmentation produced by the algorithm. + phi : (M, N) ndarray of floats + Final level set computed by the algorithm. + energies : list of floats + Shows the evolution of the 'energy' for each step of the + algorithm. This should allow to check whether the algorithm + converged. + + Notes + ----- + The Chan-Vese Algorithm is designed to segment objects without + clearly defined boundaries. This algorithm is based on level sets + that are evolved iteratively to minimize an energy, which is + defined by weighted values corresponding to the sum of differences + intensity from the average value outside the segmented region, the + sum of differences from the average value inside the segmented + region, and a term which is dependent on the length of the + boundary of the segmented region. + + This algorithm was first proposed by Tony Chan and Luminita Vese, + in a publication entitled "An Active Contour Model Without Edges" + [1]_. + + This implementation of the algorithm is somewhat simplified in the + sense that the area factor 'nu' described in the original paper is + not implemented, and is only suitable for grayscale images. + + Typical values for `lambda1` and `lambda2` are 1. If the + 'background' is very different from the segmented object in terms + of distribution (for example, a uniform black image with figures + of varying intensity), then these values should be different from + each other. + + Typical values for mu are between 0 and 1, though higher values + can be used when dealing with shapes with very ill-defined + contours. + + The 'energy' which this algorithm tries to minimize is defined + as the sum of the differences from the average within the region + squared and weighed by the 'lambda' factors to which is added the + length of the contour multiplied by the 'mu' factor. + + Supports 2D grayscale images only, and does not implement the area + term described in the original article. + + References + ---------- + .. [1] An Active Contour Model without Edges, Tony Chan and + Luminita Vese, Scale-Space Theories in Computer Vision, + 1999, :DOI:`10.1007/3-540-48236-9_13` + .. [2] Chan-Vese Segmentation, Pascal Getreuer Image Processing On + Line, 2 (2012), pp. 214-224, + :DOI:`10.5201/ipol.2012.g-cv` + .. [3] The Chan-Vese Algorithm - Project Report, Rami Cohen, 2011 + :arXiv:`1107.2782` + """ + if len(image.shape) != 2: + raise ValueError("Input image should be a 2D array.") + + float_dtype = _supported_float_type(image.dtype) + phi = _cv_init_level_set(init_level_set, image.shape, dtype=float_dtype) + + if type(phi) != np.ndarray or phi.shape != image.shape: + raise ValueError( + "The dimensions of initial level set do not " + "match the dimensions of image." + ) + + image = image.astype(float_dtype, copy=False) + image = image - np.min(image) + if np.max(image) != 0: + image = image / np.max(image) + + i = 0 + old_energy = _cv_energy(image, phi, mu, lambda1, lambda2) + energies = [] + phivar = tol + 1 + segmentation = phi > 0 + + while phivar > tol and i < max_num_iter: + # Save old level set values + oldphi = phi + + # Calculate new level set + phi = _cv_calculate_variation(image, phi, mu, lambda1, lambda2, dt) + phi = _cv_reset_level_set(phi) + phivar = np.sqrt(((phi - oldphi) ** 2).mean()) + + # Extract energy and compare to previous level set and + # segmentation to see if continuing is necessary + segmentation = phi > 0 + new_energy = _cv_energy(image, phi, mu, lambda1, lambda2) + + # Save old energy values + energies.append(old_energy) + old_energy = new_energy + i += 1 + + if extended_output: + return (segmentation, phi, energies) + else: + return segmentation diff --git a/lib/python3.10/site-packages/skimage/segmentation/_clear_border.py b/lib/python3.10/site-packages/skimage/segmentation/_clear_border.py new file mode 100644 index 0000000000000000000000000000000000000000..3ac598cde2e8b59f0b5576059d0232a536ac3358 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_clear_border.py @@ -0,0 +1,109 @@ +import numpy as np + +from ..measure import label + + +def clear_border(labels, buffer_size=0, bgval=0, mask=None, *, out=None): + """Clear objects connected to the label image border. + + Parameters + ---------- + labels : (M[, N[, ..., P]]) array of int or bool + Imaging data labels. + buffer_size : int, optional + The width of the border examined. By default, only objects + that touch the outside of the image are removed. + bgval : float or int, optional + Cleared objects are set to this value. + mask : ndarray of bool, same shape as `image`, optional. + Image data mask. Objects in labels image overlapping with + False pixels of mask will be removed. If defined, the + argument buffer_size will be ignored. + out : ndarray + Array of the same shape as `labels`, into which the + output is placed. By default, a new array is created. + + Returns + ------- + out : (M[, N[, ..., P]]) array + Imaging data labels with cleared borders + + Examples + -------- + >>> import numpy as np + >>> from skimage.segmentation import clear_border + >>> labels = np.array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + ... [1, 1, 0, 0, 1, 0, 0, 1, 0], + ... [1, 1, 0, 1, 0, 1, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 1, 0, 0], + ... [0, 1, 1, 1, 1, 1, 1, 1, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> clear_border(labels) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + >>> mask = np.array([[0, 0, 1, 1, 1, 1, 1, 1, 1], + ... [0, 0, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1], + ... [1, 1, 1, 1, 1, 1, 1, 1, 1]]).astype(bool) + >>> clear_border(labels, mask=mask) + array([[0, 0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 1, 0, 0, 1, 0], + [0, 0, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + + """ + if any(buffer_size >= s for s in labels.shape) and mask is None: + # ignore buffer_size if mask + raise ValueError("buffer size may not be greater than labels size") + + if out is None: + out = labels.copy() + + if mask is not None: + err_msg = ( + f'labels and mask should have the same shape but ' + f'are {out.shape} and {mask.shape}' + ) + if out.shape != mask.shape: + raise (ValueError, err_msg) + if mask.dtype != bool: + raise TypeError("mask should be of type bool.") + borders = ~mask + else: + # create borders with buffer_size + borders = np.zeros_like(out, dtype=bool) + ext = buffer_size + 1 + slstart = slice(ext) + slend = slice(-ext, None) + slices = [slice(None) for _ in out.shape] + for d in range(out.ndim): + slices[d] = slstart + borders[tuple(slices)] = True + slices[d] = slend + borders[tuple(slices)] = True + slices[d] = slice(None) + + # Re-label, in case we are dealing with a binary out + # and to get consistent labeling + labels, number = label(out, background=0, return_num=True) + + # determine all objects that are connected to borders + borders_indices = np.unique(labels[borders]) + indices = np.arange(number + 1) + # mask all label indices that are connected to borders + label_mask = np.isin(indices, borders_indices) + # create mask for pixels to clear + mask = label_mask[labels.reshape(-1)].reshape(labels.shape) + + # clear border pixels + out[mask] = bgval + + return out diff --git a/lib/python3.10/site-packages/skimage/segmentation/_expand_labels.py b/lib/python3.10/site-packages/skimage/segmentation/_expand_labels.py new file mode 100644 index 0000000000000000000000000000000000000000..47aaf1fc5e695728215e5151893c718d0f0471d1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_expand_labels.py @@ -0,0 +1,104 @@ +import numpy as np +from scipy.ndimage import distance_transform_edt + + +def expand_labels(label_image, distance=1, spacing=1): + """Expand labels in label image by ``distance`` pixels without overlapping. + + Given a label image, ``expand_labels`` grows label regions (connected components) + outwards by up to ``distance`` units without overflowing into neighboring regions. + More specifically, each background pixel that is within Euclidean distance + of <= ``distance`` pixels of a connected component is assigned the label of that + connected component. The `spacing` parameter can be used to specify the spacing + rate of the distance transform used to calculate the Euclidean distance for anisotropic + images. + Where multiple connected components are within ``distance`` pixels of a background + pixel, the label value of the closest connected component will be assigned (see + Notes for the case of multiple labels at equal distance). + + Parameters + ---------- + label_image : ndarray of dtype int + label image + distance : float + Euclidean distance in pixels by which to grow the labels. Default is one. + spacing : float, or sequence of float, optional + Spacing of elements along each dimension. If a sequence, must be of length + equal to the input rank; if a single number, this is used for all axes. If + not specified, a grid spacing of unity is implied. + + Returns + ------- + enlarged_labels : ndarray of dtype int + Labeled array, where all connected regions have been enlarged + + Notes + ----- + Where labels are spaced more than ``distance`` pixels are apart, this is + equivalent to a morphological dilation with a disc or hyperball of radius ``distance``. + However, in contrast to a morphological dilation, ``expand_labels`` will + not expand a label region into a neighboring region. + + This implementation of ``expand_labels`` is derived from CellProfiler [1]_, where + it is known as module "IdentifySecondaryObjects (Distance-N)" [2]_. + + There is an important edge case when a pixel has the same distance to + multiple regions, as it is not defined which region expands into that + space. Here, the exact behavior depends on the upstream implementation + of ``scipy.ndimage.distance_transform_edt``. + + See Also + -------- + :func:`skimage.measure.label`, :func:`skimage.segmentation.watershed`, :func:`skimage.morphology.dilation` + + References + ---------- + .. [1] https://cellprofiler.org + .. [2] https://github.com/CellProfiler/CellProfiler/blob/082930ea95add7b72243a4fa3d39ae5145995e9c/cellprofiler/modules/identifysecondaryobjects.py#L559 + + Examples + -------- + >>> labels = np.array([0, 1, 0, 0, 0, 0, 2]) + >>> expand_labels(labels, distance=1) + array([1, 1, 1, 0, 0, 2, 2]) + + Labels will not overwrite each other: + + >>> expand_labels(labels, distance=3) + array([1, 1, 1, 1, 2, 2, 2]) + + In case of ties, behavior is undefined, but currently resolves to the + label closest to ``(0,) * ndim`` in lexicographical order. + + >>> labels_tied = np.array([0, 1, 0, 2, 0]) + >>> expand_labels(labels_tied, 1) + array([1, 1, 1, 2, 2]) + >>> labels2d = np.array( + ... [[0, 1, 0, 0], + ... [2, 0, 0, 0], + ... [0, 3, 0, 0]] + ... ) + >>> expand_labels(labels2d, 1) + array([[2, 1, 1, 0], + [2, 2, 0, 0], + [2, 3, 3, 0]]) + >>> expand_labels(labels2d, 1, spacing=[1, 0.5]) + array([[1, 1, 1, 1], + [2, 2, 2, 0], + [3, 3, 3, 3]]) + """ + + distances, nearest_label_coords = distance_transform_edt( + label_image == 0, sampling=spacing, return_indices=True + ) + labels_out = np.zeros_like(label_image) + dilate_mask = distances <= distance + # build the coordinates to find nearest labels, + # in contrast to [1] this implementation supports label arrays + # of any dimension + masked_nearest_label_coords = [ + dimension_indices[dilate_mask] for dimension_indices in nearest_label_coords + ] + nearest_labels = label_image[tuple(masked_nearest_label_coords)] + labels_out[dilate_mask] = nearest_labels + return labels_out diff --git a/lib/python3.10/site-packages/skimage/segmentation/_felzenszwalb.py b/lib/python3.10/site-packages/skimage/segmentation/_felzenszwalb.py new file mode 100644 index 0000000000000000000000000000000000000000..74b482941ed00114871869d48e9ae90ee894c4e6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_felzenszwalb.py @@ -0,0 +1,69 @@ +import numpy as np + +from ._felzenszwalb_cy import _felzenszwalb_cython +from .._shared import utils + + +@utils.channel_as_last_axis(multichannel_output=False) +def felzenszwalb(image, scale=1, sigma=0.8, min_size=20, *, channel_axis=-1): + """Computes Felsenszwalb's efficient graph based image segmentation. + + Produces an oversegmentation of a multichannel (i.e. RGB) image + using a fast, minimum spanning tree based clustering on the image grid. + The parameter ``scale`` sets an observation level. Higher scale means + less and larger segments. ``sigma`` is the diameter of a Gaussian kernel, + used for smoothing the image prior to segmentation. + + The number of produced segments as well as their size can only be + controlled indirectly through ``scale``. Segment size within an image can + vary greatly depending on local contrast. + + For RGB images, the algorithm uses the euclidean distance between pixels in + color space. + + Parameters + ---------- + image : (M, N[, 3]) ndarray + Input image. + scale : float + Free parameter. Higher means larger clusters. + sigma : float + Width (standard deviation) of Gaussian kernel used in preprocessing. + min_size : int + Minimum component size. Enforced using postprocessing. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + segment_mask : (M, N) ndarray + Integer mask indicating segment labels. + + References + ---------- + .. [1] Efficient graph-based image segmentation, Felzenszwalb, P.F. and + Huttenlocher, D.P. International Journal of Computer Vision, 2004 + + Notes + ----- + The `k` parameter used in the original paper renamed to `scale` here. + + Examples + -------- + >>> from skimage.segmentation import felzenszwalb + >>> from skimage.data import coffee + >>> img = coffee() + >>> segments = felzenszwalb(img, scale=3.0, sigma=0.95, min_size=5) + """ + if channel_axis is None and image.ndim > 2: + raise ValueError( + "This algorithm works only on single or " "multi-channel 2d images. " + ) + + image = np.atleast_3d(image) + return _felzenszwalb_cython(image, scale=scale, sigma=sigma, min_size=min_size) diff --git a/lib/python3.10/site-packages/skimage/segmentation/_join.py b/lib/python3.10/site-packages/skimage/segmentation/_join.py new file mode 100644 index 0000000000000000000000000000000000000000..b976f7b0364c471e07cb03bf73a70acfc1ce7092 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_join.py @@ -0,0 +1,184 @@ +import numpy as np + +from ..util._map_array import map_array, ArrayMap + + +def join_segmentations(s1, s2, return_mapping: bool = False): + """Return the join of the two input segmentations. + + The join J of S1 and S2 is defined as the segmentation in which two + voxels are in the same segment if and only if they are in the same + segment in *both* S1 and S2. + + Parameters + ---------- + s1, s2 : numpy arrays + s1 and s2 are label fields of the same shape. + return_mapping : bool, optional + If true, return mappings for joined segmentation labels to the original labels. + + Returns + ------- + j : numpy array + The join segmentation of s1 and s2. + map_j_to_s1 : ArrayMap, optional + Mapping from labels of the joined segmentation j to labels of s1. + map_j_to_s2 : ArrayMap, optional + Mapping from labels of the joined segmentation j to labels of s2. + + Examples + -------- + >>> from skimage.segmentation import join_segmentations + >>> s1 = np.array([[0, 0, 1, 1], + ... [0, 2, 1, 1], + ... [2, 2, 2, 1]]) + >>> s2 = np.array([[0, 1, 1, 0], + ... [0, 1, 1, 0], + ... [0, 1, 1, 1]]) + >>> join_segmentations(s1, s2) + array([[0, 1, 3, 2], + [0, 5, 3, 2], + [4, 5, 5, 3]]) + >>> j, m1, m2 = join_segmentations(s1, s2, return_mapping=True) + >>> m1 + ArrayMap(array([0, 1, 2, 3, 4, 5]), array([0, 0, 1, 1, 2, 2])) + >>> np.all(m1[j] == s1) + True + >>> np.all(m2[j] == s2) + True + """ + if s1.shape != s2.shape: + raise ValueError( + "Cannot join segmentations of different shape. " + f"s1.shape: {s1.shape}, s2.shape: {s2.shape}" + ) + # Reindex input label images + s1_relabeled, _, backward_map1 = relabel_sequential(s1) + s2_relabeled, _, backward_map2 = relabel_sequential(s2) + # Create joined label image + factor = s2.max() + np.uint8(1) + j_initial = factor * s1_relabeled + s2_relabeled + j, _, map_j_to_j_initial = relabel_sequential(j_initial) + if not return_mapping: + return j + # Determine label mapping + labels_j = np.unique(j_initial) + labels_s1_relabeled, labels_s2_relabeled = np.divmod(labels_j, factor) + map_j_to_s1 = ArrayMap( + map_j_to_j_initial.in_values, backward_map1[labels_s1_relabeled] + ) + map_j_to_s2 = ArrayMap( + map_j_to_j_initial.in_values, backward_map2[labels_s2_relabeled] + ) + return j, map_j_to_s1, map_j_to_s2 + + +def relabel_sequential(label_field, offset=1): + """Relabel arbitrary labels to {`offset`, ... `offset` + number_of_labels}. + + This function also returns the forward map (mapping the original labels to + the reduced labels) and the inverse map (mapping the reduced labels back + to the original ones). + + Parameters + ---------- + label_field : numpy array of int, arbitrary shape + An array of labels, which must be non-negative integers. + offset : int, optional + The return labels will start at `offset`, which should be + strictly positive. + + Returns + ------- + relabeled : numpy array of int, same shape as `label_field` + The input label field with labels mapped to + {offset, ..., number_of_labels + offset - 1}. + The data type will be the same as `label_field`, except when + offset + number_of_labels causes overflow of the current data type. + forward_map : ArrayMap + The map from the original label space to the returned label + space. Can be used to re-apply the same mapping. See examples + for usage. The output data type will be the same as `relabeled`. + inverse_map : ArrayMap + The map from the new label space to the original space. This + can be used to reconstruct the original label field from the + relabeled one. The output data type will be the same as `label_field`. + + Notes + ----- + The label 0 is assumed to denote the background and is never remapped. + + The forward map can be extremely big for some inputs, since its + length is given by the maximum of the label field. However, in most + situations, ``label_field.max()`` is much smaller than + ``label_field.size``, and in these cases the forward map is + guaranteed to be smaller than either the input or output images. + + Examples + -------- + >>> from skimage.segmentation import relabel_sequential + >>> label_field = np.array([1, 1, 5, 5, 8, 99, 42]) + >>> relab, fw, inv = relabel_sequential(label_field) + >>> relab + array([1, 1, 2, 2, 3, 5, 4]) + >>> print(fw) + ArrayMap: + 1 → 1 + 5 → 2 + 8 → 3 + 42 → 4 + 99 → 5 + >>> np.array(fw) + array([0, 1, 0, 0, 0, 2, 0, 0, 3, 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, 4, 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, 5]) + >>> np.array(inv) + array([ 0, 1, 5, 8, 42, 99]) + >>> (fw[label_field] == relab).all() + True + >>> (inv[relab] == label_field).all() + True + >>> relab, fw, inv = relabel_sequential(label_field, offset=5) + >>> relab + array([5, 5, 6, 6, 7, 9, 8]) + """ + if offset <= 0: + raise ValueError("Offset must be strictly positive.") + if np.min(label_field) < 0: + raise ValueError("Cannot relabel array that contains negative values.") + offset = int(offset) + in_vals = np.unique(label_field) + if in_vals[0] == 0: + # always map 0 to 0 + out_vals = np.concatenate([[0], np.arange(offset, offset + len(in_vals) - 1)]) + else: + out_vals = np.arange(offset, offset + len(in_vals)) + input_type = label_field.dtype + if input_type.kind not in "iu": + raise TypeError("label_field must have an integer dtype") + + # Some logic to determine the output type: + # - we don't want to return a smaller output type than the input type, + # ie if we get uint32 as labels input, don't return a uint8 array. + # - but, in some cases, using the input type could result in overflow. The + # input type could be a signed integer (e.g. int32) but + # `np.min_scalar_type` will always return an unsigned type. We check for + # that by casting the largest output value to the input type. If it is + # unchanged, we use the input type, else we use the unsigned minimum + # required type + required_type = np.min_scalar_type(out_vals[-1]) + if input_type.itemsize < required_type.itemsize: + output_type = required_type + else: + if out_vals[-1] < np.iinfo(input_type).max: + output_type = input_type + else: + output_type = required_type + out_array = np.empty(label_field.shape, dtype=output_type) + out_vals = out_vals.astype(output_type) + map_array(label_field, in_vals, out_vals, out=out_array) + fw_map = ArrayMap(in_vals, out_vals) + inv_map = ArrayMap(out_vals, in_vals) + return out_array, fw_map, inv_map diff --git a/lib/python3.10/site-packages/skimage/segmentation/_quickshift.py b/lib/python3.10/site-packages/skimage/segmentation/_quickshift.py new file mode 100644 index 0000000000000000000000000000000000000000..e627bdef36647df89299ebbc8a2a618f64d7d398 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_quickshift.py @@ -0,0 +1,104 @@ +import numpy as np + +from .._shared.filters import gaussian +from .._shared.utils import _supported_float_type +from ..color import rgb2lab +from ..util import img_as_float +from ._quickshift_cy import _quickshift_cython + + +def quickshift( + image, + ratio=1.0, + kernel_size=5, + max_dist=10, + return_tree=False, + sigma=0, + convert2lab=True, + rng=42, + *, + channel_axis=-1, +): + """Segment image using quickshift clustering in Color-(x,y) space. + + Produces an oversegmentation of the image using the quickshift mode-seeking + algorithm. + + Parameters + ---------- + image : (M, N, C) ndarray + Input image. The axis corresponding to color channels can be specified + via the `channel_axis` argument. + ratio : float, optional, between 0 and 1 + Balances color-space proximity and image-space proximity. + Higher values give more weight to color-space. + kernel_size : float, optional + Width of Gaussian kernel used in smoothing the + sample density. Higher means fewer clusters. + max_dist : float, optional + Cut-off point for data distances. + Higher means fewer clusters. + return_tree : bool, optional + Whether to return the full segmentation hierarchy tree and distances. + sigma : float, optional + Width for Gaussian smoothing as preprocessing. Zero means no smoothing. + convert2lab : bool, optional + Whether the input should be converted to Lab colorspace prior to + segmentation. For this purpose, the input is assumed to be RGB. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + The PRNG is used to break ties, and is seeded with 42 by default. + channel_axis : int, optional + The axis of `image` corresponding to color channels. Defaults to the + last axis. + + Returns + ------- + segment_mask : (M, N) ndarray + Integer mask indicating segment labels. + + Notes + ----- + The authors advocate to convert the image to Lab color space prior to + segmentation, though this is not strictly necessary. For this to work, the + image must be given in RGB format. + + References + ---------- + .. [1] Quick shift and kernel methods for mode seeking, + Vedaldi, A. and Soatto, S. + European Conference on Computer Vision, 2008 + """ + + image = img_as_float(np.atleast_3d(image)) + float_dtype = _supported_float_type(image.dtype) + image = image.astype(float_dtype, copy=False) + + if image.ndim > 3: + raise ValueError("Only 2D color images are supported") + + # move channels to last position as expected by the Cython code + image = np.moveaxis(image, source=channel_axis, destination=-1) + + if convert2lab: + if image.shape[-1] != 3: + raise ValueError("Only RGB images can be converted to Lab space.") + image = rgb2lab(image) + + if kernel_size < 1: + raise ValueError("`kernel_size` should be >= 1.") + + image = gaussian(image, sigma=[sigma, sigma, 0], mode='reflect', channel_axis=-1) + image = np.ascontiguousarray(image * ratio) + + segment_mask = _quickshift_cython( + image, + kernel_size=kernel_size, + max_dist=max_dist, + return_tree=return_tree, + rng=rng, + ) + return segment_mask diff --git a/lib/python3.10/site-packages/skimage/segmentation/_watershed.py b/lib/python3.10/site-packages/skimage/segmentation/_watershed.py new file mode 100644 index 0000000000000000000000000000000000000000..8aa7c34c7e41d56eedd08f277f168c8ffad864af --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/_watershed.py @@ -0,0 +1,242 @@ +"""watershed.py - watershed algorithm + +This module implements a watershed algorithm that apportions pixels into +marked basins. The algorithm uses a priority queue to hold the pixels +with the metric for the priority queue being pixel value, then the time +of entry into the queue - this settles ties in favor of the closest marker. + +Some ideas taken from +Soille, "Automated Basin Delineation from Digital Elevation Models Using +Mathematical Morphology", Signal Processing 20 (1990) 171-182. + +The most important insight in the paper is that entry time onto the queue +solves two problems: a pixel should be assigned to the neighbor with the +largest gradient or, if there is no gradient, pixels on a plateau should +be split between markers on opposite sides. +""" + +import numpy as np +from scipy import ndimage as ndi + +from . import _watershed_cy +from ..morphology.extrema import local_minima +from ..morphology._util import _validate_connectivity, _offsets_to_raveled_neighbors +from ..util import crop, regular_seeds + + +def _validate_inputs(image, markers, mask, connectivity): + """Ensure that all inputs to watershed have matching shapes and types. + + Parameters + ---------- + image : array + The input image. + markers : int or array of int + The marker image. + mask : array, or None + A boolean mask, True where we want to compute the watershed. + connectivity : int in {1, ..., image.ndim} + The connectivity of the neighborhood of a pixel. + + Returns + ------- + image, markers, mask : arrays + The validated and formatted arrays. Image will have dtype float64, + markers int32, and mask int8. If ``None`` was given for the mask, + it is a volume of all 1s. + + Raises + ------ + ValueError + If the shapes of the given arrays don't match. + """ + n_pixels = image.size + if mask is None: + # Use a complete `True` mask if none is provided + mask = np.ones(image.shape, bool) + else: + mask = np.asanyarray(mask, dtype=bool) + n_pixels = np.sum(mask) + if mask.shape != image.shape: + message = ( + f'`mask` (shape {mask.shape}) must have same shape ' + f'as `image` (shape {image.shape})' + ) + raise ValueError(message) + if markers is None: + markers_bool = local_minima(image, connectivity=connectivity) * mask + footprint = ndi.generate_binary_structure(markers_bool.ndim, connectivity) + markers = ndi.label(markers_bool, structure=footprint)[0] + elif not isinstance(markers, (np.ndarray, list, tuple)): + # not array-like, assume int + # given int, assume that number of markers *within mask*. + markers = regular_seeds(image.shape, int(markers / (n_pixels / image.size))) + markers *= mask + else: + markers = np.asanyarray(markers) * mask + if markers.shape != image.shape: + message = ( + f'`markers` (shape {markers.shape}) must have same ' + f'shape as `image` (shape {image.shape})' + ) + raise ValueError(message) + return (image.astype(np.float64), markers, mask.astype(np.int8)) + + +def watershed( + image, + markers=None, + connectivity=1, + offset=None, + mask=None, + compactness=0, + watershed_line=False, +): + """Find watershed basins in an image flooded from given markers. + + Parameters + ---------- + image : (M, N[, ...]) ndarray + Data array where the lowest value points are labeled first. + markers : int, or (M, N[, ...]) ndarray of int, optional + The desired number of basins, or an array marking the basins with the + values to be assigned in the label matrix. Zero means not a marker. If + None, the (default) markers are determined as the local minima of + `image`. Specifically, the computation is equivalent to applying + :func:`skimage.morphology.local_minima` onto `image`, followed by + :func:`skimage.measure.label` onto the result (with the same given + `connectivity`). Generally speaking, users are encouraged to pass + markers explicitly. + connectivity : int or ndarray, optional + The neighborhood connectivity. An integer is interpreted as in + ``scipy.ndimage.generate_binary_structure``, as the maximum number + of orthogonal steps to reach a neighbor. An array is directly + interpreted as a footprint (structuring element). Default value is 1. + In 2D, 1 gives a 4-neighborhood while 2 gives an 8-neighborhood. + offset : array_like of shape image.ndim, optional + The coordinates of the center of the footprint. + mask : (M, N[, ...]) ndarray of bools or 0's and 1's, optional + Array of same shape as `image`. Only points at which mask == True + will be labeled. + compactness : float, optional + Use compact watershed [1]_ with given compactness parameter. + Higher values result in more regularly-shaped watershed basins. + watershed_line : bool, optional + If True, a one-pixel wide line separates the regions + obtained by the watershed algorithm. The line has the label 0. + Note that the method used for adding this line expects that + marker regions are not adjacent; the watershed line may not catch + borders between adjacent marker regions. + + Returns + ------- + out : ndarray + A labeled matrix of the same type and shape as `markers`. + + See Also + -------- + skimage.segmentation.random_walker + A segmentation algorithm based on anisotropic diffusion, usually + slower than the watershed but with good results on noisy data and + boundaries with holes. + + Notes + ----- + This function implements a watershed algorithm [2]_ [3]_ that apportions + pixels into marked basins. The algorithm uses a priority queue to hold + the pixels with the metric for the priority queue being pixel value, then + the time of entry into the queue -- this settles ties in favor of the + closest marker. + + Some ideas are taken from [4]_. + The most important insight in the paper is that entry time onto the queue + solves two problems: a pixel should be assigned to the neighbor with the + largest gradient or, if there is no gradient, pixels on a plateau should + be split between markers on opposite sides. + + This implementation converts all arguments to specific, lowest common + denominator types, then passes these to a C algorithm. + + Markers can be determined manually, or automatically using for example + the local minima of the gradient of the image, or the local maxima of the + distance function to the background for separating overlapping objects + (see example). + + References + ---------- + .. [1] P. Neubert and P. Protzel, "Compact Watershed and Preemptive SLIC: + On Improving Trade-offs of Superpixel Segmentation Algorithms," + 2014 22nd International Conference on Pattern Recognition, + Stockholm, Sweden, 2014, pp. 996-1001, :DOI:`10.1109/ICPR.2014.181` + https://www.tu-chemnitz.de/etit/proaut/publications/cws_pSLIC_ICPR.pdf + + .. [2] https://en.wikipedia.org/wiki/Watershed_%28image_processing%29 + + .. [3] https://web.archive.org/web/20180702213110/http://cmm.ensmp.fr/~beucher/wtshed.html + + .. [4] P. J. Soille and M. M. Ansoult, "Automated basin delineation from + digital elevation models using mathematical morphology," Signal + Processing, 20(2):171-182, :DOI:`10.1016/0165-1684(90)90127-K` + + Examples + -------- + The watershed algorithm is useful to separate overlapping objects. + + We first generate an initial image with two overlapping circles: + + >>> x, y = np.indices((80, 80)) + >>> x1, y1, x2, y2 = 28, 28, 44, 52 + >>> r1, r2 = 16, 20 + >>> mask_circle1 = (x - x1)**2 + (y - y1)**2 < r1**2 + >>> mask_circle2 = (x - x2)**2 + (y - y2)**2 < r2**2 + >>> image = np.logical_or(mask_circle1, mask_circle2) + + Next, we want to separate the two circles. We generate markers at the + maxima of the distance to the background: + + >>> from scipy import ndimage as ndi + >>> distance = ndi.distance_transform_edt(image) + >>> from skimage.feature import peak_local_max + >>> max_coords = peak_local_max(distance, labels=image, + ... footprint=np.ones((3, 3))) + >>> local_maxima = np.zeros_like(image, dtype=bool) + >>> local_maxima[tuple(max_coords.T)] = True + >>> markers = ndi.label(local_maxima)[0] + + Finally, we run the watershed on the image and markers: + + >>> labels = watershed(-distance, markers, mask=image) + + The algorithm works also for 3D images, and can be used for example to + separate overlapping spheres. + """ + image, markers, mask = _validate_inputs(image, markers, mask, connectivity) + connectivity, offset = _validate_connectivity(image.ndim, connectivity, offset) + + # pad the image, markers, and mask so that we can use the mask to + # keep from running off the edges + pad_width = [(p, p) for p in offset] + image = np.pad(image, pad_width, mode='constant') + mask = np.pad(mask, pad_width, mode='constant').ravel() + output = np.pad(markers, pad_width, mode='constant') + + flat_neighborhood = _offsets_to_raveled_neighbors( + image.shape, connectivity, center=offset + ) + marker_locations = np.flatnonzero(output) + image_strides = np.array(image.strides, dtype=np.intp) // image.itemsize + + _watershed_cy.watershed_raveled( + image.ravel(), + marker_locations, + flat_neighborhood, + mask, + image_strides, + compactness, + output.ravel(), + watershed_line, + ) + + output = crop(output, pad_width, copy=True) + + return output diff --git a/lib/python3.10/site-packages/skimage/segmentation/active_contour_model.py b/lib/python3.10/site-packages/skimage/segmentation/active_contour_model.py new file mode 100644 index 0000000000000000000000000000000000000000..0d52ab74e70899963801a880a6588252bca920dc --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/active_contour_model.py @@ -0,0 +1,250 @@ +import numpy as np +from scipy.interpolate import RectBivariateSpline + +from .._shared.utils import _supported_float_type +from ..util import img_as_float +from ..filters import sobel + + +def active_contour( + image, + snake, + alpha=0.01, + beta=0.1, + w_line=0.0, + w_edge=1, + gamma=0.01, + max_px_move=1.0, + max_num_iter=2500, + convergence=0.1, + *, + boundary_condition='periodic', +): + """Active contour model. + + Active contours by fitting snakes to features of images. Supports single + and multichannel 2D images. Snakes can be periodic (for segmentation) or + have fixed and/or free ends. + The output snake has the same length as the input boundary. + As the number of points is constant, make sure that the initial snake + has enough points to capture the details of the final contour. + + Parameters + ---------- + image : (M, N) or (M, N, 3) ndarray + Input image. + snake : (K, 2) ndarray + Initial snake coordinates. For periodic boundary conditions, endpoints + must not be duplicated. + alpha : float, optional + Snake length shape parameter. Higher values makes snake contract + faster. + beta : float, optional + Snake smoothness shape parameter. Higher values makes snake smoother. + w_line : float, optional + Controls attraction to brightness. Use negative values to attract + toward dark regions. + w_edge : float, optional + Controls attraction to edges. Use negative values to repel snake from + edges. + gamma : float, optional + Explicit time stepping parameter. + max_px_move : float, optional + Maximum pixel distance to move per iteration. + max_num_iter : int, optional + Maximum iterations to optimize snake shape. + convergence : float, optional + Convergence criteria. + boundary_condition : string, optional + Boundary conditions for the contour. Can be one of 'periodic', + 'free', 'fixed', 'free-fixed', or 'fixed-free'. 'periodic' attaches + the two ends of the snake, 'fixed' holds the end-points in place, + and 'free' allows free movement of the ends. 'fixed' and 'free' can + be combined by parsing 'fixed-free', 'free-fixed'. Parsing + 'fixed-fixed' or 'free-free' yields same behaviour as 'fixed' and + 'free', respectively. + + Returns + ------- + snake : (K, 2) ndarray + Optimised snake, same shape as input parameter. + + References + ---------- + .. [1] Kass, M.; Witkin, A.; Terzopoulos, D. "Snakes: Active contour + models". International Journal of Computer Vision 1 (4): 321 + (1988). :DOI:`10.1007/BF00133570` + + Examples + -------- + >>> from skimage.draw import circle_perimeter + >>> from skimage.filters import gaussian + + Create and smooth image: + + >>> img = np.zeros((100, 100)) + >>> rr, cc = circle_perimeter(35, 45, 25) + >>> img[rr, cc] = 1 + >>> img = gaussian(img, sigma=2, preserve_range=False) + + Initialize spline: + + >>> s = np.linspace(0, 2*np.pi, 100) + >>> init = 50 * np.array([np.sin(s), np.cos(s)]).T + 50 + + Fit spline to image: + + >>> snake = active_contour(img, init, w_edge=0, w_line=1) # doctest: +SKIP + >>> dist = np.sqrt((45-snake[:, 0])**2 + (35-snake[:, 1])**2) # doctest: +SKIP + >>> int(np.mean(dist)) # doctest: +SKIP + 25 + + """ + max_num_iter = int(max_num_iter) + if max_num_iter <= 0: + raise ValueError("max_num_iter should be >0.") + convergence_order = 10 + valid_bcs = [ + 'periodic', + 'free', + 'fixed', + 'free-fixed', + 'fixed-free', + 'fixed-fixed', + 'free-free', + ] + if boundary_condition not in valid_bcs: + raise ValueError( + "Invalid boundary condition.\n" + + "Should be one of: " + + ", ".join(valid_bcs) + + '.' + ) + + img = img_as_float(image) + float_dtype = _supported_float_type(image.dtype) + img = img.astype(float_dtype, copy=False) + + RGB = img.ndim == 3 + + # Find edges using sobel: + if w_edge != 0: + if RGB: + edge = [sobel(img[:, :, 0]), sobel(img[:, :, 1]), sobel(img[:, :, 2])] + else: + edge = [sobel(img)] + else: + edge = [0] + + # Superimpose intensity and edge images: + if RGB: + img = w_line * np.sum(img, axis=2) + w_edge * sum(edge) + else: + img = w_line * img + w_edge * edge[0] + + # Interpolate for smoothness: + intp = RectBivariateSpline( + np.arange(img.shape[1]), np.arange(img.shape[0]), img.T, kx=2, ky=2, s=0 + ) + + snake_xy = snake[:, ::-1] + x = snake_xy[:, 0].astype(float_dtype) + y = snake_xy[:, 1].astype(float_dtype) + n = len(x) + xsave = np.empty((convergence_order, n), dtype=float_dtype) + ysave = np.empty((convergence_order, n), dtype=float_dtype) + + # Build snake shape matrix for Euler equation in double precision + eye_n = np.eye(n, dtype=float) + a = ( + np.roll(eye_n, -1, axis=0) + np.roll(eye_n, -1, axis=1) - 2 * eye_n + ) # second order derivative, central difference + b = ( + np.roll(eye_n, -2, axis=0) + + np.roll(eye_n, -2, axis=1) + - 4 * np.roll(eye_n, -1, axis=0) + - 4 * np.roll(eye_n, -1, axis=1) + + 6 * eye_n + ) # fourth order derivative, central difference + A = -alpha * a + beta * b + + # Impose boundary conditions different from periodic: + sfixed = False + if boundary_condition.startswith('fixed'): + A[0, :] = 0 + A[1, :] = 0 + A[1, :3] = [1, -2, 1] + sfixed = True + efixed = False + if boundary_condition.endswith('fixed'): + A[-1, :] = 0 + A[-2, :] = 0 + A[-2, -3:] = [1, -2, 1] + efixed = True + sfree = False + if boundary_condition.startswith('free'): + A[0, :] = 0 + A[0, :3] = [1, -2, 1] + A[1, :] = 0 + A[1, :4] = [-1, 3, -3, 1] + sfree = True + efree = False + if boundary_condition.endswith('free'): + A[-1, :] = 0 + A[-1, -3:] = [1, -2, 1] + A[-2, :] = 0 + A[-2, -4:] = [-1, 3, -3, 1] + efree = True + + # Only one inversion is needed for implicit spline energy minimization: + inv = np.linalg.inv(A + gamma * eye_n) + # can use float_dtype once we have computed the inverse in double precision + inv = inv.astype(float_dtype, copy=False) + + # Explicit time stepping for image energy minimization: + for i in range(max_num_iter): + # RectBivariateSpline always returns float64, so call astype here + fx = intp(x, y, dx=1, grid=False).astype(float_dtype, copy=False) + fy = intp(x, y, dy=1, grid=False).astype(float_dtype, copy=False) + + if sfixed: + fx[0] = 0 + fy[0] = 0 + if efixed: + fx[-1] = 0 + fy[-1] = 0 + if sfree: + fx[0] *= 2 + fy[0] *= 2 + if efree: + fx[-1] *= 2 + fy[-1] *= 2 + xn = inv @ (gamma * x + fx) + yn = inv @ (gamma * y + fy) + + # Movements are capped to max_px_move per iteration: + dx = max_px_move * np.tanh(xn - x) + dy = max_px_move * np.tanh(yn - y) + if sfixed: + dx[0] = 0 + dy[0] = 0 + if efixed: + dx[-1] = 0 + dy[-1] = 0 + x += dx + y += dy + + # Convergence criteria needs to compare to a number of previous + # configurations since oscillations can occur. + j = i % (convergence_order + 1) + if j < convergence_order: + xsave[j, :] = x + ysave[j, :] = y + else: + dist = np.min( + np.max(np.abs(xsave - x[None, :]) + np.abs(ysave - y[None, :]), 1) + ) + if dist < convergence: + break + + return np.stack([y, x], axis=1) diff --git a/lib/python3.10/site-packages/skimage/segmentation/boundaries.py b/lib/python3.10/site-packages/skimage/segmentation/boundaries.py new file mode 100644 index 0000000000000000000000000000000000000000..59de91aecad875af3d62d5f95c10889b21aec127 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/boundaries.py @@ -0,0 +1,240 @@ +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import _supported_float_type +from ..morphology import dilation, erosion, footprint_rectangle +from ..util import img_as_float, view_as_windows +from ..color import gray2rgb + + +def _find_boundaries_subpixel(label_img): + """See ``find_boundaries(..., mode='subpixel')``. + + Notes + ----- + This function puts in an empty row and column between each *actual* + row and column of the image, for a corresponding shape of ``2s - 1`` + for every image dimension of size ``s``. These "interstitial" rows + and columns are filled as ``True`` if they separate two labels in + `label_img`, ``False`` otherwise. + + I used ``view_as_windows`` to get the neighborhood of each pixel. + Then I check whether there are two labels or more in that + neighborhood. + """ + ndim = label_img.ndim + max_label = np.iinfo(label_img.dtype).max + + label_img_expanded = np.zeros( + [(2 * s - 1) for s in label_img.shape], label_img.dtype + ) + pixels = (slice(None, None, 2),) * ndim + label_img_expanded[pixels] = label_img + + edges = np.ones(label_img_expanded.shape, dtype=bool) + edges[pixels] = False + label_img_expanded[edges] = max_label + windows = view_as_windows(np.pad(label_img_expanded, 1, mode='edge'), (3,) * ndim) + + boundaries = np.zeros_like(edges) + for index in np.ndindex(label_img_expanded.shape): + if edges[index]: + values = np.unique(windows[index].ravel()) + if len(values) > 2: # single value and max_label + boundaries[index] = True + return boundaries + + +def find_boundaries(label_img, connectivity=1, mode='thick', background=0): + """Return bool array where boundaries between labeled regions are True. + + Parameters + ---------- + label_img : array of int or bool + An array in which different regions are labeled with either different + integers or boolean values. + connectivity : int in {1, ..., `label_img.ndim`}, optional + A pixel is considered a boundary pixel if any of its neighbors + has a different label. `connectivity` controls which pixels are + considered neighbors. A connectivity of 1 (default) means + pixels sharing an edge (in 2D) or a face (in 3D) will be + considered neighbors. A connectivity of `label_img.ndim` means + pixels sharing a corner will be considered neighbors. + mode : string in {'thick', 'inner', 'outer', 'subpixel'} + How to mark the boundaries: + + - thick: any pixel not completely surrounded by pixels of the + same label (defined by `connectivity`) is marked as a boundary. + This results in boundaries that are 2 pixels thick. + - inner: outline the pixels *just inside* of objects, leaving + background pixels untouched. + - outer: outline pixels in the background around object + boundaries. When two objects touch, their boundary is also + marked. + - subpixel: return a doubled image, with pixels *between* the + original pixels marked as boundary where appropriate. + background : int, optional + For modes 'inner' and 'outer', a definition of a background + label is required. See `mode` for descriptions of these two. + + Returns + ------- + boundaries : array of bool, same shape as `label_img` + A bool image where ``True`` represents a boundary pixel. For + `mode` equal to 'subpixel', ``boundaries.shape[i]`` is equal + to ``2 * label_img.shape[i] - 1`` for all ``i`` (a pixel is + inserted in between all other pairs of pixels). + + Examples + -------- + >>> labels = np.array([[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, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 1, 1, 1, 5, 5, 5, 0, 0], + ... [0, 0, 0, 0, 0, 5, 5, 5, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.uint8) + >>> find_boundaries(labels, mode='thick').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], + [0, 1, 1, 0, 1, 1, 0, 1, 1, 0], + [0, 1, 1, 1, 1, 1, 0, 1, 1, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels, mode='inner').astype(np.uint8) + array([[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, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels, mode='outer').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 1, 1, 0, 0, 1, 0], + [0, 0, 1, 1, 1, 1, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 1, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> labels_small = labels[::2, ::3] + >>> labels_small + array([[0, 0, 0, 0], + [0, 0, 5, 0], + [0, 1, 5, 0], + [0, 0, 5, 0], + [0, 0, 0, 0]], dtype=uint8) + >>> find_boundaries(labels_small, mode='subpixel').astype(np.uint8) + array([[0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 0, 1, 0], + [0, 0, 0, 1, 0, 1, 0], + [0, 0, 0, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0]], dtype=uint8) + >>> bool_image = np.array([[False, False, False, False, False], + ... [False, False, False, False, False], + ... [False, False, True, True, True], + ... [False, False, True, True, True], + ... [False, False, True, True, True]], + ... dtype=bool) + >>> find_boundaries(bool_image) + array([[False, False, False, False, False], + [False, False, True, True, True], + [False, True, True, True, True], + [False, True, True, False, False], + [False, True, True, False, False]]) + """ + if label_img.dtype == 'bool': + label_img = label_img.astype(np.uint8) + ndim = label_img.ndim + footprint = ndi.generate_binary_structure(ndim, connectivity) + if mode != 'subpixel': + boundaries = dilation(label_img, footprint) != erosion(label_img, footprint) + if mode == 'inner': + foreground_image = label_img != background + boundaries &= foreground_image + elif mode == 'outer': + max_label = np.iinfo(label_img.dtype).max + background_image = label_img == background + footprint = ndi.generate_binary_structure(ndim, ndim) + inverted_background = np.array(label_img, copy=True) + inverted_background[background_image] = max_label + adjacent_objects = ( + dilation(label_img, footprint) + != erosion(inverted_background, footprint) + ) & ~background_image + boundaries &= background_image | adjacent_objects + return boundaries + else: + boundaries = _find_boundaries_subpixel(label_img) + return boundaries + + +def mark_boundaries( + image, + label_img, + color=(1, 1, 0), + outline_color=None, + mode='outer', + background_label=0, +): + """Return image with boundaries between labeled regions highlighted. + + Parameters + ---------- + image : (M, N[, 3]) array + Grayscale or RGB image. + label_img : (M, N) array of int + Label array where regions are marked by different integer values. + color : length-3 sequence, optional + RGB color of boundaries in the output image. + outline_color : length-3 sequence, optional + RGB color surrounding boundaries in the output image. If None, no + outline is drawn. + mode : string in {'thick', 'inner', 'outer', 'subpixel'}, optional + The mode for finding boundaries. + background_label : int, optional + Which label to consider background (this is only useful for + modes ``inner`` and ``outer``). + + Returns + ------- + marked : (M, N, 3) array of float + An image in which the boundaries between labels are + superimposed on the original image. + + See Also + -------- + find_boundaries + """ + float_dtype = _supported_float_type(image.dtype) + marked = img_as_float(image, force_copy=True) + marked = marked.astype(float_dtype, copy=False) + if marked.ndim == 2: + marked = gray2rgb(marked) + if mode == 'subpixel': + # Here, we want to interpose an extra line of pixels between + # each original line - except for the last axis which holds + # the RGB information. ``ndi.zoom`` then performs the (cubic) + # interpolation, filling in the values of the interposed pixels + marked = ndi.zoom( + marked, [2 - 1 / s for s in marked.shape[:-1]] + [1], mode='mirror' + ) + boundaries = find_boundaries(label_img, mode=mode, background=background_label) + if outline_color is not None: + outlines = dilation(boundaries, footprint_rectangle((3, 3))) + marked[outlines] = outline_color + marked[boundaries] = color + return marked diff --git a/lib/python3.10/site-packages/skimage/segmentation/morphsnakes.py b/lib/python3.10/site-packages/skimage/segmentation/morphsnakes.py new file mode 100644 index 0000000000000000000000000000000000000000..c65349020b664be1a5e920eeef4623ff20db9894 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/morphsnakes.py @@ -0,0 +1,449 @@ +from itertools import cycle + +import numpy as np +from scipy import ndimage as ndi + +from .._shared.utils import check_nD + +__all__ = [ + 'morphological_chan_vese', + 'morphological_geodesic_active_contour', + 'inverse_gaussian_gradient', + 'disk_level_set', + 'checkerboard_level_set', +] + + +class _fcycle: + def __init__(self, iterable): + """Call functions from the iterable each time it is called.""" + self.funcs = cycle(iterable) + + def __call__(self, *args, **kwargs): + f = next(self.funcs) + return f(*args, **kwargs) + + +# SI and IS operators for 2D and 3D. +_P2 = [ + np.eye(3), + np.array([[0, 1, 0]] * 3), + np.flipud(np.eye(3)), + np.rot90([[0, 1, 0]] * 3), +] +_P3 = [np.zeros((3, 3, 3)) for i in range(9)] + +_P3[0][:, :, 1] = 1 +_P3[1][:, 1, :] = 1 +_P3[2][1, :, :] = 1 +_P3[3][:, [0, 1, 2], [0, 1, 2]] = 1 +_P3[4][:, [0, 1, 2], [2, 1, 0]] = 1 +_P3[5][[0, 1, 2], :, [0, 1, 2]] = 1 +_P3[6][[0, 1, 2], :, [2, 1, 0]] = 1 +_P3[7][[0, 1, 2], [0, 1, 2], :] = 1 +_P3[8][[0, 1, 2], [2, 1, 0], :] = 1 + + +def sup_inf(u): + """SI operator.""" + + if np.ndim(u) == 2: + P = _P2 + elif np.ndim(u) == 3: + P = _P3 + else: + raise ValueError("u has an invalid number of dimensions " "(should be 2 or 3)") + + erosions = [] + for P_i in P: + erosions.append(ndi.binary_erosion(u, P_i).astype(np.int8)) + + return np.stack(erosions, axis=0).max(0) + + +def inf_sup(u): + """IS operator.""" + + if np.ndim(u) == 2: + P = _P2 + elif np.ndim(u) == 3: + P = _P3 + else: + raise ValueError("u has an invalid number of dimensions " "(should be 2 or 3)") + + dilations = [] + for P_i in P: + dilations.append(ndi.binary_dilation(u, P_i).astype(np.int8)) + + return np.stack(dilations, axis=0).min(0) + + +_curvop = _fcycle( + [lambda u: sup_inf(inf_sup(u)), lambda u: inf_sup(sup_inf(u))] # SIoIS +) # ISoSI + + +def _check_input(image, init_level_set): + """Check that shapes of `image` and `init_level_set` match.""" + check_nD(image, [2, 3]) + + if len(image.shape) != len(init_level_set.shape): + raise ValueError( + "The dimensions of the initial level set do not " + "match the dimensions of the image." + ) + + +def _init_level_set(init_level_set, image_shape): + """Auxiliary function for initializing level sets with a string. + + If `init_level_set` is not a string, it is returned as is. + """ + if isinstance(init_level_set, str): + if init_level_set == 'checkerboard': + res = checkerboard_level_set(image_shape) + elif init_level_set == 'disk': + res = disk_level_set(image_shape) + else: + raise ValueError("`init_level_set` not in " "['checkerboard', 'disk']") + else: + res = init_level_set + return res + + +def disk_level_set(image_shape, *, center=None, radius=None): + """Create a disk level set with binary values. + + Parameters + ---------- + image_shape : tuple of positive integers + Shape of the image + center : tuple of positive integers, optional + Coordinates of the center of the disk given in (row, column). If not + given, it defaults to the center of the image. + radius : float, optional + Radius of the disk. If not given, it is set to the 75% of the + smallest image dimension. + + Returns + ------- + out : array with shape `image_shape` + Binary level set of the disk with the given `radius` and `center`. + + See Also + -------- + checkerboard_level_set + """ + + if center is None: + center = tuple(i // 2 for i in image_shape) + + if radius is None: + radius = min(image_shape) * 3.0 / 8.0 + + grid = np.mgrid[[slice(i) for i in image_shape]] + grid = (grid.T - center).T + phi = radius - np.sqrt(np.sum((grid) ** 2, 0)) + res = np.int8(phi > 0) + return res + + +def checkerboard_level_set(image_shape, square_size=5): + """Create a checkerboard level set with binary values. + + Parameters + ---------- + image_shape : tuple of positive integers + Shape of the image. + square_size : int, optional + Size of the squares of the checkerboard. It defaults to 5. + + Returns + ------- + out : array with shape `image_shape` + Binary level set of the checkerboard. + + See Also + -------- + disk_level_set + """ + + grid = np.mgrid[[slice(i) for i in image_shape]] + grid = grid // square_size + + # Alternate 0/1 for even/odd numbers. + grid = grid & 1 + + checkerboard = np.bitwise_xor.reduce(grid, axis=0) + res = np.int8(checkerboard) + return res + + +def inverse_gaussian_gradient(image, alpha=100.0, sigma=5.0): + """Inverse of gradient magnitude. + + Compute the magnitude of the gradients in the image and then inverts the + result in the range [0, 1]. Flat areas are assigned values close to 1, + while areas close to borders are assigned values close to 0. + + This function or a similar one defined by the user should be applied over + the image as a preprocessing step before calling + `morphological_geodesic_active_contour`. + + Parameters + ---------- + image : (M, N) or (L, M, N) array + Grayscale image or volume. + alpha : float, optional + Controls the steepness of the inversion. A larger value will make the + transition between the flat areas and border areas steeper in the + resulting array. + sigma : float, optional + Standard deviation of the Gaussian filter applied over the image. + + Returns + ------- + gimage : (M, N) or (L, M, N) array + Preprocessed image (or volume) suitable for + `morphological_geodesic_active_contour`. + """ + gradnorm = ndi.gaussian_gradient_magnitude(image, sigma, mode='nearest') + return 1.0 / np.sqrt(1.0 + alpha * gradnorm) + + +def morphological_chan_vese( + image, + num_iter, + init_level_set='checkerboard', + smoothing=1, + lambda1=1, + lambda2=1, + iter_callback=lambda x: None, +): + """Morphological Active Contours without Edges (MorphACWE) + + Active contours without edges implemented with morphological operators. It + can be used to segment objects in images and volumes without well defined + borders. It is required that the inside of the object looks different on + average than the outside (i.e., the inner area of the object should be + darker or lighter than the outer area on average). + + Parameters + ---------- + image : (M, N) or (L, M, N) array + Grayscale image or volume to be segmented. + num_iter : uint + Number of num_iter to run + init_level_set : str, (M, N) array, or (L, M, N) array + Initial level set. If an array is given, it will be binarized and used + as the initial level set. If a string is given, it defines the method + to generate a reasonable initial level set with the shape of the + `image`. Accepted values are 'checkerboard' and 'disk'. See the + documentation of `checkerboard_level_set` and `disk_level_set` + respectively for details about how these level sets are created. + smoothing : uint, optional + Number of times the smoothing operator is applied per iteration. + Reasonable values are around 1-4. Larger values lead to smoother + segmentations. + lambda1 : float, optional + Weight parameter for the outer region. If `lambda1` is larger than + `lambda2`, the outer region will contain a larger range of values than + the inner region. + lambda2 : float, optional + Weight parameter for the inner region. If `lambda2` is larger than + `lambda1`, the inner region will contain a larger range of values than + the outer region. + iter_callback : function, optional + If given, this function is called once per iteration with the current + level set as the only argument. This is useful for debugging or for + plotting intermediate results during the evolution. + + Returns + ------- + out : (M, N) or (L, M, N) array + Final segmentation (i.e., the final level set) + + See Also + -------- + disk_level_set, checkerboard_level_set + + Notes + ----- + This is a version of the Chan-Vese algorithm that uses morphological + operators instead of solving a partial differential equation (PDE) for the + evolution of the contour. The set of morphological operators used in this + algorithm are proved to be infinitesimally equivalent to the Chan-Vese PDE + (see [1]_). However, morphological operators are do not suffer from the + numerical stability issues typically found in PDEs (it is not necessary to + find the right time step for the evolution), and are computationally + faster. + + The algorithm and its theoretical derivation are described in [1]_. + + References + ---------- + .. [1] A Morphological Approach to Curvature-based Evolution of Curves and + Surfaces, Pablo Márquez-Neila, Luis Baumela, Luis Álvarez. In IEEE + Transactions on Pattern Analysis and Machine Intelligence (PAMI), + 2014, :DOI:`10.1109/TPAMI.2013.106` + """ + + init_level_set = _init_level_set(init_level_set, image.shape) + + _check_input(image, init_level_set) + + u = np.int8(init_level_set > 0) + + iter_callback(u) + + for _ in range(num_iter): + # inside = u > 0 + # outside = u <= 0 + c0 = (image * (1 - u)).sum() / float((1 - u).sum() + 1e-8) + c1 = (image * u).sum() / float(u.sum() + 1e-8) + + # Image attachment + du = np.gradient(u) + abs_du = np.abs(du).sum(0) + aux = abs_du * (lambda1 * (image - c1) ** 2 - lambda2 * (image - c0) ** 2) + + u[aux < 0] = 1 + u[aux > 0] = 0 + + # Smoothing + for _ in range(smoothing): + u = _curvop(u) + + iter_callback(u) + + return u + + +def morphological_geodesic_active_contour( + gimage, + num_iter, + init_level_set='disk', + smoothing=1, + threshold='auto', + balloon=0, + iter_callback=lambda x: None, +): + """Morphological Geodesic Active Contours (MorphGAC). + + Geodesic active contours implemented with morphological operators. It can + be used to segment objects with visible but noisy, cluttered, broken + borders. + + Parameters + ---------- + gimage : (M, N) or (L, M, N) array + Preprocessed image or volume to be segmented. This is very rarely the + original image. Instead, this is usually a preprocessed version of the + original image that enhances and highlights the borders (or other + structures) of the object to segment. + :func:`morphological_geodesic_active_contour` will try to stop the contour + evolution in areas where `gimage` is small. See + :func:`inverse_gaussian_gradient` as an example function to + perform this preprocessing. Note that the quality of + :func:`morphological_geodesic_active_contour` might greatly depend on this + preprocessing. + num_iter : uint + Number of num_iter to run. + init_level_set : str, (M, N) array, or (L, M, N) array + Initial level set. If an array is given, it will be binarized and used + as the initial level set. If a string is given, it defines the method + to generate a reasonable initial level set with the shape of the + `image`. Accepted values are 'checkerboard' and 'disk'. See the + documentation of `checkerboard_level_set` and `disk_level_set` + respectively for details about how these level sets are created. + smoothing : uint, optional + Number of times the smoothing operator is applied per iteration. + Reasonable values are around 1-4. Larger values lead to smoother + segmentations. + threshold : float, optional + Areas of the image with a value smaller than this threshold will be + considered borders. The evolution of the contour will stop in these + areas. + balloon : float, optional + Balloon force to guide the contour in non-informative areas of the + image, i.e., areas where the gradient of the image is too small to push + the contour towards a border. A negative value will shrink the contour, + while a positive value will expand the contour in these areas. Setting + this to zero will disable the balloon force. + iter_callback : function, optional + If given, this function is called once per iteration with the current + level set as the only argument. This is useful for debugging or for + plotting intermediate results during the evolution. + + Returns + ------- + out : (M, N) or (L, M, N) array + Final segmentation (i.e., the final level set) + + See Also + -------- + inverse_gaussian_gradient, disk_level_set, checkerboard_level_set + + Notes + ----- + This is a version of the Geodesic Active Contours (GAC) algorithm that uses + morphological operators instead of solving partial differential equations + (PDEs) for the evolution of the contour. The set of morphological operators + used in this algorithm are proved to be infinitesimally equivalent to the + GAC PDEs (see [1]_). However, morphological operators are do not suffer + from the numerical stability issues typically found in PDEs (e.g., it is + not necessary to find the right time step for the evolution), and are + computationally faster. + + The algorithm and its theoretical derivation are described in [1]_. + + References + ---------- + .. [1] A Morphological Approach to Curvature-based Evolution of Curves and + Surfaces, Pablo Márquez-Neila, Luis Baumela, Luis Álvarez. In IEEE + Transactions on Pattern Analysis and Machine Intelligence (PAMI), + 2014, :DOI:`10.1109/TPAMI.2013.106` + """ + + image = gimage + init_level_set = _init_level_set(init_level_set, image.shape) + + _check_input(image, init_level_set) + + if threshold == 'auto': + threshold = np.percentile(image, 40) + + structure = np.ones((3,) * len(image.shape), dtype=np.int8) + dimage = np.gradient(image) + # threshold_mask = image > threshold + if balloon != 0: + threshold_mask_balloon = image > threshold / np.abs(balloon) + + u = np.int8(init_level_set > 0) + + iter_callback(u) + + for _ in range(num_iter): + # Balloon + if balloon > 0: + aux = ndi.binary_dilation(u, structure) + elif balloon < 0: + aux = ndi.binary_erosion(u, structure) + if balloon != 0: + u[threshold_mask_balloon] = aux[threshold_mask_balloon] + + # Image attachment + aux = np.zeros_like(image) + du = np.gradient(u) + for el1, el2 in zip(dimage, du): + aux += el1 * el2 + u[aux > 0] = 1 + u[aux < 0] = 0 + + # Smoothing + for _ in range(smoothing): + u = _curvop(u) + + iter_callback(u) + + return u diff --git a/lib/python3.10/site-packages/skimage/segmentation/random_walker_segmentation.py b/lib/python3.10/site-packages/skimage/segmentation/random_walker_segmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..ca74f83270d32b02ac59e68ac56cc61043841da3 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/random_walker_segmentation.py @@ -0,0 +1,593 @@ +""" +Random walker segmentation algorithm + +from *Random walks for image segmentation*, Leo Grady, IEEE Trans +Pattern Anal Mach Intell. 2006 Nov;28(11):1768-83. + +Installing pyamg and using the 'cg_mg' mode of random_walker improves +significantly the performance. +""" + +import numpy as np +from scipy import sparse, ndimage as ndi + +from .._shared import utils +from .._shared.utils import warn +from .._shared.compat import SCIPY_CG_TOL_PARAM_NAME + +# executive summary for next code block: try to import umfpack from +# scipy, but make sure not to raise a fuss if it fails since it's only +# needed to speed up a few cases. +# See discussions at: +# https://groups.google.com/d/msg/scikit-image/FrM5IGP6wh4/1hp-FtVZmfcJ +# https://stackoverflow.com/questions/13977970/ignore-exceptions-printed-to-stderr-in-del/13977992?noredirect=1#comment28386412_13977992 +try: + from scipy.sparse.linalg.dsolve.linsolve import umfpack + + old_del = umfpack.UmfpackContext.__del__ + + def new_del(self): + try: + old_del(self) + except AttributeError: + pass + + umfpack.UmfpackContext.__del__ = new_del + UmfpackContext = umfpack.UmfpackContext() +except ImportError: + UmfpackContext = None + +try: + from pyamg import ruge_stuben_solver + + amg_loaded = True +except ImportError: + amg_loaded = False + +from ..util import img_as_float + +from scipy.sparse.linalg import cg, spsolve + + +def _make_graph_edges_3d(n_x, n_y, n_z): + """Returns a list of edges for a 3D image. + + Parameters + ---------- + n_x : integer + The size of the grid in the x direction. + n_y : integer + The size of the grid in the y direction + n_z : integer + The size of the grid in the z direction + + Returns + ------- + edges : (2, N) ndarray + with the total number of edges:: + + N = n_x * n_y * (nz - 1) + + n_x * (n_y - 1) * nz + + (n_x - 1) * n_y * nz + + Graph edges with each column describing a node-id pair. + """ + vertices = np.arange(n_x * n_y * n_z).reshape((n_x, n_y, n_z)) + edges_deep = np.vstack((vertices[..., :-1].ravel(), vertices[..., 1:].ravel())) + edges_right = np.vstack((vertices[:, :-1].ravel(), vertices[:, 1:].ravel())) + edges_down = np.vstack((vertices[:-1].ravel(), vertices[1:].ravel())) + edges = np.hstack((edges_deep, edges_right, edges_down)) + return edges + + +def _compute_weights_3d(data, spacing, beta, eps, multichannel): + # Weight calculation is main difference in multispectral version + # Original gradient**2 replaced with sum of gradients ** 2 + gradients = ( + np.concatenate( + [ + np.diff(data[..., 0], axis=ax).ravel() / spacing[ax] + for ax in [2, 1, 0] + if data.shape[ax] > 1 + ], + axis=0, + ) + ** 2 + ) + for channel in range(1, data.shape[-1]): + gradients += ( + np.concatenate( + [ + np.diff(data[..., channel], axis=ax).ravel() / spacing[ax] + for ax in [2, 1, 0] + if data.shape[ax] > 1 + ], + axis=0, + ) + ** 2 + ) + + # All channels considered together in this standard deviation + scale_factor = -beta / (10 * data.std()) + if multichannel: + # New final term in beta to give == results in trivial case where + # multiple identical spectra are passed. + scale_factor /= np.sqrt(data.shape[-1]) + weights = np.exp(scale_factor * gradients) + weights += eps + return -weights + + +def _build_laplacian(data, spacing, mask, beta, multichannel): + l_x, l_y, l_z = data.shape[:3] + edges = _make_graph_edges_3d(l_x, l_y, l_z) + weights = _compute_weights_3d( + data, spacing, beta=beta, eps=1.0e-10, multichannel=multichannel + ) + if mask is not None: + # Remove edges of the graph connected to masked nodes, as well + # as corresponding weights of the edges. + mask0 = np.hstack( + [mask[..., :-1].ravel(), mask[:, :-1].ravel(), mask[:-1].ravel()] + ) + mask1 = np.hstack( + [mask[..., 1:].ravel(), mask[:, 1:].ravel(), mask[1:].ravel()] + ) + ind_mask = np.logical_and(mask0, mask1) + edges, weights = edges[:, ind_mask], weights[ind_mask] + + # Reassign edges labels to 0, 1, ... edges_number - 1 + _, inv_idx = np.unique(edges, return_inverse=True) + edges = inv_idx.reshape(edges.shape) + + # Build the sparse linear system + pixel_nb = l_x * l_y * l_z + i_indices = edges.ravel() + j_indices = edges[::-1].ravel() + data = np.hstack((weights, weights)) + lap = sparse.csr_array((data, (i_indices, j_indices)), shape=(pixel_nb, pixel_nb)) + lap.setdiag(-np.ravel(lap.sum(axis=0))) + return lap + + +def _build_linear_system(data, spacing, labels, nlabels, mask, beta, multichannel): + """ + Build the matrix A and rhs B of the linear system to solve. + A and B are two block of the laplacian of the image graph. + """ + if mask is None: + labels = labels.ravel() + else: + labels = labels[mask] + + indices = np.arange(labels.size) + seeds_mask = labels > 0 + unlabeled_indices = indices[~seeds_mask] + seeds_indices = indices[seeds_mask] + + lap_sparse = _build_laplacian( + data, spacing, mask=mask, beta=beta, multichannel=multichannel + ) + + rows = lap_sparse[unlabeled_indices, :] + lap_sparse = rows[:, unlabeled_indices] + B = -rows[:, seeds_indices] + + seeds = labels[seeds_mask] + seeds_mask = sparse.csc_array( + np.hstack([np.atleast_2d(seeds == lab).T for lab in range(1, nlabels + 1)]) + ) + rhs = B @ seeds_mask + + return lap_sparse, rhs + + +def _solve_linear_system(lap_sparse, B, tol, mode): + if mode is None: + mode = 'cg_j' + + if mode == 'cg_mg' and not amg_loaded: + warn( + '"cg_mg" not available, it requires pyamg to be installed. ' + 'The "cg_j" mode will be used instead.', + stacklevel=2, + ) + mode = 'cg_j' + + if mode == 'bf': + X = spsolve(lap_sparse, B.toarray()).T + else: + maxiter = None + if mode == 'cg': + if UmfpackContext is None: + warn( + '"cg" mode may be slow because UMFPACK is not available. ' + 'Consider building Scipy with UMFPACK or use a ' + 'preconditioned version of CG ("cg_j" or "cg_mg" modes).', + stacklevel=2, + ) + M = None + elif mode == 'cg_j': + n = lap_sparse.shape[-1] + M = sparse.dia_array((1.0 / lap_sparse.diagonal(), 0), shape=(n, n)) + else: + # mode == 'cg_mg' + lap_sparse.indices, lap_sparse.indptr = _safe_downcast_indices( + lap_sparse, np.int32, "index values too large for int32 mode 'cg_mg'" + ) + ml = ruge_stuben_solver(lap_sparse, coarse_solver='pinv') + M = ml.aspreconditioner(cycle='V') + maxiter = 30 + rtol = {SCIPY_CG_TOL_PARAM_NAME: tol} + cg_out = [ + cg(lap_sparse, B[:, [i]].toarray(), **rtol, atol=0, M=M, maxiter=maxiter) + for i in range(B.shape[1]) + ] + if np.any([info > 0 for _, info in cg_out]): + warn( + "Conjugate gradient convergence to tolerance not achieved. " + "Consider decreasing beta to improve system conditionning.", + stacklevel=2, + ) + X = np.asarray([x for x, _ in cg_out]) + + return X + + +def _safe_downcast_indices(A, itype, msg): + # check for safe downcasting + max_value = np.iinfo(itype).max + + if A.indptr[-1] > max_value: # indptr[-1] is max b/c indptr always sorted + raise ValueError(msg) + + if max(*A.shape) > max_value: # only check large enough arrays + if np.any(A.indices > max_value): + raise ValueError(msg) + + indices = A.indices.astype(itype, copy=False) + indptr = A.indptr.astype(itype, copy=False) + return indices, indptr + + +def _preprocess(labels): + label_values, inv_idx = np.unique(labels, return_inverse=True) + if max(label_values) <= 0: + raise ValueError( + 'No seeds provided in label image: please ensure ' + 'it contains at least one positive value' + ) + + if not (label_values == 0).any(): + warn( + 'Random walker only segments unlabeled areas, where ' + 'labels == 0. No zero valued areas in labels were ' + 'found. Returning provided labels.', + stacklevel=2, + ) + + return labels, None, None, None, None + + # If some labeled pixels are isolated inside pruned zones, prune them + # as well and keep the labels for the final output + + null_mask = labels == 0 + pos_mask = labels > 0 + mask = labels >= 0 + + fill = ndi.binary_propagation(null_mask, mask=mask) + isolated = np.logical_and(pos_mask, np.logical_not(fill)) + + pos_mask[isolated] = False + + # If the array has pruned zones, be sure that no isolated pixels + # exist between pruned zones (they could not be determined) + if label_values[0] < 0 or np.any(isolated): + isolated = np.logical_and( + np.logical_not(ndi.binary_propagation(pos_mask, mask=mask)), null_mask + ) + + labels[isolated] = -1 + if np.all(isolated[null_mask]): + warn( + 'All unlabeled pixels are isolated, they could not be ' + 'determined by the random walker algorithm.', + stacklevel=2, + ) + return labels, None, None, None, None + + mask[isolated] = False + mask = np.atleast_3d(mask) + else: + mask = None + + # Reorder label values to have consecutive integers (no gaps) + zero_idx = np.searchsorted(label_values, 0) + labels = np.atleast_3d(inv_idx.reshape(labels.shape) - zero_idx) + + nlabels = label_values[zero_idx + 1 :].shape[0] + + inds_isolated_seeds = np.nonzero(isolated) + isolated_values = labels[inds_isolated_seeds] + + return labels, nlabels, mask, inds_isolated_seeds, isolated_values + + +@utils.channel_as_last_axis(multichannel_output=False) +def random_walker( + data, + labels, + beta=130, + mode='cg_j', + tol=1.0e-3, + copy=True, + return_full_prob=False, + spacing=None, + *, + prob_tol=1e-3, + channel_axis=None, +): + """Random walker algorithm for segmentation from markers. + + Random walker algorithm is implemented for gray-level or multichannel + images. + + Parameters + ---------- + data : (M, N[, P][, C]) ndarray + Image to be segmented in phases. Gray-level `data` can be two- or + three-dimensional; multichannel data can be three- or four- + dimensional with `channel_axis` specifying the dimension containing + channels. Data spacing is assumed isotropic unless the `spacing` + keyword argument is used. + labels : (M, N[, P]) array of ints + Array of seed markers labeled with different positive integers + for different phases. Zero-labeled pixels are unlabeled pixels. + Negative labels correspond to inactive pixels that are not taken + into account (they are removed from the graph). If labels are not + consecutive integers, the labels array will be transformed so that + labels are consecutive. In the multichannel case, `labels` should have + the same shape as a single channel of `data`, i.e. without the final + dimension denoting channels. + beta : float, optional + Penalization coefficient for the random walker motion + (the greater `beta`, the more difficult the diffusion). + mode : string, available options {'cg', 'cg_j', 'cg_mg', 'bf'} + Mode for solving the linear system in the random walker algorithm. + + - 'bf' (brute force): an LU factorization of the Laplacian is + computed. This is fast for small images (<1024x1024), but very slow + and memory-intensive for large images (e.g., 3-D volumes). + - 'cg' (conjugate gradient): the linear system is solved iteratively + using the Conjugate Gradient method from scipy.sparse.linalg. This is + less memory-consuming than the brute force method for large images, + but it is quite slow. + - 'cg_j' (conjugate gradient with Jacobi preconditionner): the + Jacobi preconditionner is applied during the Conjugate + gradient method iterations. This may accelerate the + convergence of the 'cg' method. + - 'cg_mg' (conjugate gradient with multigrid preconditioner): a + preconditioner is computed using a multigrid solver, then the + solution is computed with the Conjugate Gradient method. This mode + requires that the pyamg module is installed. + tol : float, optional + Tolerance to achieve when solving the linear system using + the conjugate gradient based modes ('cg', 'cg_j' and 'cg_mg'). + copy : bool, optional + If copy is False, the `labels` array will be overwritten with + the result of the segmentation. Use copy=False if you want to + save on memory. + return_full_prob : bool, optional + If True, the probability that a pixel belongs to each of the + labels will be returned, instead of only the most likely + label. + spacing : iterable of floats, optional + Spacing between voxels in each spatial dimension. If `None`, then + the spacing between pixels/voxels in each dimension is assumed 1. + prob_tol : float, optional + Tolerance on the resulting probability to be in the interval [0, 1]. + If the tolerance is not satisfied, a warning is displayed. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + output : ndarray + * If `return_full_prob` is False, array of ints of same shape + and data type as `labels`, in which each pixel has been + labeled according to the marker that reached the pixel first + by anisotropic diffusion. + * If `return_full_prob` is True, array of floats of shape + `(nlabels, labels.shape)`. `output[label_nb, i, j]` is the + probability that label `label_nb` reaches the pixel `(i, j)` + first. + + See Also + -------- + skimage.segmentation.watershed + A segmentation algorithm based on mathematical morphology + and "flooding" of regions from markers. + + Notes + ----- + Multichannel inputs are scaled with all channel data combined. Ensure all + channels are separately normalized prior to running this algorithm. + + The `spacing` argument is specifically for anisotropic datasets, where + data points are spaced differently in one or more spatial dimensions. + Anisotropic data is commonly encountered in medical imaging. + + The algorithm was first proposed in [1]_. + + The algorithm solves the diffusion equation at infinite times for + sources placed on markers of each phase in turn. A pixel is labeled with + the phase that has the greatest probability to diffuse first to the pixel. + + The diffusion equation is solved by minimizing x.T L x for each phase, + where L is the Laplacian of the weighted graph of the image, and x is + the probability that a marker of the given phase arrives first at a pixel + by diffusion (x=1 on markers of the phase, x=0 on the other markers, and + the other coefficients are looked for). Each pixel is attributed the label + for which it has a maximal value of x. The Laplacian L of the image + is defined as: + + - L_ii = d_i, the number of neighbors of pixel i (the degree of i) + - L_ij = -w_ij if i and j are adjacent pixels + + The weight w_ij is a decreasing function of the norm of the local gradient. + This ensures that diffusion is easier between pixels of similar values. + + When the Laplacian is decomposed into blocks of marked and unmarked + pixels:: + + L = M B.T + B A + + with first indices corresponding to marked pixels, and then to unmarked + pixels, minimizing x.T L x for one phase amount to solving:: + + A x = - B x_m + + where x_m = 1 on markers of the given phase, and 0 on other markers. + This linear system is solved in the algorithm using a direct method for + small images, and an iterative method for larger images. + + References + ---------- + .. [1] Leo Grady, Random walks for image segmentation, IEEE Trans Pattern + Anal Mach Intell. 2006 Nov;28(11):1768-83. + :DOI:`10.1109/TPAMI.2006.233`. + + Examples + -------- + >>> rng = np.random.default_rng() + >>> a = np.zeros((10, 10)) + 0.2 * rng.random((10, 10)) + >>> a[5:8, 5:8] += 1 + >>> b = np.zeros_like(a, dtype=np.int32) + >>> b[3, 3] = 1 # Marker for first phase + >>> b[6, 6] = 2 # Marker for second phase + >>> random_walker(a, b) # doctest: +SKIP + array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 2, 2, 2, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], dtype=int32) + + """ + # Parse input data + if mode not in ('cg_mg', 'cg', 'bf', 'cg_j', None): + raise ValueError( + f"{mode} is not a valid mode. Valid modes are 'cg_mg', " + f"'cg', 'cg_j', 'bf', and None" + ) + + if data.dtype == np.float16: + # SciPy sparse, which is used later on, doesn't officially support float16 + # This led to failures when testing with NumPy 1.26 (see gh-7635). + data = data.astype(np.float32, casting="safe") + + # Spacing kwarg checks + if spacing is None: + spacing = np.ones(3) + elif len(spacing) == labels.ndim: + if len(spacing) == 2: + # Need a dummy spacing for singleton 3rd dim + spacing = np.r_[spacing, 1.0] + spacing = np.asarray(spacing) + else: + raise ValueError( + 'Input argument `spacing` incorrect, should be an ' + 'iterable with one number per spatial dimension.' + ) + + # This algorithm expects 4-D arrays of floats, where the first three + # dimensions are spatial and the final denotes channels. 2-D images have + # a singleton placeholder dimension added for the third spatial dimension, + # and single channel images likewise have a singleton added for channels. + # The following block ensures valid input and coerces it to the correct + # form. + multichannel = channel_axis is not None + if not multichannel: + if data.ndim not in (2, 3): + raise ValueError( + 'For non-multichannel input, data must be of ' 'dimension 2 or 3.' + ) + if data.shape != labels.shape: + raise ValueError('Incompatible data and labels shapes.') + data = np.atleast_3d(img_as_float(data))[..., np.newaxis] + else: + if data.ndim not in (3, 4): + raise ValueError( + 'For multichannel input, data must have 3 or 4 ' 'dimensions.' + ) + if data.shape[:-1] != labels.shape: + raise ValueError('Incompatible data and labels shapes.') + data = img_as_float(data) + if data.ndim == 3: # 2D multispectral, needs singleton in 3rd axis + data = data[:, :, np.newaxis, :] + + labels_shape = labels.shape + labels_dtype = labels.dtype + + if copy: + labels = np.copy(labels) + + (labels, nlabels, mask, inds_isolated_seeds, isolated_values) = _preprocess(labels) + + if isolated_values is None: + # No non isolated zero valued areas in labels were + # found. Returning provided labels. + if return_full_prob: + # Return the concatenation of the masks of each unique label + return np.concatenate( + [np.atleast_3d(labels == lab) for lab in np.unique(labels) if lab > 0], + axis=-1, + ) + return labels + + # Build the linear system (lap_sparse, B) + lap_sparse, B = _build_linear_system( + data, spacing, labels, nlabels, mask, beta, multichannel + ) + + # Solve the linear system lap_sparse X = B + # where X[i, j] is the probability that a marker of label i arrives + # first at pixel j by anisotropic diffusion. + X = _solve_linear_system(lap_sparse, B, tol, mode) + + if X.min() < -prob_tol or X.max() > 1 + prob_tol: + warn( + 'The probability range is outside [0, 1] given the tolerance ' + '`prob_tol`. Consider decreasing `beta` and/or decreasing ' + '`tol`.' + ) + + # Build the output according to return_full_prob value + # Put back labels of isolated seeds + labels[inds_isolated_seeds] = isolated_values + labels = labels.reshape(labels_shape) + + mask = labels == 0 + mask[inds_isolated_seeds] = False + + if return_full_prob: + out = np.zeros((nlabels,) + labels_shape) + for lab, (label_prob, prob) in enumerate(zip(out, X), start=1): + label_prob[mask] = prob + label_prob[labels == lab] = 1 + else: + X = np.argmax(X, axis=0) + 1 + out = labels.astype(labels_dtype) + out[mask] = X + + return out diff --git a/lib/python3.10/site-packages/skimage/segmentation/slic_superpixels.py b/lib/python3.10/site-packages/skimage/segmentation/slic_superpixels.py new file mode 100644 index 0000000000000000000000000000000000000000..4fa6ef1bf90c62200309e4be514e3e0aa2129c46 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/slic_superpixels.py @@ -0,0 +1,449 @@ +import math +from collections.abc import Iterable +from warnings import warn + +import numpy as np +from numpy import random +from scipy.cluster.vq import kmeans2 +from scipy.spatial.distance import pdist, squareform + +from .._shared import utils +from .._shared.filters import gaussian +from ..color import rgb2lab +from ..util import img_as_float, regular_grid +from ._slic import _enforce_label_connectivity_cython, _slic_cython + + +def _get_mask_centroids(mask, n_centroids, multichannel): + """Find regularly spaced centroids on a mask. + + Parameters + ---------- + mask : 3D ndarray + The mask within which the centroids must be positioned. + n_centroids : int + The number of centroids to be returned. + + Returns + ------- + centroids : 2D ndarray + The coordinates of the centroids with shape (n_centroids, 3). + steps : 1D ndarray + The approximate distance between two seeds in all dimensions. + + """ + + # Get tight ROI around the mask to optimize + coord = np.array(np.nonzero(mask), dtype=float).T + # Fix random seed to ensure repeatability + # Keep old-style RandomState here as expected results in tests depend on it + rng = random.RandomState(123) + + # select n_centroids randomly distributed points from within the mask + idx_full = np.arange(len(coord), dtype=int) + idx = np.sort(rng.choice(idx_full, min(n_centroids, len(coord)), replace=False)) + + # To save time, when n_centroids << len(coords), use only a subset of the + # coordinates when calling k-means. Rather than the full set of coords, + # we will use a substantially larger subset than n_centroids. Here we + # somewhat arbitrarily choose dense_factor=10 to make the samples + # 10 times closer together along each axis than the n_centroids samples. + dense_factor = 10 + ndim_spatial = mask.ndim - 1 if multichannel else mask.ndim + n_dense = int((dense_factor**ndim_spatial) * n_centroids) + if len(coord) > n_dense: + # subset of points to use for the k-means calculation + # (much denser than idx, but less than the full set) + idx_dense = np.sort(rng.choice(idx_full, n_dense, replace=False)) + else: + idx_dense = Ellipsis + centroids, _ = kmeans2(coord[idx_dense], coord[idx], iter=5) + + # Compute the minimum distance of each centroid to the others + dist = squareform(pdist(centroids)) + np.fill_diagonal(dist, np.inf) + closest_pts = dist.argmin(-1) + steps = abs(centroids - centroids[closest_pts, :]).mean(0) + + return centroids, steps + + +def _get_grid_centroids(image, n_centroids): + """Find regularly spaced centroids on the image. + + Parameters + ---------- + image : 2D, 3D or 4D ndarray + Input image, which can be 2D or 3D, and grayscale or + multichannel. + n_centroids : int + The (approximate) number of centroids to be returned. + + Returns + ------- + centroids : 2D ndarray + The coordinates of the centroids with shape (~n_centroids, 3). + steps : 1D ndarray + The approximate distance between two seeds in all dimensions. + + """ + d, h, w = image.shape[:3] + + grid_z, grid_y, grid_x = np.mgrid[:d, :h, :w] + slices = regular_grid(image.shape[:3], n_centroids) + + centroids_z = grid_z[slices].ravel()[..., np.newaxis] + centroids_y = grid_y[slices].ravel()[..., np.newaxis] + centroids_x = grid_x[slices].ravel()[..., np.newaxis] + + centroids = np.concatenate([centroids_z, centroids_y, centroids_x], axis=-1) + + steps = np.asarray([float(s.step) if s.step is not None else 1.0 for s in slices]) + return centroids, steps + + +@utils.channel_as_last_axis(multichannel_output=False) +def slic( + image, + n_segments=100, + compactness=10.0, + max_num_iter=10, + sigma=0, + spacing=None, + convert2lab=None, + enforce_connectivity=True, + min_size_factor=0.5, + max_size_factor=3, + slic_zero=False, + start_label=1, + mask=None, + *, + channel_axis=-1, +): + """Segments image using k-means clustering in Color-(x,y,z) space. + + Parameters + ---------- + image : (M, N[, P][, C]) ndarray + Input image. Can be 2D or 3D, and grayscale or multichannel + (see `channel_axis` parameter). + Input image must either be NaN-free or the NaN's must be masked out. + n_segments : int, optional + The (approximate) number of labels in the segmented output image. + compactness : float, optional + Balances color proximity and space proximity. Higher values give + more weight to space proximity, making superpixel shapes more + square/cubic. In SLICO mode, this is the initial compactness. + This parameter depends strongly on image contrast and on the + shapes of objects in the image. We recommend exploring possible + values on a log scale, e.g., 0.01, 0.1, 1, 10, 100, before + refining around a chosen value. + max_num_iter : int, optional + Maximum number of iterations of k-means. + sigma : float or array-like of floats, optional + Width of Gaussian smoothing kernel for pre-processing for each + dimension of the image. The same sigma is applied to each dimension in + case of a scalar value. Zero means no smoothing. + Note that `sigma` is automatically scaled if it is scalar and + if a manual voxel spacing is provided (see Notes section). If + sigma is array-like, its size must match ``image``'s number + of spatial dimensions. + spacing : array-like of floats, optional + The voxel spacing along each spatial dimension. By default, + `slic` assumes uniform spacing (same voxel resolution along + each spatial dimension). + This parameter controls the weights of the distances along the + spatial dimensions during k-means clustering. + convert2lab : bool, optional + Whether the input should be converted to Lab colorspace prior to + segmentation. The input image *must* be RGB. Highly recommended. + This option defaults to ``True`` when ``channel_axis` is not None *and* + ``image.shape[-1] == 3``. + enforce_connectivity : bool, optional + Whether the generated segments are connected or not + min_size_factor : float, optional + Proportion of the minimum segment size to be removed with respect + to the supposed segment size ```depth*width*height/n_segments``` + max_size_factor : float, optional + Proportion of the maximum connected segment size. A value of 3 works + in most of the cases. + slic_zero : bool, optional + Run SLIC-zero, the zero-parameter mode of SLIC. [2]_ + start_label : int, optional + The labels' index start. Should be 0 or 1. + + .. versionadded:: 0.17 + ``start_label`` was introduced in 0.17 + mask : ndarray, optional + If provided, superpixels are computed only where mask is True, + and seed points are homogeneously distributed over the mask + using a k-means clustering strategy. Mask number of dimensions + must be equal to image number of spatial dimensions. + + .. versionadded:: 0.17 + ``mask`` was introduced in 0.17 + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + labels : 2D or 3D array + Integer mask indicating segment labels. + + Raises + ------ + ValueError + If ``convert2lab`` is set to ``True`` but the last array + dimension is not of length 3. + ValueError + If ``start_label`` is not 0 or 1. + ValueError + If ``image`` contains unmasked NaN values. + ValueError + If ``image`` contains unmasked infinite values. + ValueError + If ``image`` is 2D but ``channel_axis`` is -1 (the default). + + Notes + ----- + * If `sigma > 0`, the image is smoothed using a Gaussian kernel prior to + segmentation. + + * If `sigma` is scalar and `spacing` is provided, the kernel width is + divided along each dimension by the spacing. For example, if ``sigma=1`` + and ``spacing=[5, 1, 1]``, the effective `sigma` is ``[0.2, 1, 1]``. This + ensures sensible smoothing for anisotropic images. + + * The image is rescaled to be in [0, 1] prior to processing (masked + values are ignored). + + * Images of shape (M, N, 3) are interpreted as 2D RGB images by default. To + interpret them as 3D with the last dimension having length 3, use + `channel_axis=None`. + + * `start_label` is introduced to handle the issue [4]_. Label indexing + starts at 1 by default. + + References + ---------- + .. [1] Radhakrishna Achanta, Appu Shaji, Kevin Smith, Aurelien Lucchi, + Pascal Fua, and Sabine Süsstrunk, SLIC Superpixels Compared to + State-of-the-art Superpixel Methods, TPAMI, May 2012. + :DOI:`10.1109/TPAMI.2012.120` + .. [2] https://www.epfl.ch/labs/ivrl/research/slic-superpixels/#SLICO + .. [3] Irving, Benjamin. "maskSLIC: regional superpixel generation with + application to local pathology characterisation in medical images.", + 2016, :arXiv:`1606.09518` + .. [4] https://github.com/scikit-image/scikit-image/issues/3722 + + Examples + -------- + >>> from skimage.segmentation import slic + >>> from skimage.data import astronaut + >>> img = astronaut() + >>> segments = slic(img, n_segments=100, compactness=10) + + Increasing the compactness parameter yields more square regions: + + >>> segments = slic(img, n_segments=100, compactness=20) + + """ + if image.ndim == 2 and channel_axis is not None: + raise ValueError( + f"channel_axis={channel_axis} indicates multichannel, which is not " + "supported for a two-dimensional image; use channel_axis=None if " + "the image is grayscale" + ) + + image = img_as_float(image) + float_dtype = utils._supported_float_type(image.dtype) + # copy=True so subsequent in-place operations do not modify the + # function input + image = image.astype(float_dtype, copy=True) + + if mask is not None: + # Create masked_image to rescale while ignoring masked values + mask = np.ascontiguousarray(mask, dtype=bool) + if channel_axis is not None: + mask_ = np.expand_dims(mask, axis=channel_axis) + mask_ = np.broadcast_to(mask_, image.shape) + else: + mask_ = mask + image_values = image[mask_] + else: + image_values = image + + # Rescale image to [0, 1] to make choice of compactness insensitive to + # input image scale. + imin = image_values.min() + imax = image_values.max() + if np.isnan(imin): + raise ValueError("unmasked NaN values in image are not supported") + if np.isinf(imin) or np.isinf(imax): + raise ValueError("unmasked infinite values in image are not supported") + image -= imin + if imax != imin: + image /= imax - imin + + use_mask = mask is not None + dtype = image.dtype + + is_2d = False + + multichannel = channel_axis is not None + if image.ndim == 2: + # 2D grayscale image + image = image[np.newaxis, ..., np.newaxis] + is_2d = True + elif image.ndim == 3 and multichannel: + # Make 2D multichannel image 3D with depth = 1 + image = image[np.newaxis, ...] + is_2d = True + elif image.ndim == 3 and not multichannel: + # Add channel as single last dimension + image = image[..., np.newaxis] + + if multichannel and (convert2lab or convert2lab is None): + if image.shape[channel_axis] != 3 and convert2lab: + raise ValueError("Lab colorspace conversion requires a RGB image.") + elif image.shape[channel_axis] == 3: + image = rgb2lab(image) + + if start_label not in [0, 1]: + raise ValueError("start_label should be 0 or 1.") + + # initialize cluster centroids for desired number of segments + update_centroids = False + if use_mask: + mask = mask.view('uint8') + if mask.ndim == 2: + mask = np.ascontiguousarray(mask[np.newaxis, ...]) + if mask.shape != image.shape[:3]: + raise ValueError("image and mask should have the same shape.") + centroids, steps = _get_mask_centroids(mask, n_segments, multichannel) + update_centroids = True + else: + centroids, steps = _get_grid_centroids(image, n_segments) + + if spacing is None: + spacing = np.ones(3, dtype=dtype) + elif isinstance(spacing, Iterable): + spacing = np.asarray(spacing, dtype=dtype) + if is_2d: + if spacing.size != 2: + if spacing.size == 3: + warn( + "Input image is 2D: spacing number of " + "elements must be 2. In the future, a ValueError " + "will be raised.", + FutureWarning, + stacklevel=2, + ) + else: + raise ValueError( + f"Input image is 2D, but spacing has " + f"{spacing.size} elements (expected 2)." + ) + else: + spacing = np.insert(spacing, 0, 1) + elif spacing.size != 3: + raise ValueError( + f"Input image is 3D, but spacing has " + f"{spacing.size} elements (expected 3)." + ) + spacing = np.ascontiguousarray(spacing, dtype=dtype) + else: + raise TypeError("spacing must be None or iterable.") + + if np.isscalar(sigma): + sigma = np.array([sigma, sigma, sigma], dtype=dtype) + sigma /= spacing + elif isinstance(sigma, Iterable): + sigma = np.asarray(sigma, dtype=dtype) + if is_2d: + if sigma.size != 2: + if spacing.size == 3: + warn( + "Input image is 2D: sigma number of " + "elements must be 2. In the future, a ValueError " + "will be raised.", + FutureWarning, + stacklevel=2, + ) + else: + raise ValueError( + f"Input image is 2D, but sigma has " + f"{sigma.size} elements (expected 2)." + ) + else: + sigma = np.insert(sigma, 0, 0) + elif sigma.size != 3: + raise ValueError( + f"Input image is 3D, but sigma has " + f"{sigma.size} elements (expected 3)." + ) + + if (sigma > 0).any(): + # add zero smoothing for channel dimension + sigma = list(sigma) + [0] + image = gaussian(image, sigma=sigma, mode='reflect') + + n_centroids = centroids.shape[0] + segments = np.ascontiguousarray( + np.concatenate([centroids, np.zeros((n_centroids, image.shape[3]))], axis=-1), + dtype=dtype, + ) + + # Scaling of ratio in the same way as in the SLIC paper so the + # values have the same meaning + step = max(steps) + ratio = 1.0 / compactness + + image = np.ascontiguousarray(image * ratio, dtype=dtype) + + if update_centroids: + # Step 2 of the algorithm [3]_ + _slic_cython( + image, + mask, + segments, + step, + max_num_iter, + spacing, + slic_zero, + ignore_color=True, + start_label=start_label, + ) + + labels = _slic_cython( + image, + mask, + segments, + step, + max_num_iter, + spacing, + slic_zero, + ignore_color=False, + start_label=start_label, + ) + + if enforce_connectivity: + if use_mask: + segment_size = mask.sum() / n_centroids + else: + segment_size = math.prod(image.shape[:3]) / n_centroids + min_size = int(min_size_factor * segment_size) + max_size = int(max_size_factor * segment_size) + labels = _enforce_label_connectivity_cython( + labels, min_size, max_size, start_label=start_label + ) + + if is_2d: + labels = labels[0] + + return labels diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__init__.py b/lib/python3.10/site-packages/skimage/segmentation/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/__init__.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd428acb5b46d107f891d0cf9064a4b022d6db10 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_active_contour_model.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_active_contour_model.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59325b18095a6654e421c52b392d7e64802a88ae Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_active_contour_model.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_boundaries.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_boundaries.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1084784e0d336942e7cddc6ec40d400abc6c5079 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_boundaries.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_chan_vese.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_chan_vese.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..21e786f2c357814a094318e4f9889a9434f51d74 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_chan_vese.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_clear_border.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_clear_border.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e76fd70e823686673e4f3bf1bb8a4430ad2a751 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_clear_border.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_expand_labels.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_expand_labels.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2cbc25870cce59c6d10179734edb6e95413ed894 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_expand_labels.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_felzenszwalb.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_felzenszwalb.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85696c189d71d0e7acafe12aee68be8f34fbe444 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_felzenszwalb.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_join.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_join.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1032e83e34899546dfc3bd39da65f83615162d1 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_join.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_morphsnakes.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_morphsnakes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ed554bc5fd4772264019cc0a4ae1a9260fd9e3c Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_morphsnakes.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_quickshift.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_quickshift.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e69b8581c74e38909672f71699f41db179a1186 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_quickshift.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_random_walker.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_random_walker.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8163470d1881e98c16af012cd21dc0bfbdbedc1a Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_random_walker.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_slic.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_slic.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bfa688c848c0726ec16e2c98069907890b318a8 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_slic.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_watershed.cpython-310.pyc b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_watershed.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e2c8d085d6c0786985395cba4d3bd6fab48f0c1 Binary files /dev/null and b/lib/python3.10/site-packages/skimage/segmentation/tests/__pycache__/test_watershed.cpython-310.pyc differ diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_active_contour_model.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_active_contour_model.py new file mode 100644 index 0000000000000000000000000000000000000000..79844ddd2cd7be55314d0089f120fdc7a5ccbeae --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_active_contour_model.py @@ -0,0 +1,190 @@ +import numpy as np +import pytest +from numpy.testing import assert_equal, assert_allclose + +from skimage import data +from skimage._shared.utils import _supported_float_type +from skimage.color import rgb2gray +from skimage.filters import gaussian +from skimage.segmentation import active_contour + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_periodic_reference(dtype): + img = data.astronaut() + img = rgb2gray(img) + s = np.linspace(0, 2 * np.pi, 400) + r = 100 + 100 * np.sin(s) + c = 220 + 100 * np.cos(s) + init = np.array([r, c]).T + img_smooth = gaussian(img, sigma=3, preserve_range=False).astype(dtype, copy=False) + snake = active_contour( + img_smooth, init, alpha=0.015, beta=10, w_line=0, w_edge=1, gamma=0.001 + ) + assert snake.dtype == _supported_float_type(dtype) + refr = [98, 99, 100, 101, 102, 103, 104, 105, 106, 108] + refc = [299, 298, 298, 298, 298, 297, 297, 296, 296, 295] + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_fixed_reference(dtype): + img = data.text() + r = np.linspace(136, 50, 100) + c = np.linspace(5, 424, 100) + init = np.array([r, c]).T + image_smooth = gaussian(img, sigma=1, preserve_range=False).astype( + dtype, copy=False + ) + snake = active_contour( + image_smooth, + init, + boundary_condition='fixed', + alpha=0.1, + beta=1.0, + w_line=-5, + w_edge=0, + gamma=0.1, + ) + assert snake.dtype == _supported_float_type(dtype) + refr = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125] + refc = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42] + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_free_reference(dtype): + img = data.text() + r = np.linspace(70, 40, 100) + c = np.linspace(5, 424, 100) + init = np.array([r, c]).T + img_smooth = gaussian(img, sigma=3, preserve_range=False).astype(dtype, copy=False) + snake = active_contour( + img_smooth, + init, + boundary_condition='free', + alpha=0.1, + beta=1.0, + w_line=-5, + w_edge=0, + gamma=0.1, + ) + assert snake.dtype == _supported_float_type(dtype) + refr = [76, 76, 75, 74, 73, 72, 71, 70, 69, 69] + refc = [10, 13, 16, 19, 23, 26, 29, 32, 36, 39] + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_RGB(dtype): + img = gaussian(data.text(), sigma=1, preserve_range=False) + imgR = np.zeros((img.shape[0], img.shape[1], 3), dtype=dtype) + imgG = np.zeros((img.shape[0], img.shape[1], 3), dtype=dtype) + imgRGB = np.zeros((img.shape[0], img.shape[1], 3), dtype=dtype) + imgR[:, :, 0] = img + imgG[:, :, 1] = img + imgRGB[:, :, :] = img[:, :, None] + r = np.linspace(136, 50, 100) + c = np.linspace(5, 424, 100) + init = np.array([r, c]).T + snake = active_contour( + imgR, + init, + boundary_condition='fixed', + alpha=0.1, + beta=1.0, + w_line=-5, + w_edge=0, + gamma=0.1, + ) + float_dtype = _supported_float_type(dtype) + assert snake.dtype == float_dtype + refr = [136, 135, 134, 133, 132, 131, 129, 128, 127, 125] + refc = [5, 9, 13, 17, 21, 25, 30, 34, 38, 42] + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + snake = active_contour( + imgG, + init, + boundary_condition='fixed', + alpha=0.1, + beta=1.0, + w_line=-5, + w_edge=0, + gamma=0.1, + ) + assert snake.dtype == float_dtype + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + snake = active_contour( + imgRGB, + init, + boundary_condition='fixed', + alpha=0.1, + beta=1.0, + w_line=-5 / 3.0, + w_edge=0, + gamma=0.1, + ) + assert snake.dtype == float_dtype + assert_equal(np.array(snake[:10, 0], dtype=np.int32), refr) + assert_equal(np.array(snake[:10, 1], dtype=np.int32), refc) + + +def test_end_points(): + img = data.astronaut() + img = rgb2gray(img) + s = np.linspace(0, 2 * np.pi, 400) + r = 100 + 100 * np.sin(s) + c = 220 + 100 * np.cos(s) + init = np.array([r, c]).T + snake = active_contour( + gaussian(img, sigma=3), + init, + boundary_condition='periodic', + alpha=0.015, + beta=10, + w_line=0, + w_edge=1, + gamma=0.001, + max_num_iter=100, + ) + assert np.sum(np.abs(snake[0, :] - snake[-1, :])) < 2 + snake = active_contour( + gaussian(img, sigma=3), + init, + boundary_condition='free', + alpha=0.015, + beta=10, + w_line=0, + w_edge=1, + gamma=0.001, + max_num_iter=100, + ) + assert np.sum(np.abs(snake[0, :] - snake[-1, :])) > 2 + snake = active_contour( + gaussian(img, sigma=3), + init, + boundary_condition='fixed', + alpha=0.015, + beta=10, + w_line=0, + w_edge=1, + gamma=0.001, + max_num_iter=100, + ) + assert_allclose(snake[0, :], [r[0], c[0]], atol=1e-5) + + +def test_bad_input(): + img = np.zeros((10, 10)) + r = np.linspace(136, 50, 100) + c = np.linspace(5, 424, 100) + init = np.array([r, c]).T + with pytest.raises(ValueError): + active_contour(img, init, boundary_condition='wrong') + with pytest.raises(ValueError): + active_contour(img, init, max_num_iter=-15) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_boundaries.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_boundaries.py new file mode 100644 index 0000000000000000000000000000000000000000..578c20b863f6b4ec45ea246de140ce2653f042d7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_boundaries.py @@ -0,0 +1,159 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal, assert_allclose + +from skimage._shared.utils import _supported_float_type +from skimage.segmentation import find_boundaries, mark_boundaries + + +white = (1, 1, 1) + + +def test_find_boundaries(): + image = np.zeros((10, 10), dtype=np.uint8) + image[2:7, 2:7] = 1 + + ref = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + + result = find_boundaries(image) + assert_array_equal(result, ref) + + +def test_find_boundaries_bool(): + image = np.zeros((5, 5), dtype=bool) + image[2:5, 2:5] = True + + ref = np.array( + [ + [False, False, False, False, False], + [False, False, True, True, True], + [False, True, True, True, True], + [False, True, True, False, False], + [False, True, True, False, False], + ], + dtype=bool, + ) + result = find_boundaries(image) + assert_array_equal(result, ref) + + +@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64]) +def test_mark_boundaries(dtype): + image = np.zeros((10, 10), dtype=dtype) + label_image = np.zeros((10, 10), dtype=np.uint8) + label_image[2:7, 2:7] = 1 + + ref = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + + marked = mark_boundaries(image, label_image, color=white, mode='thick') + assert marked.dtype == _supported_float_type(dtype) + result = np.mean(marked, axis=-1) + assert_array_equal(result, ref) + + ref = np.array( + [ + [0, 2, 2, 2, 2, 2, 2, 2, 0, 0], + [2, 2, 1, 1, 1, 1, 1, 2, 2, 0], + [2, 1, 1, 1, 1, 1, 1, 1, 2, 0], + [2, 1, 1, 2, 2, 2, 1, 1, 2, 0], + [2, 1, 1, 2, 0, 2, 1, 1, 2, 0], + [2, 1, 1, 2, 2, 2, 1, 1, 2, 0], + [2, 1, 1, 1, 1, 1, 1, 1, 2, 0], + [2, 2, 1, 1, 1, 1, 1, 2, 2, 0], + [0, 2, 2, 2, 2, 2, 2, 2, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + marked = mark_boundaries( + image, label_image, color=white, outline_color=(2, 2, 2), mode='thick' + ) + result = np.mean(marked, axis=-1) + assert_array_equal(result, ref) + + +def test_mark_boundaries_bool(): + image = np.zeros((10, 10), dtype=bool) + label_image = np.zeros((10, 10), dtype=np.uint8) + label_image[2:7, 2:7] = 1 + + ref = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + + marked = mark_boundaries(image, label_image, color=white, mode='thick') + result = np.mean(marked, axis=-1) + assert_array_equal(result, ref) + + +@pytest.mark.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_mark_boundaries_subpixel(dtype): + labels = np.array( + [[0, 0, 0, 0], [0, 0, 5, 0], [0, 1, 5, 0], [0, 0, 5, 0], [0, 0, 0, 0]], + dtype=np.uint8, + ) + np.random.seed(0) + image = np.round(np.random.rand(*labels.shape), 2) + image = image.astype(dtype, copy=False) + marked = mark_boundaries(image, labels, color=white, mode='subpixel') + assert marked.dtype == _supported_float_type(dtype) + marked_proj = np.round(np.mean(marked, axis=-1), 2) + + ref_result = np.array( + [ + [0.55, 0.63, 0.72, 0.69, 0.6, 0.55, 0.54], + [0.45, 0.58, 0.72, 1.0, 1.0, 1.0, 0.69], + [0.42, 0.54, 0.65, 1.0, 0.44, 1.0, 0.89], + [0.69, 1.0, 1.0, 1.0, 0.69, 1.0, 0.83], + [0.96, 1.0, 0.38, 1.0, 0.79, 1.0, 0.53], + [0.89, 1.0, 1.0, 1.0, 0.38, 1.0, 0.16], + [0.57, 0.78, 0.93, 1.0, 0.07, 1.0, 0.09], + [0.2, 0.52, 0.92, 1.0, 1.0, 1.0, 0.54], + [0.02, 0.35, 0.83, 0.9, 0.78, 0.81, 0.87], + ] + ) + assert_allclose(marked_proj, ref_result, atol=0.01) + + +@pytest.mark.parametrize('mode', ['thick', 'inner', 'outer', 'subpixel']) +def test_boundaries_constant_image(mode): + """A constant-valued image has not boundaries.""" + ones = np.ones((8, 8), dtype=int) + b = find_boundaries(ones, mode=mode) + assert np.all(b == 0) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_chan_vese.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_chan_vese.py new file mode 100644 index 0000000000000000000000000000000000000000..ba92a1e2e48fe1eb2ed3ec1b4bd5f9e7781f6c46 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_chan_vese.py @@ -0,0 +1,102 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +from skimage._shared.utils import _supported_float_type +from skimage.segmentation import chan_vese + + +@pytest.mark.parametrize('dtype', [np.float32, np.float64]) +def test_chan_vese_flat_level_set(dtype): + # because the algorithm evolves the level set around the + # zero-level, it the level-set has no zero level, the algorithm + # will not produce results in theory. However, since a continuous + # approximation of the delta function is used, the algorithm + # still affects the entirety of the level-set. Therefore with + # infinite time, the segmentation will still converge. + img = np.zeros((10, 10), dtype=dtype) + img[3:6, 3:6] = 1 + ls = np.full((10, 10), 1000, dtype=dtype) + result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set=ls) + assert_array_equal(result.astype(float), np.ones((10, 10))) + result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set=-ls) + assert_array_equal(result.astype(float), np.zeros((10, 10))) + + +def test_chan_vese_small_disk_level_set(): + img = np.zeros((10, 10)) + img[3:6, 3:6] = 1 + result = chan_vese(img, mu=0.0, tol=1e-3, init_level_set="small disk") + assert_array_equal(result.astype(float), img) + + +def test_chan_vese_simple_shape(): + img = np.zeros((10, 10)) + img[3:6, 3:6] = 1 + result = chan_vese(img, mu=0.0, tol=1e-8).astype(float) + assert_array_equal(result, img) + + +@pytest.mark.parametrize('dtype', [np.uint8, np.float16, np.float32, np.float64]) +def test_chan_vese_extended_output(dtype): + img = np.zeros((10, 10), dtype=dtype) + img[3:6, 3:6] = 1 + result = chan_vese(img, mu=0.0, tol=1e-8, extended_output=True) + float_dtype = _supported_float_type(dtype) + assert result[1].dtype == float_dtype + assert all(arr.dtype == float_dtype for arr in result[2]) + assert_array_equal(len(result), 3) + + +def test_chan_vese_remove_noise(): + ref = np.zeros((10, 10)) + ref[1:6, 1:6] = np.array( + [ + [0, 1, 1, 1, 0], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [0, 1, 1, 1, 0], + ] + ) + img = ref.copy() + img[8, 3] = 1 + result = chan_vese( + img, mu=0.3, tol=1e-3, max_num_iter=100, dt=10, init_level_set="disk" + ).astype(float) + assert_array_equal(result, ref) + + +def test_chan_vese_incorrect_image_type(): + img = np.zeros((10, 10, 3)) + ls = np.zeros((10, 9)) + with pytest.raises(ValueError): + chan_vese(img, mu=0.0, init_level_set=ls) + + +def test_chan_vese_gap_closing(): + ref = np.zeros((20, 20)) + ref[8:15, :] = np.ones((7, 20)) + img = ref.copy() + img[:, 6] = np.zeros(20) + result = chan_vese( + img, mu=0.7, tol=1e-3, max_num_iter=1000, dt=1000, init_level_set="disk" + ).astype(float) + assert_array_equal(result, ref) + + +def test_chan_vese_incorrect_level_set(): + img = np.zeros((10, 10)) + ls = np.zeros((10, 9)) + with pytest.raises(ValueError): + chan_vese(img, mu=0.0, init_level_set=ls) + with pytest.raises(ValueError): + chan_vese(img, mu=0.0, init_level_set="a") + + +def test_chan_vese_blank_image(): + img = np.zeros((10, 10)) + level_set = np.random.rand(10, 10) + ref = level_set > 0 + result = chan_vese(img, mu=0.0, tol=0.0, init_level_set=level_set) + assert_array_equal(result, ref) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_clear_border.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_clear_border.py new file mode 100644 index 0000000000000000000000000000000000000000..4131997d09ab1c1a005a7aa0f8b1a8329ee25843 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_clear_border.py @@ -0,0 +1,180 @@ +import numpy as np +from skimage.segmentation import clear_border + +from skimage._shared.testing import assert_array_equal, assert_ + + +def test_clear_border(): + image = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 1, 0], + [1, 1, 0, 0, 1, 0, 0, 1, 0], + [1, 1, 0, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 0, 0], + [0, 1, 1, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) + + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[1:3, 0:2] = 0 + ref[0:2, -2] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, np.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), buffer_size=1, bgval=2) + assert_array_equal(result, 2 * np.ones_like(image)) + + # test mask + mask = np.array( + [ + [0, 0, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 1], + ] + ).astype(bool) + result = clear_border(image.copy(), mask=mask) + ref = image.copy() + ref[1:3, 0:2] = 0 + assert_array_equal(result, ref) + + +def test_clear_border_3d(): + image = np.array( + [ + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [1, 0, 0, 0]], + [[0, 0, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + ] + ) + # test default case + result = clear_border(image.copy()) + ref = image.copy() + ref[0, 3, 0] = 0 + assert_array_equal(result, ref) + + # test buffer + result = clear_border(image.copy(), 1) + assert_array_equal(result, np.zeros(result.shape)) + + # test background value + result = clear_border(image.copy(), buffer_size=1, bgval=2) + assert_array_equal(result, 2 * np.ones_like(image)) + + +def test_clear_border_non_binary(): + image = np.array( + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]] + ) + + result = clear_border(image) + expected = np.array( + [[0, 0, 0, 0, 0], [0, 0, 5, 4, 0], [0, 4, 5, 4, 0], [0, 0, 0, 0, 0]] + ) + + assert_array_equal(result, expected) + assert_(not np.all(image == result)) + + +def test_clear_border_non_binary_3d(): + image3d = np.array( + [ + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + ] + ) + + result = clear_border(image3d) + expected = np.array( + [ + [[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, 5, 0, 0], [0, 0, 5, 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]], + ] + ) + + assert_array_equal(result, expected) + assert_(not np.all(image3d == result)) + + +def test_clear_border_non_binary_inplace(): + image = np.array( + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]] + ) + + result = clear_border(image, out=image) + expected = np.array( + [[0, 0, 0, 0, 0], [0, 0, 5, 4, 0], [0, 4, 5, 4, 0], [0, 0, 0, 0, 0]] + ) + + assert_array_equal(result, expected) + assert_array_equal(image, result) + + +def test_clear_border_non_binary_inplace_3d(): + image3d = np.array( + [ + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + ] + ) + + result = clear_border(image3d, out=image3d) + expected = np.array( + [ + [[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, 5, 0, 0], [0, 0, 5, 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]], + ] + ) + + assert_array_equal(result, expected) + assert_array_equal(image3d, result) + + +def test_clear_border_non_binary_out(): + image = np.array( + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]] + ) + out = image.copy() + result = clear_border(image, out=out) + expected = np.array( + [[0, 0, 0, 0, 0], [0, 0, 5, 4, 0], [0, 4, 5, 4, 0], [0, 0, 0, 0, 0]] + ) + + assert_array_equal(result, expected) + assert_array_equal(out, result) + + +def test_clear_border_non_binary_out_3d(): + image3d = np.array( + [ + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 5, 4, 2], [3, 4, 5, 4, 2], [3, 3, 2, 1, 2]], + [[1, 2, 3, 1, 2], [3, 3, 3, 4, 2], [3, 4, 3, 4, 2], [3, 3, 2, 1, 2]], + ] + ) + out = image3d.copy() + + result = clear_border(image3d, out=out) + expected = np.array( + [ + [[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, 5, 0, 0], [0, 0, 5, 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]], + ] + ) + + assert_array_equal(result, expected) + assert_array_equal(out, result) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_expand_labels.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_expand_labels.py new file mode 100644 index 0000000000000000000000000000000000000000..4d589a4982a0193093a38f290fca45d3b03b8790 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_expand_labels.py @@ -0,0 +1,182 @@ +from scipy import ndimage as ndi +from skimage import data + +import numpy as np + +from skimage import measure +from skimage.segmentation._expand_labels import expand_labels + +from skimage._shared import testing +from skimage._shared.testing import assert_array_equal + +SAMPLE1D = np.array([0, 0, 4, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]) +SAMPLE1D_EXPANDED_3 = np.array([4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]) + +# Some pixels are important edge cases with undefined behaviour: +# these are the pixels that are at the same distance from +# multiple labels. Ideally the label would be chosen at random +# to avoid bias, but as we are relying on the index map returned +# by the scipy.ndimage distance transform, what actually happens +# is determined by the upstream implementation of the distance +# tansform, thus we don't give any guarantees for the edge case pixels. +# +# Regardless, it seems prudent to have a test including an edge case +# so we can detect whether future upstream changes in scipy.ndimage +# modify the behaviour. + +EDGECASE1D = np.array([0, 0, 4, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0]) +EDGECASE1D_EXPANDED_3 = np.array([4, 4, 4, 4, 4, 4, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]) + +SAMPLE2D = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 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], + ] +) + +SAMPLE2D_EXPANDED_3 = np.array( + [ + [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 0, 0, 2, 0], + [1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 1, 0, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 0, 2, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 0, 0, 2, 2, 2, 2], + [0, 0, 1, 0, 0, 0, 0, 2, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0], + ] +) + +# non-integer expansion +SAMPLE2D_EXPANDED_1_5 = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0, 2, 2, 2], + [1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2], + [0, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] +) + + +EDGECASE2D = np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0], + [0, 0, 1, 1, 0, 2, 2, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 2, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], + ] +) + +EDGECASE2D_EXPANDED_4 = np.array( + [ + [1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0], + [1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2], + [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 0], + [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 0], + ] +) + +SAMPLE3D = np.array( + [ + [[0, 0, 0, 0], [0, 3, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 3, 3, 0], [0, 0, 0, 0], [0, 0, 0, 0]], + [[0, 0, 0, 0], [0, 3, 0, 0], [0, 0, 0, 0], [0, 0, 5, 0]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 5, 0]], + ] +) + +SAMPLE3D_EXPANDED_2 = np.array( + [ + [[3, 3, 3, 3], [3, 3, 3, 3], [3, 3, 3, 3], [0, 3, 5, 0]], + [[3, 3, 3, 3], [3, 3, 3, 3], [3, 3, 3, 3], [0, 5, 5, 5]], + [[3, 3, 3, 3], [3, 3, 3, 3], [3, 3, 5, 5], [5, 5, 5, 5]], + [[3, 3, 3, 0], [3, 3, 3, 0], [3, 3, 5, 5], [5, 5, 5, 5]], + ] +) +SAMPLE3D_EXPAND_SPACING = np.array( + [ + [[0, 3, 0, 0], [3, 3, 3, 0], [0, 3, 0, 0], [0, 0, 0, 0]], + [[0, 3, 3, 0], [3, 3, 3, 3], [0, 3, 3, 0], [0, 0, 0, 0]], + [[0, 3, 0, 0], [3, 3, 3, 0], [0, 3, 5, 0], [0, 5, 5, 5]], + [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 5, 0], [0, 5, 5, 5]], + ] +) + +SAMPLE_EDGECASE_BEHAVIOUR = np.array([[0, 1, 0, 0], [2, 0, 0, 0], [0, 3, 0, 0]]) + + +@testing.parametrize( + "input_array, expected_output, expand_distance, spacing", + [ + (SAMPLE1D, SAMPLE1D_EXPANDED_3, 3, 1), + (SAMPLE2D, SAMPLE2D_EXPANDED_3, 3, 1), + (SAMPLE2D, SAMPLE2D_EXPANDED_1_5, 1.5, 1), + (EDGECASE1D, EDGECASE1D_EXPANDED_3, 3, 1), + (EDGECASE2D, EDGECASE2D_EXPANDED_4, 4, 1), + (SAMPLE3D, SAMPLE3D_EXPANDED_2, 2, 1), + (SAMPLE3D, SAMPLE3D_EXPAND_SPACING, 1, [2, 1, 1]), + ], +) +def test_expand_labels(input_array, expected_output, expand_distance, spacing): + expanded = expand_labels(input_array, expand_distance, spacing) + assert_array_equal(expanded, expected_output) + + +@testing.parametrize('ndim', [2, 3]) +@testing.parametrize('distance', range(6)) +def test_binary_blobs(ndim, distance): + """Check some invariants with label expansion. + + - New labels array should exactly contain the original labels array. + - Distance to old labels array within new labels should never exceed input + distance. + - Distance beyond the expanded labels should always exceed the input + distance. + """ + img = data.binary_blobs(length=64, blob_size_fraction=0.05, n_dim=ndim) + labels = measure.label(img) + expanded = expand_labels(labels, distance=distance) + original_mask = labels != 0 + assert_array_equal(labels[original_mask], expanded[original_mask]) + expanded_only_mask = (expanded - labels).astype(bool) + distance_map = ndi.distance_transform_edt(~original_mask) + expanded_distances = distance_map[expanded_only_mask] + if expanded_distances.size > 0: + assert np.all(expanded_distances <= distance) + beyond_expanded_distances = distance_map[~expanded.astype(bool)] + if beyond_expanded_distances.size > 0: + assert np.all(beyond_expanded_distances > distance) + + +def test_edge_case_behaviour(): + """Check edge case behavior to detect upstream changes + + For edge cases where a pixel has the same distance to several regions, + lexicographical order seems to determine which region gets to expand + into this pixel given the current upstream behaviour in + scipy.ndimage.distance_map_edt. + + As a result, we expect different results when transposing the array. + If this test fails, something has changed upstream. + """ + expanded = expand_labels(SAMPLE_EDGECASE_BEHAVIOUR, 1) + expanded_transpose = expand_labels(SAMPLE_EDGECASE_BEHAVIOUR.T, 1) + assert not np.all(expanded == expanded_transpose.T) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_felzenszwalb.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_felzenszwalb.py new file mode 100644 index 0000000000000000000000000000000000000000..88597f024203b41b904d8982807f5aadfa006a79 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_felzenszwalb.py @@ -0,0 +1,90 @@ +import numpy as np +from skimage import data +from skimage.segmentation import felzenszwalb + +from skimage._shared import testing +from skimage._shared.testing import ( + assert_greater, + run_in_parallel, + assert_equal, + assert_array_equal, + assert_warns, + assert_no_warnings, +) + + +@run_in_parallel() +def test_grey(): + # very weak tests. + img = np.zeros((20, 21)) + img[:10, 10:] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + seg = felzenszwalb(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in range(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 40) + + +def test_minsize(): + # single-channel: + img = data.coins()[20:168, 0:128] + for min_size in np.arange(10, 100, 10): + segments = felzenszwalb(img, min_size=min_size, sigma=3) + counts = np.bincount(segments.ravel()) + # actually want to test greater or equal. + assert_greater(counts.min() + 1, min_size) + # multi-channel: + coffee = data.coffee()[::4, ::4] + for min_size in np.arange(10, 100, 10): + segments = felzenszwalb(coffee, min_size=min_size, sigma=3) + counts = np.bincount(segments.ravel()) + # actually want to test greater or equal. + assert_greater(counts.min() + 1, min_size) + + +@testing.parametrize('channel_axis', [0, -1]) +def test_3D(channel_axis): + grey_img = np.zeros((10, 10)) + rgb_img = np.zeros((10, 10, 3)) + three_d_img = np.zeros((10, 10, 10)) + + rgb_img = np.moveaxis(rgb_img, -1, channel_axis) + with assert_no_warnings(): + felzenszwalb(grey_img, channel_axis=-1) + felzenszwalb(grey_img, channel_axis=None) + felzenszwalb(rgb_img, channel_axis=channel_axis) + with assert_warns(RuntimeWarning): + felzenszwalb(three_d_img, channel_axis=channel_axis) + with testing.raises(ValueError): + felzenszwalb(rgb_img, channel_axis=None) + felzenszwalb(three_d_img, channel_axis=None) + + +def test_color(): + # very weak tests. + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + seg = felzenszwalb(img, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 0) + assert_array_equal(seg[10:, :10], 2) + assert_array_equal(seg[:10, 10:], 1) + assert_array_equal(seg[10:, 10:], 3) + + +def test_merging(): + # test region merging in the post-processing step + img = np.array([[0, 0.3], [0.7, 1]]) + # With scale=0, only the post-processing is performed. + seg = felzenszwalb(img, scale=0, sigma=0, min_size=2) + # we expect 2 segments: + assert_equal(len(np.unique(seg)), 2) + assert_array_equal(seg[0, :], 0) + assert_array_equal(seg[1, :], 1) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_join.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_join.py new file mode 100644 index 0000000000000000000000000000000000000000..2db2d1204d29334809853a0ed75a89f4a2ed99f2 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_join.py @@ -0,0 +1,219 @@ +import numpy as np +from skimage.segmentation import join_segmentations, relabel_sequential + +from skimage._shared import testing +from skimage._shared.testing import assert_array_equal +import pytest + + +@pytest.mark.parametrize("dtype", [int, np.uint16, np.uint]) +def test_join_segmentations(dtype): + s1 = np.array([[0, 0, 1, 1], [0, 2, 1, 1], [2, 2, 2, 1]], dtype=dtype) + s2 = np.array([[0, 1, 1, 0], [0, 1, 1, 0], [0, 1, 1, 1]], dtype=dtype) + + # test correct join + # NOTE: technically, equality to j_ref is not required, only that there + # is a one-to-one mapping between j and j_ref. I don't know of an easy way + # to check this (i.e. not as error-prone as the function being tested) + j = join_segmentations(s1, s2) + j_ref = np.array([[0, 1, 3, 2], [0, 5, 3, 2], [4, 5, 5, 3]]) + assert_array_equal(j, j_ref) + + # test correct mapping + j, m1, m2 = join_segmentations(s1, s2, return_mapping=True) + assert_array_equal(m1[j], s1) + assert_array_equal(m2[j], s2) + + # test correct exception when arrays are different shapes + s3 = np.array([[0, 0, 1, 1], [0, 2, 2, 1]]) + with testing.raises(ValueError): + join_segmentations(s1, s3) + + +def _check_maps(ar, ar_relab, fw, inv): + assert_array_equal(fw[ar], ar_relab) + assert_array_equal(inv[ar_relab], ar) + + +def test_relabel_sequential_offset1(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_sequential(ar) + _check_maps(ar, ar_relab, fw, inv) + ar_relab_ref = np.array([1, 1, 2, 2, 3, 5, 4]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 1 + fw_ref[5] = 2 + fw_ref[8] = 3 + fw_ref[42] = 4 + fw_ref[99] = 5 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_offset5(): + ar = np.array([1, 1, 5, 5, 8, 99, 42]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + _check_maps(ar, ar_relab, fw, inv) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5 + fw_ref[5] = 6 + fw_ref[8] = 7 + fw_ref[42] = 8 + fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_offset5_with0(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0]) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + _check_maps(ar, ar_relab, fw, inv) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5 + fw_ref[5] = 6 + fw_ref[8] = 7 + fw_ref[42] = 8 + fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_dtype(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.uint8) + ar_relab, fw, inv = relabel_sequential(ar, offset=5) + _check_maps(ar.astype(int), ar_relab, fw, inv) + ar_relab_ref = np.array([5, 5, 6, 6, 7, 9, 8, 0]) + assert_array_equal(ar_relab, ar_relab_ref) + fw_ref = np.zeros(100, int) + fw_ref[1] = 5 + fw_ref[5] = 6 + fw_ref[8] = 7 + fw_ref[42] = 8 + fw_ref[99] = 9 + assert_array_equal(fw, fw_ref) + inv_ref = np.array([0, 0, 0, 0, 0, 1, 5, 8, 42, 99]) + assert_array_equal(inv, inv_ref) + + +def test_relabel_sequential_signed_overflow(): + imax = np.iinfo(np.int32).max + labels = np.array([0, 1, 99, 42, 42], dtype=np.int32) + output, fw, inv = relabel_sequential(labels, offset=imax) + reference = np.array([0, imax, imax + 2, imax + 1, imax + 1], dtype=np.uint32) + assert_array_equal(output, reference) + assert output.dtype == reference.dtype + + +def test_very_large_labels(): + imax = np.iinfo(np.int64).max + labels = np.array([0, 1, imax, 42, 42], dtype=np.int64) + output, fw, inv = relabel_sequential(labels, offset=imax) + assert np.max(output) == imax + 2 + + +@pytest.mark.parametrize( + 'dtype', + ( + np.byte, + np.short, + np.intc, + int, + np.longlong, + np.ubyte, + np.ushort, + np.uintc, + np.uint, + np.ulonglong, + ), +) +@pytest.mark.parametrize('data_already_sequential', (False, True)) +def test_relabel_sequential_int_dtype_stability(data_already_sequential, dtype): + if data_already_sequential: + ar = np.array([1, 3, 0, 2, 5, 4], dtype=dtype) + else: + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=dtype) + assert all(a.dtype == dtype for a in relabel_sequential(ar)) + + +def test_relabel_sequential_int_dtype_overflow(): + ar = np.array([1, 3, 0, 2, 5, 4], dtype=np.uint8) + offset = 254 + ar_relab, fw, inv = relabel_sequential(ar, offset=offset) + _check_maps(ar, ar_relab, fw, inv) + assert all(a.dtype == np.uint16 for a in (ar_relab, fw)) + assert inv.dtype == ar.dtype + ar_relab_ref = np.where(ar > 0, ar.astype(int) + offset - 1, 0) + assert_array_equal(ar_relab, ar_relab_ref) + + +def test_relabel_sequential_negative_values(): + ar = np.array([1, 1, 5, -5, 8, 99, 42, 0]) + with pytest.raises(ValueError): + relabel_sequential(ar) + + +@pytest.mark.parametrize('offset', (0, -3)) +@pytest.mark.parametrize('data_already_sequential', (False, True)) +def test_relabel_sequential_nonpositive_offset(data_already_sequential, offset): + if data_already_sequential: + ar = np.array([1, 3, 0, 2, 5, 4]) + else: + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0]) + with pytest.raises(ValueError): + relabel_sequential(ar, offset=offset) + + +@pytest.mark.parametrize('offset', (1, 5)) +@pytest.mark.parametrize('with0', (False, True)) +@pytest.mark.parametrize('input_starts_at_offset', (False, True)) +def test_relabel_sequential_already_sequential(offset, with0, input_starts_at_offset): + if with0: + ar = np.array([1, 3, 0, 2, 5, 4]) + else: + ar = np.array([1, 3, 2, 5, 4]) + if input_starts_at_offset: + ar[ar > 0] += offset - 1 + ar_relab, fw, inv = relabel_sequential(ar, offset=offset) + _check_maps(ar, ar_relab, fw, inv) + if input_starts_at_offset: + ar_relab_ref = ar + else: + ar_relab_ref = np.where(ar > 0, ar + offset - 1, 0) + assert_array_equal(ar_relab, ar_relab_ref) + + +def test_incorrect_input_dtype(): + labels = np.array([0, 2, 2, 1, 1, 8], dtype=float) + with testing.raises(TypeError): + _ = relabel_sequential(labels) + + +def test_arraymap_call(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp) + relabeled, fw, inv = relabel_sequential(ar) + testing.assert_array_equal(relabeled, fw(ar)) + testing.assert_array_equal(ar, inv(relabeled)) + + +def test_arraymap_len(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp) + relabeled, fw, inv = relabel_sequential(ar) + assert len(fw) == 100 + assert len(fw) == len(np.array(fw)) + assert len(inv) == 6 + assert len(inv) == len(np.array(inv)) + + +def test_arraymap_set(): + ar = np.array([1, 1, 5, 5, 8, 99, 42, 0], dtype=np.intp) + relabeled, fw, inv = relabel_sequential(ar) + fw[72] = 6 + assert fw[72] == 6 diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_morphsnakes.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_morphsnakes.py new file mode 100644 index 0000000000000000000000000000000000000000..7d8ad2f2a7a8343c6efe2cd68c26ba29593fbffa --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_morphsnakes.py @@ -0,0 +1,152 @@ +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +from skimage.segmentation import ( + disk_level_set, + inverse_gaussian_gradient, + morphological_chan_vese, + morphological_geodesic_active_contour, +) + + +def gaussian_blob(): + coords = np.mgrid[-5:6, -5:6] + sqrdistances = (coords**2).sum(0) + return np.exp(-sqrdistances / 10) + + +def test_morphsnakes_incorrect_image_shape(): + img = np.zeros((10, 10, 3)) + ls = np.zeros((10, 9)) + + with pytest.raises(ValueError): + morphological_chan_vese(img, num_iter=1, init_level_set=ls) + with pytest.raises(ValueError): + morphological_geodesic_active_contour(img, num_iter=1, init_level_set=ls) + + +def test_morphsnakes_incorrect_ndim(): + img = np.zeros((4, 4, 4, 4)) + ls = np.zeros((4, 4, 4, 4)) + + with pytest.raises(ValueError): + morphological_chan_vese(img, num_iter=1, init_level_set=ls) + with pytest.raises(ValueError): + morphological_geodesic_active_contour(img, num_iter=1, init_level_set=ls) + + +def test_morphsnakes_black(): + img = np.zeros((11, 11)) + ls = disk_level_set(img.shape, center=(5, 5), radius=3) + + ref_zeros = np.zeros(img.shape, dtype=np.int8) + ref_ones = np.ones(img.shape, dtype=np.int8) + + acwe_ls = morphological_chan_vese(img, num_iter=6, init_level_set=ls) + assert_array_equal(acwe_ls, ref_zeros) + + gac_ls = morphological_geodesic_active_contour(img, num_iter=6, init_level_set=ls) + assert_array_equal(gac_ls, ref_zeros) + + gac_ls2 = morphological_geodesic_active_contour( + img, num_iter=6, init_level_set=ls, balloon=1, threshold=-1, smoothing=0 + ) + assert_array_equal(gac_ls2, ref_ones) + + assert acwe_ls.dtype == gac_ls.dtype == gac_ls2.dtype == np.int8 + + +def test_morphsnakes_simple_shape_chan_vese(): + img = gaussian_blob() + ls1 = disk_level_set(img.shape, center=(5, 5), radius=3) + ls2 = disk_level_set(img.shape, center=(5, 5), radius=6) + + acwe_ls1 = morphological_chan_vese(img, num_iter=10, init_level_set=ls1) + acwe_ls2 = morphological_chan_vese(img, num_iter=10, init_level_set=ls2) + + assert_array_equal(acwe_ls1, acwe_ls2) + + assert acwe_ls1.dtype == acwe_ls2.dtype == np.int8 + + +def test_morphsnakes_simple_shape_geodesic_active_contour(): + img = (disk_level_set((11, 11), center=(5, 5), radius=3.5)).astype(float) + gimg = inverse_gaussian_gradient(img, alpha=10.0, sigma=1.0) + ls = disk_level_set(img.shape, center=(5, 5), radius=6) + + ref = np.array( + [ + [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, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 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], + ], + dtype=np.int8, + ) + + gac_ls = morphological_geodesic_active_contour( + gimg, num_iter=10, init_level_set=ls, balloon=-1 + ) + assert_array_equal(gac_ls, ref) + assert gac_ls.dtype == np.int8 + + +def test_init_level_sets(): + image = np.zeros((6, 6)) + checkerboard_ls = morphological_chan_vese(image, 0, 'checkerboard') + checkerboard_ref = np.array( + [ + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [1, 1, 1, 1, 1, 0], + ], + dtype=np.int8, + ) + + disk_ls = morphological_geodesic_active_contour(image, 0, 'disk') + disk_ref = np.array( + [ + [0, 0, 0, 0, 0, 0], + [0, 0, 1, 1, 1, 0], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 0], + ], + dtype=np.int8, + ) + + assert_array_equal(checkerboard_ls, checkerboard_ref) + assert_array_equal(disk_ls, disk_ref) + + +def test_morphsnakes_3d(): + image = np.zeros((7, 7, 7)) + + evolution = [] + + def callback(x): + evolution.append(x.sum()) + + ls = morphological_chan_vese(image, 5, 'disk', iter_callback=callback) + + # Check that the initial disk level set is correct + assert evolution[0] == 81 + + # Check that the final level set is correct + assert ls.sum() == 0 + + # Check that the contour is shrinking at every iteration + for v1, v2 in zip(evolution[:-1], evolution[1:]): + assert v1 >= v2 diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_quickshift.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_quickshift.py new file mode 100644 index 0000000000000000000000000000000000000000..c72e330ba526cc5b5f7613ec55abed1150399487 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_quickshift.py @@ -0,0 +1,79 @@ +import numpy as np +import pytest +from skimage.segmentation import quickshift + +from skimage._shared import testing +from skimage._shared.testing import ( + assert_greater, + run_in_parallel, + assert_equal, + assert_array_equal, +) + + +@run_in_parallel() +@testing.parametrize('dtype', [np.float32, np.float64]) +def test_grey(dtype): + rng = np.random.default_rng(0) + img = np.zeros((20, 21)) + img[:10, 10:] = 0.2 + img[10:, :10] = 0.4 + img[10:, 10:] = 0.6 + img += 0.05 * rng.normal(size=img.shape) + img = img.astype(dtype, copy=False) + seg = quickshift(img, kernel_size=2, max_dist=3, rng=0, convert2lab=False, sigma=0) + quickshift(img, kernel_size=2, max_dist=3, rng=0, convert2lab=False, sigma=0) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + # that mostly respect the 4 regions: + for i in range(4): + hist = np.histogram(img[seg == i], bins=[0, 0.1, 0.3, 0.5, 1])[0] + assert_greater(hist[i], 20) + + +@testing.parametrize('dtype', [np.float32, np.float64]) +@testing.parametrize('channel_axis', [-3, -2, -1, 0, 1, 2]) +def test_color(dtype, channel_axis): + rng = np.random.default_rng(583428449) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + img = img.astype(dtype, copy=False) + + img = np.moveaxis(img, source=-1, destination=channel_axis) + seg = quickshift( + img, rng=0, max_dist=30, kernel_size=10, sigma=0, channel_axis=channel_axis + ) + # we expect 4 segments: + assert_equal(len(np.unique(seg)), 4) + assert_array_equal(seg[:10, :10], 1) + assert_array_equal(seg[10:, :10], 3) + assert_array_equal(seg[:10, 10:], 0) + assert_array_equal(seg[10:, 10:], 2) + + seg2 = quickshift( + img, + kernel_size=1, + max_dist=2, + rng=0, + convert2lab=False, + sigma=0, + channel_axis=channel_axis, + ) + # very oversegmented: + assert len(np.unique(seg2)) > 10 + # still don't cross lines + assert (seg2[9, :] != seg2[10, :]).all() + assert (seg2[:, 9] != seg2[:, 10]).all() + + +def test_convert2lab_not_rgb(): + img = np.zeros((20, 21, 2)) + with pytest.raises( + ValueError, match="Only RGB images can be converted to Lab space" + ): + quickshift(img, convert2lab=True) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_random_walker.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_random_walker.py new file mode 100644 index 0000000000000000000000000000000000000000..9a5f5eb63fdd013e20f98e20e9aad62aa3dbfa5c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_random_walker.py @@ -0,0 +1,615 @@ +import numpy as np +import pytest + +from skimage._shared import testing +from skimage._shared._warnings import expected_warnings +from skimage._shared.testing import xfail, arch32, is_wasm +from skimage.segmentation import random_walker +from skimage.transform import resize + + +def make_2d_syntheticdata(lx, ly=None): + if ly is None: + ly = lx + np.random.seed(1234) + data = np.zeros((lx, ly)) + 0.1 * np.random.randn(lx, ly) + small_l = int(lx // 5) + data[ + lx // 2 - small_l : lx // 2 + small_l, ly // 2 - small_l : ly // 2 + small_l + ] = 1 + data[ + lx // 2 - small_l + 1 : lx // 2 + small_l - 1, + ly // 2 - small_l + 1 : ly // 2 + small_l - 1, + ] = 0.1 * np.random.randn(2 * small_l - 2, 2 * small_l - 2) + data[lx // 2 - small_l, ly // 2 - small_l // 8 : ly // 2 + small_l // 8] = 0 + seeds = np.zeros_like(data) + seeds[lx // 5, ly // 5] = 1 + seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4] = 2 + return data, seeds + + +def make_3d_syntheticdata(lx, ly=None, lz=None): + if ly is None: + ly = lx + if lz is None: + lz = lx + np.random.seed(1234) + data = np.zeros((lx, ly, lz)) + 0.1 * np.random.randn(lx, ly, lz) + small_l = int(lx // 5) + data[ + lx // 2 - small_l : lx // 2 + small_l, + ly // 2 - small_l : ly // 2 + small_l, + lz // 2 - small_l : lz // 2 + small_l, + ] = 1 + data[ + lx // 2 - small_l + 1 : lx // 2 + small_l - 1, + ly // 2 - small_l + 1 : ly // 2 + small_l - 1, + lz // 2 - small_l + 1 : lz // 2 + small_l - 1, + ] = 0 + # make a hole + hole_size = np.max([1, small_l // 8]) + data[ + lx // 2 - small_l, + ly // 2 - hole_size : ly // 2 + hole_size, + lz // 2 - hole_size : lz // 2 + hole_size, + ] = 0 + seeds = np.zeros_like(data) + seeds[lx // 5, ly // 5, lz // 5] = 1 + seeds[lx // 2 + small_l // 4, ly // 2 - small_l // 4, lz // 2 - small_l // 4] = 2 + return data, seeds + + +@testing.parametrize('dtype', [np.float16, np.float32, np.float64]) +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_2d_bf(dtype): + lx = 70 + ly = 100 + + # have to use a smaller beta to avoid warning with lower precision input + beta = 90 if dtype == np.float64 else 25 + + data, labels = make_2d_syntheticdata(lx, ly) + data = data.astype(dtype, copy=False) + labels_bf = random_walker(data, labels, beta=beta, mode='bf') + assert (labels_bf[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + full_prob_bf = random_walker( + data, labels, beta=beta, mode='bf', return_full_prob=True + ) + assert (full_prob_bf[1, 25:45, 40:60] >= full_prob_bf[0, 25:45, 40:60]).all() + assert data.shape == labels.shape + # Now test with more than two labels + labels[55, 80] = 3 + full_prob_bf = random_walker( + data, labels, beta=beta, mode='bf', return_full_prob=True + ) + assert (full_prob_bf[1, 25:45, 40:60] >= full_prob_bf[0, 25:45, 40:60]).all() + assert len(full_prob_bf) == 3 + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings('ignore:"cg" mode may be slow:UserWarning:skimage') +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +@pytest.mark.filterwarnings( + 'ignore:"cg_mg" not available, it requires pyamg to be installed. The "cg_j" mode will be used instead.:UserWarning' +) # if pyamg is not available +@testing.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_2d_cg(dtype): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = data.astype(dtype, copy=False) + + labels_cg = random_walker(data, labels, beta=90, mode='cg') + assert (labels_cg[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + + full_prob = random_walker(data, labels, beta=90, mode='cg', return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= full_prob[0, 25:45, 40:60]).all() + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings("ignore:Implicit conversion of A to CSR::pyamg") +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +@pytest.mark.filterwarnings( + 'ignore:"cg_mg" not available, it requires pyamg to be installed. The "cg_j" mode will be used instead.:UserWarning' +) # if pyamg is not available +@testing.parametrize('dtype', [np.float16, np.float32, np.float64]) +def test_2d_cg_mg(dtype): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = data.astype(dtype, copy=False) + + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + assert (labels_cg_mg[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + + full_prob = random_walker( + data, labels, beta=90, mode='cg_mg', return_full_prob=True + ) + assert (full_prob[1, 25:45, 40:60] >= full_prob[0, 25:45, 40:60]).all() + assert data.shape == labels.shape + + +@testing.parametrize('dtype', [np.float16, np.float32, np.float64]) +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_2d_cg_j(dtype): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = data.astype(dtype, copy=False) + labels_cg = random_walker(data, labels, beta=90, mode='cg_j') + assert (labels_cg[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + full_prob = random_walker(data, labels, beta=90, mode='cg_j', return_full_prob=True) + assert (full_prob[1, 25:45, 40:60] >= full_prob[0, 25:45, 40:60]).all() + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +@pytest.mark.filterwarnings( + 'ignore:"cg_mg" not available, it requires pyamg to be installed. The "cg_j" mode will be used instead.:UserWarning' +) # if pyamg is not available +def test_types(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = 255 * (data - data.min()) // (data.max() - data.min()) + data = data.astype(np.uint8) + labels_cg_mg = random_walker(data, labels, beta=90, mode='cg_mg') + assert (labels_cg_mg[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_reorder_labels(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + labels[labels == 2] = 4 + labels_bf = random_walker(data, labels, beta=90, mode='bf') + assert (labels_bf[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_2d_inactive(): + lx = 70 + ly = 100 + data, labels = make_2d_syntheticdata(lx, ly) + labels[10:20, 10:20] = -1 + labels[46:50, 33:38] = -2 + labels = random_walker(data, labels, beta=90) + assert (labels.reshape((lx, ly))[25:45, 40:60] == 2).all() + assert data.shape == labels.shape + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_2d_laplacian_size(): + # test case from: https://github.com/scikit-image/scikit-image/issues/5034 + # The markers here were modified from the ones in the original issue to + # avoid a singular matrix, but still reproduce the issue. + data = np.asarray( + [[12823, 12787, 12710], [12883, 13425, 12067], [11934, 11929, 12309]] + ) + markers = np.asarray([[0, -1, 2], [0, -1, 0], [1, 0, -1]]) + expected_labels = np.asarray([[1, -1, 2], [1, -1, 2], [1, 1, -1]]) + labels = random_walker(data, markers, beta=10) + np.testing.assert_array_equal(labels, expected_labels) + + +@testing.parametrize('dtype', [np.float32, np.float64]) +def test_3d(dtype): + n = 30 + lx, ly, lz = n, n, n + data, labels = make_3d_syntheticdata(lx, ly, lz) + data = data.astype(dtype, copy=False) + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + labels = random_walker(data, labels, mode='cg') + assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() + assert data.shape == labels.shape + + +def test_3d_inactive(): + n = 30 + lx, ly, lz = n, n, n + data, labels = make_3d_syntheticdata(lx, ly, lz) + labels[5:25, 26:29, 26:29] = -1 + with expected_warnings( + [ + 'Changing the sparsity structure|"cg" mode|CObject type|scipy.sparse.linalg.cg' + ] + ): + labels = random_walker(data, labels, mode='cg') + assert (labels.reshape(data.shape)[13:17, 13:17, 13:17] == 2).all() + assert data.shape == labels.shape + + +@testing.parametrize('channel_axis', [0, 1, -1]) +@testing.parametrize('dtype', [np.float32, np.float64]) +def test_multispectral_2d(dtype, channel_axis): + lx, ly = 70, 100 + data, labels = make_2d_syntheticdata(lx, ly) + data = data.astype(dtype, copy=False) + data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output + + data = np.moveaxis(data, -1, channel_axis) + with expected_warnings( + [ + 'Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg', + 'The probability range is outside', + ] + ): + multi_labels = random_walker(data, labels, mode='cg', channel_axis=channel_axis) + data = np.moveaxis(data, channel_axis, -1) + + assert data[..., 0].shape == labels.shape + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + random_walker(data[..., 0], labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[25:45, 40:60] == 2).all() + assert data[..., 0].shape == labels.shape + + +@testing.parametrize('dtype', [np.float32, np.float64]) +def test_multispectral_3d(dtype): + n = 30 + lx, ly, lz = n, n, n + data, labels = make_3d_syntheticdata(lx, ly, lz) + data = data.astype(dtype, copy=False) + data = data[..., np.newaxis].repeat(2, axis=-1) # Expect identical output + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + multi_labels = random_walker(data, labels, mode='cg', channel_axis=-1) + assert data[..., 0].shape == labels.shape + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + single_labels = random_walker(data[..., 0], labels, mode='cg') + assert (multi_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() + assert (single_labels.reshape(labels.shape)[13:17, 13:17, 13:17] == 2).all() + assert data[..., 0].shape == labels.shape + + +def test_spacing_0(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Z axis + data_aniso = np.zeros((n, n, n // 2)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize( + yz, (n, n // 2), mode='constant', anti_aliasing=False + ) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[ + lx // 2 + small_l // 4, ly // 2 - small_l // 4, lz // 4 - small_l // 8 + ] = 2 + + # Test with `spacing` kwarg + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + labels_aniso = random_walker( + data_aniso, labels_aniso, mode='cg', spacing=(1.0, 1.0, 0.5) + ) + + assert (labels_aniso[13:17, 13:17, 7:9] == 2).all() + + +# Passing on WASM +@xfail( + condition=arch32 and not is_wasm, + reason=( + 'Known test failure on 32-bit platforms. See links for ' + 'details: ' + 'https://github.com/scikit-image/scikit-image/issues/3091 ' + 'https://github.com/scikit-image/scikit-image/issues/3092' + ), +) +def test_spacing_1(): + n = 30 + lx, ly, lz = n, n, n + data, _ = make_3d_syntheticdata(lx, ly, lz) + + # Rescale `data` along Y axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i, yz in enumerate(data): + data_aniso[i, :, :] = resize( + yz, (n * 2, n), mode='constant', anti_aliasing=False + ) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso = np.zeros_like(data_aniso) + labels_aniso[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso[lx // 2 + small_l // 4, ly - small_l // 2, lz // 2 - small_l // 4] = 2 + + # Test with `spacing` kwarg + # First, anisotropic along Y + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + labels_aniso = random_walker( + data_aniso, labels_aniso, mode='cg', spacing=(1.0, 2.0, 1.0) + ) + assert (labels_aniso[13:17, 26:34, 13:17] == 2).all() + + # Rescale `data` along X axis + # `resize` is not yet 3D capable, so this must be done by looping in 2D. + data_aniso = np.zeros((n, n * 2, n)) + for i in range(data.shape[1]): + data_aniso[i, :, :] = resize( + data[:, 1, :], (n * 2, n), mode='constant', anti_aliasing=False + ) + + # Generate new labels + small_l = int(lx // 5) + labels_aniso2 = np.zeros_like(data_aniso) + labels_aniso2[lx // 5, ly // 5, lz // 5] = 1 + labels_aniso2[lx - small_l // 2, ly // 2 + small_l // 4, lz // 2 - small_l // 4] = 2 + + # Anisotropic along X + with expected_warnings( + ['Changing the sparsity structure|"cg" mode|scipy.sparse.linalg.cg'] + ): + labels_aniso2 = random_walker( + data_aniso, labels_aniso2, mode='cg', spacing=(2.0, 1.0, 1.0) + ) + assert (labels_aniso2[26:34, 13:17, 13:17] == 2).all() + + +def test_trivial_cases(): + # When all voxels are labeled + img = np.ones((10, 10)) + labels = np.ones((10, 10)) + + with expected_warnings(["Returning provided labels"]): + pass_through = random_walker(img, labels) + np.testing.assert_array_equal(pass_through, labels) + + # When all voxels are labeled AND return_full_prob is True + labels[:, :5] = 3 + expected = np.concatenate( + ((labels == 1)[..., np.newaxis], (labels == 3)[..., np.newaxis]), axis=2 + ) + with expected_warnings(["Returning provided labels"]): + test = random_walker(img, labels, return_full_prob=True) + np.testing.assert_array_equal(test, expected) + + # Unlabeled voxels not connected to seed, so nothing can be done + img = np.full((10, 10), False) + object_A = np.array([(6, 7), (6, 8), (7, 7), (7, 8)]) + object_B = np.array([(3, 1), (4, 1), (2, 2), (3, 2), (4, 2), (2, 3), (3, 3)]) + for x, y in np.vstack((object_A, object_B)): + img[y][x] = True + + markers = np.zeros((10, 10), dtype=np.int8) + for x, y in object_B: + markers[y][x] = 1 + + markers[img == 0] = -1 + with expected_warnings(["All unlabeled pixels are isolated"]): + output_labels = random_walker(img, markers) + assert np.all(output_labels[markers == 1] == 1) + # Here 0-labeled pixels could not be determined (no connection to seed) + assert np.all(output_labels[markers == 0] == -1) + with expected_warnings(["All unlabeled pixels are isolated"]): + test = random_walker(img, markers, return_full_prob=True) + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_length2_spacing(): + # If this passes without raising an exception (warnings OK), the new + # spacing code is working properly. + np.random.seed(42) + img = np.ones((10, 10)) + 0.2 * np.random.normal(size=(10, 10)) + labels = np.zeros((10, 10), dtype=np.uint8) + labels[2, 4] = 1 + labels[6, 8] = 4 + random_walker(img, labels, spacing=(1.0, 2.0)) + + +def test_bad_inputs(): + # Too few dimensions + img = np.ones(10) + labels = np.arange(10) + with testing.raises(ValueError): + random_walker(img, labels) + with testing.raises(ValueError): + random_walker(img, labels, channel_axis=-1) + + # Too many dimensions + np.random.seed(42) + img = np.random.normal(size=(3, 3, 3, 3, 3)) + labels = np.arange(3**5).reshape(img.shape) + with testing.raises(ValueError): + random_walker(img, labels) + with testing.raises(ValueError): + random_walker(img, labels, channel_axis=-1) + + # Spacing incorrect length + img = np.random.normal(size=(10, 10)) + labels = np.zeros((10, 10)) + labels[2, 4] = 2 + labels[6, 8] = 5 + with testing.raises(ValueError): + random_walker(img, labels, spacing=(1,)) + + # Invalid mode + img = np.random.normal(size=(10, 10)) + labels = np.zeros((10, 10)) + with testing.raises(ValueError): + random_walker(img, labels, mode='bad') + + +def test_isolated_seeds(): + np.random.seed(0) + a = np.random.random((7, 7)) + mask = -np.ones(a.shape) + # This pixel is an isolated seed + mask[1, 1] = 1 + # Unlabeled pixels + mask[3:, 3:] = 0 + # Seeds connected to unlabeled pixels + mask[4, 4] = 2 + mask[6, 6] = 1 + + # Test that no error is raised, and that labels of isolated seeds are OK + with expected_warnings( + [ + 'Changing the sparsity structure|The probability range is outside|scipy.sparse.linalg.cg' + ] + ): + res = random_walker(a, mask) + assert res[1, 1] == 1 + with expected_warnings( + [ + 'Changing the sparsity structure|The probability range is outside|scipy.sparse.linalg.cg' + ] + ): + res = random_walker(a, mask, return_full_prob=True) + assert res[0, 1, 1] == 1 + assert res[1, 1, 1] == 0 + + +def test_isolated_area(): + np.random.seed(0) + a = np.random.random((7, 7)) + mask = -np.ones(a.shape) + # This pixel is an isolated seed + mask[1, 1] = 0 + # Unlabeled pixels + mask[3:, 3:] = 0 + # Seeds connected to unlabeled pixels + mask[4, 4] = 2 + mask[6, 6] = 1 + + # Test that no error is raised, and that labels of isolated seeds are OK + with expected_warnings( + [ + 'Changing the sparsity structure|The probability range is outside|scipy.sparse.linalg.cg' + ] + ): + res = random_walker(a, mask) + assert res[1, 1] == 0 + with expected_warnings( + [ + 'Changing the sparsity structure|The probability range is outside|scipy.sparse.linalg.cg' + ] + ): + res = random_walker(a, mask, return_full_prob=True) + assert res[0, 1, 1] == 0 + assert res[1, 1, 1] == 0 + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_prob_tol(): + np.random.seed(0) + a = np.random.random((7, 7)) + mask = -np.ones(a.shape) + # This pixel is an isolated seed + mask[1, 1] = 1 + # Unlabeled pixels + mask[3:, 3:] = 0 + # Seeds connected to unlabeled pixels + mask[4, 4] = 2 + mask[6, 6] = 1 + + with expected_warnings( + [ + 'Changing the sparsity structure|The probability range is outside|scipy.sparse.linalg.cg' + ] + ): + res = random_walker(a, mask, return_full_prob=True) + + # Lower beta, no warning is expected. + res = random_walker(a, mask, return_full_prob=True, beta=10) + assert res[0, 1, 1] == 1 + assert res[1, 1, 1] == 0 + + # Being more prob_tol tolerant, no warning is expected. + res = random_walker(a, mask, return_full_prob=True, prob_tol=1e-1) + assert res[0, 1, 1] == 1 + assert res[1, 1, 1] == 0 + + # Reduced tol, no warning is expected. + res = random_walker(a, mask, return_full_prob=True, tol=1e-9) + assert res[0, 1, 1] == 1 + assert res[1, 1, 1] == 0 + + +def test_umfpack_import(): + from skimage.segmentation import random_walker_segmentation + + UmfpackContext = random_walker_segmentation.UmfpackContext + try: + # when scikit-umfpack is installed UmfpackContext should not be None + import scikits.umfpack # noqa: F401 + + assert UmfpackContext is not None + except ImportError: + assert UmfpackContext is None + + +@pytest.mark.filterwarnings( + 'ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning' +) +def test_empty_labels(): + image = np.random.random((5, 5)) + labels = np.zeros((5, 5), dtype=int) + + with testing.raises(ValueError, match="No seeds provided"): + random_walker(image, labels) + + labels[1, 1] = -1 + with testing.raises(ValueError, match="No seeds provided"): + random_walker(image, labels) + + # Once seeds are provided, it should run without error + labels[3, 3] = 1 + random_walker(image, labels) + + +@pytest.mark.filterwarnings( + "ignore:Changing the sparsity structure of a csr_matrix is expensive:scipy.sparse.SparseEfficiencyWarning" +) +def test_float16_upcasting(): + data, labels = make_2d_syntheticdata(lx=70, ly=100) + data = data.astype(np.float16, copy=False) + spacing = np.ones(2, dtype=np.float16) + # Just check that this line doesn't raise an error due to data being float16 + labels_cg = random_walker(data, labels, spacing=spacing, beta=90, mode='cg_j') + assert (labels_cg[25:45, 40:60] == 2).all() + assert data.shape == labels.shape diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_slic.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_slic.py new file mode 100644 index 0000000000000000000000000000000000000000..658c2cccd71519732d6738390dad8d23a0ca921a --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_slic.py @@ -0,0 +1,632 @@ +from itertools import product + +import numpy as np +import pytest +from numpy.testing import assert_equal + +from skimage import data, filters, img_as_float +from skimage._shared.testing import run_in_parallel, expected_warnings +from skimage.segmentation import slic + + +@run_in_parallel() +def test_color_2d(): + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, n_segments=4, sigma=0, enforce_connectivity=False, start_label=0) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + + +def test_multichannel_2d(): + rng = np.random.default_rng(0) + img = np.zeros((20, 20, 8)) + img[:10, :10, 0:2] = 1 + img[:10, 10:, 2:4] = 1 + img[10:, :10, 4:6] = 1 + img[10:, 10:, 6:8] = 1 + img += 0.01 * rng.normal(size=img.shape) + img = np.clip(img, 0, 1, out=img) + seg = slic(img, n_segments=4, enforce_connectivity=False, start_label=0) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + + +def test_gray_2d(): + rng = np.random.default_rng(0) + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic( + img, + sigma=0, + n_segments=4, + compactness=1, + channel_axis=None, + convert2lab=False, + start_label=0, + ) + + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + + +def test_gray2d_default_channel_axis(): + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + with pytest.raises(ValueError, match="channel_axis=-1 indicates multichannel"): + slic(img) + slic(img, channel_axis=None) + + +def _check_segment_labels(seg1, seg2, allowed_mismatch_ratio=0.1): + size = seg1.size + ndiff = np.sum(seg1 != seg2) + assert (ndiff / size) < allowed_mismatch_ratio + + +def test_slic_consistency_across_image_magnitude(): + # verify that that images of various scales across integer and float dtypes + # give the same segmentation result + img_uint8 = data.cat()[:256, :128] + img_uint16 = 256 * img_uint8.astype(np.uint16) + img_float32 = img_as_float(img_uint8) + img_float32_norm = img_float32 / img_float32.max() + img_float32_offset = img_float32 + 1000 + + seg1 = slic(img_uint8) + seg2 = slic(img_uint16) + seg3 = slic(img_float32) + seg4 = slic(img_float32_norm) + seg5 = slic(img_float32_offset) + + np.testing.assert_array_equal(seg1, seg2) + np.testing.assert_array_equal(seg1, seg3) + # Assert that offset has no impact on result + np.testing.assert_array_equal(seg4, seg5) + # Floating point cases can have mismatch due to floating point error + # exact match was observed on x86_64, but mismatches seen no i686. + # For now just verify that a similar number of superpixels are present in + # each case. + n_seg1 = seg1.max() + n_seg4 = seg4.max() + assert abs(n_seg1 - n_seg4) / n_seg1 < 0.5 + + +def test_color_3d(): + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 22, 3)) + slices = [] + for dim_size in img.shape[:-1]: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(product(*slices)) + colors = list(product(*(([0, 1],) * 3))) + for s, c in zip(slices, colors): + img[s] = c + img += 0.01 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, sigma=0, n_segments=8, start_label=0) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_equal(seg[s], c) + + +def test_gray_3d(): + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 22)) + slices = [] + for dim_size in img.shape: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(product(*slices)) + shades = np.arange(0, 1.000001, 1.0 / 7) + for s, sh in zip(slices, shades): + img[s] = sh + img += 0.001 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic( + img, + sigma=0, + n_segments=8, + compactness=1, + channel_axis=None, + convert2lab=False, + start_label=0, + ) + + assert_equal(len(np.unique(seg)), 8) + for s, c in zip(slices, range(8)): + assert_equal(seg[s], c) + + +def test_list_sigma(): + rng = np.random.default_rng(0) + img = np.array([[1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1]], float) + img += 0.1 * rng.normal(size=img.shape) + result_sigma = np.array([[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], int) + with expected_warnings( + ["Input image is 2D: sigma number of " "elements must be 2"] + ): + seg_sigma = slic( + img, n_segments=2, sigma=[1, 50, 1], channel_axis=None, start_label=0 + ) + assert_equal(seg_sigma, result_sigma) + + +def test_spacing(): + rng = np.random.default_rng(0) + img = np.array([[1, 1, 1, 0, 0], [1, 1, 0, 0, 0]], float) + result_non_spaced = np.array([[0, 0, 0, 1, 1], [0, 0, 1, 1, 1]], int) + result_spaced = np.array([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1]], int) + img += 0.1 * rng.normal(size=img.shape) + seg_non_spaced = slic( + img, n_segments=2, sigma=0, channel_axis=None, compactness=1.0, start_label=0 + ) + seg_spaced = slic( + img, + n_segments=2, + sigma=0, + spacing=[500, 1], + compactness=1.0, + channel_axis=None, + start_label=0, + ) + assert_equal(seg_non_spaced, result_non_spaced) + assert_equal(seg_spaced, result_spaced) + + +def test_invalid_lab_conversion(): + img = np.array([[1, 1, 1, 0, 0], [1, 1, 0, 0, 0]], float) + 1 + with pytest.raises(ValueError): + slic(img, channel_axis=-1, convert2lab=True, start_label=0) + + +def test_enforce_connectivity(): + img = np.array([[0, 0, 0, 1, 1, 1], [1, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 0]], float) + + segments_connected = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=True, + convert2lab=False, + start_label=0, + channel_axis=None, + ) + segments_disconnected = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=False, + convert2lab=False, + start_label=0, + channel_axis=None, + ) + + # Make sure nothing fatal occurs (e.g. buffer overflow) at low values of + # max_size_factor + segments_connected_low_max = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=True, + convert2lab=False, + max_size_factor=0.8, + start_label=0, + channel_axis=None, + ) + + result_connected = np.array( + [[0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 1, 1]], float + ) + + result_disconnected = np.array( + [[0, 0, 0, 1, 1, 1], [1, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 0]], float + ) + + assert_equal(segments_connected, result_connected) + assert_equal(segments_disconnected, result_disconnected) + assert_equal(segments_connected_low_max, result_connected) + + +def test_slic_zero(): + # Same as test_color_2d but with slic_zero=True + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic(img, n_segments=4, sigma=0, slic_zero=True, start_label=0) + + # we expect 4 segments + assert_equal(len(np.unique(seg)), 4) + assert_equal(seg.shape, img.shape[:-1]) + assert_equal(seg[:10, :10], 0) + assert_equal(seg[10:, :10], 2) + assert_equal(seg[:10, 10:], 1) + assert_equal(seg[10:, 10:], 3) + + +def test_more_segments_than_pixels(): + rng = np.random.default_rng(0) + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rng.normal(size=img.shape) + img[img > 1] = 1 + img[img < 0] = 0 + seg = slic( + img, + sigma=0, + n_segments=500, + compactness=1, + channel_axis=None, + convert2lab=False, + start_label=0, + ) + assert np.all(seg.ravel() == np.arange(seg.size)) + + +def test_color_2d_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((20, 21)) + msk[2:-2, 2:-2] = 1 + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic(img, n_segments=4, sigma=0, enforce_connectivity=False, mask=msk) + + # we expect 4 segments + masked area + assert_equal(len(np.unique(seg)), 5) + assert_equal(seg.shape, img.shape[:-1]) + # segments + assert_equal(seg[2:10, 2:10], 1) + assert_equal(seg[10:-2, 2:10], 4) + assert_equal(seg[2:10, 10:-2], 2) + assert_equal(seg[10:-2, 10:-2], 3) + # non masked area + assert_equal(seg[:2, :], 0) + assert_equal(seg[-2:, :], 0) + assert_equal(seg[:, :2], 0) + assert_equal(seg[:, -2:], 0) + + +def test_multichannel_2d_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((20, 20)) + msk[2:-2, 2:-2] = 1 + img = np.zeros((20, 20, 8)) + img[:10, :10, 0:2] = 1 + img[:10, 10:, 2:4] = 1 + img[10:, :10, 4:6] = 1 + img[10:, 10:, 6:8] = 1 + img += 0.01 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic(img, n_segments=4, enforce_connectivity=False, mask=msk) + + # we expect 4 segments + masked area + assert_equal(len(np.unique(seg)), 5) + assert_equal(seg.shape, img.shape[:-1]) + # segments + assert_equal(seg[2:10, 2:10], 2) + assert_equal(seg[2:10, 10:-2], 1) + assert_equal(seg[10:-2, 2:10], 4) + assert_equal(seg[10:-2, 10:-2], 3) + # non masked area + assert_equal(seg[:2, :], 0) + assert_equal(seg[-2:, :], 0) + assert_equal(seg[:, :2], 0) + assert_equal(seg[:, -2:], 0) + + +def test_gray_2d_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((20, 21)) + msk[2:-2, 2:-2] = 1 + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic( + img, + sigma=0, + n_segments=4, + compactness=1, + channel_axis=None, + convert2lab=False, + mask=msk, + ) + + assert_equal(len(np.unique(seg)), 5) + assert_equal(seg.shape, img.shape) + # segments + assert_equal(seg[2:10, 2:10], 1) + assert_equal(seg[2:10, 10:-2], 2) + assert_equal(seg[10:-2, 2:10], 3) + assert_equal(seg[10:-2, 10:-2], 4) + # non masked area + assert_equal(seg[:2, :], 0) + assert_equal(seg[-2:, :], 0) + assert_equal(seg[:, :2], 0) + assert_equal(seg[:, -2:], 0) + + +def test_list_sigma_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((2, 6)) + msk[:, 1:-1] = 1 + img = np.array([[1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1]], float) + img += 0.1 * rng.normal(size=img.shape) + result_sigma = np.array([[0, 1, 1, 2, 2, 0], [0, 1, 1, 2, 2, 0]], int) + seg_sigma = slic(img, n_segments=2, sigma=[50, 1], channel_axis=None, mask=msk) + assert_equal(seg_sigma, result_sigma) + + +def test_spacing_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((2, 5)) + msk[:, 1:-1] = 1 + img = np.array([[1, 1, 1, 0, 0], [1, 1, 0, 0, 0]], float) + result_non_spaced = np.array([[0, 1, 1, 2, 0], [0, 1, 2, 2, 0]], int) + result_spaced = np.array([[0, 1, 1, 1, 0], [0, 2, 2, 2, 0]], int) + img += 0.1 * rng.normal(size=img.shape) + seg_non_spaced = slic( + img, n_segments=2, sigma=0, channel_axis=None, compactness=1.0, mask=msk + ) + seg_spaced = slic( + img, + n_segments=2, + sigma=0, + spacing=[50, 1], + compactness=1.0, + channel_axis=None, + mask=msk, + ) + assert_equal(seg_non_spaced, result_non_spaced) + assert_equal(seg_spaced, result_spaced) + + +def test_enforce_connectivity_mask(): + msk = np.zeros((3, 6)) + msk[:, 1:-1] = 1 + img = np.array([[0, 0, 0, 1, 1, 1], [1, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 0]], float) + + segments_connected = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=True, + convert2lab=False, + mask=msk, + channel_axis=None, + ) + segments_disconnected = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=False, + convert2lab=False, + mask=msk, + channel_axis=None, + ) + + # Make sure nothing fatal occurs (e.g. buffer overflow) at low values of + # max_size_factor + segments_connected_low_max = slic( + img, + 2, + compactness=0.0001, + enforce_connectivity=True, + convert2lab=False, + max_size_factor=0.8, + mask=msk, + channel_axis=None, + ) + + result_connected = np.array( + [[0, 1, 1, 2, 2, 0], [0, 1, 1, 2, 2, 0], [0, 1, 1, 2, 2, 0]], float + ) + + result_disconnected = np.array( + [[0, 1, 1, 2, 2, 0], [0, 1, 1, 2, 2, 0], [0, 1, 1, 2, 2, 0]], float + ) + + assert_equal(segments_connected, result_connected) + assert_equal(segments_disconnected, result_disconnected) + assert_equal(segments_connected_low_max, result_connected) + + +def test_slic_zero_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((20, 21)) + msk[2:-2, 2:-2] = 1 + img = np.zeros((20, 21, 3)) + img[:10, :10, 0] = 1 + img[10:, :10, 1] = 1 + img[10:, 10:, 2] = 1 + img += 0.01 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic(img, n_segments=4, sigma=0, slic_zero=True, mask=msk) + + # we expect 4 segments + masked area + assert_equal(len(np.unique(seg)), 5) + assert_equal(seg.shape, img.shape[:-1]) + # segments + assert_equal(seg[2:10, 2:10], 1) + assert_equal(seg[2:10, 10:-2], 2) + assert_equal(seg[10:-2, 2:10], 3) + assert_equal(seg[10:-2, 10:-2], 4) + # non masked area + assert_equal(seg[:2, :], 0) + assert_equal(seg[-2:, :], 0) + assert_equal(seg[:, :2], 0) + assert_equal(seg[:, -2:], 0) + + +def test_more_segments_than_pixels_mask(): + rng = np.random.default_rng(0) + msk = np.zeros((20, 21)) + msk[2:-2, 2:-2] = 1 + img = np.zeros((20, 21)) + img[:10, :10] = 0.33 + img[10:, :10] = 0.67 + img[10:, 10:] = 1.00 + img += 0.0033 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic( + img, + sigma=0, + n_segments=500, + compactness=1, + channel_axis=None, + convert2lab=False, + mask=msk, + ) + + expected = np.arange(seg[2:-2, 2:-2].size) + 1 + assert np.all(seg[2:-2, 2:-2].ravel() == expected) + + +def test_color_3d_mask(): + msk = np.zeros((20, 21, 22)) + msk[2:-2, 2:-2, 2:-2] = 1 + + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 22, 3)) + slices = [] + for dim_size in msk.shape: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(product(*slices)) + colors = list(product(*(([0, 1],) * 3))) + for s, c in zip(slices, colors): + img[s] = c + img += 0.01 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + + seg = slic(img, sigma=0, n_segments=8, mask=msk) + + # we expect 8 segments + masked area + assert_equal(len(np.unique(seg)), 9) + for s, c in zip(slices, range(1, 9)): + assert_equal(seg[s][2:-2, 2:-2, 2:-2], c) + + +def test_gray_3d_mask(): + msk = np.zeros((20, 21, 22)) + msk[2:-2, 2:-2, 2:-2] = 1 + + rng = np.random.default_rng(0) + img = np.zeros((20, 21, 22)) + slices = [] + for dim_size in img.shape: + midpoint = dim_size // 2 + slices.append((slice(None, midpoint), slice(midpoint, None))) + slices = list(product(*slices)) + shades = np.linspace(0, 1, 8) + for s, sh in zip(slices, shades): + img[s] = sh + img += 0.001 * rng.normal(size=img.shape) + np.clip(img, 0, 1, out=img) + seg = slic( + img, sigma=0, n_segments=8, channel_axis=None, convert2lab=False, mask=msk + ) + + # we expect 8 segments + masked area + assert_equal(len(np.unique(seg)), 9) + for s, c in zip(slices, range(1, 9)): + assert_equal(seg[s][2:-2, 2:-2, 2:-2], c) + + +@pytest.mark.parametrize("dtype", ['float16', 'float32', 'float64', 'uint8', 'int']) +def test_dtype_support(dtype): + img = np.random.rand(28, 28).astype(dtype) + + # Simply run the function to assert that it runs without error + slic(img, start_label=1, channel_axis=None) + + +def test_start_label_fix(): + """Tests the fix for a bug producing a label < start_label (gh-6240). + + For the v0.19.1 release, the `img` and `slic` call as below result in two + non-contiguous regions with value 0 despite `start_label=1`. We verify that + the minimum label is now `start_label` as expected. + """ + + # generate bumpy data that gives unexpected label prior to bug fix + rng = np.random.default_rng(9) + img = rng.standard_normal((8, 13)) > 0 + img = filters.gaussian(img, sigma=1) + + start_label = 1 + superp = slic( + img, + start_label=start_label, + channel_axis=None, + n_segments=6, + compactness=0.01, + enforce_connectivity=True, + max_num_iter=10, + ) + assert superp.min() == start_label + + +def test_raises_ValueError_if_input_has_NaN(): + img = np.zeros((4, 5), dtype=float) + img[2, 3] = np.nan + with pytest.raises(ValueError): + slic(img, channel_axis=None) + + mask = ~np.isnan(img) + slic(img, mask=mask, channel_axis=None) + + +@pytest.mark.parametrize("inf", [-np.inf, np.inf]) +def test_raises_ValueError_if_input_has_inf(inf): + img = np.zeros((4, 5), dtype=float) + img[2, 3] = inf + with pytest.raises(ValueError): + slic(img, channel_axis=None) + + mask = np.isfinite(img) + slic(img, mask=mask, channel_axis=None) diff --git a/lib/python3.10/site-packages/skimage/segmentation/tests/test_watershed.py b/lib/python3.10/site-packages/skimage/segmentation/tests/test_watershed.py new file mode 100644 index 0000000000000000000000000000000000000000..88f041eeae7c73fe9ddf2badc5de134e5cd5b9c8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/segmentation/tests/test_watershed.py @@ -0,0 +1,1035 @@ +"""test_watershed.py - tests the watershed function""" + +import math +import unittest + +import numpy as np +import pytest +from scipy import ndimage as ndi + +import skimage.measure +from skimage._shared.filters import gaussian +from skimage.feature import peak_local_max +from skimage.measure import label + +from .._watershed import watershed + +eps = 1e-12 +# fmt: off +blob = np.array([[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 204, 204, 204, 204, 204, 204, 255, 255, 255, 255, 255], + [255, 255, 255, 204, 204, 183, 153, 153, 153, 153, 183, 204, 204, 255, 255, 255], + [255, 255, 204, 183, 153, 141, 111, 103, 103, 111, 141, 153, 183, 204, 255, 255], + [255, 255, 204, 153, 111, 94, 72, 52, 52, 72, 94, 111, 153, 204, 255, 255], + [255, 255, 204, 153, 111, 72, 39, 1, 1, 39, 72, 111, 153, 204, 255, 255], + [255, 255, 204, 183, 141, 111, 72, 39, 39, 72, 111, 141, 183, 204, 255, 255], + [255, 255, 255, 204, 183, 141, 111, 72, 72, 111, 141, 183, 204, 255, 255, 255], + [255, 255, 255, 255, 204, 183, 141, 94, 94, 141, 183, 204, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 204, 153, 103, 103, 153, 204, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 204, 183, 141, 94, 94, 141, 183, 204, 255, 255, 255, 255], + [255, 255, 255, 204, 183, 141, 111, 72, 72, 111, 141, 183, 204, 255, 255, 255], + [255, 255, 204, 183, 141, 111, 72, 39, 39, 72, 111, 141, 183, 204, 255, 255], + [255, 255, 204, 153, 111, 72, 39, 1, 1, 39, 72, 111, 153, 204, 255, 255], + [255, 255, 204, 153, 111, 94, 72, 52, 52, 72, 94, 111, 153, 204, 255, 255], + [255, 255, 204, 183, 153, 141, 111, 103, 103, 111, 141, 153, 183, 204, 255, 255], + [255, 255, 255, 204, 204, 183, 153, 153, 153, 153, 183, 204, 204, 255, 255, 255], + [255, 255, 255, 255, 255, 204, 204, 204, 204, 204, 204, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]]) +# fmt: on + + +def diff(a, b): + if not isinstance(a, np.ndarray): + a = np.asarray(a) + if not isinstance(b, np.ndarray): + b = np.asarray(b) + if (0 in a.shape) and (0 in b.shape): + return 0.0 + b[a == 0] = 0 + if a.dtype in [np.complex64, np.complex128] or b.dtype in [ + np.complex64, + np.complex128, + ]: + a = np.asarray(a, np.complex128) + b = np.asarray(b, np.complex128) + t = ((a.real - b.real) ** 2).sum() + ((a.imag - b.imag) ** 2).sum() + else: + a = np.asarray(a) + a = a.astype(np.float64) + b = np.asarray(b) + b = b.astype(np.float64) + t = ((a - b) ** 2).sum() + return math.sqrt(t) + + +class TestWatershed(unittest.TestCase): + eight = np.ones((3, 3), bool) + + def test_watershed01(self): + "watershed 1" + data = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + np.uint8, + ) + markers = np.array( + [ + [-1, 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, 1, 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], + ], + np.int8, + ) + out = watershed(data, markers, self.eight) + expected = np.array( + [ + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ] + ) + error = diff(expected, out) + assert error < eps + + def test_watershed02(self): + "watershed 2" + data = np.array( + [ + [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, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + ], + np.uint8, + ) + markers = np.array( + [ + [-1, 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, 1, 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], + ], + np.int8, + ) + out = watershed(data, markers) + error = diff( + [ + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, 1, 1, 1, -1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, -1, 1, 1, 1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ], + out, + ) + self.assertTrue(error < eps) + + def test_watershed03(self): + "watershed 3" + data = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 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], + ], + np.uint8, + ) + markers = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 2, 0, 3, 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, -1], + ], + np.int8, + ) + out = watershed(data, markers) + error = diff( + [ + [-1, -1, -1, -1, -1, -1, -1], + [-1, 0, 2, 0, 3, 0, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 0, 2, 0, 3, 0, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ], + out, + ) + self.assertTrue(error < eps) + + def test_watershed04(self): + "watershed 4" + data = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 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], + ], + np.uint8, + ) + markers = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 2, 0, 3, 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, -1], + ], + np.int8, + ) + out = watershed(data, markers, self.eight) + error = diff( + [ + [-1, -1, -1, -1, -1, -1, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, 2, 2, 0, 3, 3, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ], + out, + ) + self.assertTrue(error < eps) + + def test_watershed05(self): + "watershed 5" + data = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 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], + ], + np.uint8, + ) + markers = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 3, 0, 2, 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, -1], + ], + np.int8, + ) + out = watershed(data, markers, self.eight) + error = diff( + [ + [-1, -1, -1, -1, -1, -1, -1], + [-1, 3, 3, 0, 2, 2, -1], + [-1, 3, 3, 0, 2, 2, -1], + [-1, 3, 3, 0, 2, 2, -1], + [-1, 3, 3, 0, 2, 2, -1], + [-1, 3, 3, 0, 2, 2, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ], + out, + ) + self.assertTrue(error < eps) + + def test_watershed06(self): + "watershed 6" + data = np.array( + [ + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 1, 1, 1, 1, 1, 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], + ], + np.uint8, + ) + markers = np.array( + [ + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 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], + [-1, 0, 0, 0, 0, 0, 0], + ], + np.int8, + ) + out = watershed(data, markers, self.eight) + error = diff( + [ + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, 1, 1, 1, 1, 1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + [-1, -1, -1, -1, -1, -1, -1], + ], + out, + ) + self.assertTrue(error < eps) + + def test_watershed07(self): + "A regression test of a competitive case that failed" + data = blob + mask = data != 255 + markers = np.zeros(data.shape, int) + markers[6, 7] = 1 + markers[14, 7] = 2 + out = watershed(data, markers, self.eight, mask=mask) + # + # The two objects should be the same size, except possibly for the + # border region + # + size1 = np.sum(out == 1) + size2 = np.sum(out == 2) + self.assertTrue(abs(size1 - size2) <= 6) + + def test_watershed08(self): + "The border pixels + an edge are all the same value" + data = blob.copy() + data[10, 7:9] = 141 + mask = data != 255 + markers = np.zeros(data.shape, int) + markers[6, 7] = 1 + markers[14, 7] = 2 + out = watershed(data, markers, self.eight, mask=mask) + # + # The two objects should be the same size, except possibly for the + # border region + # + size1 = np.sum(out == 1) + size2 = np.sum(out == 2) + self.assertTrue(abs(size1 - size2) <= 6) + + def test_watershed09(self): + """Test on an image of reasonable size + + This is here both for timing (does it take forever?) and to + ensure that the memory constraints are reasonable + """ + image = np.zeros((1000, 1000)) + coords = np.random.uniform(0, 1000, (100, 2)).astype(int) + markers = np.zeros((1000, 1000), int) + idx = 1 + for x, y in coords: + image[x, y] = 1 + markers[x, y] = idx + idx += 1 + + image = gaussian(image, sigma=4, mode='reflect') + watershed(image, markers, self.eight) + ndi.watershed_ift(image.astype(np.uint16), markers, self.eight) + + def test_watershed10(self): + "watershed 10" + data = np.array( + [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]], np.uint8 + ) + markers = np.array( + [[1, 0, 0, 2], [0, 0, 0, 0], [0, 0, 0, 0], [3, 0, 0, 4]], np.int8 + ) + out = watershed(data, markers, self.eight) + error = diff([[1, 1, 2, 2], [1, 1, 2, 2], [3, 3, 4, 4], [3, 3, 4, 4]], out) + self.assertTrue(error < eps) + + def test_watershed11(self): + '''Make sure that all points on this plateau are assigned to closest seed''' + # https://github.com/scikit-image/scikit-image/issues/803 + # + # Make sure that no point in a level image is farther away + # from its seed than any other + # + image = np.zeros((21, 21)) + markers = np.zeros((21, 21), int) + markers[5, 5] = 1 + markers[5, 10] = 2 + markers[10, 5] = 3 + markers[10, 10] = 4 + + structure = np.array( + [[False, True, False], [True, True, True], [False, True, False]] + ) + out = watershed(image, markers, structure) + i, j = np.mgrid[0:21, 0:21] + d = np.dstack( + [ + np.sqrt((i.astype(float) - i0) ** 2, (j.astype(float) - j0) ** 2) + for i0, j0 in ((5, 5), (5, 10), (10, 5), (10, 10)) + ] + ) + dmin = np.min(d, 2) + self.assertTrue(np.all(d[i, j, out[i, j] - 1] == dmin)) + + def test_watershed12(self): + "The watershed line" + data = np.array( + [ + [ + 203, + 255, + 203, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + ], + [ + 203, + 255, + 203, + 153, + 153, + 153, + 102, + 102, + 102, + 102, + 102, + 102, + 153, + 153, + 153, + 153, + ], + [ + 203, + 255, + 203, + 203, + 153, + 153, + 102, + 102, + 77, + 0, + 102, + 102, + 153, + 153, + 203, + 203, + ], + [ + 203, + 255, + 255, + 203, + 153, + 153, + 153, + 102, + 102, + 102, + 102, + 153, + 153, + 203, + 203, + 255, + ], + [ + 203, + 203, + 255, + 203, + 203, + 203, + 153, + 153, + 153, + 153, + 153, + 153, + 203, + 203, + 255, + 255, + ], + [ + 153, + 203, + 255, + 255, + 255, + 203, + 203, + 203, + 203, + 203, + 203, + 203, + 203, + 255, + 255, + 203, + ], + [ + 153, + 203, + 203, + 203, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 255, + 203, + 203, + ], + [ + 153, + 153, + 153, + 203, + 203, + 203, + 203, + 203, + 255, + 203, + 203, + 203, + 203, + 203, + 203, + 153, + ], + [ + 102, + 102, + 153, + 153, + 153, + 153, + 203, + 203, + 255, + 203, + 203, + 255, + 203, + 153, + 153, + 153, + ], + [ + 102, + 102, + 102, + 102, + 102, + 153, + 203, + 255, + 255, + 203, + 203, + 203, + 203, + 153, + 102, + 153, + ], + [ + 102, + 51, + 51, + 102, + 102, + 153, + 203, + 255, + 203, + 203, + 153, + 153, + 153, + 153, + 102, + 153, + ], + [ + 77, + 51, + 51, + 102, + 153, + 153, + 203, + 255, + 203, + 203, + 203, + 153, + 102, + 102, + 102, + 153, + ], + [ + 77, + 0, + 51, + 102, + 153, + 203, + 203, + 255, + 203, + 255, + 203, + 153, + 102, + 51, + 102, + 153, + ], + [ + 77, + 0, + 51, + 102, + 153, + 203, + 255, + 255, + 203, + 203, + 203, + 153, + 102, + 0, + 102, + 153, + ], + [ + 102, + 0, + 51, + 102, + 153, + 203, + 255, + 203, + 203, + 153, + 153, + 153, + 102, + 102, + 102, + 153, + ], + [ + 102, + 102, + 102, + 102, + 153, + 203, + 255, + 203, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + 153, + ], + ] + ) + markerbin = data == 0 + marker = label(markerbin) + ws = watershed(data, marker, connectivity=2, watershed_line=True) + for lab, area in zip(range(4), [34, 74, 74, 74]): + self.assertTrue(np.sum(ws == lab) == area) + + def test_watershed_input_not_modified(self): + """Test to ensure input markers are not modified.""" + image = np.random.default_rng().random(size=(21, 21)) + markers = np.zeros((21, 21), dtype=np.uint8) + markers[[5, 5, 15, 15], [5, 15, 5, 15]] = [1, 2, 3, 4] + original_markers = np.copy(markers) + result = watershed(image, markers) + np.testing.assert_equal(original_markers, markers) + assert not np.all(result == markers) + + +def test_compact_watershed(): + # in this test, when compactness is greater than zero the watershed line + # is labeled with the closest marker (label=2) + # when compactness is zero the watershed line is labeled with + # the marker that reaches it first (label=1) + # because it has a zero cost path to the line. + image = np.zeros((5, 6)) + image[:, 3] = 2 # watershed line + image[:, 4:] = 1 + seeds = np.zeros((5, 6), dtype=int) + seeds[2, 0] = 1 + seeds[2, 5] = 2 + compact = watershed(image, seeds, compactness=0.01) + expected = np.array( + [ + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + ], + dtype=int, + ) + np.testing.assert_equal(compact, expected) + normal = watershed(image, seeds) + expected = np.array( + [ + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + ], + dtype=int, + ) + np.testing.assert_equal(normal, expected) + + # checks that compact watershed labels with watershed lines are + # a subset of the labels from compact watershed for this specific example + compact_wsl = watershed(image, seeds, compactness=0.01, watershed_line=True) + difference = compact_wsl != compact + difference[compact_wsl == 0] = False + + assert not np.any(difference) + + +def test_watershed_with_markers_offset(): + """ + Check edge case behavior reported in gh-6632 + + While we initially viewed the behavior described in gh-6632 [1]_ as a bug, + we have reverted that decision in gh-7661. See [2]_ for an explanation. + So this test now actually asserts the behavior reported in gh-6632 as + correct. + + .. [1] https://github.com/scikit-image/scikit-image/issues/6632. + .. [2] https://github.com/scikit-image/scikit-image/issues/7661#issuecomment-2645810807 + """ + # Generate an initial image with two overlapping circles + x, y = np.indices((80, 80)) + x1, y1, x2, y2 = 28, 28, 44, 52 + r1, r2 = 16, 20 + mask_circle1 = (x - x1) ** 2 + (y - y1) ** 2 < r1**2 + mask_circle2 = (x - x2) ** 2 + (y - y2) ** 2 < r2**2 + image = np.logical_or(mask_circle1, mask_circle2) + + # Now we want to separate the two objects in image + # Generate the markers as local maxima of the distance to the background + # and then apply an y-offset + distance = ndi.distance_transform_edt(image) + coords = peak_local_max(distance, footprint=np.ones((3, 3)), labels=image) + coords[:, 0] += 6 + mask = np.zeros(distance.shape, dtype=bool) + mask[tuple(coords.T)] = True + markers, _ = ndi.label(mask) + + labels = watershed(-distance, markers, mask=image) + + props = skimage.measure.regionprops(labels, intensity_image=-distance) + + # Generally, assert that the smaller object could only conquer a thin line + # in the direction of the positive gradient + assert props[0].extent == 1 + expected_region = np.arange(start=-10, stop=0, dtype=float).reshape(-1, 1) + np.testing.assert_equal(props[0].image_intensity, expected_region) + + # Assert pixel count from reviewed reproducing example in bug report + assert props[0].num_pixels == 10 + assert props[1].num_pixels == 1928 + + +def test_watershed_simple_basin_overspill(): + """ + Test edge case behavior when markers spill over into another basin / compete. + + While we initially viewed the behavior described in gh-6632 [1]_ as a bug, + we have reverted that decision in gh-7661. See [2]_ for an explanation. + So this test now actually asserts the behavior reported in gh-6632 as + correct. + + .. [1] https://github.com/scikit-image/scikit-image/issues/6632. + .. [2] https://github.com/scikit-image/scikit-image/issues/7661#issuecomment-2645810807 + """ + # Scenario 1 + # fmt: off + image = np.array([[6, 5, 4, 3, 0, 3, 0, 1, 2], + [6, 5, 4, 3, 0, 3, 0, 1, 2]]) + markers = np.array([[0, 1, 0, 0, 0, 0, 0, 2, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0]]) + expected = np.array([[1, 1, 2, 2, 2, 2, 2, 2, 2], + [2, 2, 2, 2, 2, 2, 2, 2, 2]]) + # fmt: on + result = watershed(image, markers=markers) + np.testing.assert_equal(result, expected) + + # Scenario 2 + image = -np.array([1, 2, 2, 2, 2, 2, 3]) + markers = np.array([1, 0, 0, 0, 0, 0, 2]) + expected = np.array([1, 2, 2, 2, 2, 2, 2]) + result = watershed(image, markers=markers, mask=image != 0) + np.testing.assert_array_equal(result, expected) + + +def test_watershed_evenly_distributed_overspill(): + """ + Edge case: Basins should be distributed evenly between contesting markers. + + Markers should be prevented from spilling over into another basin and + conquering it against other markers with the same claim, just because they + get to the basin one step earlier. + """ + # Scenario 1: markers start with the same value + image = np.array([0, 2, 1, 1, 1, 1, 1, 1, 2, 0]) # fmt: skip + markers = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 2]) # fmt: skip + expected = np.array([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]) # fmt: skip + result = watershed(image, markers=markers) + np.testing.assert_equal(result, expected) + + # Scenario 2: markers start with the different values + image = np.array([2, 2, 1, 1, 1, 1, 1, 1, 2, 0]) # fmt: skip + expected = np.array([1, 1, 1, 1, 1, 2, 2, 2, 2, 2]) # fmt: skip + result = watershed(image, markers=markers) + np.testing.assert_equal(result, expected) + + +def test_markers_on_maxima(): + """Check that markers placed at maxima don't conquer other pixels. + + Regression test for gh-7661 [1]_. + + .. [1] https://github.com/scikit-image/scikit-image/issues/7661 + """ + image = np.array([[0, 1, 2, 3, 4, 5, 4], + [0, 1, 2, 3, 4, 4, 4]]) # fmt: skip + markers = np.array([[1, 0, 0, 0, 0, 2, 0], + [0, 0, 0, 0, 0, 0, 0]]) # fmt: skip + expected = np.array([[1, 1, 1, 1, 1, 2, 1], + [1, 1, 1, 1, 1, 1, 1]]) # fmt: skip + result = watershed(image, markers=markers) + np.testing.assert_equal(result, expected) + + +def test_numeric_seed_watershed(): + """Test that passing just the number of seeds to watershed works.""" + image = np.zeros((5, 6)) + image[:, 3:] = 1 + compact = watershed(image, 2, compactness=0.01) + expected = np.array( + [ + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + [1, 1, 1, 1, 2, 2], + ], + dtype=np.int32, + ) + np.testing.assert_equal(compact, expected) + + +@pytest.mark.parametrize( + 'dtype', + [np.uint8, np.int8, np.uint16, np.int16, np.uint32, np.int32, np.uint64, np.int64], +) +def test_watershed_output_dtype(dtype): + image = np.zeros((100, 100)) + markers = np.zeros((100, 100), dtype) + out = watershed(image, markers) + assert out.dtype == markers.dtype + + +def test_incorrect_markers_shape(): + image = np.ones((5, 6)) + markers = np.ones((5, 7)) + with pytest.raises(ValueError): + watershed(image, markers) + + +def test_incorrect_mask_shape(): + image = np.ones((5, 6)) + mask = np.ones((5, 7)) + with pytest.raises(ValueError): + watershed(image, markers=4, mask=mask) + + +def test_markers_in_mask(): + data = blob + mask = data != 255 + out = watershed(data, 25, connectivity=2, mask=mask) + # There should be no markers where the mask is false + assert np.all(out[~mask] == 0) + + +def test_no_markers(): + data = blob + mask = data != 255 + out = watershed(data, mask=mask) + assert np.max(out) == 2 + + +def test_connectivity(): + """ + Watershed segmentation should output different result for + different connectivity + when markers are calculated where None is supplied. + Issue = 5084 + """ + # Generate a dummy BrightnessTemperature image + x, y = np.indices((406, 270)) + x1, y1, x2, y2, x3, y3, x4, y4 = 200, 208, 300, 120, 100, 100, 340, 208 + r1, r2, r3, r4 = 100, 50, 40, 80 + mask_circle1 = (x - x1) ** 2 + (y - y1) ** 2 < r1**2 + mask_circle2 = (x - x2) ** 2 + (y - y2) ** 2 < r2**2 + mask_circle3 = (x - x3) ** 2 + (y - y3) ** 2 < r3**2 + mask_circle4 = (x - x4) ** 2 + (y - y4) ** 2 < r4**2 + image = np.logical_or(mask_circle1, mask_circle2) + image = np.logical_or(image, mask_circle3) + image = np.logical_or(image, mask_circle4) + + # calculate distance in discrete increase + DummyBT = ndi.distance_transform_edt(image) + DummyBT_dis = np.around(DummyBT / 12, decimals=0) * 12 + # calculate the mask + Img_mask = np.where(DummyBT_dis == 0, 0, 1) + + # segments for connectivity 1 and 2 + labels_c1 = watershed( + 200 - DummyBT_dis, mask=Img_mask, connectivity=1, compactness=0.01 + ) + labels_c2 = watershed( + 200 - DummyBT_dis, mask=Img_mask, connectivity=2, compactness=0.01 + ) + + # assertions + assert np.unique(labels_c1).shape[0] == 6 + assert np.unique(labels_c2).shape[0] == 5 + + # checking via area of each individual segment. + for lab, area in zip(range(6), [61824, 3653, 20467, 11097, 1301, 11278]): + assert np.sum(labels_c1 == lab) == area + + for lab, area in zip(range(5), [61824, 3653, 20466, 12386, 11291]): + assert np.sum(labels_c2 == lab) == area diff --git a/lib/python3.10/site-packages/skimage/transform/__init__.py b/lib/python3.10/site-packages/skimage/transform/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4a928f82c267c5deaf7097f0606c56dd4bd4786f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/__init__.py @@ -0,0 +1,37 @@ +"""Geometric and other transformations, e.g., rotations, Radon transform. + +- Geometric transformation: + These transforms change the shape or position of an image. + They are useful for tasks such as image registration, + alignment, and geometric correction. + Examples: :class:`~skimage.transform.AffineTransform`, + :class:`~skimage.transform.ProjectiveTransform`, + :class:`~skimage.transform.EuclideanTransform`. + +- Image resizing and rescaling: + These transforms change the size or resolution of an image. + They are useful for tasks such as down-sampling an image to + reduce its size or up-sampling an image to increase its resolution. + Examples: :func:`~skimage.transform.resize`, + :func:`~skimage.transform.rescale`. + +- Feature detection and extraction: + These transforms identify and extract specific features or + patterns in an image. They are useful for tasks such as object + detection, image segmentation, and feature matching. + Examples: :func:`~skimage.transform.hough_circle`, + :func:`~skimage.transform.pyramid_expand`, + :func:`~skimage.transform.radon`. + +- Image transformation: + These transforms change the appearance of an image without changing its + content. They are useful for tasks such a creating image mosaics, + applying artistic effects, and visualizing image data. + Examples: :func:`~skimage.transform.warp`, + :func:`~skimage.transform.iradon`. + +""" + +import lazy_loader as _lazy + +__getattr__, __dir__, __all__ = _lazy.attach_stub(__name__, __file__) diff --git a/lib/python3.10/site-packages/skimage/transform/__init__.pyi b/lib/python3.10/site-packages/skimage/transform/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..7e6f44eb0c392cffc74bb7049adf630259ebd7c3 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/__init__.pyi @@ -0,0 +1,86 @@ +# Explicitly setting `__all__` is necessary for type inference engines +# to know which symbols are exported. See +# https://peps.python.org/pep-0484/#stub-files + +__all__ = [ + 'hough_circle', + 'hough_ellipse', + 'hough_line', + 'probabilistic_hough_line', + 'hough_circle_peaks', + 'hough_line_peaks', + 'radon', + 'iradon', + 'iradon_sart', + 'order_angles_golden_ratio', + 'frt2', + 'ifrt2', + 'integral_image', + 'integrate', + 'warp', + 'warp_coords', + 'warp_polar', + 'estimate_transform', + 'matrix_transform', + 'EuclideanTransform', + 'SimilarityTransform', + 'AffineTransform', + 'ProjectiveTransform', + 'EssentialMatrixTransform', + 'FundamentalMatrixTransform', + 'PolynomialTransform', + 'PiecewiseAffineTransform', + 'ThinPlateSplineTransform', + 'swirl', + 'resize', + 'resize_local_mean', + 'rotate', + 'rescale', + 'downscale_local_mean', + 'pyramid_reduce', + 'pyramid_expand', + 'pyramid_gaussian', + 'pyramid_laplacian', +] + +from .hough_transform import ( + hough_line, + hough_line_peaks, + probabilistic_hough_line, + hough_circle, + hough_circle_peaks, + hough_ellipse, +) +from .radon_transform import radon, iradon, iradon_sart, order_angles_golden_ratio +from .finite_radon_transform import frt2, ifrt2 +from .integral import integral_image, integrate +from ._geometric import ( + estimate_transform, + matrix_transform, + EuclideanTransform, + SimilarityTransform, + AffineTransform, + ProjectiveTransform, + FundamentalMatrixTransform, + EssentialMatrixTransform, + PolynomialTransform, + PiecewiseAffineTransform, +) +from ._thin_plate_splines import ThinPlateSplineTransform +from ._warps import ( + swirl, + resize, + rotate, + rescale, + downscale_local_mean, + warp, + warp_coords, + warp_polar, + resize_local_mean, +) +from .pyramids import ( + pyramid_reduce, + pyramid_expand, + pyramid_gaussian, + pyramid_laplacian, +) diff --git a/lib/python3.10/site-packages/skimage/transform/_geometric.py b/lib/python3.10/site-packages/skimage/transform/_geometric.py new file mode 100644 index 0000000000000000000000000000000000000000..5e9ea0ba6c04a88ef1a49c50dee4a01fb857ddac --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/_geometric.py @@ -0,0 +1,1772 @@ +import math +import textwrap +from abc import ABC, abstractmethod + +import numpy as np +from scipy import spatial + +from .._shared.utils import safe_as_int +from .._shared.compat import NP_COPY_IF_NEEDED + + +def _affine_matrix_from_vector(v): + """Affine matrix from linearized (d, d + 1) matrix entries.""" + nparam = v.size + # solve for d in: d * (d + 1) = nparam + d = (1 + np.sqrt(1 + 4 * nparam)) / 2 - 1 + dimensionality = int(np.round(d)) # round to prevent approx errors + if d != dimensionality: + raise ValueError( + 'Invalid number of elements for ' f'linearized matrix: {nparam}' + ) + matrix = np.eye(dimensionality + 1) + matrix[:-1, :] = np.reshape(v, (dimensionality, dimensionality + 1)) + return matrix + + +def _center_and_normalize_points(points): + """Center and normalize image points. + + The points are transformed in a two-step procedure that is expressed + as a transformation matrix. The matrix of the resulting points is usually + better conditioned than the matrix of the original points. + + Center the image points, such that the new coordinate system has its + origin at the centroid of the image points. + + Normalize the image points, such that the mean distance from the points + to the origin of the coordinate system is sqrt(D). + + If the points are all identical, the returned values will contain nan. + + Parameters + ---------- + points : (N, D) array + The coordinates of the image points. + + Returns + ------- + matrix : (D+1, D+1) array_like + The transformation matrix to obtain the new points. + new_points : (N, D) array + The transformed image points. + + References + ---------- + .. [1] Hartley, Richard I. "In defense of the eight-point algorithm." + Pattern Analysis and Machine Intelligence, IEEE Transactions on 19.6 + (1997): 580-593. + + """ + n, d = points.shape + centroid = np.mean(points, axis=0) + + centered = points - centroid + rms = np.sqrt(np.sum(centered**2) / n) + + # if all the points are the same, the transformation matrix cannot be + # created. We return an equivalent matrix with np.nans as sentinel values. + # This obviates the need for try/except blocks in functions calling this + # one, and those are only needed when actual 0 is reached, rather than some + # small value; ie, we don't need to worry about numerical stability here, + # only actual 0. + if rms == 0: + return np.full((d + 1, d + 1), np.nan), np.full_like(points, np.nan) + + norm_factor = np.sqrt(d) / rms + + part_matrix = norm_factor * np.concatenate( + (np.eye(d), -centroid[:, np.newaxis]), axis=1 + ) + matrix = np.concatenate( + ( + part_matrix, + [ + [ + 0, + ] + * d + + [1] + ], + ), + axis=0, + ) + + points_h = np.vstack([points.T, np.ones(n)]) + + new_points_h = (matrix @ points_h).T + + new_points = new_points_h[:, :d] + new_points /= new_points_h[:, d:] + + return matrix, new_points + + +def _umeyama(src, dst, estimate_scale): + """Estimate N-D similarity transformation with or without scaling. + + Parameters + ---------- + src : (M, N) array_like + Source coordinates. + dst : (M, N) array_like + Destination coordinates. + estimate_scale : bool + Whether to estimate scaling factor. + + Returns + ------- + T : (N + 1, N + 1) + The homogeneous similarity transformation matrix. The matrix contains + NaN values only if the problem is not well-conditioned. + + References + ---------- + .. [1] "Least-squares estimation of transformation parameters between two + point patterns", Shinji Umeyama, PAMI 1991, :DOI:`10.1109/34.88573` + + """ + src = np.asarray(src) + dst = np.asarray(dst) + + num = src.shape[0] + dim = src.shape[1] + + # Compute mean of src and dst. + src_mean = src.mean(axis=0) + dst_mean = dst.mean(axis=0) + + # Subtract mean from src and dst. + src_demean = src - src_mean + dst_demean = dst - dst_mean + + # Eq. (38). + A = dst_demean.T @ src_demean / num + + # Eq. (39). + d = np.ones((dim,), dtype=np.float64) + if np.linalg.det(A) < 0: + d[dim - 1] = -1 + + T = np.eye(dim + 1, dtype=np.float64) + + U, S, V = np.linalg.svd(A) + + # Eq. (40) and (43). + rank = np.linalg.matrix_rank(A) + if rank == 0: + return np.nan * T + elif rank == dim - 1: + if np.linalg.det(U) * np.linalg.det(V) > 0: + T[:dim, :dim] = U @ V + else: + s = d[dim - 1] + d[dim - 1] = -1 + T[:dim, :dim] = U @ np.diag(d) @ V + d[dim - 1] = s + else: + T[:dim, :dim] = U @ np.diag(d) @ V + + if estimate_scale: + # Eq. (41) and (42). + scale = 1.0 / src_demean.var(axis=0).sum() * (S @ d) + else: + scale = 1.0 + + T[:dim, dim] = dst_mean - scale * (T[:dim, :dim] @ src_mean.T) + T[:dim, :dim] *= scale + + return T + + +class _GeometricTransform(ABC): + """Abstract base class for geometric transformations.""" + + @abstractmethod + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array_like + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Destination coordinates. + + """ + + @property + @abstractmethod + def inverse(self): + """Return a transform object representing the inverse.""" + + def residuals(self, src, dst): + """Determine residuals of transformed destination coordinates. + + For each transformed source coordinate the Euclidean distance to the + respective destination coordinate is determined. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + Returns + ------- + residuals : (N,) array + Residual for coordinate. + + """ + return np.sqrt(np.sum((self(src) - dst) ** 2, axis=1)) + + +class FundamentalMatrixTransform(_GeometricTransform): + """Fundamental matrix transformation. + + The fundamental matrix relates corresponding points between a pair of + uncalibrated images. The matrix transforms homogeneous image points in one + image to epipolar lines in the other image. + + The fundamental matrix is only defined for a pair of moving images. In the + case of pure rotation or planar scenes, the homography describes the + geometric relation between two images (`ProjectiveTransform`). If the + intrinsic calibration of the images is known, the essential matrix describes + the metric relation between the two images (`EssentialMatrixTransform`). + + References + ---------- + .. [1] Hartley, Richard, and Andrew Zisserman. Multiple view geometry in + computer vision. Cambridge university press, 2003. + + Parameters + ---------- + matrix : (3, 3) array_like, optional + Fundamental matrix. + + Attributes + ---------- + params : (3, 3) array + Fundamental matrix. + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + >>> tform_matrix = ski.transform.FundamentalMatrixTransform() + + Define source and destination points: + + >>> src = np.array([1.839035, 1.924743, + ... 0.543582, 0.375221, + ... 0.473240, 0.142522, + ... 0.964910, 0.598376, + ... 0.102388, 0.140092, + ... 15.994343, 9.622164, + ... 0.285901, 0.430055, + ... 0.091150, 0.254594]).reshape(-1, 2) + >>> dst = np.array([1.002114, 1.129644, + ... 1.521742, 1.846002, + ... 1.084332, 0.275134, + ... 0.293328, 0.588992, + ... 0.839509, 0.087290, + ... 1.779735, 1.116857, + ... 0.878616, 0.602447, + ... 0.642616, 1.028681]).reshape(-1, 2) + + Estimate the transformation matrix: + + >>> tform_matrix.estimate(src, dst) + True + >>> tform_matrix.params + array([[-0.21785884, 0.41928191, -0.03430748], + [-0.07179414, 0.04516432, 0.02160726], + [ 0.24806211, -0.42947814, 0.02210191]]) + + Compute the Sampson distance: + + >>> tform_matrix.residuals(src, dst) + array([0.0053886 , 0.00526101, 0.08689701, 0.01850534, 0.09418259, + 0.00185967, 0.06160489, 0.02655136]) + + Apply inverse transformation: + + >>> tform_matrix.inverse(dst) + array([[-0.0513591 , 0.04170974, 0.01213043], + [-0.21599496, 0.29193419, 0.00978184], + [-0.0079222 , 0.03758889, -0.00915389], + [ 0.14187184, -0.27988959, 0.02476507], + [ 0.05890075, -0.07354481, -0.00481342], + [-0.21985267, 0.36717464, -0.01482408], + [ 0.01339569, -0.03388123, 0.00497605], + [ 0.03420927, -0.1135812 , 0.02228236]]) + + """ + + def __init__(self, matrix=None, *, dimensionality=2): + if matrix is None: + # default to an identity transform + matrix = np.eye(dimensionality + 1) + else: + matrix = np.asarray(matrix) + dimensionality = matrix.shape[0] - 1 + if matrix.shape != (dimensionality + 1, dimensionality + 1): + raise ValueError("Invalid shape of transformation matrix") + self.params = matrix + if dimensionality != 2: + raise NotImplementedError( + f'{self.__class__} is only implemented for 2D coordinates ' + '(i.e. 3D transformation matrices).' + ) + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array_like + Source coordinates. + + Returns + ------- + coords : (N, 3) array + Epipolar lines in the destination image. + + """ + coords = np.asarray(coords) + coords_homogeneous = np.column_stack([coords, np.ones(coords.shape[0])]) + return coords_homogeneous @ self.params.T + + @property + def inverse(self): + """Return a transform object representing the inverse. + + See Hartley & Zisserman, Ch. 8: Epipolar Geometry and the Fundamental + Matrix, for an explanation of why F.T gives the inverse. + """ + return type(self)(matrix=self.params.T) + + def _setup_constraint_matrix(self, src, dst): + """Setup and solve the homogeneous epipolar constraint matrix:: + + dst' * F * src = 0. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + + Returns + ------- + F_normalized : (3, 3) array + The normalized solution to the homogeneous system. If the system + is not well-conditioned, this matrix contains NaNs. + src_matrix : (3, 3) array + The transformation matrix to obtain the normalized source + coordinates. + dst_matrix : (3, 3) array + The transformation matrix to obtain the normalized destination + coordinates. + + """ + src = np.asarray(src) + dst = np.asarray(dst) + if src.shape != dst.shape: + raise ValueError('src and dst shapes must be identical.') + if src.shape[0] < 8: + raise ValueError('src.shape[0] must be equal or larger than 8.') + + # Center and normalize image points for better numerical stability. + try: + src_matrix, src = _center_and_normalize_points(src) + dst_matrix, dst = _center_and_normalize_points(dst) + except ZeroDivisionError: + self.params = np.full((3, 3), np.nan) + return 3 * [np.full((3, 3), np.nan)] + + # Setup homogeneous linear equation as dst' * F * src = 0. + A = np.ones((src.shape[0], 9)) + A[:, :2] = src + A[:, :3] *= dst[:, 0, np.newaxis] + A[:, 3:5] = src + A[:, 3:6] *= dst[:, 1, np.newaxis] + A[:, 6:8] = src + + # Solve for the nullspace of the constraint matrix. + _, _, V = np.linalg.svd(A) + F_normalized = V[-1, :].reshape(3, 3) + + return F_normalized, src_matrix, dst_matrix + + def estimate(self, src, dst): + """Estimate fundamental matrix using 8-point algorithm. + + The 8-point algorithm requires at least 8 corresponding point pairs for + a well-conditioned solution, otherwise the over-determined solution is + estimated. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + + F_normalized, src_matrix, dst_matrix = self._setup_constraint_matrix(src, dst) + + # Enforcing the internal constraint that two singular values must be + # non-zero and one must be zero. + U, S, V = np.linalg.svd(F_normalized) + S[2] = 0 + F = U @ np.diag(S) @ V + + self.params = dst_matrix.T @ F @ src_matrix + + return True + + def residuals(self, src, dst): + """Compute the Sampson distance. + + The Sampson distance is the first approximation to the geometric error. + + Parameters + ---------- + src : (N, 2) array + Source coordinates. + dst : (N, 2) array + Destination coordinates. + + Returns + ------- + residuals : (N,) array + Sampson distance. + + """ + src_homogeneous = np.column_stack([src, np.ones(src.shape[0])]) + dst_homogeneous = np.column_stack([dst, np.ones(dst.shape[0])]) + + F_src = self.params @ src_homogeneous.T + Ft_dst = self.params.T @ dst_homogeneous.T + + dst_F_src = np.sum(dst_homogeneous * F_src.T, axis=1) + + return np.abs(dst_F_src) / np.sqrt( + F_src[0] ** 2 + F_src[1] ** 2 + Ft_dst[0] ** 2 + Ft_dst[1] ** 2 + ) + + +class EssentialMatrixTransform(FundamentalMatrixTransform): + """Essential matrix transformation. + + The essential matrix relates corresponding points between a pair of + calibrated images. The matrix transforms normalized, homogeneous image + points in one image to epipolar lines in the other image. + + The essential matrix is only defined for a pair of moving images capturing a + non-planar scene. In the case of pure rotation or planar scenes, the + homography describes the geometric relation between two images + (`ProjectiveTransform`). If the intrinsic calibration of the images is + unknown, the fundamental matrix describes the projective relation between + the two images (`FundamentalMatrixTransform`). + + References + ---------- + .. [1] Hartley, Richard, and Andrew Zisserman. Multiple view geometry in + computer vision. Cambridge university press, 2003. + + Parameters + ---------- + rotation : (3, 3) array_like, optional + Rotation matrix of the relative camera motion. + translation : (3, 1) array_like, optional + Translation vector of the relative camera motion. The vector must + have unit length. + matrix : (3, 3) array_like, optional + Essential matrix. + + Attributes + ---------- + params : (3, 3) array + Essential matrix. + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + >>> + >>> tform_matrix = ski.transform.EssentialMatrixTransform( + ... rotation=np.eye(3), translation=np.array([0, 0, 1]) + ... ) + >>> tform_matrix.params + array([[ 0., -1., 0.], + [ 1., 0., 0.], + [ 0., 0., 0.]]) + >>> src = np.array([[ 1.839035, 1.924743], + ... [ 0.543582, 0.375221], + ... [ 0.47324 , 0.142522], + ... [ 0.96491 , 0.598376], + ... [ 0.102388, 0.140092], + ... [15.994343, 9.622164], + ... [ 0.285901, 0.430055], + ... [ 0.09115 , 0.254594]]) + >>> dst = np.array([[1.002114, 1.129644], + ... [1.521742, 1.846002], + ... [1.084332, 0.275134], + ... [0.293328, 0.588992], + ... [0.839509, 0.08729 ], + ... [1.779735, 1.116857], + ... [0.878616, 0.602447], + ... [0.642616, 1.028681]]) + >>> tform_matrix.estimate(src, dst) + True + >>> tform_matrix.residuals(src, dst) + array([0.42455187, 0.01460448, 0.13847034, 0.12140951, 0.27759346, + 0.32453118, 0.00210776, 0.26512283]) + + """ + + def __init__( + self, rotation=None, translation=None, matrix=None, *, dimensionality=2 + ): + super().__init__(matrix=matrix, dimensionality=dimensionality) + if rotation is not None: + rotation = np.asarray(rotation) + if translation is None: + raise ValueError("Both rotation and translation required") + translation = np.asarray(translation) + if rotation.shape != (3, 3): + raise ValueError("Invalid shape of rotation matrix") + if abs(np.linalg.det(rotation) - 1) > 1e-6: + raise ValueError("Rotation matrix must have unit determinant") + if translation.size != 3: + raise ValueError("Invalid shape of translation vector") + if abs(np.linalg.norm(translation) - 1) > 1e-6: + raise ValueError("Translation vector must have unit length") + # Matrix representation of the cross product for t. + t_x = np.array( + [ + 0, + -translation[2], + translation[1], + translation[2], + 0, + -translation[0], + -translation[1], + translation[0], + 0, + ] + ).reshape(3, 3) + self.params = t_x @ rotation + elif matrix is not None: + matrix = np.asarray(matrix) + if matrix.shape != (3, 3): + raise ValueError("Invalid shape of transformation matrix") + self.params = matrix + else: + # default to an identity transform + self.params = np.eye(3) + + def estimate(self, src, dst): + """Estimate essential matrix using 8-point algorithm. + + The 8-point algorithm requires at least 8 corresponding point pairs for + a well-conditioned solution, otherwise the over-determined solution is + estimated. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + + E_normalized, src_matrix, dst_matrix = self._setup_constraint_matrix(src, dst) + + # Enforcing the internal constraint that two singular values must be + # equal and one must be zero. + U, S, V = np.linalg.svd(E_normalized) + S[0] = (S[0] + S[1]) / 2.0 + S[1] = S[0] + S[2] = 0 + E = U @ np.diag(S) @ V + + self.params = dst_matrix.T @ E @ src_matrix + + return True + + +class ProjectiveTransform(_GeometricTransform): + r"""Projective transformation. + + Apply a projective transformation (homography) on coordinates. + + For each homogeneous coordinate :math:`\mathbf{x} = [x, y, 1]^T`, its + target position is calculated by multiplying with the given matrix, + :math:`H`, to give :math:`H \mathbf{x}`:: + + [[a0 a1 a2] + [b0 b1 b2] + [c0 c1 1 ]]. + + E.g., to rotate by theta degrees clockwise, the matrix should be:: + + [[cos(theta) -sin(theta) 0] + [sin(theta) cos(theta) 0] + [0 0 1]] + + or, to translate x by 10 and y by 20:: + + [[1 0 10] + [0 1 20] + [0 0 1 ]]. + + Parameters + ---------- + matrix : (D+1, D+1) array_like, optional + Homogeneous transformation matrix. + dimensionality : int, optional + The number of dimensions of the transform. This is ignored if + ``matrix`` is not None. + + Attributes + ---------- + params : (D+1, D+1) array + Homogeneous transformation matrix. + + """ + + def __init__(self, matrix=None, *, dimensionality=2): + if matrix is None: + # default to an identity transform + matrix = np.eye(dimensionality + 1) + else: + matrix = np.asarray(matrix) + dimensionality = matrix.shape[0] - 1 + if matrix.shape != (dimensionality + 1, dimensionality + 1): + raise ValueError("invalid shape of transformation matrix") + self.params = matrix + self._coeffs = range(matrix.size - 1) + + @property + def _inv_matrix(self): + return np.linalg.inv(self.params) + + def _apply_mat(self, coords, matrix): + ndim = matrix.shape[0] - 1 + coords = np.array(coords, copy=NP_COPY_IF_NEEDED, ndmin=2) + + src = np.concatenate([coords, np.ones((coords.shape[0], 1))], axis=1) + dst = src @ matrix.T + + # below, we will divide by the last dimension of the homogeneous + # coordinate matrix. In order to avoid division by zero, + # we replace exact zeros in this column with a very small number. + dst[dst[:, ndim] == 0, ndim] = np.finfo(float).eps + # rescale to homogeneous coordinates + dst[:, :ndim] /= dst[:, ndim : ndim + 1] + + return dst[:, :ndim] + + def __array__(self, dtype=None, copy=None): + if dtype is None: + return self.params + else: + return self.params.astype(dtype) + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, D) array_like + Source coordinates. + + Returns + ------- + coords_out : (N, D) array + Destination coordinates. + + """ + return self._apply_mat(coords, self.params) + + @property + def inverse(self): + """Return a transform object representing the inverse.""" + return type(self)(matrix=self._inv_matrix) + + def estimate(self, src, dst, weights=None): + """Estimate the transformation from a set of corresponding points. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + The transformation is defined as:: + + X = (a0*x + a1*y + a2) / (c0*x + c1*y + 1) + Y = (b0*x + b1*y + b2) / (c0*x + c1*y + 1) + + These equations can be transformed to the following form:: + + 0 = a0*x + a1*y + a2 - c0*x*X - c1*y*X - X + 0 = b0*x + b1*y + b2 - c0*x*Y - c1*y*Y - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[x y 1 0 0 0 -x*X -y*X -X] + [0 0 0 x y 1 -x*Y -y*Y -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c0 c1 c3] + + In case of total least-squares the solution of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + Weights can be applied to each pair of corresponding points to + indicate, particularly in an overdetermined system, if point pairs have + higher or lower confidence or uncertainties associated with them. From + the matrix treatment of least squares problems, these weight values are + normalised, square-rooted, then built into a diagonal matrix, by which + A is multiplied. + + In case of the affine transformation the coefficients c0 and c1 are 0. + Thus the system of equations is:: + + A = [[x y 1 0 0 0 -X] + [0 0 0 x y 1 -Y] + ... + ... + ] + x.T = [a0 a1 a2 b0 b1 b2 c3] + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + weights : (N,) array_like, optional + Relative weight values for each pair of points. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + src = np.asarray(src) + dst = np.asarray(dst) + n, d = src.shape + + src_matrix, src = _center_and_normalize_points(src) + dst_matrix, dst = _center_and_normalize_points(dst) + if not np.all(np.isfinite(src_matrix + dst_matrix)): + self.params = np.full((d + 1, d + 1), np.nan) + return False + + # params: a0, a1, a2, b0, b1, b2, c0, c1 + A = np.zeros((n * d, (d + 1) ** 2)) + # fill the A matrix with the appropriate block matrices; see docstring + # for 2D example — this can be generalised to more blocks in the 3D and + # higher-dimensional cases. + for ddim in range(d): + A[ddim * n : (ddim + 1) * n, ddim * (d + 1) : ddim * (d + 1) + d] = src + A[ddim * n : (ddim + 1) * n, ddim * (d + 1) + d] = 1 + A[ddim * n : (ddim + 1) * n, -d - 1 : -1] = src + A[ddim * n : (ddim + 1) * n, -1] = -1 + A[ddim * n : (ddim + 1) * n, -d - 1 :] *= -dst[:, ddim : (ddim + 1)] + + # Select relevant columns, depending on params + A = A[:, list(self._coeffs) + [-1]] + + # Get the vectors that correspond to singular values, also applying + # the weighting if provided + if weights is None: + _, _, V = np.linalg.svd(A) + else: + weights = np.asarray(weights) + W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), d)) + _, _, V = np.linalg.svd(W @ A) + + # if the last element of the vector corresponding to the smallest + # singular value is close to zero, this implies a degenerate case + # because it is a rank-defective transform, which would map points + # to a line rather than a plane. + if np.isclose(V[-1, -1], 0): + self.params = np.full((d + 1, d + 1), np.nan) + return False + + H = np.zeros((d + 1, d + 1)) + # solution is right singular vector that corresponds to smallest + # singular value + H.flat[list(self._coeffs) + [-1]] = -V[-1, :-1] / V[-1, -1] + H[d, d] = 1 + + # De-center and de-normalize + H = np.linalg.inv(dst_matrix) @ H @ src_matrix + + # Small errors can creep in if points are not exact, causing the last + # element of H to deviate from unity. Correct for that here. + H /= H[-1, -1] + + self.params = H + + return True + + def __add__(self, other): + """Combine this transformation with another.""" + if isinstance(other, ProjectiveTransform): + # combination of the same types result in a transformation of this + # type again, otherwise use general projective transformation + if type(self) == type(other): + tform = self.__class__ + else: + tform = ProjectiveTransform + return tform(other.params @ self.params) + else: + raise TypeError("Cannot combine transformations of differing " "types.") + + def __nice__(self): + """common 'paramstr' used by __str__ and __repr__""" + npstring = np.array2string(self.params, separator=', ') + paramstr = 'matrix=\n' + textwrap.indent(npstring, ' ') + return paramstr + + def __repr__(self): + """Add standard repr formatting around a __nice__ string""" + paramstr = self.__nice__() + classname = self.__class__.__name__ + classstr = classname + return f'<{classstr}({paramstr}) at {hex(id(self))}>' + + def __str__(self): + """Add standard str formatting around a __nice__ string""" + paramstr = self.__nice__() + classname = self.__class__.__name__ + classstr = classname + return f'<{classstr}({paramstr})>' + + @property + def dimensionality(self): + """The dimensionality of the transformation.""" + return self.params.shape[0] - 1 + + +class AffineTransform(ProjectiveTransform): + """Affine transformation. + + Has the following form:: + + X = a0 * x + a1 * y + a2 + = sx * x * [cos(rotation) + tan(shear_y) * sin(rotation)] + - sy * y * [tan(shear_x) * cos(rotation) + sin(rotation)] + + translation_x + + Y = b0 * x + b1 * y + b2 + = sx * x * [sin(rotation) - tan(shear_y) * cos(rotation)] + - sy * y * [tan(shear_x) * sin(rotation) - cos(rotation)] + + translation_y + + where ``sx`` and ``sy`` are scale factors in the x and y directions. + + This is equivalent to applying the operations in the following order: + + 1. Scale + 2. Shear + 3. Rotate + 4. Translate + + The homogeneous transformation matrix is:: + + [[a0 a1 a2] + [b0 b1 b2] + [0 0 1]] + + In 2D, the transformation parameters can be given as the homogeneous + transformation matrix, above, or as the implicit parameters, scale, + rotation, shear, and translation in x (a2) and y (b2). For 3D and higher, + only the matrix form is allowed. + + In narrower transforms, such as the Euclidean (only rotation and + translation) or Similarity (rotation, translation, and a global scale + factor) transforms, it is possible to specify 3D transforms using implicit + parameters also. + + Parameters + ---------- + matrix : (D+1, D+1) array_like, optional + Homogeneous transformation matrix. If this matrix is provided, it is an + error to provide any of scale, rotation, shear, or translation. + scale : {s as float or (sx, sy) as array, list or tuple}, optional + Scale factor(s). If a single value, it will be assigned to both + sx and sy. Only available for 2D. + + .. versionadded:: 0.17 + Added support for supplying a single scalar value. + rotation : float, optional + Rotation angle, clockwise, as radians. Only available for 2D. + shear : float or 2-tuple of float, optional + The x and y shear angles, clockwise, by which these axes are + rotated around the origin [2]. + If a single value is given, take that to be the x shear angle, with + the y angle remaining 0. Only available in 2D. + translation : (tx, ty) as array, list or tuple, optional + Translation parameters. Only available for 2D. + dimensionality : int, optional + The dimensionality of the transform. This is not used if any other + parameters are provided. + + Attributes + ---------- + params : (D+1, D+1) array + Homogeneous transformation matrix. + + Raises + ------ + ValueError + If both ``matrix`` and any of the other parameters are provided. + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + >>> img = ski.data.astronaut() + + Define source and destination points: + + >>> src = np.array([[150, 150], + ... [250, 100], + ... [150, 200]]) + >>> dst = np.array([[200, 200], + ... [300, 150], + ... [150, 400]]) + + Estimate the transformation matrix: + + >>> tform = ski.transform.AffineTransform() + >>> tform.estimate(src, dst) + True + + Apply the transformation: + + >>> warped = ski.transform.warp(img, inverse_map=tform.inverse) + + References + ---------- + .. [1] Wikipedia, "Affine transformation", + https://en.wikipedia.org/wiki/Affine_transformation#Image_transformation + .. [2] Wikipedia, "Shear mapping", + https://en.wikipedia.org/wiki/Shear_mapping + """ + + def __init__( + self, + matrix=None, + scale=None, + rotation=None, + shear=None, + translation=None, + *, + dimensionality=2, + ): + params = any( + param is not None for param in (scale, rotation, shear, translation) + ) + + # these parameters get overwritten if a higher-D matrix is given + self._coeffs = range(dimensionality * (dimensionality + 1)) + + if params and matrix is not None: + raise ValueError( + "You cannot specify the transformation matrix and" + " the implicit parameters at the same time." + ) + if params and dimensionality > 2: + raise ValueError('Parameter input is only supported in 2D.') + elif matrix is not None: + matrix = np.asarray(matrix) + if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]: + raise ValueError("Invalid shape of transformation matrix.") + else: + dimensionality = matrix.shape[0] - 1 + nparam = dimensionality * (dimensionality + 1) + self._coeffs = range(nparam) + self.params = matrix + elif params: # note: 2D only + if scale is None: + scale = (1, 1) + if rotation is None: + rotation = 0 + if shear is None: + shear = 0 + if translation is None: + translation = (0, 0) + + if np.isscalar(scale): + sx = sy = scale + else: + sx, sy = scale + + if np.isscalar(shear): + shear_x, shear_y = (shear, 0) + else: + shear_x, shear_y = shear + + a0 = sx * (math.cos(rotation) + math.tan(shear_y) * math.sin(rotation)) + a1 = -sy * (math.tan(shear_x) * math.cos(rotation) + math.sin(rotation)) + a2 = translation[0] + + b0 = sx * (math.sin(rotation) - math.tan(shear_y) * math.cos(rotation)) + b1 = -sy * (math.tan(shear_x) * math.sin(rotation) - math.cos(rotation)) + b2 = translation[1] + self.params = np.array([[a0, a1, a2], [b0, b1, b2], [0, 0, 1]]) + else: + # default to an identity transform + self.params = np.eye(dimensionality + 1) + + @property + def scale(self): + if self.dimensionality != 2: + return np.sqrt(np.sum(self.params**2, axis=0))[: self.dimensionality] + else: + ss = np.sum(self.params**2, axis=0) + ss[1] = ss[1] / (math.tan(self.shear) ** 2 + 1) + return np.sqrt(ss)[: self.dimensionality] + + @property + def rotation(self): + if self.dimensionality != 2: + raise NotImplementedError( + 'The rotation property is only implemented for 2D transforms.' + ) + return math.atan2(self.params[1, 0], self.params[0, 0]) + + @property + def shear(self): + if self.dimensionality != 2: + raise NotImplementedError( + 'The shear property is only implemented for 2D transforms.' + ) + beta = math.atan2(-self.params[0, 1], self.params[1, 1]) + return beta - self.rotation + + @property + def translation(self): + return self.params[0 : self.dimensionality, self.dimensionality] + + +class PiecewiseAffineTransform(_GeometricTransform): + """Piecewise affine transformation. + + Control points are used to define the mapping. The transform is based on + a Delaunay triangulation of the points to form a mesh. Each triangle is + used to find a local affine transform. + + Attributes + ---------- + affines : list of AffineTransform objects + Affine transformations for each triangle in the mesh. + inverse_affines : list of AffineTransform objects + Inverse affine transformations for each triangle in the mesh. + + """ + + def __init__(self): + self._tesselation = None + self._inverse_tesselation = None + self.affines = None + self.inverse_affines = None + + def estimate(self, src, dst): + """Estimate the transformation from a set of corresponding points. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, D) array_like + Source coordinates. + dst : (N, D) array_like + Destination coordinates. + + Returns + ------- + success : bool + True, if all pieces of the model are successfully estimated. + + """ + src = np.asarray(src) + dst = np.asarray(dst) + + ndim = src.shape[1] + # forward piecewise affine + # triangulate input positions into mesh + self._tesselation = spatial.Delaunay(src) + + success = True + + # find affine mapping from source positions to destination + self.affines = [] + for tri in self._tesselation.simplices: + affine = AffineTransform(dimensionality=ndim) + success &= affine.estimate(src[tri, :], dst[tri, :]) + self.affines.append(affine) + + # inverse piecewise affine + # triangulate input positions into mesh + self._inverse_tesselation = spatial.Delaunay(dst) + # find affine mapping from source positions to destination + self.inverse_affines = [] + for tri in self._inverse_tesselation.simplices: + affine = AffineTransform(dimensionality=ndim) + success &= affine.estimate(dst[tri, :], src[tri, :]) + self.inverse_affines.append(affine) + + return success + + def __call__(self, coords): + """Apply forward transformation. + + Coordinates outside of the mesh will be set to `- 1`. + + Parameters + ---------- + coords : (N, D) array_like + Source coordinates. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + coords = np.asarray(coords) + out = np.empty_like(coords, np.float64) + + # determine triangle index for each coordinate + simplex = self._tesselation.find_simplex(coords) + + # coordinates outside of mesh + out[simplex == -1, :] = -1 + + for index in range(len(self._tesselation.simplices)): + # affine transform for triangle + affine = self.affines[index] + # all coordinates within triangle + index_mask = simplex == index + + out[index_mask, :] = affine(coords[index_mask, :]) + + return out + + @property + def inverse(self): + """Return a transform object representing the inverse.""" + tform = type(self)() + tform._tesselation = self._inverse_tesselation + tform._inverse_tesselation = self._tesselation + tform.affines = self.inverse_affines + tform.inverse_affines = self.affines + return tform + + +def _euler_rotation_matrix(angles, degrees=False): + """Produce an Euler rotation matrix from the given intrinsic rotation angles + for the axes x, y and z. + + Parameters + ---------- + angles : array of float, shape (3,) + The transformation angles in radians. + degrees : bool, optional + If True, then the given angles are assumed to be in degrees. Default is False. + + Returns + ------- + R : array of float, shape (3, 3) + The Euler rotation matrix. + """ + return spatial.transform.Rotation.from_euler( + 'XYZ', angles=angles, degrees=degrees + ).as_matrix() + + +class EuclideanTransform(ProjectiveTransform): + """Euclidean transformation, also known as a rigid transform. + + Has the following form:: + + X = a0 * x - b0 * y + a1 = + = x * cos(rotation) - y * sin(rotation) + a1 + + Y = b0 * x + a0 * y + b1 = + = x * sin(rotation) + y * cos(rotation) + b1 + + where the homogeneous transformation matrix is:: + + [[a0 -b0 a1] + [b0 a0 b1] + [0 0 1 ]] + + The Euclidean transformation is a rigid transformation with rotation and + translation parameters. The similarity transformation extends the Euclidean + transformation with a single scaling factor. + + In 2D and 3D, the transformation parameters may be provided either via + `matrix`, the homogeneous transformation matrix, above, or via the + implicit parameters `rotation` and/or `translation` (where `a1` is the + translation along `x`, `b1` along `y`, etc.). Beyond 3D, if the + transformation is only a translation, you may use the implicit parameter + `translation`; otherwise, you must use `matrix`. + + Parameters + ---------- + matrix : (D+1, D+1) array_like, optional + Homogeneous transformation matrix. + rotation : float or sequence of float, optional + Rotation angle, clockwise, as radians. If given as + a vector, it is interpreted as Euler rotation angles [1]_. Only 2D + (single rotation) and 3D (Euler rotations) values are supported. For + higher dimensions, you must provide or estimate the transformation + matrix. + translation : (x, y[, z, ...]) sequence of float, length D, optional + Translation parameters for each axis. + dimensionality : int, optional + The dimensionality of the transform. + + Attributes + ---------- + params : (D+1, D+1) array + Homogeneous transformation matrix. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions + """ + + def __init__( + self, matrix=None, rotation=None, translation=None, *, dimensionality=2 + ): + params_given = rotation is not None or translation is not None + + if params_given and matrix is not None: + raise ValueError( + "You cannot specify the transformation matrix and" + " the implicit parameters at the same time." + ) + elif matrix is not None: + matrix = np.asarray(matrix) + if matrix.shape[0] != matrix.shape[1]: + raise ValueError("Invalid shape of transformation matrix.") + self.params = matrix + elif params_given: + if rotation is None: + dimensionality = len(translation) + if dimensionality == 2: + rotation = 0 + elif dimensionality == 3: + rotation = np.zeros(3) + else: + raise ValueError( + 'Parameters cannot be specified for dimension ' + f'{dimensionality} transforms' + ) + else: + if not np.isscalar(rotation) and len(rotation) != 3: + raise ValueError( + 'Parameters cannot be specified for dimension ' + f'{dimensionality} transforms' + ) + if translation is None: + translation = (0,) * dimensionality + + if dimensionality == 2: + self.params = np.array( + [ + [math.cos(rotation), -math.sin(rotation), 0], + [math.sin(rotation), math.cos(rotation), 0], + [0, 0, 1], + ] + ) + elif dimensionality == 3: + self.params = np.eye(dimensionality + 1) + self.params[:dimensionality, :dimensionality] = _euler_rotation_matrix( + rotation + ) + self.params[0:dimensionality, dimensionality] = translation + else: + # default to an identity transform + self.params = np.eye(dimensionality + 1) + + def estimate(self, src, dst): + """Estimate the transformation from a set of corresponding points. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + self.params = _umeyama(src, dst, False) + + # _umeyama will return nan if the problem is not well-conditioned. + return not np.any(np.isnan(self.params)) + + @property + def rotation(self): + if self.dimensionality == 2: + return math.atan2(self.params[1, 0], self.params[1, 1]) + elif self.dimensionality == 3: + # Returning 3D Euler rotation matrix + return self.params[:3, :3] + else: + raise NotImplementedError( + 'Rotation only implemented for 2D and 3D transforms.' + ) + + @property + def translation(self): + return self.params[0 : self.dimensionality, self.dimensionality] + + +class SimilarityTransform(EuclideanTransform): + """Similarity transformation. + + Has the following form in 2D:: + + X = a0 * x - b0 * y + a1 = + = s * x * cos(rotation) - s * y * sin(rotation) + a1 + + Y = b0 * x + a0 * y + b1 = + = s * x * sin(rotation) + s * y * cos(rotation) + b1 + + where ``s`` is a scale factor and the homogeneous transformation matrix is:: + + [[a0 -b0 a1] + [b0 a0 b1] + [0 0 1 ]] + + The similarity transformation extends the Euclidean transformation with a + single scaling factor in addition to the rotation and translation + parameters. + + Parameters + ---------- + matrix : (dim+1, dim+1) array_like, optional + Homogeneous transformation matrix. + scale : float, optional + Scale factor. Implemented only for 2D and 3D. + rotation : float, optional + Rotation angle, clockwise, as radians. + Implemented only for 2D and 3D. For 3D, this is given in ZYX Euler + angles. + translation : (dim,) array_like, optional + x, y[, z] translation parameters. Implemented only for 2D and 3D. + + Attributes + ---------- + params : (dim+1, dim+1) array + Homogeneous transformation matrix. + + """ + + def __init__( + self, + matrix=None, + scale=None, + rotation=None, + translation=None, + *, + dimensionality=2, + ): + self.params = None + params = any(param is not None for param in (scale, rotation, translation)) + + if params and matrix is not None: + raise ValueError( + "You cannot specify the transformation matrix and" + " the implicit parameters at the same time." + ) + elif matrix is not None: + matrix = np.asarray(matrix) + if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]: + raise ValueError("Invalid shape of transformation matrix.") + else: + self.params = matrix + dimensionality = matrix.shape[0] - 1 + if params: + if dimensionality not in (2, 3): + raise ValueError('Parameters only supported for 2D and 3D.') + matrix = np.eye(dimensionality + 1, dtype=float) + if scale is None: + scale = 1 + if rotation is None: + rotation = 0 if dimensionality == 2 else (0, 0, 0) + if translation is None: + translation = (0,) * dimensionality + if dimensionality == 2: + ax = (0, 1) + c, s = np.cos(rotation), np.sin(rotation) + matrix[ax, ax] = c + matrix[ax, ax[::-1]] = -s, s + else: # 3D rotation + matrix[:3, :3] = _euler_rotation_matrix(rotation) + + matrix[:dimensionality, :dimensionality] *= scale + matrix[:dimensionality, dimensionality] = translation + self.params = matrix + elif self.params is None: + # default to an identity transform + self.params = np.eye(dimensionality + 1) + + def estimate(self, src, dst): + """Estimate the transformation from a set of corresponding points. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + + self.params = _umeyama(src, dst, estimate_scale=True) + + # _umeyama will return nan if the problem is not well-conditioned. + return not np.any(np.isnan(self.params)) + + @property + def scale(self): + # det = scale**(# of dimensions), therefore scale = det**(1/ndim) + if self.dimensionality == 2: + return np.sqrt(np.linalg.det(self.params)) + elif self.dimensionality == 3: + return np.cbrt(np.linalg.det(self.params)) + else: + raise NotImplementedError('Scale is only implemented for 2D and 3D.') + + +class PolynomialTransform(_GeometricTransform): + """2D polynomial transformation. + + Has the following form:: + + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + Parameters + ---------- + params : (2, N) array_like, optional + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + + Attributes + ---------- + params : (2, N) array + Polynomial coefficients where `N * 2 = (order + 1) * (order + 2)`. So, + a_ji is defined in `params[0, :]` and b_ji in `params[1, :]`. + + """ + + def __init__(self, params=None, *, dimensionality=2): + if dimensionality != 2: + raise NotImplementedError( + 'Polynomial transforms are only implemented for 2D.' + ) + if params is None: + # default to transformation which preserves original coordinates + params = np.array([[0, 1, 0], [0, 0, 1]]) + else: + params = np.asarray(params) + if params.shape[0] != 2: + raise ValueError("invalid shape of transformation parameters") + self.params = params + + def estimate(self, src, dst, order=2, weights=None): + """Estimate the transformation from a set of corresponding points. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + The transformation is defined as:: + + X = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) + Y = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) + + These equations can be transformed to the following form:: + + 0 = sum[j=0:order]( sum[i=0:j]( a_ji * x**(j - i) * y**i )) - X + 0 = sum[j=0:order]( sum[i=0:j]( b_ji * x**(j - i) * y**i )) - Y + + which exist for each set of corresponding points, so we have a set of + N * 2 equations. The coefficients appear linearly so we can write + A x = 0, where:: + + A = [[1 x y x**2 x*y y**2 ... 0 ... 0 -X] + [0 ... 0 1 x y x**2 x*y y**2 -Y] + ... + ... + ] + x.T = [a00 a10 a11 a20 a21 a22 ... ann + b00 b10 b11 b20 b21 b22 ... bnn c3] + + In case of total least-squares the solution of this homogeneous system + of equations is the right singular vector of A which corresponds to the + smallest singular value normed by the coefficient c3. + + Weights can be applied to each pair of corresponding points to + indicate, particularly in an overdetermined system, if point pairs have + higher or lower confidence or uncertainties associated with them. From + the matrix treatment of least squares problems, these weight values are + normalised, square-rooted, then built into a diagonal matrix, by which + A is multiplied. + + Parameters + ---------- + src : (N, 2) array_like + Source coordinates. + dst : (N, 2) array_like + Destination coordinates. + order : int, optional + Polynomial order (number of coefficients is order + 1). + weights : (N,) array_like, optional + Relative weight values for each pair of points. + + Returns + ------- + success : bool + True, if model estimation succeeds. + + """ + src = np.asarray(src) + dst = np.asarray(dst) + xs = src[:, 0] + ys = src[:, 1] + xd = dst[:, 0] + yd = dst[:, 1] + rows = src.shape[0] + + # number of unknown polynomial coefficients + order = safe_as_int(order) + u = (order + 1) * (order + 2) + + A = np.zeros((rows * 2, u + 1)) + pidx = 0 + for j in range(order + 1): + for i in range(j + 1): + A[:rows, pidx] = xs ** (j - i) * ys**i + A[rows:, pidx + u // 2] = xs ** (j - i) * ys**i + pidx += 1 + + A[:rows, -1] = xd + A[rows:, -1] = yd + + # Get the vectors that correspond to singular values, also applying + # the weighting if provided + if weights is None: + _, _, V = np.linalg.svd(A) + else: + weights = np.asarray(weights) + W = np.diag(np.tile(np.sqrt(weights / np.max(weights)), 2)) + _, _, V = np.linalg.svd(W @ A) + + # solution is right singular vector that corresponds to smallest + # singular value + params = -V[-1, :-1] / V[-1, -1] + + self.params = params.reshape((2, u // 2)) + + return True + + def __call__(self, coords): + """Apply forward transformation. + + Parameters + ---------- + coords : (N, 2) array_like + source coordinates + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + coords = np.asarray(coords) + x = coords[:, 0] + y = coords[:, 1] + u = len(self.params.ravel()) + # number of coefficients -> u = (order + 1) * (order + 2) + order = int((-3 + math.sqrt(9 - 4 * (2 - u))) / 2) + dst = np.zeros(coords.shape) + + pidx = 0 + for j in range(order + 1): + for i in range(j + 1): + dst[:, 0] += self.params[0, pidx] * x ** (j - i) * y**i + dst[:, 1] += self.params[1, pidx] * x ** (j - i) * y**i + pidx += 1 + + return dst + + @property + def inverse(self): + raise NotImplementedError( + 'There is no explicit way to do the inverse polynomial ' + 'transformation. Instead, estimate the inverse transformation ' + 'parameters by exchanging source and destination coordinates,' + 'then apply the forward transformation.' + ) + + +TRANSFORMS = { + 'euclidean': EuclideanTransform, + 'similarity': SimilarityTransform, + 'affine': AffineTransform, + 'piecewise-affine': PiecewiseAffineTransform, + 'projective': ProjectiveTransform, + 'fundamental': FundamentalMatrixTransform, + 'essential': EssentialMatrixTransform, + 'polynomial': PolynomialTransform, +} + + +def estimate_transform(ttype, src, dst, *args, **kwargs): + """Estimate 2D geometric transformation parameters. + + You can determine the over-, well- and under-determined parameters + with the total least-squares method. + + Number of source and destination coordinates must match. + + Parameters + ---------- + ttype : {'euclidean', similarity', 'affine', 'piecewise-affine', \ + 'projective', 'polynomial'} + Type of transform. + kwargs : array_like or int + Function parameters (src, dst, n, angle):: + + NAME / TTYPE FUNCTION PARAMETERS + 'euclidean' `src, `dst` + 'similarity' `src, `dst` + 'affine' `src, `dst` + 'piecewise-affine' `src, `dst` + 'projective' `src, `dst` + 'polynomial' `src, `dst`, `order` (polynomial order, + default order is 2) + + Also see examples below. + + Returns + ------- + tform : :class:`_GeometricTransform` + Transform object containing the transformation parameters and providing + access to forward and inverse transformation functions. + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + + >>> # estimate transformation parameters + >>> src = np.array([0, 0, 10, 10]).reshape((2, 2)) + >>> dst = np.array([12, 14, 1, -20]).reshape((2, 2)) + + >>> tform = ski.transform.estimate_transform('similarity', src, dst) + + >>> np.allclose(tform.inverse(tform(src)), src) + True + + >>> # warp image using the estimated transformation + >>> image = ski.data.camera() + + >>> ski.transform.warp(image, inverse_map=tform.inverse) # doctest: +SKIP + + >>> # create transformation with explicit parameters + >>> tform2 = ski.transform.SimilarityTransform(scale=1.1, rotation=1, + ... translation=(10, 20)) + + >>> # unite transformations, applied in order from left to right + >>> tform3 = tform + tform2 + >>> np.allclose(tform3(src), tform2(tform(src))) + True + + """ + ttype = ttype.lower() + if ttype not in TRANSFORMS: + raise ValueError(f'the transformation type \'{ttype}\' is not implemented') + + tform = TRANSFORMS[ttype](dimensionality=src.shape[1]) + tform.estimate(src, dst, *args, **kwargs) + + return tform + + +def matrix_transform(coords, matrix): + """Apply 2D matrix transform. + + Parameters + ---------- + coords : (N, 2) array_like + x, y coordinates to transform + matrix : (3, 3) array_like + Homogeneous transformation matrix. + + Returns + ------- + coords : (N, 2) array + Transformed coordinates. + + """ + return ProjectiveTransform(matrix)(coords) diff --git a/lib/python3.10/site-packages/skimage/transform/_thin_plate_splines.py b/lib/python3.10/site-packages/skimage/transform/_thin_plate_splines.py new file mode 100644 index 0000000000000000000000000000000000000000..4e1ac406255a1d047663990c2790601e5fb1134f --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/_thin_plate_splines.py @@ -0,0 +1,188 @@ +import numpy as np +from scipy.spatial import distance_matrix + +from .._shared.utils import check_nD + + +class ThinPlateSplineTransform: + """Thin-plate spline transformation. + + Given two matching sets of points, source and destination, this class + estimates the thin-plate spline (TPS) transformation which transforms + each point in source into its destination counterpart. + + Attributes + ---------- + src : (N, 2) array_like + Coordinates of control points in source image. + + References + ---------- + .. [1] Bookstein, Fred L. "Principal warps: Thin-plate splines and the + decomposition of deformations," IEEE Transactions on pattern analysis + and machine intelligence 11.6 (1989): 567–585. + DOI:`10.1109/34.24792` + https://user.engineering.uiowa.edu/~aip/papers/bookstein-89.pdf + + Examples + -------- + >>> import skimage as ski + + Define source and destination control points such that they simulate + rotating by 90 degrees and generate a meshgrid from them: + + >>> src = np.array([[0, 0], [0, 5], [5, 5], [5, 0]]) + >>> dst = np.array([[5, 0], [0, 0], [0, 5], [5, 5]]) + + Estimate the transformation: + + >>> tps = ski.transform.ThinPlateSplineTransform() + >>> tps.estimate(src, dst) + True + + Appyling the transformation to `src` approximates `dst`: + + >>> np.round(tps(src)) + array([[5., 0.], + [0., 0.], + [0., 5.], + [5., 5.]]) + + Create a meshgrid to apply the transformation to: + + >>> grid = np.meshgrid(np.arange(5), np.arange(5)) + >>> grid[1] + array([[0, 0, 0, 0, 0], + [1, 1, 1, 1, 1], + [2, 2, 2, 2, 2], + [3, 3, 3, 3, 3], + [4, 4, 4, 4, 4]]) + + >>> coords = np.vstack([grid[0].ravel(), grid[1].ravel()]).T + >>> transformed = tps(coords) + >>> np.round(transformed[:, 1]).reshape(5, 5).astype(int) + array([[0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4]]) + """ + + def __init__(self): + self._estimated = False + self._spline_mappings = None + self.src = None + + def __call__(self, coords): + """Estimate the transformation from a set of corresponding points. + + Parameters + ---------- + coords : (N, 2) array_like + x, y coordinates to transform + + Returns + ------- + transformed_coords: (N, D) array + Destination coordinates + """ + if self._spline_mappings is None: + msg = ( + "Transformation is undefined, define it by calling `estimate` " + "before applying it" + ) + raise ValueError(msg) + coords = np.array(coords) + + if coords.ndim != 2 or coords.shape[1] != 2: + msg = "Input `coords` must have shape (N, 2)" + raise ValueError(msg) + + radial_dist = self._radial_distance(coords) + transformed_coords = self._spline_function(coords, radial_dist) + + return transformed_coords + + @property + def inverse(self): + raise NotImplementedError("Not supported") + + def estimate(self, src, dst): + """Estimate optimal spline mappings between source and destination points. + + Parameters + ---------- + src : (N, 2) array_like + Control points at source coordinates. + dst : (N, 2) array_like + Control points at destination coordinates. + + Returns + ------- + success: bool + True indicates that the estimation was successful. + + Notes + ----- + The number N of source and destination points must match. + """ + check_nD(src, 2, arg_name="src") + check_nD(dst, 2, arg_name="dst") + + if src.shape[0] < 3 or dst.shape[0] < 3: + msg = "Need at least 3 points in in `src` and `dst`" + raise ValueError(msg) + if src.shape != dst.shape: + msg = f"Shape of `src` and `dst` didn't match, {src.shape} != {dst.shape}" + raise ValueError(msg) + + self.src = src + n, d = src.shape + + dist = distance_matrix(src, src) + K = self._radial_basis_kernel(dist) + P = np.hstack([np.ones((n, 1)), src]) + n_plus_3 = n + 3 + L = np.zeros((n_plus_3, n_plus_3), dtype=np.float32) + L[:n, :n] = K + L[:n, -3:] = P + L[-3:, :n] = P.T + V = np.vstack([dst, np.zeros((d + 1, d))]) + try: + self._spline_mappings = np.linalg.solve(L, V) + except np.linalg.LinAlgError: + return False + return True + + def _radial_distance(self, coords): + """Compute the radial distance between input points and source points.""" + dists = distance_matrix(coords, self.src) + return self._radial_basis_kernel(dists) + + def _spline_function(self, coords, radial_dist): + """Estimate the spline function in X and Y directions.""" + n = self.src.shape[0] + w = self._spline_mappings[:n] + a = self._spline_mappings[n:] + transformed_coords = a[0] + np.dot(coords, a[1:]) + np.dot(radial_dist, w) + return transformed_coords + + @staticmethod + def _radial_basis_kernel(r): + """Compute the radial basis function for thin-plate splines. + + Parameters + ---------- + r : (4, N) ndarray + Input array representing the Euclidean distance between each pair of + two collections of control points. + + Returns + ------- + U : (4, N) ndarray + Calculated kernel function U. + """ + _small = 1e-8 # Small value to avoid divide-by-zero + r_sq = r**2 + U = np.where(r == 0.0, 0.0, r_sq * np.log(r_sq + _small)) + return U diff --git a/lib/python3.10/site-packages/skimage/transform/_warps.py b/lib/python3.10/site-packages/skimage/transform/_warps.py new file mode 100644 index 0000000000000000000000000000000000000000..d7b38efdb1402781be4630db82f0a89ed6c6f0d7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/_warps.py @@ -0,0 +1,1388 @@ +import numpy as np +from scipy import ndimage as ndi + +from ._geometric import SimilarityTransform, AffineTransform, ProjectiveTransform +from ._warps_cy import _warp_fast +from ..measure import block_reduce + +from .._shared.utils import ( + get_bound_method_class, + safe_as_int, + warn, + convert_to_float, + _to_ndimage_mode, + _validate_interpolation_order, + channel_as_last_axis, +) + +HOMOGRAPHY_TRANSFORMS = (SimilarityTransform, AffineTransform, ProjectiveTransform) + + +def _preprocess_resize_output_shape(image, output_shape): + """Validate resize output shape according to input image. + + Parameters + ---------- + image: ndarray + Image to be resized. + output_shape: iterable + Size of the generated output image `(rows, cols[, ...][, dim])`. If + `dim` is not provided, the number of channels is preserved. + + Returns + ------- + image: ndarray + The input image, but with additional singleton dimensions appended in + the case where ``len(output_shape) > input.ndim``. + output_shape: tuple + The output image converted to tuple. + + Raises + ------ + ValueError: + If output_shape length is smaller than the image number of + dimensions + + Notes + ----- + The input image is reshaped if its number of dimensions is not + equal to output_shape_length. + + """ + output_shape = tuple(output_shape) + output_ndim = len(output_shape) + input_shape = image.shape + if output_ndim > image.ndim: + # append dimensions to input_shape + input_shape += (1,) * (output_ndim - image.ndim) + image = np.reshape(image, input_shape) + elif output_ndim == image.ndim - 1: + # multichannel case: append shape of last axis + output_shape = output_shape + (image.shape[-1],) + elif output_ndim < image.ndim: + raise ValueError( + "output_shape length cannot be smaller than the " + "image number of dimensions" + ) + + return image, output_shape + + +def resize( + image, + output_shape, + order=None, + mode='reflect', + cval=0, + clip=True, + preserve_range=False, + anti_aliasing=None, + anti_aliasing_sigma=None, +): + """Resize image to match a certain size. + + Performs interpolation to up-size or down-size N-dimensional images. Note + that anti-aliasing should be enabled when down-sizing images to avoid + aliasing artifacts. For downsampling with an integer factor also see + `skimage.transform.downscale_local_mean`. + + Parameters + ---------- + image : ndarray + Input image. + output_shape : iterable + Size of the generated output image `(rows, cols[, ...][, dim])`. If + `dim` is not provided, the number of channels is preserved. In case the + number of input channels does not equal the number of output channels a + n-dimensional interpolation is applied. + + Returns + ------- + resized : ndarray + Resized version of the input. + + Other parameters + ---------------- + order : int, optional + The order of the spline interpolation, default is 0 if + image.dtype is bool and 1 otherwise. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional + Points outside the boundaries of the input are filled according + to the given mode. Modes match the behaviour of `numpy.pad`. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + anti_aliasing : bool, optional + Whether to apply a Gaussian filter to smooth the image prior + to downsampling. It is crucial to filter when downsampling + the image to avoid aliasing artifacts. If not specified, it is set to + True when downsampling an image whose data type is not bool. + It is also set to False when using nearest neighbor interpolation + (``order`` == 0) with integer input data type. + anti_aliasing_sigma : {float, tuple of floats}, optional + Standard deviation for Gaussian filtering used when anti-aliasing. + By default, this value is chosen as (s - 1) / 2 where s is the + downsampling factor, where s > 1. For the up-size case, s < 1, no + anti-aliasing is performed prior to rescaling. + + Notes + ----- + Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge + pixels are duplicated during the reflection. As an example, if an array + has values [0, 1, 2] and was padded to the right by four values using + symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it + would be [0, 1, 2, 1, 0, 1, 2]. + + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import resize + >>> image = data.camera() + >>> resize(image, (100, 100)).shape + (100, 100) + + """ + + image, output_shape = _preprocess_resize_output_shape(image, output_shape) + input_shape = image.shape + input_type = image.dtype + + if input_type == np.float16: + image = image.astype(np.float32) + + if anti_aliasing is None: + anti_aliasing = ( + not input_type == bool + and not (np.issubdtype(input_type, np.integer) and order == 0) + and any(x < y for x, y in zip(output_shape, input_shape)) + ) + + if input_type == bool and anti_aliasing: + raise ValueError("anti_aliasing must be False for boolean images") + + factors = np.divide(input_shape, output_shape) + order = _validate_interpolation_order(input_type, order) + if order > 0: + image = convert_to_float(image, preserve_range) + + # Translate modes used by np.pad to those used by scipy.ndimage + ndi_mode = _to_ndimage_mode(mode) + if anti_aliasing: + if anti_aliasing_sigma is None: + anti_aliasing_sigma = np.maximum(0, (factors - 1) / 2) + else: + anti_aliasing_sigma = np.atleast_1d(anti_aliasing_sigma) * np.ones_like( + factors + ) + if np.any(anti_aliasing_sigma < 0): + raise ValueError( + "Anti-aliasing standard deviation must be " + "greater than or equal to zero" + ) + elif np.any((anti_aliasing_sigma > 0) & (factors <= 1)): + warn( + "Anti-aliasing standard deviation greater than zero but " + "not down-sampling along all axes" + ) + filtered = ndi.gaussian_filter( + image, anti_aliasing_sigma, cval=cval, mode=ndi_mode + ) + else: + filtered = image + + zoom_factors = [1 / f for f in factors] + out = ndi.zoom( + filtered, zoom_factors, order=order, mode=ndi_mode, cval=cval, grid_mode=True + ) + + _clip_warp_output(image, out, mode, cval, clip) + + return out + + +@channel_as_last_axis() +def rescale( + image, + scale, + order=None, + mode='reflect', + cval=0, + clip=True, + preserve_range=False, + anti_aliasing=None, + anti_aliasing_sigma=None, + *, + channel_axis=None, +): + """Scale image by a certain factor. + + Performs interpolation to up-scale or down-scale N-dimensional images. + Note that anti-aliasing should be enabled when down-sizing images to avoid + aliasing artifacts. For down-sampling with an integer factor also see + `skimage.transform.downscale_local_mean`. + + Parameters + ---------- + image : (M, N[, ...][, C]) ndarray + Input image. + scale : {float, tuple of floats} + Scale factors for spatial dimensions. Separate scale factors can be defined as + (m, n[, ...]). + + Returns + ------- + scaled : ndarray + Scaled version of the input. + + Other parameters + ---------------- + order : int, optional + The order of the spline interpolation, default is 0 if + image.dtype is bool and 1 otherwise. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional + Points outside the boundaries of the input are filled according + to the given mode. Modes match the behaviour of `numpy.pad`. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + anti_aliasing : bool, optional + Whether to apply a Gaussian filter to smooth the image prior + to down-scaling. It is crucial to filter when down-sampling + the image to avoid aliasing artifacts. If input image data + type is bool, no anti-aliasing is applied. + anti_aliasing_sigma : {float, tuple of floats}, optional + Standard deviation for Gaussian filtering to avoid aliasing artifacts. + By default, this value is chosen as (s - 1) / 2 where s is the + down-scaling factor. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Notes + ----- + Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge + pixels are duplicated during the reflection. As an example, if an array + has values [0, 1, 2] and was padded to the right by four values using + symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it + would be [0, 1, 2, 1, 0, 1, 2]. + + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import rescale + >>> image = data.camera() + >>> rescale(image, 0.1).shape + (51, 51) + >>> rescale(image, 0.5).shape + (256, 256) + + """ + scale = np.atleast_1d(scale) + multichannel = channel_axis is not None + if len(scale) > 1: + if (not multichannel and len(scale) != image.ndim) or ( + multichannel and len(scale) != image.ndim - 1 + ): + raise ValueError("Supply a single scale, or one value per spatial " "axis") + if multichannel: + scale = np.concatenate((scale, [1])) + orig_shape = np.asarray(image.shape) + output_shape = np.maximum(np.round(scale * orig_shape), 1) + if multichannel: # don't scale channel dimension + output_shape[-1] = orig_shape[-1] + + return resize( + image, + output_shape, + order=order, + mode=mode, + cval=cval, + clip=clip, + preserve_range=preserve_range, + anti_aliasing=anti_aliasing, + anti_aliasing_sigma=anti_aliasing_sigma, + ) + + +def rotate( + image, + angle, + resize=False, + center=None, + order=None, + mode='constant', + cval=0, + clip=True, + preserve_range=False, +): + """Rotate image by a certain angle around its center. + + Parameters + ---------- + image : ndarray + Input image. + angle : float + Rotation angle in degrees in counter-clockwise direction. + resize : bool, optional + Determine whether the shape of the output image will be automatically + calculated, so the complete rotated image exactly fits. Default is + False. + center : iterable of length 2 + The rotation center. If ``center=None``, the image is rotated around + its center, i.e. ``center=(cols / 2 - 0.5, rows / 2 - 0.5)``. Please + note that this parameter is (cols, rows), contrary to normal skimage + ordering. + + Returns + ------- + rotated : ndarray + Rotated version of the input. + + Other parameters + ---------------- + order : int, optional + The order of the spline interpolation, default is 0 if + image.dtype is bool and 1 otherwise. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional + Points outside the boundaries of the input are filled according + to the given mode. Modes match the behaviour of `numpy.pad`. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + + Notes + ----- + Modes 'reflect' and 'symmetric' are similar, but differ in whether the edge + pixels are duplicated during the reflection. As an example, if an array + has values [0, 1, 2] and was padded to the right by four values using + symmetric, the result would be [0, 1, 2, 2, 1, 0, 0], while for reflect it + would be [0, 1, 2, 1, 0, 1, 2]. + + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import rotate + >>> image = data.camera() + >>> rotate(image, 2).shape + (512, 512) + >>> rotate(image, 2, resize=True).shape + (530, 530) + >>> rotate(image, 90, resize=True).shape + (512, 512) + + """ + + rows, cols = image.shape[0], image.shape[1] + + if image.dtype == np.float16: + image = image.astype(np.float32) + + # rotation around center + if center is None: + center = np.array((cols, rows)) / 2.0 - 0.5 + else: + center = np.asarray(center) + tform1 = SimilarityTransform(translation=center) + tform2 = SimilarityTransform(rotation=np.deg2rad(angle)) + tform3 = SimilarityTransform(translation=-center) + tform = tform3 + tform2 + tform1 + + output_shape = None + if resize: + # determine shape of output image + corners = np.array([[0, 0], [0, rows - 1], [cols - 1, rows - 1], [cols - 1, 0]]) + corners = tform.inverse(corners) + minc = corners[:, 0].min() + minr = corners[:, 1].min() + maxc = corners[:, 0].max() + maxr = corners[:, 1].max() + out_rows = maxr - minr + 1 + out_cols = maxc - minc + 1 + output_shape = np.around((out_rows, out_cols)) + + # fit output image in new shape + translation = (minc, minr) + tform4 = SimilarityTransform(translation=translation) + tform = tform4 + tform + + # Make sure the transform is exactly affine, to ensure fast warping. + tform.params[2] = (0, 0, 1) + + return warp( + image, + tform, + output_shape=output_shape, + order=order, + mode=mode, + cval=cval, + clip=clip, + preserve_range=preserve_range, + ) + + +def downscale_local_mean(image, factors, cval=0, clip=True): + """Down-sample N-dimensional image by local averaging. + + The image is padded with `cval` if it is not perfectly divisible by the + integer factors. + + In contrast to interpolation in `skimage.transform.resize` and + `skimage.transform.rescale` this function calculates the local mean of + elements in each block of size `factors` in the input image. + + Parameters + ---------- + image : (M[, ...]) ndarray + Input image. + factors : array_like + Array containing down-sampling integer factor along each axis. + cval : float, optional + Constant padding value if image is not perfectly divisible by the + integer factors. + clip : bool, optional + Unused, but kept here for API consistency with the other transforms + in this module. (The local mean will never fall outside the range + of values in the input image, assuming the provided `cval` also + falls within that range.) + + Returns + ------- + image : ndarray + Down-sampled image with same number of dimensions as input image. + For integer inputs, the output dtype will be ``float64``. + See :func:`numpy.mean` for details. + + Examples + -------- + >>> a = np.arange(15).reshape(3, 5) + >>> a + array([[ 0, 1, 2, 3, 4], + [ 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14]]) + >>> downscale_local_mean(a, (2, 3)) + array([[3.5, 4. ], + [5.5, 4.5]]) + + """ + return block_reduce(image, factors, np.mean, cval) + + +def _swirl_mapping(xy, center, rotation, strength, radius): + x, y = xy.T + x0, y0 = center + rho = np.sqrt((x - x0) ** 2 + (y - y0) ** 2) + + # Ensure that the transformation decays to approximately 1/1000-th + # within the specified radius. + radius = radius / 5 * np.log(2) + + theta = rotation + strength * np.exp(-rho / radius) + np.arctan2(y - y0, x - x0) + + xy[..., 0] = x0 + rho * np.cos(theta) + xy[..., 1] = y0 + rho * np.sin(theta) + + return xy + + +def swirl( + image, + center=None, + strength=1, + radius=100, + rotation=0, + output_shape=None, + order=None, + mode='reflect', + cval=0, + clip=True, + preserve_range=False, +): + """Perform a swirl transformation. + + Parameters + ---------- + image : ndarray + Input image. + center : (column, row) tuple or (2,) ndarray, optional + Center coordinate of transformation. + strength : float, optional + The amount of swirling applied. + radius : float, optional + The extent of the swirl in pixels. The effect dies out + rapidly beyond `radius`. + rotation : float, optional + Additional rotation applied to the image. + + Returns + ------- + swirled : ndarray + Swirled version of the input. + + Other parameters + ---------------- + output_shape : tuple (rows, cols), optional + Shape of the output image generated. By default the shape of the input + image is preserved. + order : int, optional + The order of the spline interpolation, default is 0 if + image.dtype is bool and 1 otherwise. The order has to be in + the range 0-5. See `skimage.transform.warp` for detail. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional + Points outside the boundaries of the input are filled according + to the given mode, with 'reflect' used as the default. Modes match + the behaviour of `numpy.pad`. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + + """ + if center is None: + center = np.array(image.shape)[:2][::-1] / 2 + + warp_args = { + 'center': center, + 'rotation': rotation, + 'strength': strength, + 'radius': radius, + } + + return warp( + image, + _swirl_mapping, + map_args=warp_args, + output_shape=output_shape, + order=order, + mode=mode, + cval=cval, + clip=clip, + preserve_range=preserve_range, + ) + + +def _stackcopy(a, b): + """Copy b into each color layer of a, such that:: + + a[:,:,0] = a[:,:,1] = ... = b + + Parameters + ---------- + a : (M, N) or (M, N, P) ndarray + Target array. + b : (M, N) + Source array. + + Notes + ----- + Color images are stored as an ``(M, N, 3)`` or ``(M, N, 4)`` arrays. + + """ + if a.ndim == 3: + a[:] = b[:, :, np.newaxis] + else: + a[:] = b + + +def warp_coords(coord_map, shape, dtype=np.float64): + """Build the source coordinates for the output of a 2-D image warp. + + Parameters + ---------- + coord_map : callable like GeometricTransform.inverse + Return input coordinates for given output coordinates. + Coordinates are in the shape (P, 2), where P is the number + of coordinates and each element is a ``(row, col)`` pair. + shape : tuple + Shape of output image ``(rows, cols[, bands])``. + dtype : np.dtype or string + dtype for return value (sane choices: float32 or float64). + + Returns + ------- + coords : (ndim, rows, cols[, bands]) array of dtype `dtype` + Coordinates for `scipy.ndimage.map_coordinates`, that will yield + an image of shape (orows, ocols, bands) by drawing from source + points according to the `coord_transform_fn`. + + Notes + ----- + + This is a lower-level routine that produces the source coordinates for 2-D + images used by `warp()`. + + It is provided separately from `warp` to give additional flexibility to + users who would like, for example, to re-use a particular coordinate + mapping, to use specific dtypes at various points along the the + image-warping process, or to implement different post-processing logic + than `warp` performs after the call to `ndi.map_coordinates`. + + + Examples + -------- + Produce a coordinate map that shifts an image up and to the right: + + >>> from skimage import data + >>> from scipy.ndimage import map_coordinates + >>> + >>> def shift_up10_left20(xy): + ... return xy - np.array([-20, 10])[None, :] + >>> + >>> image = data.astronaut().astype(np.float32) + >>> coords = warp_coords(shift_up10_left20, image.shape) + >>> warped_image = map_coordinates(image, coords) + + """ + shape = safe_as_int(shape) + rows, cols = shape[0], shape[1] + coords_shape = [len(shape), rows, cols] + if len(shape) == 3: + coords_shape.append(shape[2]) + coords = np.empty(coords_shape, dtype=dtype) + + # Reshape grid coordinates into a (P, 2) array of (row, col) pairs + tf_coords = np.indices((cols, rows), dtype=dtype).reshape(2, -1).T + + # Map each (row, col) pair to the source image according to + # the user-provided mapping + tf_coords = coord_map(tf_coords) + + # Reshape back to a (2, M, N) coordinate grid + tf_coords = tf_coords.T.reshape((-1, cols, rows)).swapaxes(1, 2) + + # Place the y-coordinate mapping + _stackcopy(coords[1, ...], tf_coords[0, ...]) + + # Place the x-coordinate mapping + _stackcopy(coords[0, ...], tf_coords[1, ...]) + + if len(shape) == 3: + coords[2, ...] = range(shape[2]) + + return coords + + +def _clip_warp_output(input_image, output_image, mode, cval, clip): + """Clip output image to range of values of input image. + + Note that this function modifies the values of `output_image` in-place + and it is only modified if ``clip=True``. + + Parameters + ---------- + input_image : ndarray + Input image. + output_image : ndarray + Output image, which is modified in-place. + + Other parameters + ---------------- + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'} + Points outside the boundaries of the input are filled according + to the given mode. Modes match the behaviour of `numpy.pad`. + cval : float + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + + """ + if clip: + min_val = np.min(input_image) + if np.isnan(min_val): + # NaNs detected, use NaN-safe min/max + min_func = np.nanmin + max_func = np.nanmax + min_val = min_func(input_image) + else: + min_func = np.min + max_func = np.max + max_val = max_func(input_image) + + # Check if cval has been used such that it expands the effective input + # range + preserve_cval = ( + mode == 'constant' + and not min_val <= cval <= max_val + and min_func(output_image) <= cval <= max_func(output_image) + ) + + # expand min/max range to account for cval + if preserve_cval: + # cast cval to the same dtype as the input image + cval = input_image.dtype.type(cval) + min_val = min(min_val, cval) + max_val = max(max_val, cval) + + # Convert array-like types to ndarrays (gh-7159) + min_val, max_val = np.asarray(min_val), np.asarray(max_val) + np.clip(output_image, min_val, max_val, out=output_image) + + +def warp( + image, + inverse_map, + map_args=None, + output_shape=None, + order=None, + mode='constant', + cval=0.0, + clip=True, + preserve_range=False, +): + """Warp an image according to a given coordinate transformation. + + Parameters + ---------- + image : ndarray + Input image. + inverse_map : transformation object, callable ``cr = f(cr, **kwargs)``, or ndarray + Inverse coordinate map, which transforms coordinates in the output + images into their corresponding coordinates in the input image. + + There are a number of different options to define this map, depending + on the dimensionality of the input image. A 2-D image can have 2 + dimensions for gray-scale images, or 3 dimensions with color + information. + + - For 2-D images, you can directly pass a transformation object, + e.g. `skimage.transform.SimilarityTransform`, or its inverse. + - For 2-D images, you can pass a ``(3, 3)`` homogeneous + transformation matrix, e.g. + `skimage.transform.SimilarityTransform.params`. + - For 2-D images, a function that transforms a ``(M, 2)`` array of + ``(col, row)`` coordinates in the output image to their + corresponding coordinates in the input image. Extra parameters to + the function can be specified through `map_args`. + - For N-D images, you can directly pass an array of coordinates. + The first dimension specifies the coordinates in the input image, + while the subsequent dimensions determine the position in the + output image. E.g. in case of 2-D images, you need to pass an array + of shape ``(2, rows, cols)``, where `rows` and `cols` determine the + shape of the output image, and the first dimension contains the + ``(row, col)`` coordinate in the input image. + See `scipy.ndimage.map_coordinates` for further documentation. + + Note, that a ``(3, 3)`` matrix is interpreted as a homogeneous + transformation matrix, so you cannot interpolate values from a 3-D + input, if the output is of shape ``(3,)``. + + See example section for usage. + map_args : dict, optional + Keyword arguments passed to `inverse_map`. + output_shape : tuple (rows, cols), optional + Shape of the output image generated. By default the shape of the input + image is preserved. Note that, even for multi-band images, only rows + and columns need to be specified. + order : int, optional + The order of interpolation. The order has to be in the range 0-5: + - 0: Nearest-neighbor + - 1: Bi-linear (default) + - 2: Bi-quadratic + - 3: Bi-cubic + - 4: Bi-quartic + - 5: Bi-quintic + + Default is 0 if image.dtype is bool and 1 otherwise. + mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional + Points outside the boundaries of the input are filled according + to the given mode. Modes match the behaviour of `numpy.pad`. + cval : float, optional + Used in conjunction with mode 'constant', the value outside + the image boundaries. + clip : bool, optional + Whether to clip the output to the range of values of the input image. + This is enabled by default, since higher order interpolation may + produce values outside the given input range. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + + Returns + ------- + warped : double ndarray + The warped input image. + + Notes + ----- + - The input image is converted to a `double` image. + - In case of a `SimilarityTransform`, `AffineTransform` and + `ProjectiveTransform` and `order` in [0, 3] this function uses the + underlying transformation matrix to warp the image with a much faster + routine. + + Examples + -------- + >>> from skimage.transform import warp + >>> from skimage import data + >>> image = data.camera() + + The following image warps are all equal but differ substantially in + execution time. The image is shifted to the bottom. + + Use a geometric transform to warp an image (fast): + + >>> from skimage.transform import SimilarityTransform + >>> tform = SimilarityTransform(translation=(0, -10)) + >>> warped = warp(image, tform) + + Use a callable (slow): + + >>> def shift_down(xy): + ... xy[:, 1] -= 10 + ... return xy + >>> warped = warp(image, shift_down) + + Use a transformation matrix to warp an image (fast): + + >>> matrix = np.array([[1, 0, 0], [0, 1, -10], [0, 0, 1]]) + >>> warped = warp(image, matrix) + >>> from skimage.transform import ProjectiveTransform + >>> warped = warp(image, ProjectiveTransform(matrix=matrix)) + + You can also use the inverse of a geometric transformation (fast): + + >>> warped = warp(image, tform.inverse) + + For N-D images you can pass a coordinate array, that specifies the + coordinates in the input image for every element in the output image. E.g. + if you want to rescale a 3-D cube, you can do: + + >>> cube_shape = np.array([30, 30, 30]) + >>> rng = np.random.default_rng() + >>> cube = rng.random(cube_shape) + + Setup the coordinate array, that defines the scaling: + + >>> scale = 0.1 + >>> output_shape = (scale * cube_shape).astype(int) + >>> coords0, coords1, coords2 = np.mgrid[:output_shape[0], + ... :output_shape[1], :output_shape[2]] + >>> coords = np.array([coords0, coords1, coords2]) + + Assume that the cube contains spatial data, where the first array element + center is at coordinate (0.5, 0.5, 0.5) in real space, i.e. we have to + account for this extra offset when scaling the image: + + >>> coords = (coords + 0.5) / scale - 0.5 + >>> warped = warp(cube, coords) + + """ + if map_args is None: + map_args = {} + + if image.size == 0: + raise ValueError("Cannot warp empty image with dimensions", image.shape) + + order = _validate_interpolation_order(image.dtype, order) + + if order > 0: + image = convert_to_float(image, preserve_range) + if image.dtype == np.float16: + image = image.astype(np.float32) + + input_shape = np.array(image.shape) + + if output_shape is None: + output_shape = input_shape + else: + output_shape = safe_as_int(output_shape) + + warped = None + + if order == 2: + # When fixing this issue, make sure to fix the branches further + # below in this function + warn( + "Bi-quadratic interpolation behavior has changed due " + "to a bug in the implementation of scikit-image. " + "The new version now serves as a wrapper " + "around SciPy's interpolation functions, which itself " + "is not verified to be a correct implementation. Until " + "skimage's implementation is fixed, we recommend " + "to use bi-linear or bi-cubic interpolation instead." + ) + + if order in (1, 3) and not map_args: + # use fast Cython version for specific interpolation orders and input + + matrix = None + + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + # inverse_map is a transformation matrix as numpy array + matrix = inverse_map + + elif isinstance(inverse_map, HOMOGRAPHY_TRANSFORMS): + # inverse_map is a homography + matrix = inverse_map.params + + elif ( + hasattr(inverse_map, '__name__') + and inverse_map.__name__ == 'inverse' + and get_bound_method_class(inverse_map) in HOMOGRAPHY_TRANSFORMS + ): + # inverse_map is the inverse of a homography + matrix = np.linalg.inv(inverse_map.__self__.params) + + if matrix is not None: + matrix = matrix.astype(image.dtype) + ctype = 'float32_t' if image.dtype == np.float32 else 'float64_t' + if image.ndim == 2: + warped = _warp_fast[ctype]( + image, + matrix, + output_shape=output_shape, + order=order, + mode=mode, + cval=cval, + ) + elif image.ndim == 3: + dims = [] + for dim in range(image.shape[2]): + dims.append( + _warp_fast[ctype]( + image[..., dim], + matrix, + output_shape=output_shape, + order=order, + mode=mode, + cval=cval, + ) + ) + warped = np.dstack(dims) + + if warped is None: + # use ndi.map_coordinates + + if isinstance(inverse_map, np.ndarray) and inverse_map.shape == (3, 3): + # inverse_map is a transformation matrix as numpy array, + # this is only used for order >= 4. + inverse_map = ProjectiveTransform(matrix=inverse_map) + + if isinstance(inverse_map, np.ndarray): + # inverse_map is directly given as coordinates + coords = inverse_map + else: + # inverse_map is given as function, that transforms (N, 2) + # destination coordinates to their corresponding source + # coordinates. This is only supported for 2(+1)-D images. + + if image.ndim < 2 or image.ndim > 3: + raise ValueError( + "Only 2-D images (grayscale or color) are " + "supported, when providing a callable " + "`inverse_map`." + ) + + def coord_map(*args): + return inverse_map(*args, **map_args) + + if len(input_shape) == 3 and len(output_shape) == 2: + # Input image is 2D and has color channel, but output_shape is + # given for 2-D images. Automatically add the color channel + # dimensionality. + output_shape = (output_shape[0], output_shape[1], input_shape[2]) + + coords = warp_coords(coord_map, output_shape) + + # Pre-filtering not necessary for order 0, 1 interpolation + prefilter = order > 1 + + ndi_mode = _to_ndimage_mode(mode) + warped = ndi.map_coordinates( + image, coords, prefilter=prefilter, mode=ndi_mode, order=order, cval=cval + ) + + _clip_warp_output(image, warped, mode, cval, clip) + + return warped + + +def _linear_polar_mapping(output_coords, k_angle, k_radius, center): + """Inverse mapping function to convert from cartesian to polar coordinates + + Parameters + ---------- + output_coords : (M, 2) ndarray + Array of `(col, row)` coordinates in the output image. + k_angle : float + Scaling factor that relates the intended number of rows in the output + image to angle: ``k_angle = nrows / (2 * np.pi)``. + k_radius : float + Scaling factor that relates the radius of the circle bounding the + area to be transformed to the intended number of columns in the output + image: ``k_radius = ncols / radius``. + center : tuple (row, col) + Coordinates that represent the center of the circle that bounds the + area to be transformed in an input image. + + Returns + ------- + coords : (M, 2) ndarray + Array of `(col, row)` coordinates in the input image that + correspond to the `output_coords` given as input. + """ + angle = output_coords[:, 1] / k_angle + rr = ((output_coords[:, 0] / k_radius) * np.sin(angle)) + center[0] + cc = ((output_coords[:, 0] / k_radius) * np.cos(angle)) + center[1] + coords = np.column_stack((cc, rr)) + return coords + + +def _log_polar_mapping(output_coords, k_angle, k_radius, center): + """Inverse mapping function to convert from cartesian to polar coordinates + + Parameters + ---------- + output_coords : (M, 2) ndarray + Array of `(col, row)` coordinates in the output image. + k_angle : float + Scaling factor that relates the intended number of rows in the output + image to angle: ``k_angle = nrows / (2 * np.pi)``. + k_radius : float + Scaling factor that relates the radius of the circle bounding the + area to be transformed to the intended number of columns in the output + image: ``k_radius = width / np.log(radius)``. + center : 2-tuple + `(row, col)` coordinates that represent the center of the circle that bounds the + area to be transformed in an input image. + + Returns + ------- + coords : ndarray, shape (M, 2) + Array of `(col, row)` coordinates in the input image that + correspond to the `output_coords` given as input. + """ + angle = output_coords[:, 1] / k_angle + rr = ((np.exp(output_coords[:, 0] / k_radius)) * np.sin(angle)) + center[0] + cc = ((np.exp(output_coords[:, 0] / k_radius)) * np.cos(angle)) + center[1] + coords = np.column_stack((cc, rr)) + return coords + + +@channel_as_last_axis() +def warp_polar( + image, + center=None, + *, + radius=None, + output_shape=None, + scaling='linear', + channel_axis=None, + **kwargs, +): + """Remap image to polar or log-polar coordinates space. + + Parameters + ---------- + image : (M, N[, C]) ndarray + Input image. For multichannel images `channel_axis` has to be specified. + center : 2-tuple, optional + `(row, col)` coordinates of the point in `image` that represents the center of + the transformation (i.e., the origin in Cartesian space). Values can be of + type `float`. If no value is given, the center is assumed to be the center point + of `image`. + radius : float, optional + Radius of the circle that bounds the area to be transformed. + output_shape : tuple (row, col), optional + scaling : {'linear', 'log'}, optional + Specify whether the image warp is polar or log-polar. Defaults to + 'linear'. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + **kwargs : keyword arguments + Passed to `transform.warp`. + + Returns + ------- + warped : ndarray + The polar or log-polar warped image. + + Examples + -------- + Perform a basic polar warp on a grayscale image: + + >>> from skimage import data + >>> from skimage.transform import warp_polar + >>> image = data.checkerboard() + >>> warped = warp_polar(image) + + Perform a log-polar warp on a grayscale image: + + >>> warped = warp_polar(image, scaling='log') + + Perform a log-polar warp on a grayscale image while specifying center, + radius, and output shape: + + >>> warped = warp_polar(image, (100,100), radius=100, + ... output_shape=image.shape, scaling='log') + + Perform a log-polar warp on a color image: + + >>> image = data.astronaut() + >>> warped = warp_polar(image, scaling='log', channel_axis=-1) + """ + multichannel = channel_axis is not None + if image.ndim != 2 and not multichannel: + raise ValueError( + f'Input array must be 2-dimensional when ' + f'`channel_axis=None`, got {image.ndim}' + ) + + if image.ndim != 3 and multichannel: + raise ValueError( + f'Input array must be 3-dimensional when ' + f'`channel_axis` is specified, got {image.ndim}' + ) + + if center is None: + center = (np.array(image.shape)[:2] / 2) - 0.5 + + if radius is None: + w, h = np.array(image.shape)[:2] / 2 + radius = np.sqrt(w**2 + h**2) + + if output_shape is None: + height = 360 + width = int(np.ceil(radius)) + output_shape = (height, width) + else: + output_shape = safe_as_int(output_shape) + height = output_shape[0] + width = output_shape[1] + + if scaling == 'linear': + k_radius = width / radius + map_func = _linear_polar_mapping + elif scaling == 'log': + k_radius = width / np.log(radius) + map_func = _log_polar_mapping + else: + raise ValueError("Scaling value must be in {'linear', 'log'}") + + k_angle = height / (2 * np.pi) + warp_args = {'k_angle': k_angle, 'k_radius': k_radius, 'center': center} + + warped = warp( + image, map_func, map_args=warp_args, output_shape=output_shape, **kwargs + ) + + return warped + + +def _local_mean_weights(old_size, new_size, grid_mode, dtype): + """Create a 2D weight matrix for resizing with the local mean. + + Parameters + ---------- + old_size: int + Old size. + new_size: int + New size. + grid_mode : bool + Whether to use grid data model of pixel/voxel model for + average weights computation. + dtype: dtype + Output array data type. + + Returns + ------- + weights: (new_size, old_size) array + Rows sum to 1. + + """ + if grid_mode: + old_breaks = np.linspace(0, old_size, num=old_size + 1, dtype=dtype) + new_breaks = np.linspace(0, old_size, num=new_size + 1, dtype=dtype) + else: + old, new = old_size - 1, new_size - 1 + old_breaks = np.pad( + np.linspace(0.5, old - 0.5, old, dtype=dtype), + 1, + 'constant', + constant_values=(0, old), + ) + if new == 0: + val = np.inf + else: + val = 0.5 * old / new + new_breaks = np.pad( + np.linspace(val, old - val, new, dtype=dtype), + 1, + 'constant', + constant_values=(0, old), + ) + + upper = np.minimum(new_breaks[1:, np.newaxis], old_breaks[np.newaxis, 1:]) + lower = np.maximum(new_breaks[:-1, np.newaxis], old_breaks[np.newaxis, :-1]) + + weights = np.maximum(upper - lower, 0) + weights /= weights.sum(axis=1, keepdims=True) + + return weights + + +def resize_local_mean( + image, output_shape, grid_mode=True, preserve_range=False, *, channel_axis=None +): + """Resize an array with the local mean / bilinear scaling. + + Parameters + ---------- + image : ndarray + Input image. If this is a multichannel image, the axis corresponding + to channels should be specified using `channel_axis`. + output_shape : iterable + Size of the generated output image. When `channel_axis` is not None, + the `channel_axis` should either be omitted from `output_shape` or the + ``output_shape[channel_axis]`` must match + ``image.shape[channel_axis]``. If the length of `output_shape` exceeds + image.ndim, additional singleton dimensions will be appended to the + input ``image`` as needed. + grid_mode : bool, optional + Defines ``image`` pixels position: if True, pixels are assumed to be at + grid intersections, otherwise at cell centers. As a consequence, + for example, a 1d signal of length 5 is considered to have length 4 + when `grid_mode` is False, but length 5 when `grid_mode` is True. See + the following visual illustration: + + .. code-block:: text + + | pixel 1 | pixel 2 | pixel 3 | pixel 4 | pixel 5 | + |<-------------------------------------->| + vs. + |<----------------------------------------------->| + + The starting point of the arrow in the diagram above corresponds to + coordinate location 0 in each mode. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see + https://scikit-image.org/docs/dev/user_guide/data_types.html + + Returns + ------- + resized : ndarray + Resized version of the input. + + See Also + -------- + resize, downscale_local_mean + + Notes + ----- + This method is sometimes referred to as "area-based" interpolation or + "pixel mixing" interpolation [1]_. When `grid_mode` is True, it is + equivalent to using OpenCV's resize with `INTER_AREA` interpolation mode. + It is commonly used for image downsizing. If the downsizing factors are + integers, then `downscale_local_mean` should be preferred instead. + + References + ---------- + .. [1] http://entropymine.com/imageworsener/pixelmixing/ + + Examples + -------- + >>> from skimage import data + >>> from skimage.transform import resize_local_mean + >>> image = data.camera() + >>> resize_local_mean(image, (100, 100)).shape + (100, 100) + + """ + if channel_axis is not None: + if channel_axis < -image.ndim or channel_axis >= image.ndim: + raise ValueError("invalid channel_axis") + + # move channels to last position + image = np.moveaxis(image, channel_axis, -1) + nc = image.shape[-1] + + output_ndim = len(output_shape) + if output_ndim == image.ndim - 1: + # insert channels dimension at the end + output_shape = output_shape + (nc,) + elif output_ndim == image.ndim: + if output_shape[channel_axis] != nc: + raise ValueError( + "Cannot reshape along the channel_axis. Use " + "channel_axis=None to reshape along all axes." + ) + # move channels to last position in output_shape + channel_axis = channel_axis % image.ndim + output_shape = ( + output_shape[:channel_axis] + output_shape[channel_axis:] + (nc,) + ) + else: + raise ValueError( + "len(output_shape) must be image.ndim or (image.ndim - 1) " + "when a channel_axis is specified." + ) + resized = image + else: + resized, output_shape = _preprocess_resize_output_shape(image, output_shape) + resized = convert_to_float(resized, preserve_range) + dtype = resized.dtype + + for axis, (old_size, new_size) in enumerate(zip(image.shape, output_shape)): + if old_size == new_size: + continue + weights = _local_mean_weights(old_size, new_size, grid_mode, dtype) + product = np.tensordot(resized, weights, [[axis], [-1]]) + resized = np.moveaxis(product, -1, axis) + + if channel_axis is not None: + # restore channels to original axis + resized = np.moveaxis(resized, -1, channel_axis) + + return resized diff --git a/lib/python3.10/site-packages/skimage/transform/finite_radon_transform.py b/lib/python3.10/site-packages/skimage/transform/finite_radon_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..a04a0bad568e45abad2082ff049fdac994023b00 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/finite_radon_transform.py @@ -0,0 +1,132 @@ +""" +:author: Gary Ruben, 2009 +:license: modified BSD +""" + +__all__ = ["frt2", "ifrt2"] + +import numpy as np +from numpy import roll, newaxis + + +def frt2(a): + """Compute the 2-dimensional finite Radon transform (FRT) for the input array. + + Parameters + ---------- + a : ndarray of int, shape (M, M) + Input array. + + Returns + ------- + FRT : ndarray of int, shape (M+1, M) + Finite Radon Transform array of coefficients. + + See Also + -------- + ifrt2 : The two-dimensional inverse FRT. + + Notes + ----- + The FRT has a unique inverse if and only if M is prime. [FRT] + The idea for this algorithm is due to Vlad Negnevitski. + + Examples + -------- + + Generate a test image: + Use a prime number for the array dimensions + + >>> SIZE = 59 + >>> img = np.tri(SIZE, dtype=np.int32) + + Apply the Finite Radon Transform: + + >>> f = frt2(img) + + References + ---------- + .. [FRT] A. Kingston and I. Svalbe, "Projective transforms on periodic + discrete image arrays," in P. Hawkes (Ed), Advances in Imaging + and Electron Physics, 139 (2006) + + """ + if a.ndim != 2 or a.shape[0] != a.shape[1]: + raise ValueError("Input must be a square, 2-D array") + + ai = a.copy() + n = ai.shape[0] + f = np.empty((n + 1, n), np.uint32) + f[0] = ai.sum(axis=0) + for m in range(1, n): + # Roll the pth row of ai left by p places + for row in range(1, n): + ai[row] = roll(ai[row], -row) + f[m] = ai.sum(axis=0) + f[n] = ai.sum(axis=1) + return f + + +def ifrt2(a): + """Compute the 2-dimensional inverse finite Radon transform (iFRT) for the input array. + + Parameters + ---------- + a : ndarray of int, shape (M+1, M) + Input array. + + Returns + ------- + iFRT : ndarray of int, shape (M, M) + Inverse Finite Radon Transform coefficients. + + See Also + -------- + frt2 : The two-dimensional FRT + + Notes + ----- + The FRT has a unique inverse if and only if M is prime. + See [1]_ for an overview. + The idea for this algorithm is due to Vlad Negnevitski. + + Examples + -------- + + >>> SIZE = 59 + >>> img = np.tri(SIZE, dtype=np.int32) + + Apply the Finite Radon Transform: + + >>> f = frt2(img) + + Apply the Inverse Finite Radon Transform to recover the input + + >>> fi = ifrt2(f) + + Check that it's identical to the original + + >>> assert len(np.nonzero(img-fi)[0]) == 0 + + References + ---------- + .. [1] A. Kingston and I. Svalbe, "Projective transforms on periodic + discrete image arrays," in P. Hawkes (Ed), Advances in Imaging + and Electron Physics, 139 (2006) + + """ + if a.ndim != 2 or a.shape[0] != a.shape[1] + 1: + raise ValueError("Input must be an (n+1) row x n column, 2-D array") + + ai = a.copy()[:-1] + n = ai.shape[1] + f = np.empty((n, n), np.uint32) + f[0] = ai.sum(axis=0) + for m in range(1, n): + # Rolls the pth row of ai right by p places. + for row in range(1, ai.shape[0]): + ai[row] = roll(ai[row], row) + f[m] = ai.sum(axis=0) + f += a[-1][newaxis].T + f = (f - ai[0].sum()) / n + return f diff --git a/lib/python3.10/site-packages/skimage/transform/hough_transform.py b/lib/python3.10/site-packages/skimage/transform/hough_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..80c5f49af1e969b218a2521435f8e71a656283e6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/hough_transform.py @@ -0,0 +1,473 @@ +import numpy as np +from scipy.spatial import cKDTree + +from ._hough_transform import _hough_circle, _hough_ellipse, _hough_line +from ._hough_transform import _probabilistic_hough_line as _prob_hough_line + + +def hough_line_peaks( + hspace, + angles, + dists, + min_distance=9, + min_angle=10, + threshold=None, + num_peaks=np.inf, +): + """Return peaks in a straight line Hough transform. + + Identifies most prominent lines separated by a certain angle and distance + in a Hough transform. Non-maximum suppression with different sizes is + applied separately in the first (distances) and second (angles) dimension + of the Hough space to identify peaks. + + Parameters + ---------- + hspace : ndarray, shape (M, N) + Hough space returned by the `hough_line` function. + angles : array, shape (N,) + Angles returned by the `hough_line` function. Assumed to be continuous. + (`angles[-1] - angles[0] == PI`). + dists : array, shape (M,) + Distances returned by the `hough_line` function. + min_distance : int, optional + Minimum distance separating lines (maximum filter size for first + dimension of hough space). + min_angle : int, optional + Minimum angle separating lines (maximum filter size for second + dimension of hough space). + threshold : float, optional + Minimum intensity of peaks. Default is `0.5 * max(hspace)`. + num_peaks : int, optional + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` coordinates based on peak intensity. + + Returns + ------- + accum, angles, dists : tuple of array + Peak values in Hough space, angles and distances. + + Examples + -------- + >>> from skimage.transform import hough_line, hough_line_peaks + >>> from skimage.draw import line + >>> img = np.zeros((15, 15), dtype=bool) + >>> rr, cc = line(0, 0, 14, 14) + >>> img[rr, cc] = 1 + >>> rr, cc = line(0, 14, 14, 0) + >>> img[cc, rr] = 1 + >>> hspace, angles, dists = hough_line(img) + >>> hspace, angles, dists = hough_line_peaks(hspace, angles, dists) + >>> len(angles) + 2 + + """ + from ..feature.peak import _prominent_peaks + + min_angle = min(min_angle, hspace.shape[1]) + h, a, d = _prominent_peaks( + hspace, + min_xdistance=min_angle, + min_ydistance=min_distance, + threshold=threshold, + num_peaks=num_peaks, + ) + if a.size > 0: + return (h, angles[a], dists[d]) + else: + return (h, np.array([]), np.array([])) + + +def hough_circle(image, radius, normalize=True, full_output=False): + """Perform a circular Hough transform. + + Parameters + ---------- + image : ndarray, shape (M, N) + Input image with nonzero values representing edges. + radius : scalar or sequence of scalars + Radii at which to compute the Hough transform. + Floats are converted to integers. + normalize : boolean, optional + Normalize the accumulator with the number + of pixels used to draw the radius. + full_output : boolean, optional + Extend the output size by twice the largest + radius in order to detect centers outside the + input picture. + + Returns + ------- + H : ndarray, shape (radius index, M + 2R, N + 2R) + Hough transform accumulator for each radius. + R designates the larger radius if full_output is True. + Otherwise, R = 0. + + Examples + -------- + >>> from skimage.transform import hough_circle + >>> from skimage.draw import circle_perimeter + >>> img = np.zeros((100, 100), dtype=bool) + >>> rr, cc = circle_perimeter(25, 35, 23) + >>> img[rr, cc] = 1 + >>> try_radii = np.arange(5, 50) + >>> res = hough_circle(img, try_radii) + >>> ridx, r, c = np.unravel_index(np.argmax(res), res.shape) + >>> r, c, try_radii[ridx] + (25, 35, 23) + + """ + radius = np.atleast_1d(np.asarray(radius)) + return _hough_circle( + image, radius.astype(np.intp), normalize=normalize, full_output=full_output + ) + + +def hough_ellipse(image, threshold=4, accuracy=1, min_size=4, max_size=None): + """Perform an elliptical Hough transform. + + Parameters + ---------- + image : (M, N) ndarray + Input image with nonzero values representing edges. + threshold : int, optional + Accumulator threshold value. A lower value will return more ellipses. + accuracy : double, optional + Bin size on the minor axis used in the accumulator. A higher value + will return more ellipses, but lead to a less precise estimation of + the minor axis lengths. + min_size : int, optional + Minimal major axis length. + max_size : int, optional + Maximal minor axis length. + If None, the value is set to half of the smaller + image dimension. + + Returns + ------- + result : ndarray with fields [(accumulator, yc, xc, a, b, orientation)]. + Where ``(yc, xc)`` is the center, ``(a, b)`` the major and minor + axes, respectively. The `orientation` value follows the + `skimage.draw.ellipse_perimeter` convention. + + Examples + -------- + >>> from skimage.transform import hough_ellipse + >>> from skimage.draw import ellipse_perimeter + >>> img = np.zeros((25, 25), dtype=np.uint8) + >>> rr, cc = ellipse_perimeter(10, 10, 6, 8) + >>> img[cc, rr] = 1 + >>> result = hough_ellipse(img, threshold=8) + >>> result.tolist() + [(10, 10.0, 10.0, 8.0, 6.0, 0.0)] + + Notes + ----- + Potential ellipses in the image are characterized by their major and + minor axis lengths. For any pair of nonzero pixels in the image that + are at least half of `min_size` apart, an accumulator keeps track of + the minor axis lengths of potential ellipses formed with all the + other nonzero pixels. If any bin (with `bin_size = accuracy * accuracy`) + in the histogram of those accumulated minor axis lengths is above + `threshold`, the corresponding ellipse is added to the results. + + A higher `accuracy` will therefore lead to more ellipses being found + in the image, at the cost of a less precise estimation of the minor + axis length. + + References + ---------- + .. [1] Xie, Yonghong, and Qiang Ji. "A new efficient ellipse detection + method." Pattern Recognition, 2002. Proceedings. 16th International + Conference on. Vol. 2. IEEE, 2002 + """ + return _hough_ellipse( + image, + threshold=threshold, + accuracy=accuracy, + min_size=min_size, + max_size=max_size, + ) + + +def hough_line(image, theta=None): + """Perform a straight line Hough transform. + + Parameters + ---------- + image : (M, N) ndarray + Input image with nonzero values representing edges. + theta : ndarray of double, shape (K,), optional + Angles at which to compute the transform, in radians. + Defaults to a vector of 180 angles evenly spaced in the + range [-pi/2, pi/2). + + Returns + ------- + hspace : ndarray of uint64, shape (P, Q) + Hough transform accumulator. + angles : ndarray + Angles at which the transform is computed, in radians. + distances : ndarray + Distance values. + + Notes + ----- + The origin is the top left corner of the original image. + X and Y axis are horizontal and vertical edges respectively. + The distance is the minimal algebraic distance from the origin + to the detected line. + The angle accuracy can be improved by decreasing the step size in + the `theta` array. + + Examples + -------- + Generate a test image: + + >>> img = np.zeros((100, 150), dtype=bool) + >>> img[30, :] = 1 + >>> img[:, 65] = 1 + >>> img[35:45, 35:50] = 1 + >>> for i in range(90): + ... img[i, i] = 1 + >>> rng = np.random.default_rng() + >>> img += rng.random(img.shape) > 0.95 + + Apply the Hough transform: + + >>> out, angles, d = hough_line(img) + """ + if image.ndim != 2: + raise ValueError('The input image `image` must be 2D.') + + if theta is None: + # These values are approximations of pi/2 + theta = np.linspace(-np.pi / 2, np.pi / 2, 180, endpoint=False) + + return _hough_line(image, theta=theta) + + +def probabilistic_hough_line( + image, threshold=10, line_length=50, line_gap=10, theta=None, rng=None +): + """Return lines from a progressive probabilistic line Hough transform. + + Parameters + ---------- + image : ndarray, shape (M, N) + Input image with nonzero values representing edges. + threshold : int, optional + Threshold + line_length : int, optional + Minimum accepted length of detected lines. + Increase the parameter to extract longer lines. + line_gap : int, optional + Maximum gap between pixels to still form a line. + Increase the parameter to merge broken lines more aggressively. + theta : ndarray of dtype, shape (K,), optional + Angles at which to compute the transform, in radians. + Defaults to a vector of 180 angles evenly spaced in the + range [-pi/2, pi/2). + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + + Returns + ------- + lines : list + List of lines identified, lines in format ((x0, y0), (x1, y1)), + indicating line start and end. + + References + ---------- + .. [1] C. Galamhos, J. Matas and J. Kittler, "Progressive probabilistic + Hough transform for line detection", in IEEE Computer Society + Conference on Computer Vision and Pattern Recognition, 1999. + """ + + if image.ndim != 2: + raise ValueError('The input image `image` must be 2D.') + + if theta is None: + theta = np.linspace(-np.pi / 2, np.pi / 2, 180, endpoint=False) + + return _prob_hough_line( + image, + threshold=threshold, + line_length=line_length, + line_gap=line_gap, + theta=theta, + rng=rng, + ) + + +def hough_circle_peaks( + hspaces, + radii, + min_xdistance=1, + min_ydistance=1, + threshold=None, + num_peaks=np.inf, + total_num_peaks=np.inf, + normalize=False, +): + """Return peaks in a circle Hough transform. + + Identifies most prominent circles separated by certain distances in given + Hough spaces. Non-maximum suppression with different sizes is applied + separately in the first and second dimension of the Hough space to + identify peaks. For circles with different radius but close in distance, + only the one with highest peak is kept. + + Parameters + ---------- + hspaces : (M, N, P) array + Hough spaces returned by the `hough_circle` function. + radii : (M,) array + Radii corresponding to Hough spaces. + min_xdistance : int, optional + Minimum distance separating centers in the x dimension. + min_ydistance : int, optional + Minimum distance separating centers in the y dimension. + threshold : float, optional + Minimum intensity of peaks in each Hough space. + Default is `0.5 * max(hspace)`. + num_peaks : int, optional + Maximum number of peaks in each Hough space. When the + number of peaks exceeds `num_peaks`, only `num_peaks` + coordinates based on peak intensity are considered for the + corresponding radius. + total_num_peaks : int, optional + Maximum number of peaks. When the number of peaks exceeds `num_peaks`, + return `num_peaks` coordinates based on peak intensity. + normalize : bool, optional + If True, normalize the accumulator by the radius to sort the prominent + peaks. + + Returns + ------- + accum, cx, cy, rad : tuple of array + Peak values in Hough space, x and y center coordinates and radii. + + Examples + -------- + >>> from skimage import transform, draw + >>> img = np.zeros((120, 100), dtype=int) + >>> radius, x_0, y_0 = (20, 99, 50) + >>> y, x = draw.circle_perimeter(y_0, x_0, radius) + >>> img[x, y] = 1 + >>> hspaces = transform.hough_circle(img, radius) + >>> accum, cx, cy, rad = hough_circle_peaks(hspaces, [radius,]) + + Notes + ----- + Circles with bigger radius have higher peaks in Hough space. If larger + circles are preferred over smaller ones, `normalize` should be False. + Otherwise, circles will be returned in the order of decreasing voting + number. + """ + from ..feature.peak import _prominent_peaks + + r = [] + cx = [] + cy = [] + accum = [] + + for rad, hp in zip(radii, hspaces): + h_p, x_p, y_p = _prominent_peaks( + hp, + min_xdistance=min_xdistance, + min_ydistance=min_ydistance, + threshold=threshold, + num_peaks=num_peaks, + ) + r.extend((rad,) * len(h_p)) + cx.extend(x_p) + cy.extend(y_p) + accum.extend(h_p) + + r = np.array(r) + cx = np.array(cx) + cy = np.array(cy) + accum = np.array(accum) + if normalize: + s = np.argsort(accum / r) + else: + s = np.argsort(accum) + accum_sorted, cx_sorted, cy_sorted, r_sorted = ( + accum[s][::-1], + cx[s][::-1], + cy[s][::-1], + r[s][::-1], + ) + + tnp = len(accum_sorted) if total_num_peaks == np.inf else total_num_peaks + + # Skip searching for neighboring circles + # if default min_xdistance and min_ydistance are used + # or if no peak was detected + if (min_xdistance == 1 and min_ydistance == 1) or len(accum_sorted) == 0: + return (accum_sorted[:tnp], cx_sorted[:tnp], cy_sorted[:tnp], r_sorted[:tnp]) + + # For circles with centers too close, only keep the one with + # the highest peak + should_keep = label_distant_points( + cx_sorted, cy_sorted, min_xdistance, min_ydistance, tnp + ) + return ( + accum_sorted[should_keep], + cx_sorted[should_keep], + cy_sorted[should_keep], + r_sorted[should_keep], + ) + + +def label_distant_points(xs, ys, min_xdistance, min_ydistance, max_points): + """Keep points that are separated by certain distance in each dimension. + + The first point is always accepted and all subsequent points are selected + so that they are distant from all their preceding ones. + + Parameters + ---------- + xs : array, shape (M,) + X coordinates of points. + ys : array, shape (M,) + Y coordinates of points. + min_xdistance : int + Minimum distance separating points in the x dimension. + min_ydistance : int + Minimum distance separating points in the y dimension. + max_points : int + Max number of distant points to keep. + + Returns + ------- + should_keep : array of bool + A mask array for distant points to keep. + """ + is_neighbor = np.zeros(len(xs), dtype=bool) + coordinates = np.stack([xs, ys], axis=1) + # Use a KDTree to search for neighboring points effectively + kd_tree = cKDTree(coordinates) + n_pts = 0 + for i in range(len(xs)): + if n_pts >= max_points: + # Ignore the point if points to keep reaches maximum + is_neighbor[i] = True + elif not is_neighbor[i]: + # Find a short list of candidates to remove + # by searching within a circle + neighbors_i = kd_tree.query_ball_point( + (xs[i], ys[i]), np.hypot(min_xdistance, min_ydistance) + ) + # Check distance in both dimensions and mark if close + for ni in neighbors_i: + x_close = abs(xs[ni] - xs[i]) <= min_xdistance + y_close = abs(ys[ni] - ys[i]) <= min_ydistance + if x_close and y_close and ni > i: + is_neighbor[ni] = True + n_pts += 1 + should_keep = ~is_neighbor + return should_keep diff --git a/lib/python3.10/site-packages/skimage/transform/integral.py b/lib/python3.10/site-packages/skimage/transform/integral.py new file mode 100644 index 0000000000000000000000000000000000000000..852448b614d6818a857f37e099847550416e5bb6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/integral.py @@ -0,0 +1,145 @@ +import numpy as np + + +def integral_image(image, *, dtype=None): + r"""Integral image / summed area table. + + The integral image contains the sum of all elements above and to the + left of it, i.e.: + + .. math:: + + S[m, n] = \sum_{i \leq m} \sum_{j \leq n} X[i, j] + + Parameters + ---------- + image : ndarray + Input image. + + Returns + ------- + S : ndarray + Integral image/summed area table of same shape as input image. + + Notes + ----- + For better accuracy and to avoid potential overflow, the data type of the + output may differ from the input's when the default dtype of None is used. + For inputs with integer dtype, the behavior matches that for + :func:`numpy.cumsum`. Floating point inputs will be promoted to at least + double precision. The user can set `dtype` to override this behavior. + + References + ---------- + .. [1] F.C. Crow, "Summed-area tables for texture mapping," + ACM SIGGRAPH Computer Graphics, vol. 18, 1984, pp. 207-212. + + """ + if dtype is None and image.real.dtype.kind == 'f': + # default to at least double precision cumsum for accuracy + dtype = np.promote_types(image.dtype, np.float64) + + S = image + for i in range(image.ndim): + S = S.cumsum(axis=i, dtype=dtype) + return S + + +def integrate(ii, start, end): + """Use an integral image to integrate over a given window. + + Parameters + ---------- + ii : ndarray + Integral image. + start : List of tuples, each tuple of length equal to dimension of `ii` + Coordinates of top left corner of window(s). + Each tuple in the list contains the starting row, col, ... index + i.e `[(row_win1, col_win1, ...), (row_win2, col_win2,...), ...]`. + end : List of tuples, each tuple of length equal to dimension of `ii` + Coordinates of bottom right corner of window(s). + Each tuple in the list containing the end row, col, ... index i.e + `[(row_win1, col_win1, ...), (row_win2, col_win2, ...), ...]`. + + Returns + ------- + S : scalar or ndarray + Integral (sum) over the given window(s). + + See Also + -------- + integral_image : Create an integral image / summed area table. + + Examples + -------- + >>> arr = np.ones((5, 6), dtype=float) + >>> ii = integral_image(arr) + >>> integrate(ii, (1, 0), (1, 2)) # sum from (1, 0) to (1, 2) + array([3.]) + >>> integrate(ii, [(3, 3)], [(4, 5)]) # sum from (3, 3) to (4, 5) + array([6.]) + >>> # sum from (1, 0) to (1, 2) and from (3, 3) to (4, 5) + >>> integrate(ii, [(1, 0), (3, 3)], [(1, 2), (4, 5)]) + array([3., 6.]) + """ + start = np.atleast_2d(np.array(start)) + end = np.atleast_2d(np.array(end)) + rows = start.shape[0] + + total_shape = ii.shape + total_shape = np.tile(total_shape, [rows, 1]) + + # convert negative indices into equivalent positive indices + start_negatives = start < 0 + end_negatives = end < 0 + start = (start + total_shape) * start_negatives + start * ~(start_negatives) + end = (end + total_shape) * end_negatives + end * ~(end_negatives) + + if np.any((end - start) < 0): + raise IndexError('end coordinates must be greater or equal to start') + + # bit_perm is the total number of terms in the expression + # of S. For example, in the case of a 4x4 2D image + # sum of image from (1,1) to (2,2) is given by + # S = + ii[2, 2] + # - ii[0, 2] - ii[2, 0] + # + ii[0, 0] + # The total terms = 4 = 2 ** 2(dims) + + S = np.zeros(rows) + bit_perm = 2**ii.ndim + width = len(bin(bit_perm - 1)[2:]) + + # Sum of a (hyper)cube, from an integral image is computed using + # values at the corners of the cube. The corners of cube are + # selected using binary numbers as described in the following example. + # In a 3D cube there are 8 corners. The corners are selected using + # binary numbers 000 to 111. Each number is called a permutation, where + # perm(000) means, select end corner where none of the coordinates + # is replaced, i.e ii[end_row, end_col, end_depth]. Similarly, perm(001) + # means replace last coordinate by start - 1, i.e + # ii[end_row, end_col, start_depth - 1], and so on. + # Sign of even permutations is positive, while those of odd is negative. + # If 'start_coord - 1' is -ve it is labeled bad and not considered in + # the final sum. + + for i in range(bit_perm): # for all permutations + # boolean permutation array eg [True, False] for '10' + binary = bin(i)[2:].zfill(width) + bool_mask = [bit == '1' for bit in binary] + + sign = (-1) ** sum(bool_mask) # determine sign of permutation + + bad = [ + np.any(((start[r] - 1) * bool_mask) < 0) for r in range(rows) + ] # find out bad start rows + + corner_points = (end * (np.invert(bool_mask))) + ( + (start - 1) * bool_mask + ) # find corner for each row + + S += [ + sign * float(ii[tuple(corner_points[r])]) if (not bad[r]) else 0 + for r in range(rows) + ] # add only good rows + return S diff --git a/lib/python3.10/site-packages/skimage/transform/pyramids.py b/lib/python3.10/site-packages/skimage/transform/pyramids.py new file mode 100644 index 0000000000000000000000000000000000000000..3d20c4680158421ef6ef6b457448ac9189739cd7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/pyramids.py @@ -0,0 +1,408 @@ +import math + +import numpy as np + +from .._shared.filters import gaussian +from .._shared.utils import convert_to_float +from ._warps import resize + + +def _smooth(image, sigma, mode, cval, channel_axis): + """Return image with each channel smoothed by the Gaussian filter.""" + smoothed = np.empty_like(image) + + # apply Gaussian filter to all channels independently + if channel_axis is not None: + # can rely on gaussian to insert a 0 entry at channel_axis + channel_axis = channel_axis % image.ndim + sigma = (sigma,) * (image.ndim - 1) + else: + channel_axis = None + gaussian( + image, + sigma=sigma, + out=smoothed, + mode=mode, + cval=cval, + channel_axis=channel_axis, + ) + return smoothed + + +def _check_factor(factor): + if factor <= 1: + raise ValueError('scale factor must be greater than 1') + + +def pyramid_reduce( + image, + downscale=2, + sigma=None, + order=1, + mode='reflect', + cval=0, + preserve_range=False, + *, + channel_axis=None, +): + """Smooth and then downsample image. + + Parameters + ---------- + image : ndarray + Input image. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the Gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'edge', 'symmetric', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : array + Smoothed and downsampled float image. + + References + ---------- + .. [1] http://persci.mit.edu/pub_pdfs/pyramid83.pdf + + """ + _check_factor(downscale) + + image = convert_to_float(image, preserve_range) + if channel_axis is not None: + channel_axis = channel_axis % image.ndim + out_shape = tuple( + math.ceil(d / float(downscale)) if ax != channel_axis else d + for ax, d in enumerate(image.shape) + ) + else: + out_shape = tuple(math.ceil(d / float(downscale)) for d in image.shape) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * downscale / 6.0 + + smoothed = _smooth(image, sigma, mode, cval, channel_axis) + out = resize( + smoothed, out_shape, order=order, mode=mode, cval=cval, anti_aliasing=False + ) + + return out + + +def pyramid_expand( + image, + upscale=2, + sigma=None, + order=1, + mode='reflect', + cval=0, + preserve_range=False, + *, + channel_axis=None, +): + """Upsample and then smooth image. + + Parameters + ---------- + image : ndarray + Input image. + upscale : float, optional + Upscale factor. + sigma : float, optional + Sigma for Gaussian filter. Default is `2 * upscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the Gaussian distribution. + order : int, optional + Order of splines used in interpolation of upsampling. See + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'edge', 'symmetric', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + out : array + Upsampled and smoothed float image. + + References + ---------- + .. [1] http://persci.mit.edu/pub_pdfs/pyramid83.pdf + + """ + _check_factor(upscale) + image = convert_to_float(image, preserve_range) + if channel_axis is not None: + channel_axis = channel_axis % image.ndim + out_shape = tuple( + math.ceil(upscale * d) if ax != channel_axis else d + for ax, d in enumerate(image.shape) + ) + else: + out_shape = tuple(math.ceil(upscale * d) for d in image.shape) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * upscale / 6.0 + + resized = resize( + image, out_shape, order=order, mode=mode, cval=cval, anti_aliasing=False + ) + out = _smooth(resized, sigma, mode, cval, channel_axis) + + return out + + +def pyramid_gaussian( + image, + max_layer=-1, + downscale=2, + sigma=None, + order=1, + mode='reflect', + cval=0, + preserve_range=False, + *, + channel_axis=None, +): + """Yield images of the Gaussian pyramid formed by the input image. + + Recursively applies the `pyramid_reduce` function to the image, and yields + the downscaled images. + + Note that the first image of the pyramid will be the original, unscaled + image. The total number of images is `max_layer + 1`. In case all layers + are computed, the last image is either a one-pixel image or the image where + the reduction does not change its shape. + + Parameters + ---------- + image : ndarray + Input image. + max_layer : int, optional + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the Gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'edge', 'symmetric', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + pyramid : generator + Generator yielding pyramid layers as float images. + + References + ---------- + .. [1] http://persci.mit.edu/pub_pdfs/pyramid83.pdf + + """ + _check_factor(downscale) + + # cast to float for consistent data type in pyramid + image = convert_to_float(image, preserve_range) + + layer = 0 + current_shape = image.shape + + prev_layer_image = image + yield image + + # build downsampled images until max_layer is reached or downscale process + # does not change image size + while layer != max_layer: + layer += 1 + + layer_image = pyramid_reduce( + prev_layer_image, + downscale, + sigma, + order, + mode, + cval, + channel_axis=channel_axis, + ) + + prev_shape = current_shape + prev_layer_image = layer_image + current_shape = layer_image.shape + + # no change to previous pyramid layer + if current_shape == prev_shape: + break + + yield layer_image + + +def pyramid_laplacian( + image, + max_layer=-1, + downscale=2, + sigma=None, + order=1, + mode='reflect', + cval=0, + preserve_range=False, + *, + channel_axis=None, +): + """Yield images of the laplacian pyramid formed by the input image. + + Each layer contains the difference between the downsampled and the + downsampled, smoothed image:: + + layer = resize(prev_layer) - smooth(resize(prev_layer)) + + Note that the first image of the pyramid will be the difference between the + original, unscaled image and its smoothed version. The total number of + images is `max_layer + 1`. In case all layers are computed, the last image + is either a one-pixel image or the image where the reduction does not + change its shape. + + Parameters + ---------- + image : ndarray + Input image. + max_layer : int, optional + Number of layers for the pyramid. 0th layer is the original image. + Default is -1 which builds all possible layers. + downscale : float, optional + Downscale factor. + sigma : float, optional + Sigma for Gaussian filter. Default is `2 * downscale / 6.0` which + corresponds to a filter mask twice the size of the scale factor that + covers more than 99% of the Gaussian distribution. + order : int, optional + Order of splines used in interpolation of downsampling. See + `skimage.transform.warp` for detail. + mode : {'reflect', 'constant', 'edge', 'symmetric', 'wrap'}, optional + The mode parameter determines how the array borders are handled, where + cval is the value when mode is equal to 'constant'. + cval : float, optional + Value to fill past edges of input if mode is 'constant'. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + .. versionadded:: 0.19 + ``channel_axis`` was added in 0.19. + + Returns + ------- + pyramid : generator + Generator yielding pyramid layers as float images. + + References + ---------- + .. [1] http://persci.mit.edu/pub_pdfs/pyramid83.pdf + .. [2] http://sepwww.stanford.edu/data/media/public/sep/morgan/texturematch/paper_html/node3.html + + """ + _check_factor(downscale) + + # cast to float for consistent data type in pyramid + image = convert_to_float(image, preserve_range) + + if sigma is None: + # automatically determine sigma which covers > 99% of distribution + sigma = 2 * downscale / 6.0 + + current_shape = image.shape + + smoothed_image = _smooth(image, sigma, mode, cval, channel_axis) + yield image - smoothed_image + + if channel_axis is not None: + channel_axis = channel_axis % image.ndim + shape_without_channels = list(current_shape) + shape_without_channels.pop(channel_axis) + shape_without_channels = tuple(shape_without_channels) + else: + shape_without_channels = current_shape + + # build downsampled images until max_layer is reached or downscale process + # does not change image size + if max_layer == -1: + max_layer = math.ceil(math.log(max(shape_without_channels), downscale)) + + for layer in range(max_layer): + if channel_axis is not None: + out_shape = tuple( + math.ceil(d / float(downscale)) if ax != channel_axis else d + for ax, d in enumerate(current_shape) + ) + else: + out_shape = tuple(math.ceil(d / float(downscale)) for d in current_shape) + + resized_image = resize( + smoothed_image, + out_shape, + order=order, + mode=mode, + cval=cval, + anti_aliasing=False, + ) + smoothed_image = _smooth(resized_image, sigma, mode, cval, channel_axis) + current_shape = resized_image.shape + + yield resized_image - smoothed_image diff --git a/lib/python3.10/site-packages/skimage/transform/radon_transform.py b/lib/python3.10/site-packages/skimage/transform/radon_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..711269d9a8875e8ef7f88d1dbdcd48cb77ef0a94 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/transform/radon_transform.py @@ -0,0 +1,536 @@ +import numpy as np + +from scipy.interpolate import interp1d +from scipy.constants import golden_ratio +from scipy.fft import fft, ifft, fftfreq, fftshift +from ._warps import warp +from ._radon_transform import sart_projection_update +from .._shared.utils import convert_to_float +from warnings import warn +from functools import partial + + +__all__ = ['radon', 'order_angles_golden_ratio', 'iradon', 'iradon_sart'] + + +def radon(image, theta=None, circle=True, *, preserve_range=False): + """ + Calculates the radon transform of an image given specified + projection angles. + + Parameters + ---------- + image : ndarray + Input image. The rotation axis will be located in the pixel with + indices ``(image.shape[0] // 2, image.shape[1] // 2)``. + theta : array, optional + Projection angles (in degrees). If `None`, the value is set to + np.arange(180). + circle : boolean, optional + Assume image is zero outside the inscribed circle, making the + width of each projection (the first dimension of the sinogram) + equal to ``min(image.shape)``. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + + Returns + ------- + radon_image : ndarray + Radon transform (sinogram). The tomography rotation axis will lie + at the pixel index ``radon_image.shape[0] // 2`` along the 0th + dimension of ``radon_image``. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] B.R. Ramesh, N. Srinivasa, K. Rajgopal, "An Algorithm for Computing + the Discrete Radon Transform With Some Applications", Proceedings of + the Fourth IEEE Region 10 International Conference, TENCON '89, 1989 + + Notes + ----- + Based on code of Justin K. Romberg + (https://www.clear.rice.edu/elec431/projects96/DSP/bpanalysis.html) + + """ + if image.ndim != 2: + raise ValueError('The input image must be 2-D') + if theta is None: + theta = np.arange(180) + + image = convert_to_float(image, preserve_range) + + if circle: + shape_min = min(image.shape) + radius = shape_min // 2 + img_shape = np.array(image.shape) + coords = np.array(np.ogrid[: image.shape[0], : image.shape[1]], dtype=object) + dist = ((coords - img_shape // 2) ** 2).sum(0) + outside_reconstruction_circle = dist > radius**2 + if np.any(image[outside_reconstruction_circle]): + warn( + 'Radon transform: image must be zero outside the ' + 'reconstruction circle' + ) + # Crop image to make it square + slices = tuple( + ( + slice(int(np.ceil(excess / 2)), int(np.ceil(excess / 2) + shape_min)) + if excess > 0 + else slice(None) + ) + for excess in (img_shape - shape_min) + ) + padded_image = image[slices] + else: + diagonal = np.sqrt(2) * max(image.shape) + pad = [int(np.ceil(diagonal - s)) for s in image.shape] + new_center = [(s + p) // 2 for s, p in zip(image.shape, pad)] + old_center = [s // 2 for s in image.shape] + pad_before = [nc - oc for oc, nc in zip(old_center, new_center)] + pad_width = [(pb, p - pb) for pb, p in zip(pad_before, pad)] + padded_image = np.pad(image, pad_width, mode='constant', constant_values=0) + + # padded_image is always square + if padded_image.shape[0] != padded_image.shape[1]: + raise ValueError('padded_image must be a square') + center = padded_image.shape[0] // 2 + radon_image = np.zeros((padded_image.shape[0], len(theta)), dtype=image.dtype) + + for i, angle in enumerate(np.deg2rad(theta)): + cos_a, sin_a = np.cos(angle), np.sin(angle) + R = np.array( + [ + [cos_a, sin_a, -center * (cos_a + sin_a - 1)], + [-sin_a, cos_a, -center * (cos_a - sin_a - 1)], + [0, 0, 1], + ] + ) + rotated = warp(padded_image, R, clip=False) + radon_image[:, i] = rotated.sum(0) + return radon_image + + +def _sinogram_circle_to_square(sinogram): + diagonal = int(np.ceil(np.sqrt(2) * sinogram.shape[0])) + pad = diagonal - sinogram.shape[0] + old_center = sinogram.shape[0] // 2 + new_center = diagonal // 2 + pad_before = new_center - old_center + pad_width = ((pad_before, pad - pad_before), (0, 0)) + return np.pad(sinogram, pad_width, mode='constant', constant_values=0) + + +def _get_fourier_filter(size, filter_name): + """Construct the Fourier filter. + + This computation lessens artifacts and removes a small bias as + explained in [1], Chap 3. Equation 61. + + Parameters + ---------- + size : int + filter size. Must be even. + filter_name : str + Filter used in frequency domain filtering. Filters available: + ramp, shepp-logan, cosine, hamming, hann. Assign None to use + no filter. + + Returns + ------- + fourier_filter: ndarray + The computed Fourier filter. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + + """ + n = np.concatenate( + ( + np.arange(1, size / 2 + 1, 2, dtype=int), + np.arange(size / 2 - 1, 0, -2, dtype=int), + ) + ) + f = np.zeros(size) + f[0] = 0.25 + f[1::2] = -1 / (np.pi * n) ** 2 + + # Computing the ramp filter from the fourier transform of its + # frequency domain representation lessens artifacts and removes a + # small bias as explained in [1], Chap 3. Equation 61 + fourier_filter = 2 * np.real(fft(f)) # ramp filter + if filter_name == "ramp": + pass + elif filter_name == "shepp-logan": + # Start from first element to avoid divide by zero + omega = np.pi * fftfreq(size)[1:] + fourier_filter[1:] *= np.sin(omega) / omega + elif filter_name == "cosine": + freq = np.linspace(0, np.pi, size, endpoint=False) + cosine_filter = fftshift(np.sin(freq)) + fourier_filter *= cosine_filter + elif filter_name == "hamming": + fourier_filter *= fftshift(np.hamming(size)) + elif filter_name == "hann": + fourier_filter *= fftshift(np.hanning(size)) + elif filter_name is None: + fourier_filter[:] = 1 + + return fourier_filter[:, np.newaxis] + + +def iradon( + radon_image, + theta=None, + output_size=None, + filter_name="ramp", + interpolation="linear", + circle=True, + preserve_range=True, +): + """Inverse radon transform. + + Reconstruct an image from the radon transform, using the filtered + back projection algorithm. + + Parameters + ---------- + radon_image : ndarray + Image containing radon transform (sinogram). Each column of + the image corresponds to a projection along a different + angle. The tomography rotation axis should lie at the pixel + index ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. + theta : array, optional + Reconstruction angles (in degrees). Default: m angles evenly spaced + between 0 and 180 (if the shape of `radon_image` is (N, M)). + output_size : int, optional + Number of rows and columns in the reconstruction. + filter_name : str, optional + Filter used in frequency domain filtering. Ramp filter used by default. + Filters available: ramp, shepp-logan, cosine, hamming, hann. + Assign None to use no filter. + interpolation : str, optional + Interpolation method used in reconstruction. Methods available: + 'linear', 'nearest', and 'cubic' ('cubic' is slow). + circle : boolean, optional + Assume the reconstructed image is zero outside the inscribed circle. + Also changes the default output_size to match the behaviour of + ``radon`` called with ``circle=True``. + preserve_range : bool, optional + Whether to keep the original range of values. Otherwise, the input + image is converted according to the conventions of `img_as_float`. + Also see https://scikit-image.org/docs/dev/user_guide/data_types.html + + Returns + ------- + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. + + .. versionchanged:: 0.19 + In ``iradon``, ``filter`` argument is deprecated in favor of + ``filter_name``. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] B.R. Ramesh, N. Srinivasa, K. Rajgopal, "An Algorithm for Computing + the Discrete Radon Transform With Some Applications", Proceedings of + the Fourth IEEE Region 10 International Conference, TENCON '89, 1989 + + Notes + ----- + It applies the Fourier slice theorem to reconstruct an image by + multiplying the frequency domain of the filter with the FFT of the + projection data. This algorithm is called filtered back projection. + + """ + if radon_image.ndim != 2: + raise ValueError('The input image must be 2-D') + + if theta is None: + theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False) + + angles_count = len(theta) + if angles_count != radon_image.shape[1]: + raise ValueError( + "The given ``theta`` does not match the number of " + "projections in ``radon_image``." + ) + + interpolation_types = ('linear', 'nearest', 'cubic') + if interpolation not in interpolation_types: + raise ValueError(f"Unknown interpolation: {interpolation}") + + filter_types = ('ramp', 'shepp-logan', 'cosine', 'hamming', 'hann', None) + if filter_name not in filter_types: + raise ValueError(f"Unknown filter: {filter_name}") + + radon_image = convert_to_float(radon_image, preserve_range) + dtype = radon_image.dtype + + img_shape = radon_image.shape[0] + if output_size is None: + # If output size not specified, estimate from input radon image + if circle: + output_size = img_shape + else: + output_size = int(np.floor(np.sqrt((img_shape) ** 2 / 2.0))) + + if circle: + radon_image = _sinogram_circle_to_square(radon_image) + img_shape = radon_image.shape[0] + + # Resize image to next power of two (but no less than 64) for + # Fourier analysis; speeds up Fourier and lessens artifacts + projection_size_padded = max(64, int(2 ** np.ceil(np.log2(2 * img_shape)))) + pad_width = ((0, projection_size_padded - img_shape), (0, 0)) + img = np.pad(radon_image, pad_width, mode='constant', constant_values=0) + + # Apply filter in Fourier domain + fourier_filter = _get_fourier_filter(projection_size_padded, filter_name) + projection = fft(img, axis=0) * fourier_filter + radon_filtered = np.real(ifft(projection, axis=0)[:img_shape, :]) + + # Reconstruct image by interpolation + reconstructed = np.zeros((output_size, output_size), dtype=dtype) + radius = output_size // 2 + xpr, ypr = np.mgrid[:output_size, :output_size] - radius + x = np.arange(img_shape) - img_shape // 2 + + for col, angle in zip(radon_filtered.T, np.deg2rad(theta)): + t = ypr * np.cos(angle) - xpr * np.sin(angle) + if interpolation == 'linear': + interpolant = partial(np.interp, xp=x, fp=col, left=0, right=0) + else: + interpolant = interp1d( + x, col, kind=interpolation, bounds_error=False, fill_value=0 + ) + reconstructed += interpolant(t) + + if circle: + out_reconstruction_circle = (xpr**2 + ypr**2) > radius**2 + reconstructed[out_reconstruction_circle] = 0.0 + + return reconstructed * np.pi / (2 * angles_count) + + +def order_angles_golden_ratio(theta): + """Order angles to reduce the amount of correlated information in + subsequent projections. + + Parameters + ---------- + theta : array of floats, shape (M,) + Projection angles in degrees. Duplicate angles are not allowed. + + Returns + ------- + indices_generator : generator yielding unsigned integers + The returned generator yields indices into ``theta`` such that + ``theta[indices]`` gives the approximate golden ratio ordering + of the projections. In total, ``len(theta)`` indices are yielded. + All non-negative integers < ``len(theta)`` are yielded exactly once. + + Notes + ----- + The method used here is that of the golden ratio introduced + by T. Kohler. + + References + ---------- + .. [1] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [2] Winkelmann, Stefanie, et al. "An optimal radial profile order + based on the Golden Ratio for time-resolved MRI." + Medical Imaging, IEEE Transactions on 26.1 (2007): 68-76. + + """ + interval = 180 + + remaining_indices = list(np.argsort(theta)) # indices into theta + # yield an arbitrary angle to start things off + angle = theta[remaining_indices[0]] + yield remaining_indices.pop(0) + # determine subsequent angles using the golden ratio method + angle_increment = interval / golden_ratio**2 + while remaining_indices: + remaining_angles = theta[remaining_indices] + angle = (angle + angle_increment) % interval + index_above = np.searchsorted(remaining_angles, angle) + index_below = index_above - 1 + index_above %= len(remaining_indices) + + diff_below = abs(angle - remaining_angles[index_below]) + distance_below = min(diff_below % interval, diff_below % -interval) + + diff_above = abs(angle - remaining_angles[index_above]) + distance_above = min(diff_above % interval, diff_above % -interval) + + if distance_below < distance_above: + yield remaining_indices.pop(index_below) + else: + yield remaining_indices.pop(index_above) + + +def iradon_sart( + radon_image, + theta=None, + image=None, + projection_shifts=None, + clip=None, + relaxation=0.15, + dtype=None, +): + """Inverse radon transform. + + Reconstruct an image from the radon transform, using a single iteration of + the Simultaneous Algebraic Reconstruction Technique (SART) algorithm. + + Parameters + ---------- + radon_image : ndarray, shape (M, N) + Image containing radon transform (sinogram). Each column of + the image corresponds to a projection along a different angle. The + tomography rotation axis should lie at the pixel index + ``radon_image.shape[0] // 2`` along the 0th dimension of + ``radon_image``. + theta : array, shape (N,), optional + Reconstruction angles (in degrees). Default: m angles evenly spaced + between 0 and 180 (if the shape of `radon_image` is (N, M)). + image : ndarray, shape (M, M), optional + Image containing an initial reconstruction estimate. Default is an array of zeros. + projection_shifts : array, shape (N,), optional + Shift the projections contained in ``radon_image`` (the sinogram) by + this many pixels before reconstructing the image. The i'th value + defines the shift of the i'th column of ``radon_image``. + clip : length-2 sequence of floats, optional + Force all values in the reconstructed tomogram to lie in the range + ``[clip[0], clip[1]]`` + relaxation : float, optional + Relaxation parameter for the update step. A higher value can + improve the convergence rate, but one runs the risk of instabilities. + Values close to or higher than 1 are not recommended. + dtype : dtype, optional + Output data type, must be floating point. By default, if input + data type is not float, input is cast to double, otherwise + dtype is set to input data type. + + Returns + ------- + reconstructed : ndarray + Reconstructed image. The rotation axis will be located in the pixel + with indices + ``(reconstructed.shape[0] // 2, reconstructed.shape[1] // 2)``. + + Notes + ----- + Algebraic Reconstruction Techniques are based on formulating the tomography + reconstruction problem as a set of linear equations. Along each ray, + the projected value is the sum of all the values of the cross section along + the ray. A typical feature of SART (and a few other variants of algebraic + techniques) is that it samples the cross section at equidistant points + along the ray, using linear interpolation between the pixel values of the + cross section. The resulting set of linear equations are then solved using + a slightly modified Kaczmarz method. + + When using SART, a single iteration is usually sufficient to obtain a good + reconstruction. Further iterations will tend to enhance high-frequency + information, but will also often increase the noise. + + References + ---------- + .. [1] AC Kak, M Slaney, "Principles of Computerized Tomographic + Imaging", IEEE Press 1988. + .. [2] AH Andersen, AC Kak, "Simultaneous algebraic reconstruction + technique (SART): a superior implementation of the ART algorithm", + Ultrasonic Imaging 6 pp 81--94 (1984) + .. [3] S Kaczmarz, "Angenäherte auflösung von systemen linearer + gleichungen", Bulletin International de l’Academie Polonaise des + Sciences et des Lettres 35 pp 355--357 (1937) + .. [4] Kohler, T. "A projection access scheme for iterative + reconstruction based on the golden section." Nuclear Science + Symposium Conference Record, 2004 IEEE. Vol. 6. IEEE, 2004. + .. [5] Kaczmarz' method, Wikipedia, + https://en.wikipedia.org/wiki/Kaczmarz_method + + """ + if radon_image.ndim != 2: + raise ValueError('radon_image must be two dimensional') + + if dtype is None: + if radon_image.dtype.char in 'fd': + dtype = radon_image.dtype + else: + warn( + "Only floating point data type are valid for SART inverse " + "radon transform. Input data is cast to float. To disable " + "this warning, please cast image_radon to float." + ) + dtype = np.dtype(float) + elif np.dtype(dtype).char not in 'fd': + raise ValueError( + "Only floating point data type are valid for inverse " "radon transform." + ) + + dtype = np.dtype(dtype) + radon_image = radon_image.astype(dtype, copy=False) + + reconstructed_shape = (radon_image.shape[0], radon_image.shape[0]) + + if theta is None: + theta = np.linspace(0, 180, radon_image.shape[1], endpoint=False, dtype=dtype) + elif len(theta) != radon_image.shape[1]: + raise ValueError( + f'Shape of theta ({len(theta)}) does not match the ' + f'number of projections ({radon_image.shape[1]})' + ) + else: + theta = np.asarray(theta, dtype=dtype) + + if image is None: + image = np.zeros(reconstructed_shape, dtype=dtype) + elif image.shape != reconstructed_shape: + raise ValueError( + f'Shape of image ({image.shape}) does not match first dimension ' + f'of radon_image ({reconstructed_shape})' + ) + elif image.dtype != dtype: + warn(f'image dtype does not match output dtype: ' f'image is cast to {dtype}') + + image = np.asarray(image, dtype=dtype) + + if projection_shifts is None: + projection_shifts = np.zeros((radon_image.shape[1],), dtype=dtype) + elif len(projection_shifts) != radon_image.shape[1]: + raise ValueError( + f'Shape of projection_shifts ({len(projection_shifts)}) does not match the ' + f'number of projections ({radon_image.shape[1]})' + ) + else: + projection_shifts = np.asarray(projection_shifts, dtype=dtype) + if clip is not None: + if len(clip) != 2: + raise ValueError('clip must be a length-2 sequence') + clip = np.asarray(clip, dtype=dtype) + + for angle_index in order_angles_golden_ratio(theta): + image_update = sart_projection_update( + image, + theta[angle_index], + radon_image[:, angle_index], + projection_shifts[angle_index], + ) + image += relaxation * image_update + if clip is not None: + image = np.clip(image, clip[0], clip[1]) + return image diff --git a/lib/python3.10/site-packages/skimage/util/__init__.py b/lib/python3.10/site-packages/skimage/util/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ec4220035145c89d939571d064d5a455072c23c7 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/__init__.py @@ -0,0 +1,61 @@ +"""Generic utilities. + +This module contains a number of utility functions to work with images in general. +""" + +import functools +import warnings + +import numpy as np + +# keep .dtype imports first to avoid circular imports +from .dtype import ( + dtype_limits, + img_as_float, + img_as_float32, + img_as_float64, + img_as_bool, + img_as_int, + img_as_ubyte, + img_as_uint, +) +from ._slice_along_axes import slice_along_axes +from ._invert import invert +from ._label import label_points +from ._montage import montage +from ._map_array import map_array +from ._regular_grid import regular_grid, regular_seeds +from .apply_parallel import apply_parallel +from .arraycrop import crop +from .compare import compare_images +from .noise import random_noise +from .shape import view_as_blocks, view_as_windows +from .unique import unique_rows +from .lookfor import lookfor + + +__all__ = [ + 'img_as_float32', + 'img_as_float64', + 'img_as_float', + 'img_as_int', + 'img_as_uint', + 'img_as_ubyte', + 'img_as_bool', + 'dtype_limits', + 'view_as_blocks', + 'view_as_windows', + 'slice_along_axes', + 'crop', + 'compare_images', + 'map_array', + 'montage', + 'random_noise', + 'regular_grid', + 'regular_seeds', + 'apply_parallel', + 'invert', + 'unique_rows', + 'label_points', + 'lookfor', +] diff --git a/lib/python3.10/site-packages/skimage/util/_invert.py b/lib/python3.10/site-packages/skimage/util/_invert.py new file mode 100644 index 0000000000000000000000000000000000000000..bef2865d5679aa08307022ed14246a29471e6cef --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_invert.py @@ -0,0 +1,74 @@ +import numpy as np +from .dtype import dtype_limits + + +def invert(image, signed_float=False): + """Invert an image. + + Invert the intensity range of the input image, so that the dtype maximum + is now the dtype minimum, and vice-versa. This operation is + slightly different depending on the input dtype: + + - unsigned integers: subtract the image from the dtype maximum + - signed integers: subtract the image from -1 (see Notes) + - floats: subtract the image from 1 (if signed_float is False, so we + assume the image is unsigned), or from 0 (if signed_float is True). + + See the examples for clarification. + + Parameters + ---------- + image : ndarray + Input image. + signed_float : bool, optional + If True and the image is of type float, the range is assumed to + be [-1, 1]. If False and the image is of type float, the range is + assumed to be [0, 1]. + + Returns + ------- + inverted : ndarray + Inverted image. + + Notes + ----- + Ideally, for signed integers we would simply multiply by -1. However, + signed integer ranges are asymmetric. For example, for np.int8, the range + of possible values is [-128, 127], so that -128 * -1 equals -128! By + subtracting from -1, we correctly map the maximum dtype value to the + minimum. + + Examples + -------- + >>> img = np.array([[100, 0, 200], + ... [ 0, 50, 0], + ... [ 30, 0, 255]], np.uint8) + >>> invert(img) + array([[155, 255, 55], + [255, 205, 255], + [225, 255, 0]], dtype=uint8) + >>> img2 = np.array([[ -2, 0, -128], + ... [127, 0, 5]], np.int8) + >>> invert(img2) + array([[ 1, -1, 127], + [-128, -1, -6]], dtype=int8) + >>> img3 = np.array([[ 0., 1., 0.5, 0.75]]) + >>> invert(img3) + array([[1. , 0. , 0.5 , 0.25]]) + >>> img4 = np.array([[ 0., 1., -1., -0.25]]) + >>> invert(img4, signed_float=True) + array([[-0. , -1. , 1. , 0.25]]) + """ + if image.dtype == 'bool': + inverted = ~image + elif np.issubdtype(image.dtype, np.unsignedinteger): + max_val = dtype_limits(image, clip_negative=False)[1] + inverted = np.subtract(max_val, image, dtype=image.dtype) + elif np.issubdtype(image.dtype, np.signedinteger): + inverted = np.subtract(-1, image, dtype=image.dtype) + else: # float dtype + if signed_float: + inverted = -image + else: + inverted = np.subtract(1, image, dtype=image.dtype) + return inverted diff --git a/lib/python3.10/site-packages/skimage/util/_label.py b/lib/python3.10/site-packages/skimage/util/_label.py new file mode 100644 index 0000000000000000000000000000000000000000..1fd04ac85dc0a7ccd4ac0ca76088ff4d3a8dc931 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_label.py @@ -0,0 +1,51 @@ +import numpy as np + +__all__ = ["label_points"] + + +def label_points(coords, output_shape): + """Assign unique integer labels to coordinates on an image mask + + Parameters + ---------- + coords: ndarray + An array of N coordinates with dimension D + output_shape: tuple + The shape of the mask on which `coords` are labelled + + Returns + ------- + labels: ndarray + A mask of zeroes containing unique integer labels at the `coords` + + Examples + -------- + >>> import numpy as np + >>> from skimage.util._label import label_points + >>> coords = np.array([[0, 1], [2, 2]]) + >>> output_shape = (5, 5) + >>> mask = label_points(coords, output_shape) + >>> mask + array([[0, 1, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 2, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]], dtype=uint64) + + Notes + ----- + - The labels are assigned to coordinates that are converted to + integer and considered to start from 0. + - Coordinates that are out of range of the mask raise an IndexError. + - Negative coordinates raise a ValueError + """ + if coords.shape[1] != len(output_shape): + raise ValueError("Dimensionality of points should match the " "output shape") + + if np.any(coords < 0): + raise ValueError("Coordinates should be positive and start from 0") + + np_indices = tuple(np.transpose(np.round(coords).astype(int, copy=False))) + labels = np.zeros(output_shape, dtype=np.uint64) + labels[np_indices] = np.arange(1, coords.shape[0] + 1) + return labels diff --git a/lib/python3.10/site-packages/skimage/util/_map_array.py b/lib/python3.10/site-packages/skimage/util/_map_array.py new file mode 100644 index 0000000000000000000000000000000000000000..125dfb2116faac264fc322c363690022155aada5 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_map_array.py @@ -0,0 +1,199 @@ +import numpy as np + + +def map_array(input_arr, input_vals, output_vals, out=None): + """Map values from input array from input_vals to output_vals. + + Parameters + ---------- + input_arr : array of int, shape (M[, ...]) + The input label image. + input_vals : array of int, shape (K,) + The values to map from. + output_vals : array, shape (K,) + The values to map to. + out: array, same shape as `input_arr` + The output array. Will be created if not provided. It should + have the same dtype as `output_vals`. + + Returns + ------- + out : array, same shape as `input_arr` + The array of mapped values. + + Notes + ----- + If `input_arr` contains values that aren't covered by `input_vals`, they + are set to 0. + + Examples + -------- + >>> import numpy as np + >>> import skimage as ski + >>> ski.util.map_array( + ... input_arr=np.array([[0, 2, 2, 0], [3, 4, 5, 0]]), + ... input_vals=np.array([1, 2, 3, 4, 6]), + ... output_vals=np.array([6, 7, 8, 9, 10]), + ... ) + array([[0, 7, 7, 0], + [8, 9, 0, 0]]) + """ + from ._remap import _map_array + + if not np.issubdtype(input_arr.dtype, np.integer): + raise TypeError('The dtype of an array to be remapped should be integer.') + # We ravel the input array for simplicity of iteration in Cython: + orig_shape = input_arr.shape + # NumPy docs for `np.ravel()` says: + # "When a view is desired in as many cases as possible, + # arr.reshape(-1) may be preferable." + input_arr = input_arr.reshape(-1) + if out is None: + out = np.empty(orig_shape, dtype=output_vals.dtype) + elif out.shape != orig_shape: + raise ValueError( + 'If out array is provided, it should have the same shape as ' + f'the input array. Input array has shape {orig_shape}, provided ' + f'output array has shape {out.shape}.' + ) + try: + out_view = out.view() + out_view.shape = (-1,) # no-copy reshape/ravel + except AttributeError: # if out strides are not compatible with 0-copy + raise ValueError( + 'If out array is provided, it should be either contiguous ' + f'or 1-dimensional. Got array with shape {out.shape} and ' + f'strides {out.strides}.' + ) + + # ensure all arrays have matching types before sending to Cython + input_vals = input_vals.astype(input_arr.dtype, copy=False) + output_vals = output_vals.astype(out.dtype, copy=False) + _map_array(input_arr, out_view, input_vals, output_vals) + return out + + +class ArrayMap: + """Class designed to mimic mapping by NumPy array indexing. + + This class is designed to replicate the use of NumPy arrays for mapping + values with indexing: + + >>> values = np.array([0.25, 0.5, 1.0]) + >>> indices = np.array([[0, 0, 1], [2, 2, 1]]) + >>> values[indices] + array([[0.25, 0.25, 0.5 ], + [1. , 1. , 0.5 ]]) + + The issue with this indexing is that you need a very large ``values`` + array if the values in the ``indices`` array are large. + + >>> values = np.array([0.25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1.0]) + >>> indices = np.array([[0, 0, 10], [0, 10, 10]]) + >>> values[indices] + array([[0.25, 0.25, 1. ], + [0.25, 1. , 1. ]]) + + Using this class, the approach is similar, but there is no need to + create a large values array: + + >>> in_indices = np.array([0, 10]) + >>> out_values = np.array([0.25, 1.0]) + >>> values = ArrayMap(in_indices, out_values) + >>> values + ArrayMap(array([ 0, 10]), array([0.25, 1. ])) + >>> print(values) + ArrayMap: + 0 → 0.25 + 10 → 1.0 + >>> indices = np.array([[0, 0, 10], [0, 10, 10]]) + >>> values[indices] + array([[0.25, 0.25, 1. ], + [0.25, 1. , 1. ]]) + + Parameters + ---------- + in_values : array of int, shape (K,) + The source values from which to map. + out_values : array, shape (K,) + The destination values from which to map. + """ + + def __init__(self, in_values, out_values): + self.in_values = in_values + self.out_values = out_values + self._max_str_lines = 4 + self._array = None + + def __len__(self): + """Return one more than the maximum label value being remapped.""" + return np.max(self.in_values) + 1 + + def __array__(self, dtype=None, copy=None): + """Return an array that behaves like the arraymap when indexed. + + This array can be very large: it is the size of the largest value + in the ``in_vals`` array, plus one. + """ + if dtype is None: + dtype = self.out_values.dtype + output = np.zeros(np.max(self.in_values) + 1, dtype=dtype) + output[self.in_values] = self.out_values + return output + + @property + def dtype(self): + return self.out_values.dtype + + def __repr__(self): + return f'ArrayMap({repr(self.in_values)}, {repr(self.out_values)})' + + def __str__(self): + if len(self.in_values) <= self._max_str_lines + 1: + rows = range(len(self.in_values)) + string = '\n'.join( + ['ArrayMap:'] + + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows] + ) + else: + rows0 = list(range(0, self._max_str_lines // 2)) + rows1 = list(range(-self._max_str_lines // 2, 0)) + string = '\n'.join( + ['ArrayMap:'] + + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows0] + + [' ...'] + + [f' {self.in_values[i]} → {self.out_values[i]}' for i in rows1] + ) + return string + + def __call__(self, arr): + return self.__getitem__(arr) + + def __getitem__(self, index): + scalar = np.isscalar(index) + if scalar: + index = np.array([index]) + elif isinstance(index, slice): + start = index.start or 0 # treat None or 0 the same way + stop = index.stop if index.stop is not None else len(self) + step = index.step + index = np.arange(start, stop, step) + if index.dtype == bool: + index = np.flatnonzero(index) + + out = map_array( + index, + self.in_values.astype(index.dtype, copy=False), + self.out_values, + ) + + if scalar: + out = out[0] + return out + + def __setitem__(self, indices, values): + if self._array is None: + self._array = self.__array__() + self._array[indices] = values + self.in_values = np.flatnonzero(self._array) + self.out_values = self._array[self.in_values] diff --git a/lib/python3.10/site-packages/skimage/util/_montage.py b/lib/python3.10/site-packages/skimage/util/_montage.py new file mode 100644 index 0000000000000000000000000000000000000000..d95bb57241dc90a4e20ec0fca5a4f66b2c442bbf --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_montage.py @@ -0,0 +1,158 @@ +import numpy as np + +from .._shared import utils +from .. import exposure + +__all__ = ['montage'] + + +@utils.channel_as_last_axis(multichannel_output=False) +def montage( + arr_in, + fill='mean', + rescale_intensity=False, + grid_shape=None, + padding_width=0, + *, + channel_axis=None, +): + """Create a montage of several single- or multichannel images. + + Create a rectangular montage from an input array representing an ensemble + of equally shaped single- (gray) or multichannel (color) images. + + For example, ``montage(arr_in)`` called with the following `arr_in` + + +---+---+---+ + | 1 | 2 | 3 | + +---+---+---+ + + will return + + +---+---+ + | 1 | 2 | + +---+---+ + | 3 | * | + +---+---+ + + where the '*' patch will be determined by the `fill` parameter. + + Parameters + ---------- + arr_in : ndarray, shape (K, M, N[, C]) + An array representing an ensemble of `K` images of equal shape. + fill : float or array-like of floats or 'mean', optional + Value to fill the padding areas and/or the extra tiles in + the output array. Has to be `float` for single channel collections. + For multichannel collections has to be an array-like of shape of + number of channels. If `mean`, uses the mean value over all images. + rescale_intensity : bool, optional + Whether to rescale the intensity of each image to [0, 1]. + grid_shape : tuple, optional + The desired grid shape for the montage `(ntiles_row, ntiles_column)`. + The default aspect ratio is square. + padding_width : int, optional + The size of the spacing between the tiles and between the tiles and + the borders. If non-zero, makes the boundaries of individual images + easier to perceive. + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + Returns + ------- + arr_out : (K*(M+p)+p, K*(N+p)+p[, C]) ndarray + Output array with input images glued together (including padding `p`). + + Examples + -------- + >>> import numpy as np + >>> from skimage.util import montage + >>> arr_in = np.arange(3 * 2 * 2).reshape(3, 2, 2) + >>> arr_in # doctest: +NORMALIZE_WHITESPACE + array([[[ 0, 1], + [ 2, 3]], + [[ 4, 5], + [ 6, 7]], + [[ 8, 9], + [10, 11]]]) + >>> arr_out = montage(arr_in) + >>> arr_out.shape + (4, 4) + >>> arr_out + array([[ 0, 1, 4, 5], + [ 2, 3, 6, 7], + [ 8, 9, 5, 5], + [10, 11, 5, 5]]) + >>> arr_in.mean() + 5.5 + >>> arr_out_nonsquare = montage(arr_in, grid_shape=(1, 3)) + >>> arr_out_nonsquare + array([[ 0, 1, 4, 5, 8, 9], + [ 2, 3, 6, 7, 10, 11]]) + >>> arr_out_nonsquare.shape + (2, 6) + """ + + if channel_axis is not None: + arr_in = np.asarray(arr_in) + else: + arr_in = np.asarray(arr_in)[..., np.newaxis] + + if arr_in.ndim != 4: + raise ValueError( + 'Input array has to be 3-dimensional for grayscale ' + 'images, or 4-dimensional with a `channel_axis` ' + 'specified.' + ) + + n_images, n_rows, n_cols, n_chan = arr_in.shape + + if grid_shape: + ntiles_row, ntiles_col = (int(s) for s in grid_shape) + else: + ntiles_row = ntiles_col = int(np.ceil(np.sqrt(n_images))) + + # Rescale intensity if necessary + if rescale_intensity: + for i in range(n_images): + arr_in[i] = exposure.rescale_intensity(arr_in[i]) + + # Calculate the fill value + if fill == 'mean': + fill = arr_in.mean(axis=(0, 1, 2)) + fill = np.atleast_1d(fill).astype(arr_in.dtype) + + # Pre-allocate an array with padding for montage + n_pad = padding_width + arr_out = np.empty( + ( + (n_rows + n_pad) * ntiles_row + n_pad, + (n_cols + n_pad) * ntiles_col + n_pad, + n_chan, + ), + dtype=arr_in.dtype, + ) + for idx_chan in range(n_chan): + arr_out[..., idx_chan] = fill[idx_chan] + + slices_row = [ + slice(n_pad + (n_rows + n_pad) * n, n_pad + (n_rows + n_pad) * n + n_rows) + for n in range(ntiles_row) + ] + slices_col = [ + slice(n_pad + (n_cols + n_pad) * n, n_pad + (n_cols + n_pad) * n + n_cols) + for n in range(ntiles_col) + ] + + # Copy the data to the output array + for idx_image, image in enumerate(arr_in): + idx_sr = idx_image // ntiles_col + idx_sc = idx_image % ntiles_col + arr_out[slices_row[idx_sr], slices_col[idx_sc], :] = image + + if channel_axis is not None: + return arr_out + else: + return arr_out[..., 0] diff --git a/lib/python3.10/site-packages/skimage/util/_regular_grid.py b/lib/python3.10/site-packages/skimage/util/_regular_grid.py new file mode 100644 index 0000000000000000000000000000000000000000..13bf47ef0cb035abbe51a077b162cb5e69930ad1 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_regular_grid.py @@ -0,0 +1,114 @@ +import numpy as np + + +def regular_grid(ar_shape, n_points): + """Find `n_points` regularly spaced along `ar_shape`. + + The returned points (as slices) should be as close to cubically-spaced as + possible. Essentially, the points are spaced by the Nth root of the input + array size, where N is the number of dimensions. However, if an array + dimension cannot fit a full step size, it is "discarded", and the + computation is done for only the remaining dimensions. + + Parameters + ---------- + ar_shape : array-like of ints + The shape of the space embedding the grid. ``len(ar_shape)`` is the + number of dimensions. + n_points : int + The (approximate) number of points to embed in the space. + + Returns + ------- + slices : tuple of slice objects + A slice along each dimension of `ar_shape`, such that the intersection + of all the slices give the coordinates of regularly spaced points. + + .. versionchanged:: 0.14.1 + In scikit-image 0.14.1 and 0.15, the return type was changed from a + list to a tuple to ensure `compatibility with Numpy 1.15`_ and + higher. If your code requires the returned result to be a list, you + may convert the output of this function to a list with: + + >>> result = list(regular_grid(ar_shape=(3, 20, 40), n_points=8)) + + .. _compatibility with NumPy 1.15: https://github.com/numpy/numpy/blob/master/doc/release/1.15.0-notes.rst#deprecations + + Examples + -------- + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + (slice(5, None, 10), slice(5, None, 10)) + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + >>> ar = np.zeros((20, 40)) + >>> g = regular_grid(ar.shape, 32) + >>> g + (slice(2, None, 5), slice(2, None, 5)) + >>> ar[g] = 1 + >>> ar.sum() + 32.0 + >>> ar = np.zeros((3, 20, 40)) + >>> g = regular_grid(ar.shape, 8) + >>> g + (slice(1, None, 3), slice(5, None, 10), slice(5, None, 10)) + >>> ar[g] = 1 + >>> ar.sum() + 8.0 + """ + ar_shape = np.asanyarray(ar_shape) + ndim = len(ar_shape) + unsort_dim_idxs = np.argsort(np.argsort(ar_shape)) + sorted_dims = np.sort(ar_shape) + space_size = float(np.prod(ar_shape)) + if space_size <= n_points: + return (slice(None),) * ndim + stepsizes = np.full(ndim, (space_size / n_points) ** (1.0 / ndim), dtype='float64') + if (sorted_dims < stepsizes).any(): + for dim in range(ndim): + stepsizes[dim] = sorted_dims[dim] + space_size = float(np.prod(sorted_dims[dim + 1 :])) + stepsizes[dim + 1 :] = (space_size / n_points) ** (1.0 / (ndim - dim - 1)) + if (sorted_dims >= stepsizes).all(): + break + starts = (stepsizes // 2).astype(int) + stepsizes = np.round(stepsizes).astype(int) + slices = [slice(start, None, step) for start, step in zip(starts, stepsizes)] + slices = tuple(slices[i] for i in unsort_dim_idxs) + return slices + + +def regular_seeds(ar_shape, n_points, dtype=int): + """Return an image with ~`n_points` regularly-spaced nonzero pixels. + + Parameters + ---------- + ar_shape : tuple of int + The shape of the desired output image. + n_points : int + The desired number of nonzero points. + dtype : numpy data type, optional + The desired data type of the output. + + Returns + ------- + seed_img : array of int or bool + The desired image. + + Examples + -------- + >>> regular_seeds((5, 5), 4) + array([[0, 0, 0, 0, 0], + [0, 1, 0, 2, 0], + [0, 0, 0, 0, 0], + [0, 3, 0, 4, 0], + [0, 0, 0, 0, 0]]) + """ + grid = regular_grid(ar_shape, n_points) + seed_img = np.zeros(ar_shape, dtype=dtype) + seed_img[grid] = 1 + np.reshape( + np.arange(seed_img[grid].size), seed_img[grid].shape + ) + return seed_img diff --git a/lib/python3.10/site-packages/skimage/util/_slice_along_axes.py b/lib/python3.10/site-packages/skimage/util/_slice_along_axes.py new file mode 100644 index 0000000000000000000000000000000000000000..4cf6fd6cd235281efe300e725d8c80991d4d4b93 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/_slice_along_axes.py @@ -0,0 +1,86 @@ +__all__ = ['slice_along_axes'] + + +def slice_along_axes(image, slices, axes=None, copy=False): + """Slice an image along given axes. + + Parameters + ---------- + image : ndarray + Input image. + slices : list of 2-tuple (a, b) where a < b. + For each axis in `axes`, a corresponding 2-tuple + ``(min_val, max_val)`` to slice with (as with Python slices, + ``max_val`` is non-inclusive). + axes : int or tuple, optional + Axes corresponding to the limits given in `slices`. If None, + axes are in ascending order, up to the length of `slices`. + copy : bool, optional + If True, ensure that the output is not a view of `image`. + + Returns + ------- + out : ndarray + The region of `image` corresponding to the given slices and axes. + + Examples + -------- + >>> from skimage import data + >>> img = data.camera() + >>> img.shape + (512, 512) + >>> cropped_img = slice_along_axes(img, [(0, 100)]) + >>> cropped_img.shape + (100, 512) + >>> cropped_img = slice_along_axes(img, [(0, 100), (0, 100)]) + >>> cropped_img.shape + (100, 100) + >>> cropped_img = slice_along_axes(img, [(0, 100), (0, 75)], axes=[1, 0]) + >>> cropped_img.shape + (75, 100) + """ + + # empty length of bounding box detected on None + if not slices: + return image + + if axes is None: + axes = list(range(image.ndim)) + if len(axes) < len(slices): + raise ValueError("More `slices` than available axes") + + elif len(axes) != len(slices): + raise ValueError("`axes` and `slices` must have equal length") + + if len(axes) != len(set(axes)): + raise ValueError("`axes` must be unique") + + if not all(a >= 0 and a < image.ndim for a in axes): + raise ValueError( + f"axes {axes} out of range; image has only " f"{image.ndim} dimensions" + ) + + _slices = [ + slice(None), + ] * image.ndim + for (a, b), ax in zip(slices, axes): + if a < 0: + a %= image.shape[ax] + if b < 0: + b %= image.shape[ax] + if a > b: + raise ValueError( + f"Invalid slice ({a}, {b}): must be ordered `(min_val, max_val)`" + ) + if a < 0 or b > image.shape[ax]: + raise ValueError( + f"Invalid slice ({a}, {b}) for image with dimensions {image.shape}" + ) + _slices[ax] = slice(a, b) + + image_slice = image[tuple(_slices)] + + if copy and image_slice.base is not None: + image_slice = image_slice.copy() + + return image_slice diff --git a/lib/python3.10/site-packages/skimage/util/apply_parallel.py b/lib/python3.10/site-packages/skimage/util/apply_parallel.py new file mode 100644 index 0000000000000000000000000000000000000000..57a7638c2a2e1deba587537f96335f6f99702665 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/apply_parallel.py @@ -0,0 +1,213 @@ +import numpy + +__all__ = ['apply_parallel'] + + +def _get_chunks(shape, ncpu): + """Split the array into equal sized chunks based on the number of + available processors. The last chunk in each dimension absorbs the + remainder array elements if the number of CPUs does not divide evenly into + the number of array elements. + + Examples + -------- + >>> _get_chunks((4, 4), 4) + ((2, 2), (2, 2)) + >>> _get_chunks((4, 4), 2) + ((2, 2), (4,)) + >>> _get_chunks((5, 5), 2) + ((2, 3), (5,)) + >>> _get_chunks((2, 4), 2) + ((1, 1), (4,)) + """ + # since apply_parallel is in the critical import path, we lazy import + # math just when we need it. + from math import ceil + + chunks = [] + nchunks_per_dim = int(ceil(ncpu ** (1.0 / len(shape)))) + + used_chunks = 1 + for i in shape: + if used_chunks < ncpu: + regular_chunk = i // nchunks_per_dim + remainder_chunk = regular_chunk + (i % nchunks_per_dim) + + if regular_chunk == 0: + chunk_lens = (remainder_chunk,) + else: + chunk_lens = (regular_chunk,) * (nchunks_per_dim - 1) + ( + remainder_chunk, + ) + else: + chunk_lens = (i,) + + chunks.append(chunk_lens) + used_chunks *= nchunks_per_dim + return tuple(chunks) + + +def _ensure_dask_array(array, chunks=None): + import dask.array as da + + if isinstance(array, da.Array): + return array + + return da.from_array(array, chunks=chunks) + + +def apply_parallel( + function, + array, + chunks=None, + depth=0, + mode=None, + extra_arguments=(), + extra_keywords=None, + *, + dtype=None, + compute=None, + channel_axis=None, +): + """Map a function in parallel across an array. + + Split an array into possibly overlapping chunks of a given depth and + boundary type, call the given function in parallel on the chunks, combine + the chunks and return the resulting array. + + Parameters + ---------- + function : function + Function to be mapped which takes an array as an argument. + array : numpy array or dask array + Array which the function will be applied to. + chunks : int, tuple, or tuple of tuples, optional + A single integer is interpreted as the length of one side of a square + chunk that should be tiled across the array. One tuple of length + ``array.ndim`` represents the shape of a chunk, and it is tiled across + the array. A list of tuples of length ``ndim``, where each sub-tuple + is a sequence of chunk sizes along the corresponding dimension. If + None, the array is broken up into chunks based on the number of + available cpus. More information about chunks is in the documentation + `here `_. When + `channel_axis` is not None, the tuples can be length ``ndim - 1`` and + a single chunk will be used along the channel axis. + depth : int or sequence of int, optional + The depth of the added boundary cells. A tuple can be used to specify a + different depth per array axis. Defaults to zero. When `channel_axis` + is not None, and a tuple of length ``ndim - 1`` is provided, a depth of + 0 will be used along the channel axis. + mode : {'reflect', 'symmetric', 'periodic', 'wrap', 'nearest', 'edge'}, optional + Type of external boundary padding. + extra_arguments : tuple, optional + Tuple of arguments to be passed to the function. + extra_keywords : dictionary, optional + Dictionary of keyword arguments to be passed to the function. + dtype : data-type or None, optional + The data-type of the `function` output. If None, Dask will attempt to + infer this by calling the function on data of shape ``(1,) * ndim``. + For functions expecting RGB or multichannel data this may be + problematic. In such cases, the user should manually specify this dtype + argument instead. + + .. versionadded:: 0.18 + ``dtype`` was added in 0.18. + compute : bool, optional + If ``True``, compute eagerly returning a NumPy Array. + If ``False``, compute lazily returning a Dask Array. + If ``None`` (default), compute based on array type provided + (eagerly for NumPy Arrays and lazily for Dask Arrays). + channel_axis : int or None, optional + If None, the image is assumed to be a grayscale (single channel) image. + Otherwise, this parameter indicates which axis of the array corresponds + to channels. + + Returns + ------- + out : ndarray or dask Array + Returns the result of the applying the operation. + Type is dependent on the ``compute`` argument. + + Notes + ----- + Numpy edge modes 'symmetric', 'wrap', and 'edge' are converted to the + equivalent ``dask`` boundary modes 'reflect', 'periodic' and 'nearest', + respectively. + Setting ``compute=False`` can be useful for chaining later operations. + For example region selection to preview a result or storing large data + to disk instead of loading in memory. + + """ + try: + # Importing dask takes time. since apply_parallel is on the + # minimum import path of skimage, we lazy attempt to import dask + import dask.array as da + except ImportError: + raise RuntimeError( + "Could not import 'dask'. Please install " "using 'pip install dask'" + ) + + if extra_keywords is None: + extra_keywords = {} + + if compute is None: + compute = not isinstance(array, da.Array) + + if channel_axis is not None: + channel_axis = channel_axis % array.ndim + + if chunks is None: + shape = array.shape + try: + # since apply_parallel is in the critical import path, we lazy + # import multiprocessing just when we need it. + from multiprocessing import cpu_count + + ncpu = cpu_count() + except NotImplementedError: + ncpu = 4 + if channel_axis is not None: + # use a single chunk along the channel axis + spatial_shape = shape[:channel_axis] + shape[channel_axis + 1 :] + chunks = list(_get_chunks(spatial_shape, ncpu)) + chunks.insert(channel_axis, shape[channel_axis]) + chunks = tuple(chunks) + else: + chunks = _get_chunks(shape, ncpu) + elif channel_axis is not None and len(chunks) == array.ndim - 1: + # insert a single chunk along the channel axis + chunks = list(chunks) + chunks.insert(channel_axis, array.shape[channel_axis]) + chunks = tuple(chunks) + + if mode == 'wrap': + mode = 'periodic' + elif mode == 'symmetric': + mode = 'reflect' + elif mode == 'edge': + mode = 'nearest' + elif mode is None: + # default value for Dask. + # Note: that for dask >= 2022.03 it will change to 'none' so we set it + # here for consistent behavior across Dask versions. + mode = 'reflect' + + if channel_axis is not None: + if numpy.isscalar(depth): + # depth is zero along channel_axis + depth = [depth] * (array.ndim - 1) + depth = list(depth) + if len(depth) == array.ndim - 1: + depth.insert(channel_axis, 0) + depth = tuple(depth) + + def wrapped_func(arr): + return function(arr, *extra_arguments, **extra_keywords) + + darr = _ensure_dask_array(array, chunks=chunks) + + res = darr.map_overlap(wrapped_func, depth, boundary=mode, dtype=dtype) + if compute: + res = res.compute() + + return res diff --git a/lib/python3.10/site-packages/skimage/util/arraycrop.py b/lib/python3.10/site-packages/skimage/util/arraycrop.py new file mode 100644 index 0000000000000000000000000000000000000000..9d02ad16e26ca42992a205876036ffa4c891d796 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/arraycrop.py @@ -0,0 +1,72 @@ +""" +The arraycrop module contains functions to crop values from the edges of an +n-dimensional array. +""" + +import numpy as np +from numbers import Integral + +__all__ = ['crop'] + + +def crop(ar, crop_width, copy=False, order='K'): + """Crop array `ar` by `crop_width` along each dimension. + + Parameters + ---------- + ar : array-like of rank N + Input array. + crop_width : {sequence, int} + Number of values to remove from the edges of each axis. + ``((before_1, after_1),`` ... ``(before_N, after_N))`` specifies + unique crop widths at the start and end of each axis. + ``((before, after),) or (before, after)`` specifies + a fixed start and end crop for every axis. + ``(n,)`` or ``n`` for integer ``n`` is a shortcut for + before = after = ``n`` for all axes. + copy : bool, optional + If `True`, ensure the returned array is a contiguous copy. Normally, + a crop operation will return a discontiguous view of the underlying + input array. + order : {'C', 'F', 'A', 'K'}, optional + If ``copy==True``, control the memory layout of the copy. See + ``np.copy``. + + Returns + ------- + cropped : array + The cropped array. If ``copy=False`` (default), this is a sliced + view of the input array. + """ + ar = np.array(ar, copy=False) + + if isinstance(crop_width, Integral): + crops = [[crop_width, crop_width]] * ar.ndim + elif isinstance(crop_width[0], Integral): + if len(crop_width) == 1: + crops = [[crop_width[0], crop_width[0]]] * ar.ndim + elif len(crop_width) == 2: + crops = [crop_width] * ar.ndim + else: + raise ValueError( + f'crop_width has an invalid length: {len(crop_width)}\n' + f'crop_width should be a sequence of N pairs, ' + f'a single pair, or a single integer' + ) + elif len(crop_width) == 1: + crops = [crop_width[0]] * ar.ndim + elif len(crop_width) == ar.ndim: + crops = crop_width + else: + raise ValueError( + f'crop_width has an invalid length: {len(crop_width)}\n' + f'crop_width should be a sequence of N pairs, ' + f'a single pair, or a single integer' + ) + + slices = tuple(slice(a, ar.shape[i] - b) for i, (a, b) in enumerate(crops)) + if copy: + cropped = np.array(ar[slices], order=order, copy=True) + else: + cropped = ar[slices] + return cropped diff --git a/lib/python3.10/site-packages/skimage/util/compare.py b/lib/python3.10/site-packages/skimage/util/compare.py new file mode 100644 index 0000000000000000000000000000000000000000..c2e17d233d9a731a933c2c10a438e8ecca6e878b --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/compare.py @@ -0,0 +1,132 @@ +import functools +import warnings +from itertools import product + +import numpy as np + +from .dtype import img_as_float + + +def _rename_image_params(func): + wm_images = ( + "Since version 0.24, the two input images are named `image0` and " + "`image1` (instead of `image1` and `image2`, respectively). Please use " + "`image0, image1` to avoid this warning for now, and avoid an error " + "from version 0.26 onwards." + ) + + wm_method = ( + "Starting in version 0.24, all arguments following `image0, image1` " + "(including `method`) will be keyword-only. Please pass `method=` " + "in the function call to avoid this warning for now, and avoid an error " + "from version 0.26 onwards." + ) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Turn all args into kwargs + for i, (value, param) in enumerate( + zip(args, ["image0", "image1", "method", "n_tiles"]) + ): + if i >= 2: + warnings.warn(wm_method, category=FutureWarning, stacklevel=2) + if param in kwargs: + raise ValueError( + f"{param} passed both as positional and keyword argument." + ) + else: + kwargs[param] = value + args = tuple() + + # Account for `image2` if given + if "image2" in kwargs.keys(): + warnings.warn(wm_images, category=FutureWarning, stacklevel=2) + + # Safely move `image2` to `image1` if that's empty + if "image1" in kwargs.keys(): + # Safely move `image1` to `image0` + if "image0" in kwargs.keys(): + raise ValueError( + "Three input images given; please use only `image0` " + "and `image1`." + ) + kwargs["image0"] = kwargs.pop("image1") + kwargs["image1"] = kwargs.pop("image2") + + return func(*args, **kwargs) + + return wrapper + + +@_rename_image_params +def compare_images(image0, image1, *, method='diff', n_tiles=(8, 8)): + """ + Return an image showing the differences between two images. + + .. versionadded:: 0.16 + + Parameters + ---------- + image0, image1 : ndarray, shape (M, N) + Images to process, must be of the same shape. + + .. versionchanged:: 0.24 + `image1` and `image2` were renamed into `image0` and `image1` + respectively. + method : string, optional + Method used for the comparison. + Valid values are {'diff', 'blend', 'checkerboard'}. + Details are provided in the note section. + + .. versionchanged:: 0.24 + This parameter and following ones are keyword-only. + n_tiles : tuple, optional + Used only for the `checkerboard` method. Specifies the number + of tiles (row, column) to divide the image. + + Returns + ------- + comparison : ndarray, shape (M, N) + Image showing the differences. + + Notes + ----- + ``'diff'`` computes the absolute difference between the two images. + ``'blend'`` computes the mean value. + ``'checkerboard'`` makes tiles of dimension `n_tiles` that display + alternatively the first and the second image. Note that images must be + 2-dimensional to be compared with the checkerboard method. + """ + + if image1.shape != image0.shape: + raise ValueError('Images must have the same shape.') + + img1 = img_as_float(image0) + img2 = img_as_float(image1) + + if method == 'diff': + comparison = np.abs(img2 - img1) + elif method == 'blend': + comparison = 0.5 * (img2 + img1) + elif method == 'checkerboard': + if img1.ndim != 2: + raise ValueError( + 'Images must be 2-dimensional to be compared with the ' + 'checkerboard method.' + ) + shapex, shapey = img1.shape + mask = np.full((shapex, shapey), False) + stepx = int(shapex / n_tiles[0]) + stepy = int(shapey / n_tiles[1]) + for i, j in product(range(n_tiles[0]), range(n_tiles[1])): + if (i + j) % 2 == 0: + mask[i * stepx : (i + 1) * stepx, j * stepy : (j + 1) * stepy] = True + comparison = np.zeros_like(img1) + comparison[mask] = img1[mask] + comparison[~mask] = img2[~mask] + else: + raise ValueError( + 'Wrong value for `method`. ' + 'Must be either "diff", "blend" or "checkerboard".' + ) + return comparison diff --git a/lib/python3.10/site-packages/skimage/util/dtype.py b/lib/python3.10/site-packages/skimage/util/dtype.py new file mode 100644 index 0000000000000000000000000000000000000000..0b69b7b33acf9a8728102968305a60ea05f40910 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/dtype.py @@ -0,0 +1,600 @@ +import warnings +from warnings import warn + +import numpy as np + + +__all__ = [ + 'img_as_float32', + 'img_as_float64', + 'img_as_float', + 'img_as_int', + 'img_as_uint', + 'img_as_ubyte', + 'img_as_bool', + 'dtype_limits', +] + +# Some of these may or may not be aliases depending on architecture & platform +_integer_types = ( + np.int8, + np.byte, + np.int16, + np.short, + np.int32, + np.int64, + np.longlong, + np.int_, + np.intp, + np.intc, + int, + np.uint8, + np.ubyte, + np.uint16, + np.ushort, + np.uint32, + np.uint64, + np.ulonglong, + np.uint, + np.uintp, + np.uintc, +) +_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types} +dtype_range = { + bool: (False, True), + np.bool_: (False, True), + float: (-1, 1), + np.float16: (-1, 1), + np.float32: (-1, 1), + np.float64: (-1, 1), +} + +with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=DeprecationWarning) + + # np.bool8 is a deprecated alias of np.bool_ + if hasattr(np, 'bool8'): + dtype_range[np.bool8] = (False, True) + +dtype_range.update(_integer_ranges) + +_supported_types = list(dtype_range.keys()) + + +def dtype_limits(image, clip_negative=False): + """Return intensity limits, i.e. (min, max) tuple, of the image's dtype. + + Parameters + ---------- + image : ndarray + Input image. + clip_negative : bool, optional + If True, clip the negative range (i.e. return 0 for min intensity) + even if the image dtype allows negative values. + + Returns + ------- + imin, imax : tuple + Lower and upper intensity limits. + """ + imin, imax = dtype_range[image.dtype.type] + if clip_negative: + imin = 0 + return imin, imax + + +def _dtype_itemsize(itemsize, *dtypes): + """Return first of `dtypes` with itemsize greater than `itemsize` + + Parameters + ---------- + itemsize: int + The data type object element size. + + Other Parameters + ---------------- + *dtypes: + Any Object accepted by `np.dtype` to be converted to a data + type object + + Returns + ------- + dtype: data type object + First of `dtypes` with itemsize greater than `itemsize`. + + """ + return next(dt for dt in dtypes if np.dtype(dt).itemsize >= itemsize) + + +def _dtype_bits(kind, bits, itemsize=1): + """Return dtype of `kind` that can store a `bits` wide unsigned int + + Parameters: + kind: str + Data type kind. + bits: int + Desired number of bits. + itemsize: int + The data type object element size. + + Returns + ------- + dtype: data type object + Data type of `kind` that can store a `bits` wide unsigned int + + """ + + s = next( + i + for i in (itemsize,) + (2, 4, 8) + if bits < (i * 8) or (bits == (i * 8) and kind == 'u') + ) + + return np.dtype(kind + str(s)) + + +def _scale(a, n, m, copy=True): + """Scale an array of unsigned/positive integers from `n` to `m` bits. + + Numbers can be represented exactly only if `m` is a multiple of `n`. + + Parameters + ---------- + a : ndarray + Input image array. + n : int + Number of bits currently used to encode the values in `a`. + m : int + Desired number of bits to encode the values in `out`. + copy : bool, optional + If True, allocates and returns new array. Otherwise, modifies + `a` in place. + + Returns + ------- + out : array + Output image array. Has the same kind as `a`. + """ + kind = a.dtype.kind + if n > m and a.max() < 2**m: + mnew = int(np.ceil(m / 2) * 2) + if mnew > m: + dtype = f'int{mnew}' + else: + dtype = f'uint{mnew}' + n = int(np.ceil(n / 2) * 2) + warn( + f'Downcasting {a.dtype} to {dtype} without scaling because max ' + f'value {a.max()} fits in {dtype}', + stacklevel=3, + ) + return a.astype(_dtype_bits(kind, m)) + elif n == m: + return a.copy() if copy else a + elif n > m: + # downscale with precision loss + if copy: + b = np.empty(a.shape, _dtype_bits(kind, m)) + np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting='unsafe') + return b + else: + a //= 2 ** (n - m) + return a + elif m % n == 0: + # exact upscale to a multiple of `n` bits + if copy: + b = np.empty(a.shape, _dtype_bits(kind, m)) + np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) + return b + else: + a = a.astype(_dtype_bits(kind, m, a.dtype.itemsize), copy=False) + a *= (2**m - 1) // (2**n - 1) + return a + else: + # upscale to a multiple of `n` bits, + # then downscale with precision loss + o = (m // n + 1) * n + if copy: + b = np.empty(a.shape, _dtype_bits(kind, o)) + np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) + b //= 2 ** (o - m) + return b + else: + a = a.astype(_dtype_bits(kind, o, a.dtype.itemsize), copy=False) + a *= (2**o - 1) // (2**n - 1) + a //= 2 ** (o - m) + return a + + +def _convert(image, dtype, force_copy=False, uniform=False): + """ + Convert an image to the requested data-type. + + Warnings are issued in case of precision loss, or when negative values + are clipped during conversion to unsigned integer types (sign loss). + + Floating point values are expected to be normalized and will be clipped + to the range [0.0, 1.0] or [-1.0, 1.0] when converting to unsigned or + signed integers respectively. + + Numbers are not shifted to the negative side when converting from + unsigned to signed integer types. Negative values will be clipped when + converting to unsigned integers. + + Parameters + ---------- + image : ndarray + Input image. + dtype : dtype + Target data-type. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + uniform : bool, optional + Uniformly quantize the floating point range to the integer range. + By default (uniform=False) floating point values are scaled and + rounded to the nearest integers, which minimizes back and forth + conversion errors. + + .. versionchanged:: 0.15 + ``_convert`` no longer warns about possible precision or sign + information loss. See discussions on these warnings at: + https://github.com/scikit-image/scikit-image/issues/2602 + https://github.com/scikit-image/scikit-image/issues/543#issuecomment-208202228 + https://github.com/scikit-image/scikit-image/pull/3575 + + References + ---------- + .. [1] DirectX data conversion rules. + https://msdn.microsoft.com/en-us/library/windows/desktop/dd607323%28v=vs.85%29.aspx + .. [2] Data Conversions. In "OpenGL ES 2.0 Specification v2.0.25", + pp 7-8. Khronos Group, 2010. + .. [3] Proper treatment of pixels as integers. A.W. Paeth. + In "Graphics Gems I", pp 249-256. Morgan Kaufmann, 1990. + .. [4] Dirty Pixels. J. Blinn. In "Jim Blinn's corner: Dirty Pixels", + pp 47-57. Morgan Kaufmann, 1998. + + """ + image = np.asarray(image) + dtypeobj_in = image.dtype + if dtype is np.floating: + dtypeobj_out = np.dtype('float64') + else: + dtypeobj_out = np.dtype(dtype) + dtype_in = dtypeobj_in.type + dtype_out = dtypeobj_out.type + kind_in = dtypeobj_in.kind + kind_out = dtypeobj_out.kind + itemsize_in = dtypeobj_in.itemsize + itemsize_out = dtypeobj_out.itemsize + + # Below, we do an `issubdtype` check. Its purpose is to find out + # whether we can get away without doing any image conversion. This happens + # when: + # + # - the output and input dtypes are the same or + # - when the output is specified as a type, and the input dtype + # is a subclass of that type (e.g. `np.floating` will allow + # `float32` and `float64` arrays through) + + if np.issubdtype(dtype_in, dtype): + if force_copy: + image = image.copy() + return image + + if not (dtype_in in _supported_types and dtype_out in _supported_types): + raise ValueError(f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.') + + if kind_in in 'ui': + imin_in = np.iinfo(dtype_in).min + imax_in = np.iinfo(dtype_in).max + if kind_out in 'ui': + imin_out = np.iinfo(dtype_out).min + imax_out = np.iinfo(dtype_out).max + + # any -> binary + if kind_out == 'b': + return image > dtype_in(dtype_range[dtype_in][1] / 2) + + # binary -> any + if kind_in == 'b': + result = image.astype(dtype_out) + if kind_out != 'f': + result *= dtype_out(dtype_range[dtype_out][1]) + return result + + # float -> any + if kind_in == 'f': + if kind_out == 'f': + # float -> float + return image.astype(dtype_out) + + if np.min(image) < -1.0 or np.max(image) > 1.0: + raise ValueError("Images of type float must be between -1 and 1.") + # floating point -> integer + # use float type that can represent output integer type + computation_type = _dtype_itemsize( + itemsize_out, dtype_in, np.float32, np.float64 + ) + + if not uniform: + if kind_out == 'u': + image_out = np.multiply(image, imax_out, dtype=computation_type) + else: + image_out = np.multiply( + image, (imax_out - imin_out) / 2, dtype=computation_type + ) + image_out -= 1.0 / 2.0 + np.rint(image_out, out=image_out) + np.clip(image_out, imin_out, imax_out, out=image_out) + elif kind_out == 'u': + image_out = np.multiply(image, imax_out + 1, dtype=computation_type) + np.clip(image_out, 0, imax_out, out=image_out) + else: + image_out = np.multiply( + image, (imax_out - imin_out + 1.0) / 2.0, dtype=computation_type + ) + np.floor(image_out, out=image_out) + np.clip(image_out, imin_out, imax_out, out=image_out) + return image_out.astype(dtype_out) + + # signed/unsigned int -> float + if kind_out == 'f': + # use float type that can exactly represent input integers + computation_type = _dtype_itemsize( + itemsize_in, dtype_out, np.float32, np.float64 + ) + + if kind_in == 'u': + # using np.divide or np.multiply doesn't copy the data + # until the computation time + image = np.multiply(image, 1.0 / imax_in, dtype=computation_type) + # DirectX uses this conversion also for signed ints + # if imin_in: + # np.maximum(image, -1.0, out=image) + elif kind_in == 'i': + # From DirectX conversions: + # The most negative value maps to -1.0f + # Every other value is converted to a float (call it c) + # and then result = c * (1.0f / (2⁽ⁿ⁻¹⁾-1)). + + image = np.multiply(image, 1.0 / imax_in, dtype=computation_type) + np.maximum(image, -1.0, out=image) + + else: + image = np.add(image, 0.5, dtype=computation_type) + image *= 2 / (imax_in - imin_in) + + return np.asarray(image, dtype_out) + + # unsigned int -> signed/unsigned int + if kind_in == 'u': + if kind_out == 'i': + # unsigned int -> signed int + image = _scale(image, 8 * itemsize_in, 8 * itemsize_out - 1) + return image.view(dtype_out) + else: + # unsigned int -> unsigned int + return _scale(image, 8 * itemsize_in, 8 * itemsize_out) + + # signed int -> unsigned int + if kind_out == 'u': + image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out) + result = np.empty(image.shape, dtype_out) + np.maximum(image, 0, out=result, dtype=image.dtype, casting='unsafe') + return result + + # signed int -> signed int + if itemsize_in > itemsize_out: + return _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out - 1) + + image = image.astype(_dtype_bits('i', itemsize_out * 8)) + image -= imin_in + image = _scale(image, 8 * itemsize_in, 8 * itemsize_out, copy=False) + image += imin_out + return image.astype(dtype_out) + + +def convert(image, dtype, force_copy=False, uniform=False): + warn( + "The use of this function is discouraged as its behavior may change " + "dramatically in scikit-image 1.0. This function will be removed " + "in scikit-image 1.0.", + FutureWarning, + stacklevel=2, + ) + return _convert(image=image, dtype=dtype, force_copy=force_copy, uniform=uniform) + + +if _convert.__doc__ is not None: + convert.__doc__ = ( + _convert.__doc__ + + """ + + Warns + ----- + FutureWarning: + .. versionadded:: 0.17 + + The use of this function is discouraged as its behavior may change + dramatically in scikit-image 1.0. This function will be removed + in scikit-image 1.0. + """ + ) + + +def img_as_float32(image, force_copy=False): + """Convert an image to single-precision (32-bit) floating point format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of float32 + Output image. + + Notes + ----- + The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when + converting from unsigned or signed datatypes, respectively. + If the input image has a float type, intensity values are not modified + and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. + + """ + return _convert(image, np.float32, force_copy) + + +def img_as_float64(image, force_copy=False): + """Convert an image to double-precision (64-bit) floating point format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of float64 + Output image. + + Notes + ----- + The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when + converting from unsigned or signed datatypes, respectively. + If the input image has a float type, intensity values are not modified + and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. + + """ + return _convert(image, np.float64, force_copy) + + +def img_as_float(image, force_copy=False): + """Convert an image to floating point format. + + This function is similar to `img_as_float64`, but will not convert + lower-precision floating point arrays to `float64`. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of float + Output image. + + Notes + ----- + The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when + converting from unsigned or signed datatypes, respectively. + If the input image has a float type, intensity values are not modified + and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. + + """ + return _convert(image, np.floating, force_copy) + + +def img_as_uint(image, force_copy=False): + """Convert an image to 16-bit unsigned integer format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of uint16 + Output image. + + Notes + ----- + Negative input values will be clipped. + Positive values are scaled between 0 and 65535. + + """ + return _convert(image, np.uint16, force_copy) + + +def img_as_int(image, force_copy=False): + """Convert an image to 16-bit signed integer format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of int16 + Output image. + + Notes + ----- + The values are scaled between -32768 and 32767. + If the input data-type is positive-only (e.g., uint8), then + the output image will still only have positive values. + + """ + return _convert(image, np.int16, force_copy) + + +def img_as_ubyte(image, force_copy=False): + """Convert an image to 8-bit unsigned integer format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of ubyte (uint8) + Output image. + + Notes + ----- + Negative input values will be clipped. + Positive values are scaled between 0 and 255. + + """ + return _convert(image, np.uint8, force_copy) + + +def img_as_bool(image, force_copy=False): + """Convert an image to boolean format. + + Parameters + ---------- + image : ndarray + Input image. + force_copy : bool, optional + Force a copy of the data, irrespective of its current dtype. + + Returns + ------- + out : ndarray of bool (`bool_`) + Output image. + + Notes + ----- + The upper half of the input dtype's positive range is True, and the lower + half is False. All negative values (if present) are False. + + """ + return _convert(image, bool, force_copy) diff --git a/lib/python3.10/site-packages/skimage/util/lookfor.py b/lib/python3.10/site-packages/skimage/util/lookfor.py new file mode 100644 index 0000000000000000000000000000000000000000..f8c7e966819c72fb62841783c58c42342516fd4c --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/lookfor.py @@ -0,0 +1,30 @@ +import sys + +from .._vendored.numpy_lookfor import lookfor as _lookfor + + +def lookfor(what): + """Do a keyword search on scikit-image docstrings and print results. + + .. warning:: + + This function may also print results that are not part of + scikit-image's public API. + + Parameters + ---------- + what : str + Words to look for. + + Examples + -------- + >>> import skimage as ski + >>> ski.util.lookfor('regular_grid') + Search results for 'regular_grid' + --------------------------------- + skimage.util.regular_grid + Find `n_points` regularly spaced along `ar_shape`. + skimage.util.lookfor + Do a keyword search on scikit-image docstrings and print results. + """ + return _lookfor(what, sys.modules[__name__.split('.')[0]]) diff --git a/lib/python3.10/site-packages/skimage/util/noise.py b/lib/python3.10/site-packages/skimage/util/noise.py new file mode 100644 index 0000000000000000000000000000000000000000..fa1264a33a20247f22265ef7f312343cca1e9ba8 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/noise.py @@ -0,0 +1,233 @@ +__all__ = ['random_noise'] + + +import numpy as np +from .dtype import img_as_float + + +def _bernoulli(p, shape, *, rng): + """ + Bernoulli trials at a given probability of a given size. + + This function is meant as a lower-memory alternative to calls such as + `np.random.choice([True, False], size=image.shape, p=[p, 1-p])`. + While `np.random.choice` can handle many classes, for the 2-class case + (Bernoulli trials), this function is much more efficient. + + Parameters + ---------- + p : float + The probability that any given trial returns `True`. + shape : int or tuple of ints + The shape of the ndarray to return. + rng : `numpy.random.Generator` + ``Generator`` instance, typically obtained via `np.random.default_rng()`. + + Returns + ------- + out : ndarray[bool] + The results of Bernoulli trials in the given `size` where success + occurs with probability `p`. + """ + if p == 0: + return np.zeros(shape, dtype=bool) + if p == 1: + return np.ones(shape, dtype=bool) + return rng.random(shape) <= p + + +def random_noise(image, mode='gaussian', rng=None, clip=True, **kwargs): + """ + Function to add random noise of various types to a floating-point image. + + Parameters + ---------- + image : ndarray + Input image data. Will be converted to float. + mode : str, optional + One of the following strings, selecting the type of noise to add: + + 'gaussian' (default) + Gaussian-distributed additive noise. + 'localvar' + Gaussian-distributed additive noise, with specified local variance + at each point of `image`. + 'poisson' + Poisson-distributed noise generated from the data. + 'salt' + Replaces random pixels with 1. + 'pepper' + Replaces random pixels with 0 (for unsigned images) or -1 (for + signed images). + 's&p' + Replaces random pixels with either 1 or `low_val`, where `low_val` + is 0 for unsigned images or -1 for signed images. + 'speckle' + Multiplicative noise using ``out = image + n * image``, where ``n`` + is Gaussian noise with specified mean & variance. + rng : {`numpy.random.Generator`, int}, optional + Pseudo-random number generator. + By default, a PCG64 generator is used (see :func:`numpy.random.default_rng`). + If `rng` is an int, it is used to seed the generator. + clip : bool, optional + If True (default), the output will be clipped after noise applied + for modes `'speckle'`, `'poisson'`, and `'gaussian'`. This is + needed to maintain the proper image data range. If False, clipping + is not applied, and the output may extend beyond the range [-1, 1]. + mean : float, optional + Mean of random distribution. Used in 'gaussian' and 'speckle'. + Default : 0. + var : float, optional + Variance of random distribution. Used in 'gaussian' and 'speckle'. + Note: variance = (standard deviation) ** 2. Default : 0.01 + local_vars : ndarray, optional + Array of positive floats, same shape as `image`, defining the local + variance at every image point. Used in 'localvar'. + amount : float, optional + Proportion of image pixels to replace with noise on range [0, 1]. + Used in 'salt', 'pepper', and 'salt & pepper'. Default : 0.05 + salt_vs_pepper : float, optional + Proportion of salt vs. pepper noise for 's&p' on range [0, 1]. + Higher values represent more salt. Default : 0.5 (equal amounts) + + Returns + ------- + out : ndarray + Output floating-point image data on range [0, 1] or [-1, 1] if the + input `image` was unsigned or signed, respectively. + + Notes + ----- + Speckle, Poisson, Localvar, and Gaussian noise may generate noise outside + the valid image range. The default is to clip (not alias) these values, + but they may be preserved by setting `clip=False`. Note that in this case + the output may contain values outside the ranges [0, 1] or [-1, 1]. + Use this option with care. + + Because of the prevalence of exclusively positive floating-point images in + intermediate calculations, it is not possible to intuit if an input is + signed based on dtype alone. Instead, negative values are explicitly + searched for. Only if found does this function assume signed input. + Unexpected results only occur in rare, poorly exposes cases (e.g. if all + values are above 50 percent gray in a signed `image`). In this event, + manually scaling the input to the positive domain will solve the problem. + + The Poisson distribution is only defined for positive integers. To apply + this noise type, the number of unique values in the image is found and + the next round power of two is used to scale up the floating-point result, + after which it is scaled back down to the floating-point image range. + + To generate Poisson noise against a signed image, the signed image is + temporarily converted to an unsigned image in the floating point domain, + Poisson noise is generated, then it is returned to the original range. + + """ + mode = mode.lower() + + # Detect if a signed image was input + if image.min() < 0: + low_clip = -1.0 + else: + low_clip = 0.0 + + image = img_as_float(image) + + rng = np.random.default_rng(rng) + + allowedtypes = { + 'gaussian': 'gaussian_values', + 'localvar': 'localvar_values', + 'poisson': 'poisson_values', + 'salt': 'sp_values', + 'pepper': 'sp_values', + 's&p': 's&p_values', + 'speckle': 'gaussian_values', + } + + kwdefaults = { + 'mean': 0.0, + 'var': 0.01, + 'amount': 0.05, + 'salt_vs_pepper': 0.5, + 'local_vars': np.zeros_like(image) + 0.01, + } + + allowedkwargs = { + 'gaussian_values': ['mean', 'var'], + 'localvar_values': ['local_vars'], + 'sp_values': ['amount'], + 's&p_values': ['amount', 'salt_vs_pepper'], + 'poisson_values': [], + } + + for key in kwargs: + if key not in allowedkwargs[allowedtypes[mode]]: + raise ValueError( + f"{key} keyword not in allowed keywords " + f"{allowedkwargs[allowedtypes[mode]]}" + ) + + # Set kwarg defaults + for kw in allowedkwargs[allowedtypes[mode]]: + kwargs.setdefault(kw, kwdefaults[kw]) + + if mode == 'gaussian': + noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape) + out = image + noise + + elif mode == 'localvar': + # Ensure local variance input is correct + if (kwargs['local_vars'] <= 0).any(): + raise ValueError('All values of `local_vars` must be > 0.') + + # Safe shortcut usage broadcasts kwargs['local_vars'] as a ufunc + out = image + rng.normal(0, kwargs['local_vars'] ** 0.5) + + elif mode == 'poisson': + # Determine unique values in image & calculate the next power of two + vals = len(np.unique(image)) + vals = 2 ** np.ceil(np.log2(vals)) + + # Ensure image is exclusively positive + if low_clip == -1.0: + old_max = image.max() + image = (image + 1.0) / (old_max + 1.0) + + # Generating noise for each unique value in image. + out = rng.poisson(image * vals) / float(vals) + + # Return image to original range if input was signed + if low_clip == -1.0: + out = out * (old_max + 1.0) - 1.0 + + elif mode == 'salt': + # Re-call function with mode='s&p' and p=1 (all salt noise) + out = random_noise( + image, mode='s&p', rng=rng, amount=kwargs['amount'], salt_vs_pepper=1.0 + ) + + elif mode == 'pepper': + # Re-call function with mode='s&p' and p=1 (all pepper noise) + out = random_noise( + image, mode='s&p', rng=rng, amount=kwargs['amount'], salt_vs_pepper=0.0 + ) + + elif mode == 's&p': + out = image.copy() + p = kwargs['amount'] + q = kwargs['salt_vs_pepper'] + flipped = _bernoulli(p, image.shape, rng=rng) + salted = _bernoulli(q, image.shape, rng=rng) + peppered = ~salted + out[flipped & salted] = 1 + out[flipped & peppered] = low_clip + + elif mode == 'speckle': + noise = rng.normal(kwargs['mean'], kwargs['var'] ** 0.5, image.shape) + out = image + image * noise + + # Clip back to original range, if necessary + if clip: + out = np.clip(out, low_clip, 1.0) + + return out diff --git a/lib/python3.10/site-packages/skimage/util/shape.py b/lib/python3.10/site-packages/skimage/util/shape.py new file mode 100644 index 0000000000000000000000000000000000000000..a42df4c4074ea99568776bd31fc3ed9f884cba78 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/shape.py @@ -0,0 +1,247 @@ +import numbers +import numpy as np +from numpy.lib.stride_tricks import as_strided + +__all__ = ['view_as_blocks', 'view_as_windows'] + + +def view_as_blocks(arr_in, block_shape): + """Block view of the input n-dimensional array (using re-striding). + + Blocks are non-overlapping views of the input array. + + Parameters + ---------- + arr_in : ndarray, shape (M[, ...]) + Input array. + block_shape : tuple + The shape of the block. Each dimension must divide evenly into the + corresponding dimensions of `arr_in`. + + Returns + ------- + arr_out : ndarray + Block view of the input array. + + Examples + -------- + >>> import numpy as np + >>> from skimage.util.shape import view_as_blocks + >>> A = np.arange(4*4).reshape(4,4) + >>> A + array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]]) + >>> B = view_as_blocks(A, block_shape=(2, 2)) + >>> B[0, 0] + array([[0, 1], + [4, 5]]) + >>> B[0, 1] + array([[2, 3], + [6, 7]]) + >>> B[1, 0, 1, 1] + 13 + + >>> A = np.arange(4*4*6).reshape(4,4,6) + >>> A # doctest: +NORMALIZE_WHITESPACE + array([[[ 0, 1, 2, 3, 4, 5], + [ 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23]], + [[24, 25, 26, 27, 28, 29], + [30, 31, 32, 33, 34, 35], + [36, 37, 38, 39, 40, 41], + [42, 43, 44, 45, 46, 47]], + [[48, 49, 50, 51, 52, 53], + [54, 55, 56, 57, 58, 59], + [60, 61, 62, 63, 64, 65], + [66, 67, 68, 69, 70, 71]], + [[72, 73, 74, 75, 76, 77], + [78, 79, 80, 81, 82, 83], + [84, 85, 86, 87, 88, 89], + [90, 91, 92, 93, 94, 95]]]) + >>> B = view_as_blocks(A, block_shape=(1, 2, 2)) + >>> B.shape + (4, 2, 3, 1, 2, 2) + >>> B[2:, 0, 2] # doctest: +NORMALIZE_WHITESPACE + array([[[[52, 53], + [58, 59]]], + [[[76, 77], + [82, 83]]]]) + """ + if not isinstance(block_shape, tuple): + raise TypeError('block needs to be a tuple') + + block_shape = np.array(block_shape) + if (block_shape <= 0).any(): + raise ValueError("'block_shape' elements must be strictly positive") + + if block_shape.size != arr_in.ndim: + raise ValueError("'block_shape' must have the same length " "as 'arr_in.shape'") + + arr_shape = np.array(arr_in.shape) + if (arr_shape % block_shape).sum() != 0: + raise ValueError("'block_shape' is not compatible with 'arr_in'") + + # -- restride the array to build the block view + new_shape = tuple(arr_shape // block_shape) + tuple(block_shape) + new_strides = tuple(arr_in.strides * block_shape) + arr_in.strides + + arr_out = as_strided(arr_in, shape=new_shape, strides=new_strides) + + return arr_out + + +def view_as_windows(arr_in, window_shape, step=1): + """Rolling window view of the input n-dimensional array. + + Windows are overlapping views of the input array, with adjacent windows + shifted by a single row or column (or an index of a higher dimension). + + Parameters + ---------- + arr_in : ndarray, shape (M[, ...]) + Input array. + window_shape : integer or tuple of length arr_in.ndim + Defines the shape of the elementary n-dimensional orthotope + (better know as hyperrectangle [1]_) of the rolling window view. + If an integer is given, the shape will be a hypercube of + sidelength given by its value. + step : integer or tuple of length arr_in.ndim + Indicates step size at which extraction shall be performed. + If integer is given, then the step is uniform in all dimensions. + + Returns + ------- + arr_out : ndarray + (rolling) window view of the input array. + + Notes + ----- + One should be very careful with rolling views when it comes to + memory usage. Indeed, although a 'view' has the same memory + footprint as its base array, the actual array that emerges when this + 'view' is used in a computation is generally a (much) larger array + than the original, especially for 2-dimensional arrays and above. + + For example, let us consider a 3 dimensional array of size (100, + 100, 100) of ``float64``. This array takes about 8*100**3 Bytes for + storage which is just 8 MB. If one decides to build a rolling view + on this array with a window of (3, 3, 3) the hypothetical size of + the rolling view (if one was to reshape the view for example) would + be 8*(100-3+1)**3*3**3 which is about 203 MB! The scaling becomes + even worse as the dimension of the input array becomes larger. + + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Hyperrectangle + + Examples + -------- + >>> import numpy as np + >>> from skimage.util.shape import view_as_windows + >>> A = np.arange(4*4).reshape(4,4) + >>> A + array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15]]) + >>> window_shape = (2, 2) + >>> B = view_as_windows(A, window_shape) + >>> B[0, 0] + array([[0, 1], + [4, 5]]) + >>> B[0, 1] + array([[1, 2], + [5, 6]]) + + >>> A = np.arange(10) + >>> A + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + >>> window_shape = (3,) + >>> B = view_as_windows(A, window_shape) + >>> B.shape + (8, 3) + >>> B + array([[0, 1, 2], + [1, 2, 3], + [2, 3, 4], + [3, 4, 5], + [4, 5, 6], + [5, 6, 7], + [6, 7, 8], + [7, 8, 9]]) + + >>> A = np.arange(5*4).reshape(5, 4) + >>> A + array([[ 0, 1, 2, 3], + [ 4, 5, 6, 7], + [ 8, 9, 10, 11], + [12, 13, 14, 15], + [16, 17, 18, 19]]) + >>> window_shape = (4, 3) + >>> B = view_as_windows(A, window_shape) + >>> B.shape + (2, 2, 4, 3) + >>> B # doctest: +NORMALIZE_WHITESPACE + array([[[[ 0, 1, 2], + [ 4, 5, 6], + [ 8, 9, 10], + [12, 13, 14]], + [[ 1, 2, 3], + [ 5, 6, 7], + [ 9, 10, 11], + [13, 14, 15]]], + [[[ 4, 5, 6], + [ 8, 9, 10], + [12, 13, 14], + [16, 17, 18]], + [[ 5, 6, 7], + [ 9, 10, 11], + [13, 14, 15], + [17, 18, 19]]]]) + """ + + # -- basic checks on arguments + if not isinstance(arr_in, np.ndarray): + raise TypeError("`arr_in` must be a numpy ndarray") + + ndim = arr_in.ndim + + if isinstance(window_shape, numbers.Number): + window_shape = (window_shape,) * ndim + if not (len(window_shape) == ndim): + raise ValueError("`window_shape` is incompatible with `arr_in.shape`") + + if isinstance(step, numbers.Number): + if step < 1: + raise ValueError("`step` must be >= 1") + step = (step,) * ndim + if len(step) != ndim: + raise ValueError("`step` is incompatible with `arr_in.shape`") + + arr_shape = np.array(arr_in.shape) + window_shape = np.array(window_shape, dtype=arr_shape.dtype) + + if ((arr_shape - window_shape) < 0).any(): + raise ValueError("`window_shape` is too large") + + if ((window_shape - 1) < 0).any(): + raise ValueError("`window_shape` is too small") + + # -- build rolling window view + slices = tuple(slice(None, None, st) for st in step) + window_strides = np.array(arr_in.strides) + + indexing_strides = arr_in[slices].strides + + win_indices_shape = ( + (np.array(arr_in.shape) - np.array(window_shape)) // np.array(step) + ) + 1 + + new_shape = tuple(list(win_indices_shape) + list(window_shape)) + strides = tuple(list(indexing_strides) + list(window_strides)) + + arr_out = as_strided(arr_in, shape=new_shape, strides=strides) + return arr_out diff --git a/lib/python3.10/site-packages/skimage/util/tests/test_apply_parallel.py b/lib/python3.10/site-packages/skimage/util/tests/test_apply_parallel.py new file mode 100644 index 0000000000000000000000000000000000000000..7eaf569fb0deb0f9191aa61c4de38c639d886912 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/tests/test_apply_parallel.py @@ -0,0 +1,173 @@ +import numpy as np + +from skimage._shared.testing import assert_array_almost_equal, assert_equal +from skimage import color, data, img_as_float +from skimage.filters import threshold_local, gaussian +from skimage.util.apply_parallel import apply_parallel + +import pytest + +da = pytest.importorskip('dask.array') + + +def test_apply_parallel(): + # data + a = np.arange(144).reshape(12, 12).astype(float) + + # apply the filter + expected1 = threshold_local(a, 3) + result1 = apply_parallel( + threshold_local, + a, + chunks=(6, 6), + depth=5, + extra_arguments=(3,), + extra_keywords={'mode': 'reflect'}, + ) + + assert_array_almost_equal(result1, expected1) + + def wrapped_gauss(arr): + return gaussian(arr, sigma=1, mode='reflect') + + expected2 = gaussian(a, sigma=1, mode='reflect') + result2 = apply_parallel(wrapped_gauss, a, chunks=(6, 6), depth=5) + + assert_array_almost_equal(result2, expected2) + + expected3 = gaussian(a, sigma=1, mode='reflect') + result3 = apply_parallel( + wrapped_gauss, da.from_array(a, chunks=(6, 6)), depth=5, compute=True + ) + + assert isinstance(result3, np.ndarray) + assert_array_almost_equal(result3, expected3) + + +def test_apply_parallel_lazy(): + # data + a = np.arange(144).reshape(12, 12).astype(float) + d = da.from_array(a, chunks=(6, 6)) + + # apply the filter + expected1 = threshold_local(a, 3) + result1 = apply_parallel( + threshold_local, + a, + chunks=(6, 6), + depth=5, + extra_arguments=(3,), + extra_keywords={'mode': 'reflect'}, + compute=False, + ) + + # apply the filter on a Dask Array + result2 = apply_parallel( + threshold_local, + d, + depth=5, + extra_arguments=(3,), + extra_keywords={'mode': 'reflect'}, + ) + + assert isinstance(result1, da.Array) + + assert_array_almost_equal(result1.compute(), expected1) + + assert isinstance(result2, da.Array) + + assert_array_almost_equal(result2.compute(), expected1) + + +def test_no_chunks(): + a = np.ones(1 * 4 * 8 * 9).reshape(1, 4, 8, 9) + + def add_42(arr): + return arr + 42 + + expected = add_42(a) + result = apply_parallel(add_42, a) + + assert_array_almost_equal(result, expected) + + +def test_apply_parallel_wrap(): + def wrapped(arr): + return gaussian(arr, sigma=1, mode='wrap') + + a = np.arange(144).reshape(12, 12).astype(float) + expected = gaussian(a, sigma=1, mode='wrap') + result = apply_parallel(wrapped, a, chunks=(6, 6), depth=5, mode='wrap') + + assert_array_almost_equal(result, expected) + + +def test_apply_parallel_nearest(): + def wrapped(arr): + return gaussian(arr, sigma=1, mode='nearest') + + a = np.arange(144).reshape(12, 12).astype(float) + expected = gaussian(a, sigma=1, mode='nearest') + result = apply_parallel( + wrapped, a, chunks=(6, 6), depth={0: 5, 1: 5}, mode='nearest' + ) + + assert_array_almost_equal(result, expected) + + +@pytest.mark.parametrize('dtype', (np.float32, np.float64)) +@pytest.mark.parametrize('chunks', (None, (128, 128, 3))) +@pytest.mark.parametrize('depth', (0, 8, (8, 8, 0))) +def test_apply_parallel_rgb(depth, chunks, dtype): + cat = data.chelsea().astype(dtype) / 255.0 + + func = color.rgb2ycbcr + cat_ycbcr_expected = func(cat) + cat_ycbcr = apply_parallel( + func, cat, chunks=chunks, depth=depth, dtype=dtype, channel_axis=-1 + ) + + assert_equal(cat_ycbcr.dtype, cat.dtype) + + assert_array_almost_equal(cat_ycbcr_expected, cat_ycbcr) + + +@pytest.mark.parametrize('chunks', (None, (128, 256), 'ndim')) +@pytest.mark.parametrize('depth', (0, 8, (8, 16), 'ndim')) +@pytest.mark.parametrize('channel_axis', (0, 1, 2, -1, -2, -3)) +def test_apply_parallel_rgb_channel_axis(depth, chunks, channel_axis): + """Test channel_axis combinations. + + For depth and chunks, test in three ways: + 1.) scalar (to be applied over all axes) + 2.) tuple of length ``image.ndim - 1`` corresponding to spatial axes + 3.) tuple of length ``image.ndim`` corresponding to all axes + """ + cat = img_as_float(data.chelsea()) + + func = color.rgb2ycbcr + cat_ycbcr_expected = func(cat, channel_axis=-1) + + # move channel axis to another position + cat = np.moveaxis(cat, -1, channel_axis) + if chunks == 'ndim': + # explicitly specify the chunksize for the channel axis + chunks = [128, 128] + chunks.insert(channel_axis % cat.ndim, cat.shape[channel_axis]) + if depth == 'ndim': + # explicitly specify the depth for the channel axis + depth = [8, 8] + depth.insert(channel_axis % cat.ndim, 0) + cat_ycbcr = apply_parallel( + func, + cat, + chunks=chunks, + depth=depth, + dtype=cat.dtype, + channel_axis=channel_axis, + extra_keywords=dict(channel_axis=channel_axis), + ) + # move channels of output back to the last dimension + cat_ycbcr = np.moveaxis(cat_ycbcr, channel_axis, -1) + + assert_array_almost_equal(cat_ycbcr_expected, cat_ycbcr) diff --git a/lib/python3.10/site-packages/skimage/util/tests/test_arraycrop.py b/lib/python3.10/site-packages/skimage/util/tests/test_arraycrop.py new file mode 100644 index 0000000000000000000000000000000000000000..2aba640bc23e20db897568d7f2471b9911d3f9fc --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/tests/test_arraycrop.py @@ -0,0 +1,71 @@ +import numpy as np +from skimage.util import crop +from skimage._shared.testing import assert_array_equal, assert_equal + + +def test_multi_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, ((1, 2), (2, 1))) + assert_array_equal(out[0], [7, 8]) + assert_array_equal(out[-1], [32, 33]) + assert_equal(out.shape, (6, 2)) + + +def test_pair_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, (1, 2)) + assert_array_equal(out[0], [6, 7]) + assert_array_equal(out[-1], [31, 32]) + assert_equal(out.shape, (6, 2)) + + +def test_pair_tuple_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, ((1, 2),)) + assert_array_equal(out[0], [6, 7]) + assert_array_equal(out[-1], [31, 32]) + assert_equal(out.shape, (6, 2)) + + +def test_int_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, 1) + assert_array_equal(out[0], [6, 7, 8]) + assert_array_equal(out[-1], [36, 37, 38]) + assert_equal(out.shape, (7, 3)) + + +def test_int_tuple_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, (1,)) + assert_array_equal(out[0], [6, 7, 8]) + assert_array_equal(out[-1], [36, 37, 38]) + assert_equal(out.shape, (7, 3)) + + +def test_copy_crop(): + arr = np.arange(45).reshape(9, 5) + out0 = crop(arr, 1, copy=True) + assert out0.flags.c_contiguous + out0[0, 0] = 100 + assert not np.any(arr == 100) + assert not np.may_share_memory(arr, out0) + + out1 = crop(arr, 1) + out1[0, 0] = 100 + assert arr[1, 1] == 100 + assert np.may_share_memory(arr, out1) + + +def test_zero_crop(): + arr = np.arange(45).reshape(9, 5) + out = crop(arr, 0) + assert out.shape == (9, 5) + + +def test_np_int_crop(): + arr = np.arange(45).reshape(9, 5) + out1 = crop(arr, np.int64(1)) + out2 = crop(arr, np.int32(1)) + assert_array_equal(out1, out2) + assert out1.shape == (7, 3) diff --git a/lib/python3.10/site-packages/skimage/util/tests/test_compare.py b/lib/python3.10/site-packages/skimage/util/tests/test_compare.py new file mode 100644 index 0000000000000000000000000000000000000000..9317e56b94cd90c13fc314c78f722067a5d1f445 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/tests/test_compare.py @@ -0,0 +1,105 @@ +import numpy as np +import pytest + +from skimage.util.compare import compare_images +from skimage._shared.testing import assert_stacklevel + + +def test_compare_images_ValueError_shape(): + img1 = np.zeros((10, 10), dtype=np.uint8) + img2 = np.zeros((10, 1), dtype=np.uint8) + with pytest.raises(ValueError): + compare_images(img1, img2) + + +def test_compare_images_ValueError_args(): + a = np.ones((10, 10)) * 3 + b = np.zeros((10, 10)) + with pytest.raises(ValueError): + compare_images(a, b, method="unknown") + + +def test_compare_images_diff(): + img1 = np.zeros((10, 10), dtype=np.uint8) + img1[3:8, 3:8] = 255 + img2 = np.zeros_like(img1) + img2[3:8, 0:8] = 255 + expected_result = np.zeros_like(img1, dtype=np.float64) + expected_result[3:8, 0:3] = 1 + result = compare_images(img1, img2, method='diff') + np.testing.assert_array_equal(result, expected_result) + + +def test_compare_images_replaced_param(): + img1 = np.zeros((10, 10), dtype=np.uint8) + img1[3:8, 3:8] = 255 + img2 = np.zeros_like(img1) + img2[3:8, 0:8] = 255 + expected_result = np.zeros_like(img1, dtype=np.float64) + expected_result[3:8, 0:3] = 1 + + regex = ".*Please use `image0, image1`.*" + with pytest.warns(FutureWarning, match=regex) as record: + result = compare_images(image1=img1, image2=img2) + assert_stacklevel(record) + np.testing.assert_array_equal(result, expected_result) + + with pytest.warns(FutureWarning, match=regex) as record: + result = compare_images(image0=img1, image2=img2) + assert_stacklevel(record) + np.testing.assert_array_equal(result, expected_result) + + with pytest.warns(FutureWarning, match=regex) as record: + result = compare_images(img1, image2=img2) + assert_stacklevel(record) + np.testing.assert_array_equal(result, expected_result) + + # Test making "method" keyword-only here as well + # so whole test can be removed in one go + regex = ".*Please pass `method=`.*" + with pytest.warns(FutureWarning, match=regex) as record: + result = compare_images(img1, img2, "diff") + assert_stacklevel(record) + np.testing.assert_array_equal(result, expected_result) + + +def test_compare_images_blend(): + img1 = np.zeros((10, 10), dtype=np.uint8) + img1[3:8, 3:8] = 255 + img2 = np.zeros_like(img1) + img2[3:8, 0:8] = 255 + expected_result = np.zeros_like(img1, dtype=np.float64) + expected_result[3:8, 3:8] = 1 + expected_result[3:8, 0:3] = 0.5 + result = compare_images(img1, img2, method='blend') + np.testing.assert_array_equal(result, expected_result) + + +def test_compare_images_checkerboard_default(): + img1 = np.zeros((2**4, 2**4), dtype=np.uint8) + img2 = np.full(img1.shape, fill_value=255, dtype=np.uint8) + res = compare_images(img1, img2, method='checkerboard') + # fmt: off + exp_row1 = np.array([0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1.]) + exp_row2 = np.array([1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0., 1., 1., 0., 0.]) + # fmt: on + for i in (0, 1, 4, 5, 8, 9, 12, 13): + np.testing.assert_array_equal(res[i, :], exp_row1) + for i in (2, 3, 6, 7, 10, 11, 14, 15): + np.testing.assert_array_equal(res[i, :], exp_row2) + + +def test_compare_images_checkerboard_tuple(): + img1 = np.zeros((2**4, 2**4), dtype=np.uint8) + img2 = np.full(img1.shape, fill_value=255, dtype=np.uint8) + res = compare_images(img1, img2, method='checkerboard', n_tiles=(4, 8)) + exp_row1 = np.array( + [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0] + ) + exp_row2 = np.array( + [1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0] + ) + for i in (0, 1, 2, 3, 8, 9, 10, 11): + np.testing.assert_array_equal(res[i, :], exp_row1) + for i in (4, 5, 6, 7, 12, 13, 14, 15): + np.testing.assert_array_equal(res[i, :], exp_row2) diff --git a/lib/python3.10/site-packages/skimage/util/tests/test_dtype.py b/lib/python3.10/site-packages/skimage/util/tests/test_dtype.py new file mode 100644 index 0000000000000000000000000000000000000000..63483cdef7ff023f1c0e6ee2089187e3618e54b6 --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/tests/test_dtype.py @@ -0,0 +1,241 @@ +import numpy as np +import itertools +from skimage import ( + img_as_float, + img_as_float32, + img_as_float64, + img_as_int, + img_as_uint, + img_as_ubyte, +) +from skimage.util.dtype import _convert + +from skimage._shared._warnings import expected_warnings +from skimage._shared import testing +from skimage._shared.testing import assert_equal, parametrize + + +dtype_range = { + np.uint8: (0, 255), + np.uint16: (0, 65535), + np.int8: (-128, 127), + np.int16: (-32768, 32767), + np.float32: (-1.0, 1.0), + np.float64: (-1.0, 1.0), +} + + +img_funcs = (img_as_int, img_as_float64, img_as_float32, img_as_uint, img_as_ubyte) +dtypes_for_img_funcs = (np.int16, np.float64, np.float32, np.uint16, np.ubyte) +img_funcs_and_types = zip(img_funcs, dtypes_for_img_funcs) + + +def _verify_range(msg, x, vmin, vmax, dtype): + assert_equal(x[0], vmin) + assert_equal(x[-1], vmax) + assert x.dtype == dtype + + +@parametrize("dtype, f_and_dt", itertools.product(dtype_range, img_funcs_and_types)) +def test_range(dtype, f_and_dt): + imin, imax = dtype_range[dtype] + x = np.linspace(imin, imax, 10).astype(dtype) + + f, dt = f_and_dt + + y = f(x) + + omin, omax = dtype_range[dt] + + if imin == 0 or omin == 0: + omin = 0 + imin = 0 + + _verify_range( + f"From {np.dtype(dtype)} to {np.dtype(dt)}", y, omin, omax, np.dtype(dt) + ) + + +# Add non-standard data types that are allowed by the `_convert` function. +dtype_range_extra = dtype_range.copy() +dtype_range_extra.update( + {np.int32: (-2147483648, 2147483647), np.uint32: (0, 4294967295)} +) + +dtype_pairs = [ + (np.uint8, np.uint32), + (np.int8, np.uint32), + (np.int8, np.int32), + (np.int32, np.int8), + (np.float64, np.float32), + (np.int32, np.float32), +] + + +@parametrize("dtype_in, dt", dtype_pairs) +def test_range_extra_dtypes(dtype_in, dt): + """Test code paths that are not skipped by `test_range`""" + + imin, imax = dtype_range_extra[dtype_in] + x = np.linspace(imin, imax, 10).astype(dtype_in) + + y = _convert(x, dt) + + omin, omax = dtype_range_extra[dt] + _verify_range( + f"From {np.dtype(dtype_in)} to {np.dtype(dt)}", y, omin, omax, np.dtype(dt) + ) + + +def test_downcast(): + x = np.arange(10).astype(np.uint64) + with expected_warnings(['Downcasting']): + y = img_as_int(x) + assert np.allclose(y, x.astype(np.int16)) + assert y.dtype == np.int16, y.dtype + + +def test_float_out_of_range(): + too_high = np.array([2], dtype=np.float32) + with testing.raises(ValueError): + img_as_int(too_high) + too_low = np.array([-2], dtype=np.float32) + with testing.raises(ValueError): + img_as_int(too_low) + + +def test_float_float_all_ranges(): + arr_in = np.array([[-10.0, 10.0, 1e20]], dtype=np.float32) + np.testing.assert_array_equal(img_as_float(arr_in), arr_in) + + +def test_copy(): + x = np.array([1], dtype=np.float64) + y = img_as_float(x) + z = img_as_float(x, force_copy=True) + + assert y is x + assert z is not x + + +def test_bool(): + img_ = np.zeros((10, 10), bool) + img8 = np.zeros((10, 10), np.bool_) + img_[1, 1] = True + img8[1, 1] = True + for func, dt in [ + (img_as_int, np.int16), + (img_as_float, np.float64), + (img_as_uint, np.uint16), + (img_as_ubyte, np.ubyte), + ]: + converted_ = func(img_) + assert np.sum(converted_) == dtype_range[dt][1] + converted8 = func(img8) + assert np.sum(converted8) == dtype_range[dt][1] + + +def test_clobber(): + # The `img_as_*` functions should never modify input arrays. + for func_input_type in img_funcs: + for func_output_type in img_funcs: + img = np.random.rand(5, 5) + + img_in = func_input_type(img) + img_in_before = img_in.copy() + func_output_type(img_in) + + assert_equal(img_in, img_in_before) + + +def test_signed_scaling_float32(): + x = np.array([-128, 127], dtype=np.int8) + y = img_as_float32(x) + assert_equal(y.max(), 1) + + +def test_float32_passthrough(): + x = np.array([-1, 1], dtype=np.float32) + y = img_as_float(x) + assert_equal(y.dtype, x.dtype) + + +float_dtype_list = [ + float, + float, + np.float64, + np.single, + np.float32, + np.float64, + 'float32', + 'float64', +] + + +def test_float_conversion_dtype(): + """Test any conversion from a float dtype to an other.""" + x = np.array([-1, 1]) + + # Test all combinations of dtypes conversions + dtype_combin = np.array(np.meshgrid(float_dtype_list, float_dtype_list)).T.reshape( + -1, 2 + ) + + for dtype_in, dtype_out in dtype_combin: + x = x.astype(dtype_in) + y = _convert(x, dtype_out) + assert y.dtype == np.dtype(dtype_out) + + +def test_float_conversion_dtype_warns(): + """Test that convert issues a warning when called""" + from skimage.util.dtype import convert + + x = np.array([-1, 1]) + + # Test all combinations of dtypes conversions + dtype_combin = np.array(np.meshgrid(float_dtype_list, float_dtype_list)).T.reshape( + -1, 2 + ) + + for dtype_in, dtype_out in dtype_combin: + x = x.astype(dtype_in) + with expected_warnings(["The use of this function is discouraged"]): + y = convert(x, dtype_out) + assert y.dtype == np.dtype(dtype_out) + + +def test_subclass_conversion(): + """Check subclass conversion behavior""" + x = np.array([-1, 1]) + + for dtype in float_dtype_list: + x = x.astype(dtype) + y = _convert(x, np.floating) + assert y.dtype == x.dtype + + +def test_int_to_float(): + """Check Normalization when casting img_as_float from int types to float""" + int_list = np.arange(9, dtype=np.int64) + converted = img_as_float(int_list) + assert np.allclose(converted, int_list * 1e-19, atol=0.0, rtol=0.1) + + ii32 = np.iinfo(np.int32) + ii_list = np.array([ii32.min, ii32.max], dtype=np.int32) + floats = img_as_float(ii_list) + + assert_equal(floats.max(), 1) + assert_equal(floats.min(), -1) + + +def test_img_as_ubyte_supports_npulonglong(): + # Pre NumPy <2.0.0, `data_scaled.dtype.type` is `np.ulonglong` instead of + # np.uint64 as one might expect. This caused issues with `img_as_ubyte` due + # to `np.ulonglong` missing from `skimage.util.dtype._integer_types`. + # This doesn't seem to be an issue for NumPy >=2.0.0. + # https://github.com/scikit-image/scikit-image/issues/7385 + data = np.arange(50, dtype=np.uint64) + data_scaled = data * 256 ** (data.dtype.itemsize - 1) + result = img_as_ubyte(data_scaled) + assert result.dtype == np.uint8 diff --git a/lib/python3.10/site-packages/skimage/util/unique.py b/lib/python3.10/site-packages/skimage/util/unique.py new file mode 100644 index 0000000000000000000000000000000000000000..8a857bde5dadc4ab40ff38f2c330b7346eaa4dcd --- /dev/null +++ b/lib/python3.10/site-packages/skimage/util/unique.py @@ -0,0 +1,51 @@ +import numpy as np + + +def unique_rows(ar): + """Remove repeated rows from a 2D array. + + In particular, if given an array of coordinates of shape + (Npoints, Ndim), it will remove repeated points. + + Parameters + ---------- + ar : ndarray, shape (M, N) + The input array. + + Returns + ------- + ar_out : ndarray, shape (P, N) + A copy of the input array with repeated rows removed. + + Raises + ------ + ValueError : if `ar` is not two-dimensional. + + Notes + ----- + The function will generate a copy of `ar` if it is not + C-contiguous, which will negatively affect performance for large + input arrays. + + Examples + -------- + >>> ar = np.array([[1, 0, 1], + ... [0, 1, 0], + ... [1, 0, 1]], np.uint8) + >>> unique_rows(ar) + array([[0, 1, 0], + [1, 0, 1]], dtype=uint8) + """ + if ar.ndim != 2: + raise ValueError( + "unique_rows() only makes sense for 2D arrays, " f"got {ar.ndim}" + ) + # the view in the next line only works if the array is C-contiguous + ar = np.ascontiguousarray(ar) + # np.unique() finds identical items in a raveled array. To make it + # see each row as a single item, we create a view of each row as a + # byte string of length itemsize times number of columns in `ar` + ar_row_view = ar.view(f"|S{ar.itemsize * ar.shape[1]}") + _, unique_row_indices = np.unique(ar_row_view, return_index=True) + ar_out = ar[unique_row_indices] + return ar_out