PKiV coincidence/__init__.py#!/usr/bin/env python3 # # __init__.py """ Helper functions for pytest. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # stdlib import datetime # this package from coincidence.fixtures import fixed_datetime, original_datadir, path_separator, tmp_pathplus # noqa: F401 from coincidence.params import count, testing_boolean_values, whitespace_perms # noqa: F401 from coincidence.regressions import ( # noqa: F401 AdvancedDataRegressionFixture, AdvancedFileRegressionFixture, SupportsAsDict, advanced_data_regression, advanced_file_regression, check_file_output, check_file_regression ) from coincidence.selectors import ( # noqa: F401 max_version, min_version, not_docker, not_macos, not_pypy, not_windows, only_docker, only_macos, only_pypy, only_version, only_windows, platform_boolean_factory ) from coincidence.utils import ( # noqa: F401 generate_falsy_values, generate_truthy_values, is_docker, whitespace, whitespace_perms_list, with_fixed_datetime ) # import sys __author__: str = "Dominic Davis-Foster" __copyright__: str = "2020-2021 Dominic Davis-Foster" __license__: str = "MIT License" __version__: str = "0.6.4" __email__: str = "dominic@davis-foster.co.uk" __all__ = ("pytest_report_header", "PEP_563") def pytest_report_header(config, startdir) -> str: # noqa: MAN001 """ Prints the start time of the pytest session. """ return f"Test session started at {datetime.datetime.now():%H:%M:%S}" PEP_563: bool = False # (sys.version_info[:2] >= (3, 11)) """ :py:obj:`True` if the current Python version implements :pep:`563` -- Postponed Evaluation of Annotations. .. note:: This is currently set to :py:obj:`False` until the future of typing PEPs has been determined. No released versions of Python currently have :pep:`563` enabled by default. .. versionchanged:: 0.6.0 Temporarily set to :py:obj:`False` regardless of version. """ PKiVHgcoincidence/fixtures.py#!/usr/bin/env python # # fixtures.py r""" Pytest fixtures. To enable the fixtures add the following to ``conftest.py`` in your test directory: .. code-block:: python pytest_plugins = ("coincidence", ) See `the pytest documentation`_ for more information. .. _the pytest documentation: https://pytest.org/en/latest/how-to/plugins.html """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # stdlib import datetime import os from pathlib import Path from typing import Iterator # 3rd party import pytest from domdf_python_tools.paths import PathPlus # this package from coincidence.utils import with_fixed_datetime __import__("pytest_datadir") __all__ = ("fixed_datetime", "original_datadir", "tmp_pathplus", "path_separator") @pytest.fixture() def tmp_pathplus(tmp_path: Path) -> PathPlus: """ Pytest fixture which returns a temporary directory in the form of a :class:`~domdf_python_tools.paths.PathPlus` object. The directory is unique to each test function invocation, created as a sub directory of the base temporary directory. Use it as follows: .. code-block:: python pytest_plugins = ("coincidence", ) def test_something(tmp_pathplus: PathPlus): assert True """ # noqa: D400 return PathPlus(tmp_path) @pytest.fixture() def original_datadir(request) -> Path: # noqa: D103 # Work around pycharm confusing datadir with test file. return PathPlus(os.path.splitext(request.module.__file__)[0] + '_') @pytest.fixture() def fixed_datetime(monkeypatch) -> Iterator: """ Pytest fixture to pretend the current datetime is 2:20 AM on 13th October 2020. .. seealso:: The :func:`~.with_fixed_datetime` contextmanager. .. attention:: The monkeypatching only works when datetime is used and imported like: .. code-block:: python import datetime print(datetime.datetime.now()) Using ``from datetime import datetime`` won't work. """ with with_fixed_datetime(datetime.datetime(2020, 10, 13, 2, 20)): yield @pytest.fixture( params=[ pytest.param( '/', id="forward", marks=pytest.mark.skipif( os.sep == '\\', reason=r"Output differs on platforms where os.sep == '\\'" ) ), pytest.param( '\\', id="backward", marks=pytest.mark.skipif( os.sep == '/', reason="Output differs on platforms where os.sep == '/'" ) ), ] ) def path_separator(request) -> str: r""" Parametrized pytest fixture which returns the current filesystem path separator and skips the test for the other. This is useful when the test output differs on platforms with ``\`` as the path separator, such as windows. .. versionadded:: 0.4.0 :rtype: .. clearpage:: """ return request.param PKiVHΕ< < coincidence/params.py#!/usr/bin/env python # # params.py """ `pytest.mark.parametrize `_ decorators. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # "param" based on pytest # Copyright (c) 2004-2020 Holger Krekel and others # MIT Licensed # # stdlib import itertools import random from typing import Callable, Collection, Iterable, List, Optional, Sequence, Tuple, TypeVar, Union, cast, overload # 3rd party import pytest from _pytest.mark import Mark, MarkDecorator, ParameterSet # nodep from domdf_python_tools.iterative import extend_with_none # this package from coincidence.selectors import _make_version, only_version from coincidence.utils import generate_falsy_values, generate_truthy_values, whitespace_perms_list __all__ = ("count", "whitespace_perms", "testing_boolean_values", "param", "parametrized_versions") _T = TypeVar("_T") MarkDecorator.__module__ = "_pytest.mark" def testing_boolean_values( extra_truthy: Sequence = (), extra_falsy: Sequence = (), ratio: float = 1, ) -> MarkDecorator: """ Returns a `pytest.mark.parametrize `__ decorator which provides a list of strings, integers and booleans, and the boolean representations of them. The parametrized arguments are ``boolean_string`` for the input value, and ``expected_boolean`` for the expected output. Optionally, a random selection of the values can be returned using the ``ratio`` argument. :param extra_truthy: Additional values to treat as :py:obj:`True`. :param extra_falsy: Additional values to treat as :py:obj:`False`. :param ratio: The ratio of the number of values to select to the total number of values. """ # noqa: D400 truthy = generate_truthy_values(extra_truthy, ratio) falsy = generate_falsy_values(extra_falsy, ratio) boolean_strings = [ # pylint: disable=use-tuple-over-list *itertools.zip_longest(truthy, [], fillvalue=True), *itertools.zip_longest(falsy, [], fillvalue=False), ] return pytest.mark.parametrize("boolean_string, expected_boolean", boolean_strings) def whitespace_perms(ratio: float = 0.5) -> MarkDecorator: r""" Returns a `pytest.mark.parametrize `__ decorator which provides permutations of whitespace. For this function whitespace is only ``␣\n\t\r``. Not all permutations are returned, as there are a lot of them; instead a random selection of the permutations is returned. By default ½ of the permutations are returned, but this can be configured using the ``ratio`` argument. The single parametrized argument is ``char``. :param ratio: The ratio of the number of permutations to select to the total number of permutations. """ # noqa: D400 perms = whitespace_perms_list() return pytest.mark.parametrize("char", random.sample(perms, int(len(perms) * ratio))) def count(stop: int, start: int = 0, step: int = 1) -> MarkDecorator: """ Returns a `pytest.mark.parametrize `__ decorator which provides a list of numbers between ``start`` and ``stop`` with an interval of ``step``. The single parametrized argument is ``count``. :param stop: The stop value passed to :class:`range`. :param start: The start value passed to :class:`range`. :param step: The step passed to :class:`range`. """ # noqa: D400 return pytest.mark.parametrize("count", range(start, stop, step)) @overload def param( *values: object, marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), id: Optional[str] = ..., # noqa: A002 # pylint: disable=redefined-builtin ) -> ParameterSet: ... @overload def param( *values: object, marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), idx: Optional[int], ) -> ParameterSet: ... @overload def param( *values: _T, marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), key: Optional[Callable[[Tuple[_T, ...]], str]], ) -> ParameterSet: ... def param( *values: _T, marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), id: Optional[str] = None, # noqa: A002 # pylint: disable=redefined-builtin idx: Optional[int] = None, key: Optional[Callable[[Tuple[_T, ...]], str]] = None, ) -> ParameterSet: r""" Specify a parameter in `pytest.mark.parametrize `__ calls or :ref:`parametrized fixtures `. **Examples:** .. code-block:: python @pytest.mark.parametrize("test_input, expected", [ ("3+5", 8), param("6*9", 42, marks=pytest.mark.xfail), param("2**2", 4, idx=0), param("3**2", 9, id="3^2"), param("sqrt(9)", 3, key=itemgetter(0)), ]) def test_eval(test_input, expected): assert eval (test_input) == expected .. versionadded:: 0.4.0 :param \*values: Variable args of the values of the parameter set, in order. :param marks: A single mark or a list of marks to be applied to this parameter set. :param id: The id to attribute to this parameter set. :param idx: The index of the value in ``*values`` to use as the id. :param key: A callable which is given ``values`` (as a :class:`tuple`) and returns the value to use as the id. :rtype: .. clearpage:: """ # noqa: D400 if len([x for x in (id, idx, key) if x is not None]) > 1: raise ValueError("'id', 'idx' and 'key' are mutually exclusive.") if idx is not None: # pytest will catch the type error later on id = cast(str, values[idx]) # noqa: A001 # pylint: disable=redefined-builtin elif key is not None: id = key(values) # noqa: A001 # pylint: disable=redefined-builtin return ParameterSet.param(*values, marks=marks, id=id) def parametrized_versions( *versions: Union[str, float, Tuple[int, ...]], reasons: Union[str, Iterable[Optional[str]]] = (), ) -> List[ParameterSet]: r""" Return a list of parametrized version numbers. **Examples:** .. code-block:: python @pytest.mark.parametrize( "version", parametrized_versions( 3.6, 3.7, 3.8, reason="Output differs on each version.", ), ) def test_something(version: str): pass .. code-block:: python @pytest.fixture( params=parametrized_versions( 3.6, 3.7, 3.8, reason="Output differs on each version.", ), ) def version(request): return request.param def test_something(version: str): pass .. versionadded:: 0.4.0 :param \*versions: The Python versions to parametrize. :param reasons: The reasons to use when skipping versions. Either a string value to use for all versions, or a list of values which correspond to ``*versions``. """ version_list = list(versions) params = [] if isinstance(reasons, str): reasons = [reasons] * len(version_list) else: reasons = extend_with_none(reasons, len(version_list)) for version, reason in zip(version_list, reasons): version_ = _make_version(version) the_param = pytest.param( f"{version_.major}.{version_.minor}", marks=only_version(version_, reason=reason), ) params.append(the_param) return params PKiVcoincidence/py.typedPKiVd.//coincidence/regressions.py#!/usr/bin/env python # # regressions.py """ Regression test helpers. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # Based on https://github.com/ESSS/pytest-regressions # Copyright (c) 2018 ESSS # MIT Licensed # # stdlib import collections import pathlib from collections import ChainMap, Counter, OrderedDict, defaultdict from contextlib import suppress from functools import partial from types import MappingProxyType from typing import Any, Callable, Collection, Dict, Mapping, Optional, Sequence, Type, TypeVar, Union, cast # 3rd party import pytest from _pytest.capture import CaptureResult # nodep from domdf_python_tools.paths import PathPlus from domdf_python_tools.stringlist import StringList from domdf_python_tools.typing import PathLike from pytest_regressions.common import check_text_files, perform_regression_check from pytest_regressions.file_regression import FileRegressionFixture from typing_extensions import Protocol, runtime_checkable __all__ = ( "AdvancedDataRegressionFixture", "AdvancedFileRegressionFixture", "SupportsAsDict", "advanced_data_regression", "advanced_file_regression", "check_file_regression", "check_file_output", ) _C = TypeVar("_C", bound=Callable) try: # 3rd party from pytest_regressions.data_regression import DataRegressionFixture, RegressionYamlDumper def _representer_for(*data_type: Type): # noqa: MAN002 def deco(representer_fn: _C) -> _C: for dtype in data_type: RegressionYamlDumper.add_custom_yaml_representer(dtype, representer_fn) return representer_fn return deco @_representer_for( collections.abc.Mapping, collections.OrderedDict, collections.Counter, collections.defaultdict, MappingProxyType, ) def _represent_mappings(dumper: RegressionYamlDumper, data: Mapping): # noqa: MAN002 data = dict(data) return dumper.represent_data(data) @_representer_for(collections.abc.Sequence, tuple) def _represent_sequences(dumper: RegressionYamlDumper, data: Collection): # noqa: MAN002 if isinstance(data, SupportsAsDict): data = dict(data._asdict()) else: data = list(data) return dumper.represent_data(data) @_representer_for(CaptureResult) def _represent_captureresult(dumper: RegressionYamlDumper, data): # noqa: MAN001,MAN002 data = dict(out=data.out.splitlines(), err=data.err.splitlines()) return dumper.represent_data(data) with suppress(ImportError): # 3rd party import toml _representer_for(toml.decoder.InlineTableDict)(_represent_mappings) @_representer_for( pathlib.PurePath, pathlib.PurePosixPath, pathlib.PureWindowsPath, pathlib.Path, PathPlus, ) def _represent_pathlib(dumper: RegressionYamlDumper, data: pathlib.PurePath): # noqa: MAN002 return dumper.represent_str(data.as_posix()) except ImportError as e: # pragma: no cover if not str(e).endswith("'yaml'"): raise class DataRegressionFixture: # type: ignore[no-redef] """ Placeholder ``DataRegressionFixture`` for when PyYAML can't be imported. """ def __init__(self, *args, **kwargs): raise e def check_file_regression( data: Union[str, StringList], file_regression: FileRegressionFixture, extension: str = ".txt", **kwargs, ) -> bool: r""" Check the given data against that in the reference file. :param data: :param file_regression: The file regression fixture for the test. :param extension: The extension of the reference file. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. .. seealso:: :meth:`.AdvancedFileRegressionFixture.check` """ __tracebackhide__ = True if isinstance(data, StringList): data = str(data) file_regression.check(data, encoding="UTF-8", extension=extension, **kwargs) return True def check_file_output( filename: PathLike, file_regression: FileRegressionFixture, extension: Optional[str] = None, newline: Optional[str] = '\n', **kwargs, ) -> bool: r""" Check the content of the given text file against the reference file. :param filename: :param file_regression: The file regression fixture for the test. :param extension: The extension of the reference file. If :py:obj:`None` the extension is determined from ``filename``. :param newline: Controls how universal newlines mode works. See :func:`open`. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. .. seealso:: :meth:`.AdvancedFileRegressionFixture.check_file` """ filename = PathPlus(filename) data = filename.read_text(encoding="UTF-8") extension = extension or filename.suffix if extension == ".py": extension = "._py_" __tracebackhide__ = True return check_file_regression(data, file_regression, extension, newline=newline, **kwargs) @runtime_checkable class SupportsAsDict(Protocol): """ :class:`typing.Protocol` for classes like :func:`collections.namedtuple` and :class:`typing.NamedTuple` which implement an :meth:`~._asdict` method. """ # noqa: D400 def _asdict(self) -> Dict[str, Any]: """ Return a new dict which maps field names to their corresponding values. """ class AdvancedDataRegressionFixture(DataRegressionFixture): """ Subclass of :class:`~pytest_regressions.data_regression.DataRegressionFixture` with support for additional types. The following types and their subclasses are supported: * :class:`collections.abc.Mapping`, :class:`typing.Mapping` (including :class:`dict` and :class:`typing.Dict`) * :class:`collections.abc.Sequence`, :class:`typing.Sequence` (including :class:`list`, :py:obj:`typing.Tuple` etc.) * :class:`collections.OrderedDict`, :class:`typing.OrderedDict` * :class:`collections.Counter`, :class:`typing.Counter` * :class:`types.MappingProxyType` (cannot be subclassed) * :class:`_pytest.capture.CaptureResult` (the type of :meth:`capsys.readouterr() `) * Any type which implements the :protocol:`SupportsAsDict` protocol (including :func:`collections.namedtuple` and :class:`typing.NamedTuple`) .. clearpage:: .. autoclasssumm:: AdvancedDataRegressionFixture :autosummary-sections: ;; """ def check( self, data_dict: Union[Sequence, SupportsAsDict, Mapping, MappingProxyType, CaptureResult], basename: Optional[str] = None, fullpath: Optional[str] = None, ) -> None: """ Checks ``data`` against a previously recorded version, or generates a new file. :param data_dict: :param basename: The basename of the file to test/record. If not given the name of the test is used. :param fullpath: The complete path to use as a reference file. This option will ignore ``datadir`` fixture when reading *expected* files, but will still use it to write *obtained* files. Useful if a reference file is located in the session data dir, for example. .. note:: ``basename`` and ``fullpath`` are exclusive. """ if isinstance(data_dict, (Mapping, OrderedDict, Counter, defaultdict, MappingProxyType, ChainMap)): data_dict = dict(data_dict) elif isinstance(data_dict, SupportsAsDict): data_dict = dict(data_dict._asdict()) elif isinstance(data_dict, CaptureResult): data_dict = dict(out=data_dict.out.splitlines(), err=data_dict.err.splitlines()) elif isinstance(data_dict, Sequence): data_dict = list(data_dict) __tracebackhide__ = True super().check(data_dict, basename=basename, fullpath=fullpath) @pytest.fixture() def advanced_data_regression(datadir, original_datadir, request) -> AdvancedDataRegressionFixture: # noqa: MAN001 """ Pytest fixture for performing regression tests on lists, dictionaries and namedtuples. """ return AdvancedDataRegressionFixture(datadir, original_datadir, request) class AdvancedFileRegressionFixture(FileRegressionFixture): """ Subclass of :class:`~pytest_regressions.file_regression.FileRegressionFixture` with UTF-8 by default and some extra methods. .. versionadded:: 0.2.0 """ # noqa: D400 def check( # type: ignore[override] self, contents: Union[str, StringList], encoding: Optional[str] = "UTF-8", extension: str = ".txt", newline: Optional[str] = None, basename: Optional[str] = None, fullpath: Optional[str] = None, binary: bool = False, obtained_filename: Optional[str] = None, check_fn: Optional[Callable[[Any, Any], Any]] = None, ) -> None: r""" Checks the contents against a previously recorded version, or generates a new file. :param contents: :param extension: The extension of the reference file. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. .. seealso:: :func:`~.check_file_regression` """ __tracebackhide__ = True if isinstance(contents, StringList): contents = str(contents) if not isinstance(contents, str): raise TypeError(f"Expected text contents but received type {type(contents).__name__!r}") if check_fn is None: check_fn = partial(check_text_files, encoding="UTF-8") def dump_fn(filename: PathLike) -> None: PathPlus(filename).write_clean(cast(str, contents)) perform_regression_check( datadir=self.datadir, original_datadir=self.original_datadir, request=self.request, check_fn=check_fn, dump_fn=dump_fn, extension=extension, basename=basename, fullpath=fullpath, force_regen=self.force_regen, obtained_filename=obtained_filename, ) def check_bytes(self, contents: bytes, **kwargs) -> None: # pragma: no cover (Windows) r""" Checks the bytes contents against a previously recorded version, or generates a new file. :param contents: :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. """ __tracebackhide__ = True super().check(contents, binary=True, **kwargs) def check_file( self, filename: PathLike, extension: Optional[str] = None, newline: Optional[str] = '\n', **kwargs, ) -> None: r""" Check the content of the given text file against the reference file. :param filename: :param extension: The extension of the reference file. If :py:obj:`None` the extension is determined from ``filename``. :param newline: Controls how universal newlines mode works. See :func:`open`. :param \*\*kwargs: Additional keyword arguments passed to :meth:`pytest_regressions.file_regression.FileRegressionFixture.check`. .. seealso:: :func:`~.check_file_output` """ filename = PathPlus(filename) data = filename.read_text(encoding="UTF-8") extension = extension or filename.suffix if extension == ".py": extension = "._py_" __tracebackhide__ = True return self.check(data, extension=extension, newline=newline, **kwargs) @pytest.fixture() def advanced_file_regression(datadir, original_datadir, request) -> AdvancedFileRegressionFixture: # noqa: MAN001 r""" Pytest fixture for performing regression tests on strings, bytes and files. .. versionadded:: 0.2.0 """ return AdvancedFileRegressionFixture(datadir, original_datadir, request) PKiVܨcoincidence/selectors.py#!/usr/bin/env python # # selectors.py """ Pytest decorators for selectively running tests. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # stdlib import inspect import sys from textwrap import dedent from typing import Callable, Optional, Tuple, Union, cast # 3rd party import pytest from _pytest.mark import MarkDecorator # nodep from domdf_python_tools.compat import PYPY from domdf_python_tools.versions import Version # this package from coincidence.utils import is_docker __all__ = ( "min_version", "max_version", "only_version", "not_windows", "only_windows", "not_pypy", "only_pypy", "not_macos", "only_macos", "not_docker", "not_linux", "only_linux", "only_docker", "platform_boolean_factory", ) def _make_version(version: Union[str, float, Tuple[int, ...]]) -> Version: if isinstance(version, float): return Version.from_float(version) elif isinstance(version, str): return Version.from_str(version) else: return Version.from_tuple(version) def min_version( version: Union[str, float, Tuple[int, ...]], reason: Optional[str] = None, ) -> MarkDecorator: """ Factory function to return a ``@pytest.mark.skipif`` decorator which will skip a test if the current Python version is less than the required one. :param version: The version number to compare to :py:data:`sys.version_info`. :param reason: The reason to display when skipping. :default reason: :file:`'Requires Python {} or greater.'` """ # noqa: D400 version_ = _make_version(version) if reason is None: # pragma: no cover reason = f"Requires Python {version_} or greater." return pytest.mark.skipif(condition=sys.version_info[:3] < version_, reason=reason) def max_version( version: Union[str, float, Tuple[int, ...]], reason: Optional[str] = None, ) -> MarkDecorator: """ Factory function to return a ``@pytest.mark.skipif`` decorator which will skip a test if the current Python version is greater than the required one. :param version: The version number to compare to :py:data:`sys.version_info`. :param reason: The reason to display when skipping. :default reason: :file:`'Not needed after Python {}.'` """ # noqa: D400 version_ = _make_version(version) if reason is None: # pragma: no cover reason = f"Not needed after Python {version_}." return pytest.mark.skipif(condition=sys.version_info[:3] > version_, reason=reason) def only_version( version: Union[str, float, Tuple[int, ...]], reason: Optional[str] = None, ) -> MarkDecorator: """ Factory function to return a ``@pytest.mark.skipif`` decorator which will skip a test if the current Python version not the required one. :param version: The version number to compare to :py:data:`sys.version_info`. :param reason: The reason to display when skipping. :default reason: :file:`'Not needed on Python {}.'` """ # noqa: D400 version_ = _make_version(version) if reason is None: # pragma: no cover reason = f"Not needed on Python {version_}." return pytest.mark.skipif(condition=sys.version_info[:2] != version_[:2], reason=reason) def platform_boolean_factory( condition: bool, platform: str, versionadded: Optional[str] = None, *, module: Optional[str] = None, ) -> Tuple[Callable[..., MarkDecorator], Callable[..., MarkDecorator]]: """ Factory function to return decorators such as :func:`~.not_pypy` and :func:`~.only_windows`. :param condition: Should evaluate to :py:obj:`True` if the test should be skipped. :param platform: :param versionadded: :param module: The module to set the function as belonging to in ``__module__``. If :py:obj:`None` ``__module__`` is set to ``'coincidence.selectors'``. :return: 2-element tuple of ``not_function``, ``only_function``. """ default_reason = f"{{}} required on {platform}" module = module or platform_boolean_factory.__module__ def not_function(reason: str = default_reason.format("Not")) -> MarkDecorator: return pytest.mark.skipif(condition=condition, reason=reason) def only_function(reason: str = default_reason.format("Only")) -> MarkDecorator: return pytest.mark.skipif(condition=not condition, reason=reason) docstring = dedent( """\ Factory function to return a ``@pytest.mark.skipif`` decorator which will skip a test {why} the current platform is {platform}. {versionadded_string} :param reason: The reason to display when skipping. """ ) if versionadded: versionadded_string = f".. versionadded:: {versionadded}\n" else: versionadded_string = '' not_function.__name__ = not_function.__qualname__ = f"not_{platform.lower()}" not_function.__module__ = module not_function.__doc__ = docstring.format(why="if", platform=platform, versionadded_string=versionadded_string) only_function.__name__ = only_function.__qualname__ = f"only_{platform.lower()}" only_function.__module__ = module only_function.__doc__ = docstring.format( why="unless", platform=platform, versionadded_string=versionadded_string ) return not_function, only_function not_windows, only_windows = platform_boolean_factory(condition=sys.platform == "win32", platform="Windows") only_windows.__doc__ = f"""\ {inspect.cleandoc(only_windows.__doc__ or '')} :rtype: .. latex:clearpage:: """ not_macos, only_macos = platform_boolean_factory(condition=sys.platform == "darwin", platform="macOS") not_linux, only_linux = platform_boolean_factory( condition=sys.platform == "linux", platform="Linux", versionadded="0.2.0" ) not_linux.__doc__ = f"""\ {inspect.cleandoc(not_linux.__doc__ or '')} :rtype: .. latex:clearpage:: """ not_docker, only_docker = platform_boolean_factory(condition=is_docker(), platform="Docker") not_docker.__doc__ = cast(str, not_docker.__doc__).replace("the current platform is", "running on") only_docker.__doc__ = cast(str, only_docker.__doc__).replace("the current platform is", "running on") not_pypy, only_pypy = platform_boolean_factory(condition=PYPY, platform="PyPy") not_pypy.__doc__ = cast(str, not_pypy.__doc__).replace("current platform", "current Python implementation") only_pypy.__doc__ = cast(str, only_pypy.__doc__).replace("current platform", "current Python implementation") PKiV9"yqcoincidence/utils.py#!/usr/bin/env python # # utils.py """ Test helper utilities. """ # # Copyright © 2020-2021 Dominic Davis-Foster # # 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. # # is_docker based on https://github.com/jaraco/jaraco.docker # Copyright Jason R. Coombs # MIT Licensed # # stdlib import datetime import os import random from contextlib import contextmanager from functools import lru_cache from itertools import chain, permutations from typing import Any, Iterable, Iterator, List, Optional, Sequence, TypeVar, Union # 3rd party import pytest from domdf_python_tools.compat import PYPY from domdf_python_tools.iterative import Len from domdf_python_tools.paths import PathPlus __all__ = ( "generate_truthy_values", "generate_falsy_values", "is_docker", "with_fixed_datetime", "whitespace", "whitespace_perms_list", ) _T = TypeVar("_T") _cgroup = PathPlus("/proc/self/cgroup") _dockerenv = "/.dockerenv" def is_docker() -> bool: """ Returns whether the current Python instance is running in Docker. """ if os.path.exists(_dockerenv): return True if _cgroup.is_file(): try: return any("docker" in line for line in _cgroup.read_lines()) except FileNotFoundError: return False return False class _DateMeta(type): # pragma: no cover (PyPy) _date = datetime.date def __instancecheck__(self, instance: Any): # noqa: MAN002 return isinstance(instance, self._date) class _DatetimeMeta(type): # pragma: no cover (PyPy) _datetime = datetime.datetime def __instancecheck__(self, instance: Any) -> bool: return isinstance(instance, self._datetime) @contextmanager def with_fixed_datetime(fixed_datetime: datetime.datetime) -> Iterator: """ Context manager to set a fixed datetime for the duration of the ``with`` block. :param fixed_datetime: .. seealso:: The :fixture:`~.fixed_datetime` fixture. .. attention:: The monkeypatching only works when datetime is used and imported like: .. code-block:: python import datetime print(datetime.datetime.now()) Using ``from datetime import datetime`` won't work. """ if PYPY: # pragma: no cover (!PyPy) with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr( datetime.date, "today", lambda *args: datetime.date( fixed_datetime.year, fixed_datetime.month, fixed_datetime.day, ) ) monkeypatch.setattr( datetime.datetime, "today", lambda *args: datetime.datetime( fixed_datetime.year, fixed_datetime.month, fixed_datetime.day, ) ) monkeypatch.setattr(datetime.datetime, "now", lambda *args: fixed_datetime) yield else: # pragma: no cover (PyPy) class D(datetime.date, metaclass=_DateMeta): @classmethod def today(cls) -> datetime.date: # type: ignore[override] return datetime.date( fixed_datetime.year, fixed_datetime.month, fixed_datetime.day, ) class DT(datetime.datetime, metaclass=_DatetimeMeta): @classmethod def today(cls) -> datetime.datetime: # type: ignore[override] return datetime.datetime( fixed_datetime.year, fixed_datetime.month, fixed_datetime.day, ) @classmethod def now(cls, tz: Optional[datetime.tzinfo] = None) -> datetime.datetime: # type: ignore[override] return datetime.datetime.fromtimestamp(fixed_datetime.timestamp()) D.__name__ = "date" D.__qualname__ = "date" DT.__qualname__ = "datetime" DT.__name__ = "datetime" D.__module__ = "datetime" DT.__module__ = "datetime" with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(datetime, "date", D) monkeypatch.setattr(datetime, "datetime", DT) yield def generate_truthy_values( extra_truthy: Iterable[Union[str, int, _T]] = (), ratio: float = 1, ) -> Iterator[Union[str, int, _T]]: """ Returns an iterator of strings, integers and booleans which should be considered :py:obj:`True`. Optionally, a random selection of the values can be returned using the ``ratio`` argument. :param extra_truthy: Additional values which should be considered :py:obj:`True`. :param ratio: The ratio of the number of values to select to the total number of values. """ truthy_values: Sequence[Union[str, int, _T]] = [ True, "True", "true", "tRUe", 'y', 'Y', "YES", "yes", "Yes", "yEs", "ON", "on", '1', 1, *extra_truthy, ] if ratio < 1: truthy_values = random.sample(truthy_values, int(len(truthy_values) * ratio)) yield from truthy_values def generate_falsy_values( extra_falsy: Iterable[Union[str, int, _T]] = (), ratio: float = 1, ) -> Iterator[Union[str, int, _T]]: """ Returns an iterator of strings, integers and booleans which should be considered :py:obj:`False`. Optionally, a random selection of the values can be returned using the ``ratio`` argument. :param extra_falsy: Additional values which should be considered :py:obj:`True`. :param ratio: The ratio of the number of values to select to the total number of values. """ falsy_values: Sequence[Union[str, int, _T]] = [ False, "False", "false", "falSE", 'n', 'N', "NO", "no", "nO", "OFF", "off", "oFF", '0', 0, *extra_falsy, ] if ratio < 1: falsy_values = random.sample(falsy_values, int(len(falsy_values) * ratio)) yield from falsy_values whitespace = " \t\n\r" @lru_cache(1) def whitespace_perms_list() -> List[str]: # noqa: D103 perms = chain.from_iterable(permutations(whitespace, n) for n in Len(whitespace)) return list(''.join(x) for x in perms) PK$iVl}((#coincidence-0.6.4.dist-info/LICENSECopyright (c) 2021 Dominic Davis-Foster 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. PK$iVn$coincidence-0.6.4.dist-info/METADATAMetadata-Version: 2.1 Name: coincidence Version: 0.6.4 Summary: Helper functions for pytest. Author-email: Dominic Davis-Foster License: MIT Keywords: pytest,regression,testing,unittest,utilities Home-page: https://github.com/python-coincidence/coincidence Project-URL: Issue Tracker, https://github.com/python-coincidence/coincidence/issues Project-URL: Source Code, https://github.com/python-coincidence/coincidence Project-URL: Documentation, https://coincidence.readthedocs.io/en/latest Platform: Windows Platform: macOS Platform: Linux Classifier: Development Status :: 4 - Beta Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Software Development :: Testing :: Unit Classifier: Typing :: Typed Requires-Python: >=3.6 Requires-Dist: domdf-python-tools>=2.8.0 Requires-Dist: pytest>=6.2.0 Requires-Dist: pytest-regressions>=2.0.2 Requires-Dist: typing-extensions>=3.7.4.3 Description-Content-Type: text/x-rst ############ coincidence ############ .. start short_desc **Helper functions for pytest.** .. end short_desc .. start shields .. list-table:: :stub-columns: 1 :widths: 10 90 * - Docs - |docs| |docs_check| * - Tests - |actions_linux| |actions_windows| |actions_macos| |coveralls| * - PyPI - |pypi-version| |supported-versions| |supported-implementations| |wheel| * - Anaconda - |conda-version| |conda-platform| * - Activity - |commits-latest| |commits-since| |maintained| |pypi-downloads| * - QA - |codefactor| |actions_flake8| |actions_mypy| * - Other - |license| |language| |requires| .. |docs| image:: https://img.shields.io/readthedocs/coincidence/latest?logo=read-the-docs :target: https://coincidence.readthedocs.io/en/latest :alt: Documentation Build Status .. |docs_check| image:: https://github.com/python-coincidence/coincidence/workflows/Docs%20Check/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22Docs+Check%22 :alt: Docs Check Status .. |actions_linux| image:: https://github.com/python-coincidence/coincidence/workflows/Linux/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22Linux%22 :alt: Linux Test Status .. |actions_windows| image:: https://github.com/python-coincidence/coincidence/workflows/Windows/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22Windows%22 :alt: Windows Test Status .. |actions_macos| image:: https://github.com/python-coincidence/coincidence/workflows/macOS/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22macOS%22 :alt: macOS Test Status .. |actions_flake8| image:: https://github.com/python-coincidence/coincidence/workflows/Flake8/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22Flake8%22 :alt: Flake8 Status .. |actions_mypy| image:: https://github.com/python-coincidence/coincidence/workflows/mypy/badge.svg :target: https://github.com/python-coincidence/coincidence/actions?query=workflow%3A%22mypy%22 :alt: mypy status .. |requires| image:: https://dependency-dash.repo-helper.uk/github/python-coincidence/coincidence/badge.svg :target: https://dependency-dash.repo-helper.uk/github/python-coincidence/coincidence/ :alt: Requirements Status .. |coveralls| image:: https://img.shields.io/coveralls/github/python-coincidence/coincidence/master?logo=coveralls :target: https://coveralls.io/github/python-coincidence/coincidence?branch=master :alt: Coverage .. |codefactor| image:: https://img.shields.io/codefactor/grade/github/python-coincidence/coincidence?logo=codefactor :target: https://www.codefactor.io/repository/github/python-coincidence/coincidence :alt: CodeFactor Grade .. |pypi-version| image:: https://img.shields.io/pypi/v/coincidence :target: https://pypi.org/project/coincidence/ :alt: PyPI - Package Version .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/coincidence?logo=python&logoColor=white :target: https://pypi.org/project/coincidence/ :alt: PyPI - Supported Python Versions .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/coincidence :target: https://pypi.org/project/coincidence/ :alt: PyPI - Supported Implementations .. |wheel| image:: https://img.shields.io/pypi/wheel/coincidence :target: https://pypi.org/project/coincidence/ :alt: PyPI - Wheel .. |conda-version| image:: https://img.shields.io/conda/v/domdfcoding/coincidence?logo=anaconda :target: https://anaconda.org/domdfcoding/coincidence :alt: Conda - Package Version .. |conda-platform| image:: https://img.shields.io/conda/pn/domdfcoding/coincidence?label=conda%7Cplatform :target: https://anaconda.org/domdfcoding/coincidence :alt: Conda - Platform .. |license| image:: https://img.shields.io/github/license/python-coincidence/coincidence :target: https://github.com/python-coincidence/coincidence/blob/master/LICENSE :alt: License .. |language| image:: https://img.shields.io/github/languages/top/python-coincidence/coincidence :alt: GitHub top language .. |commits-since| image:: https://img.shields.io/github/commits-since/python-coincidence/coincidence/v0.6.4 :target: https://github.com/python-coincidence/coincidence/pulse :alt: GitHub commits since tagged version .. |commits-latest| image:: https://img.shields.io/github/last-commit/python-coincidence/coincidence :target: https://github.com/python-coincidence/coincidence/commit/master :alt: GitHub last commit .. |maintained| image:: https://img.shields.io/maintenance/yes/2023 :alt: Maintenance .. |pypi-downloads| image:: https://img.shields.io/pypi/dm/coincidence :target: https://pypi.org/project/coincidence/ :alt: PyPI - Downloads .. end shields Installation -------------- .. start installation ``coincidence`` can be installed from PyPI or Anaconda. To install with ``pip``: .. code-block:: bash $ python -m pip install coincidence To install with ``conda``: * First add the required channels .. code-block:: bash $ conda config --add channels https://conda.anaconda.org/conda-forge $ conda config --add channels https://conda.anaconda.org/domdfcoding * Then install .. code-block:: bash $ conda install coincidence .. end installation PK$iV=TT!coincidence-0.6.4.dist-info/WHEELWheel-Version: 1.0 Generator: whey (0.0.23) Root-Is-Purelib: true Tag: py3-none-any PK$iV,coincidence-0.6.4.dist-info/entry_points.txtPK$iV.,"coincidence-0.6.4.dist-info/RECORDcoincidence/__init__.py,sha256=QsYjGzQDp4vrxJMHNfAiav7o1geSPi6z-y1p2P0PUv4,3035 coincidence/fixtures.py,sha256=jX7JP475NLs8DfGAByPQABODAcCypU56U-yH3tMxYgw,3822 coincidence/params.py,sha256=e8WE6EC4bz9lsbe6fnnMQwmLM1CYuYsXKtu3Vf-hSoI,8252 coincidence/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 coincidence/regressions.py,sha256=wm_S-DLiVDQRZBxK71pgdWcKcVKu-mxYQL44w3FGW_c,12252 coincidence/selectors.py,sha256=PX-KN3aOUnI_w1OGvZMZQ4534vaMD8kDP0ykGyxxfRE,7312 coincidence/utils.py,sha256=QLuBTf_ol1fm9IN2SJ33CkkB3G644S6hrB3kYxgxAZQ,6657 coincidence-0.6.4.dist-info/LICENSE,sha256=TXanB-tJYmHhByFsynyh-UGwh2VdTVVjbZeFz1utSWc,1064 coincidence-0.6.4.dist-info/METADATA,sha256=CTK9nbc-3UzfxVy43UfZEdyMWpzv3NtiPlZpdvJDd18,7189 coincidence-0.6.4.dist-info/WHEEL,sha256=eWG-VKlkr28uNKLeibEoqxkr0uD-5B1bTkj-6eVDwmk,84 coincidence-0.6.4.dist-info/entry_points.txt,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 coincidence-0.6.4.dist-info/RECORD,, PKiV coincidence/__init__.pyPKiVHg coincidence/fixtures.pyPKiVHΕ< < 3coincidence/params.pyPKiV;coincidence/py.typedPKiVd.//;coincidence/regressions.pyPKiVܨkcoincidence/selectors.pyPKiV9"yqcoincidence/utils.pyPK$iVl}((#coincidence-0.6.4.dist-info/LICENSEPK$iVn$Jcoincidence-0.6.4.dist-info/METADATAPK$iV=TT!coincidence-0.6.4.dist-info/WHEELPK$iV,4coincidence-0.6.4.dist-info/entry_points.txtPK$iV.,"~coincidence-0.6.4.dist-info/RECORDPK {