summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/certifi/py2/ya.make2
-rw-r--r--contrib/python/certifi/py3/ya.make2
-rw-r--r--contrib/python/hypothesis/py3/.dist-info/METADATA10
-rw-r--r--contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py6
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/reporting.py13
-rw-r--r--contrib/python/hypothesis/py3/hypothesis/version.py2
-rw-r--r--contrib/python/hypothesis/py3/ya.make2
-rw-r--r--contrib/python/importlib-metadata/py3/.dist-info/METADATA4
-rw-r--r--contrib/python/importlib-metadata/py3/importlib_metadata/__init__.py14
-rw-r--r--contrib/python/importlib-metadata/py3/importlib_metadata/_itertools.py98
-rw-r--r--contrib/python/importlib-metadata/py3/ya.make2
-rw-r--r--contrib/python/importlib-resources/ya.make3
-rw-r--r--contrib/python/mergedeep/.dist-info/METADATA154
-rw-r--r--contrib/python/mergedeep/.dist-info/top_level.txt1
-rw-r--r--contrib/python/mergedeep/.yandex_meta/yamaker.yaml2
-rw-r--r--contrib/python/mergedeep/LICENSE21
-rw-r--r--contrib/python/mergedeep/README.md133
-rw-r--r--contrib/python/mergedeep/mergedeep/__init__.py5
-rw-r--r--contrib/python/mergedeep/mergedeep/mergedeep.py100
-rw-r--r--contrib/python/mergedeep/mergedeep/test_mergedeep.py397
-rw-r--r--contrib/python/mergedeep/tests/ya.make17
-rw-r--r--contrib/python/mergedeep/ya.make29
-rw-r--r--contrib/python/pure-eval/.dist-info/METADATA14
-rw-r--r--contrib/python/pure-eval/README.md2
-rw-r--r--contrib/python/pure-eval/pure_eval/__init__.py9
-rw-r--r--contrib/python/pure-eval/pure_eval/my_getattr_static.py6
-rw-r--r--contrib/python/pure-eval/pure_eval/utils.py7
-rw-r--r--contrib/python/pure-eval/pure_eval/version.py2
-rw-r--r--contrib/python/pure-eval/ya.make2
-rw-r--r--contrib/python/pytest-lazy-fixtures/.dist-info/METADATA10
-rw-r--r--contrib/python/pytest-lazy-fixtures/README.md8
-rw-r--r--contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/loader.py2
-rw-r--r--contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/normalizer.py7
-rw-r--r--contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/plugin.py8
-rw-r--r--contrib/python/pytest-lazy-fixtures/ya.make2
36 files changed, 1035 insertions, 67 deletions
diff --git a/contrib/python/certifi/py2/ya.make b/contrib/python/certifi/py2/ya.make
index 274da44efc0..27aa0018f46 100644
--- a/contrib/python/certifi/py2/ya.make
+++ b/contrib/python/certifi/py2/ya.make
@@ -2,6 +2,8 @@ PY2_LIBRARY()
LICENSE(Service-Py23-Proxy)
+VERSION(Service-no-external-sources)
+
PEERDIR(
library/python/certifi
)
diff --git a/contrib/python/certifi/py3/ya.make b/contrib/python/certifi/py3/ya.make
index fa3aa2c9d01..e841bd82d54 100644
--- a/contrib/python/certifi/py3/ya.make
+++ b/contrib/python/certifi/py3/ya.make
@@ -2,6 +2,8 @@ PY3_LIBRARY()
LICENSE(Service-Py23-Proxy)
+VERSION(Service-no-external-sources)
+
PEERDIR(
library/python/certifi
)
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA
index ffee48a82f0..223aafcb694 100644
--- a/contrib/python/hypothesis/py3/.dist-info/METADATA
+++ b/contrib/python/hypothesis/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: hypothesis
-Version: 6.108.2
+Version: 6.108.4
Summary: A library for property-based testing
Home-page: https://hypothesis.works
Author: David R. MacIver and Zac Hatfield-Dodds
@@ -41,10 +41,10 @@ Requires-Dist: exceptiongroup >=1.0.0 ; python_version < "3.11"
Provides-Extra: all
Requires-Dist: black >=19.10b0 ; extra == 'all'
Requires-Dist: click >=7.0 ; extra == 'all'
-Requires-Dist: crosshair-tool >=0.0.61 ; extra == 'all'
+Requires-Dist: crosshair-tool >=0.0.63 ; extra == 'all'
Requires-Dist: django >=3.2 ; extra == 'all'
Requires-Dist: dpcontracts >=0.4 ; extra == 'all'
-Requires-Dist: hypothesis-crosshair >=0.0.7 ; extra == 'all'
+Requires-Dist: hypothesis-crosshair >=0.0.9 ; extra == 'all'
Requires-Dist: lark >=0.10.1 ; extra == 'all'
Requires-Dist: libcst >=0.3.16 ; extra == 'all'
Requires-Dist: numpy >=1.17.3 ; extra == 'all'
@@ -63,8 +63,8 @@ Requires-Dist: rich >=9.0.0 ; extra == 'cli'
Provides-Extra: codemods
Requires-Dist: libcst >=0.3.16 ; extra == 'codemods'
Provides-Extra: crosshair
-Requires-Dist: hypothesis-crosshair >=0.0.7 ; extra == 'crosshair'
-Requires-Dist: crosshair-tool >=0.0.61 ; extra == 'crosshair'
+Requires-Dist: hypothesis-crosshair >=0.0.9 ; extra == 'crosshair'
+Requires-Dist: crosshair-tool >=0.0.63 ; extra == 'crosshair'
Provides-Extra: dateutil
Requires-Dist: python-dateutil >=1.4 ; extra == 'dateutil'
Provides-Extra: django
diff --git a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
index e82b528bb53..9df7817c68c 100644
--- a/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
+++ b/contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py
@@ -309,7 +309,7 @@ else:
with current_pytest_item.with_value(item):
yield
if store.results:
- item.hypothesis_report_information = list(store.results)
+ item.hypothesis_report_information = "\n".join(store.results)
def _stash_get(config, key, default):
if hasattr(config, "stash"):
@@ -325,9 +325,7 @@ else:
def pytest_runtest_makereport(item, call):
report = (yield).get_result()
if hasattr(item, "hypothesis_report_information"):
- report.sections.append(
- ("Hypothesis", "\n".join(item.hypothesis_report_information))
- )
+ report.sections.append(("Hypothesis", item.hypothesis_report_information))
if report.when != "teardown":
return
diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
index 7dbab5b9717..4465f59e5c4 100644
--- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
+++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py
@@ -421,6 +421,10 @@ _gc_initialized = False
_gc_start = 0
_gc_cumulative_time = 0
+# Since gc_callback potentially runs in test context, and perf_counter
+# might be monkeypatched, we store a reference to the real one.
+_perf_counter = time.perf_counter
+
def gc_cumulative_time() -> float:
global _gc_initialized
@@ -430,7 +434,7 @@ def gc_cumulative_time() -> float:
def gc_callback(phase, info):
global _gc_start, _gc_cumulative_time
try:
- now = time.perf_counter()
+ now = _perf_counter()
if phase == "start":
_gc_start = now
elif phase == "stop" and _gc_start > 0:
diff --git a/contrib/python/hypothesis/py3/hypothesis/reporting.py b/contrib/python/hypothesis/py3/hypothesis/reporting.py
index a0f300b2bcb..19073c5aff2 100644
--- a/contrib/python/hypothesis/py3/hypothesis/reporting.py
+++ b/contrib/python/hypothesis/py3/hypothesis/reporting.py
@@ -8,8 +8,6 @@
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
-import inspect
-
from hypothesis._settings import Verbosity, settings
from hypothesis.internal.compat import escape_unicode_characters
from hypothesis.utils.dynamicvariables import DynamicVariable
@@ -37,14 +35,6 @@ def current_verbosity():
return settings.default.verbosity
-def to_text(textish):
- if inspect.isfunction(textish):
- textish = textish()
- if isinstance(textish, bytes):
- textish = textish.decode()
- return textish
-
-
def verbose_report(text):
if current_verbosity() >= Verbosity.verbose:
base_report(text)
@@ -61,4 +51,5 @@ def report(text):
def base_report(text):
- current_reporter()(to_text(text))
+ assert isinstance(text, str), f"unexpected non-str {text=}"
+ current_reporter()(text)
diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py
index 4858f7fb0ca..29e2144b92a 100644
--- a/contrib/python/hypothesis/py3/hypothesis/version.py
+++ b/contrib/python/hypothesis/py3/hypothesis/version.py
@@ -8,5 +8,5 @@
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
-__version_info__ = (6, 108, 2)
+__version_info__ = (6, 108, 4)
__version__ = ".".join(map(str, __version_info__))
diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make
index 63eb354c8e2..4efa89329dd 100644
--- a/contrib/python/hypothesis/py3/ya.make
+++ b/contrib/python/hypothesis/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(6.108.2)
+VERSION(6.108.4)
LICENSE(MPL-2.0)
diff --git a/contrib/python/importlib-metadata/py3/.dist-info/METADATA b/contrib/python/importlib-metadata/py3/.dist-info/METADATA
index d7dd4a31a2e..691fb545bff 100644
--- a/contrib/python/importlib-metadata/py3/.dist-info/METADATA
+++ b/contrib/python/importlib-metadata/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: importlib_metadata
-Version: 8.0.0
+Version: 8.1.0
Summary: Read metadata from Python packages
Author-email: "Jason R. Coombs" <[email protected]>
Project-URL: Source, https://github.com/python/importlib_metadata
@@ -28,13 +28,13 @@ Requires-Dist: pytest-checkdocs >=2.4 ; extra == 'test'
Requires-Dist: pytest-cov ; extra == 'test'
Requires-Dist: pytest-mypy ; extra == 'test'
Requires-Dist: pytest-enabler >=2.2 ; extra == 'test'
-Requires-Dist: pytest-ruff >=0.2.1 ; extra == 'test'
Requires-Dist: packaging ; extra == 'test'
Requires-Dist: pyfakefs ; extra == 'test'
Requires-Dist: flufl.flake8 ; extra == 'test'
Requires-Dist: pytest-perf >=0.9.2 ; extra == 'test'
Requires-Dist: jaraco.test >=5.4 ; extra == 'test'
Requires-Dist: importlib-resources >=1.3 ; (python_version < "3.9") and extra == 'test'
+Requires-Dist: pytest-ruff >=0.2.1 ; (sys_platform != "cygwin") and extra == 'test'
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
:target: https://pypi.org/project/importlib_metadata
diff --git a/contrib/python/importlib-metadata/py3/importlib_metadata/__init__.py b/contrib/python/importlib-metadata/py3/importlib_metadata/__init__.py
index 6147ca9f5e7..c9730079926 100644
--- a/contrib/python/importlib-metadata/py3/importlib_metadata/__init__.py
+++ b/contrib/python/importlib-metadata/py3/importlib_metadata/__init__.py
@@ -24,7 +24,7 @@ from ._compat import (
install,
)
from ._functools import method_cache, pass_none
-from ._itertools import always_iterable, unique_everseen
+from ._itertools import always_iterable, bucket, unique_everseen
from ._meta import PackageMetadata, SimplePath
from contextlib import suppress
@@ -393,7 +393,7 @@ class Distribution(metaclass=abc.ABCMeta):
if not name:
raise ValueError("A distribution name is required.")
try:
- return next(iter(cls.discover(name=name)))
+ return next(iter(cls._prefer_valid(cls.discover(name=name))))
except StopIteration:
raise PackageNotFoundError(name)
@@ -418,6 +418,16 @@ class Distribution(metaclass=abc.ABCMeta):
)
@staticmethod
+ def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]:
+ """
+ Prefer (move to the front) distributions that have metadata.
+
+ Ref python/importlib_resources#489.
+ """
+ buckets = bucket(dists, lambda dist: bool(dist.metadata))
+ return itertools.chain(buckets[True], buckets[False])
+
+ @staticmethod
def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path.
diff --git a/contrib/python/importlib-metadata/py3/importlib_metadata/_itertools.py b/contrib/python/importlib-metadata/py3/importlib_metadata/_itertools.py
index d4ca9b9140e..79d37198ce7 100644
--- a/contrib/python/importlib-metadata/py3/importlib_metadata/_itertools.py
+++ b/contrib/python/importlib-metadata/py3/importlib_metadata/_itertools.py
@@ -1,3 +1,4 @@
+from collections import defaultdict, deque
from itertools import filterfalse
@@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)):
return iter(obj)
except TypeError:
return iter((obj,))
+
+
+# Copied from more_itertools 10.3
+class bucket:
+ """Wrap *iterable* and return an object that buckets the iterable into
+ child iterables based on a *key* function.
+
+ >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
+ >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character
+ >>> sorted(list(s)) # Get the keys
+ ['a', 'b', 'c']
+ >>> a_iterable = s['a']
+ >>> next(a_iterable)
+ 'a1'
+ >>> next(a_iterable)
+ 'a2'
+ >>> list(s['b'])
+ ['b1', 'b2', 'b3']
+
+ The original iterable will be advanced and its items will be cached until
+ they are used by the child iterables. This may require significant storage.
+
+ By default, attempting to select a bucket to which no items belong will
+ exhaust the iterable and cache all values.
+ If you specify a *validator* function, selected buckets will instead be
+ checked against it.
+
+ >>> from itertools import count
+ >>> it = count(1, 2) # Infinite sequence of odd numbers
+ >>> key = lambda x: x % 10 # Bucket by last digit
+ >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only
+ >>> s = bucket(it, key=key, validator=validator)
+ >>> 2 in s
+ False
+ >>> list(s[2])
+ []
+
+ """
+
+ def __init__(self, iterable, key, validator=None):
+ self._it = iter(iterable)
+ self._key = key
+ self._cache = defaultdict(deque)
+ self._validator = validator or (lambda x: True)
+
+ def __contains__(self, value):
+ if not self._validator(value):
+ return False
+
+ try:
+ item = next(self[value])
+ except StopIteration:
+ return False
+ else:
+ self._cache[value].appendleft(item)
+
+ return True
+
+ def _get_values(self, value):
+ """
+ Helper to yield items from the parent iterator that match *value*.
+ Items that don't match are stored in the local cache as they
+ are encountered.
+ """
+ while True:
+ # If we've cached some items that match the target value, emit
+ # the first one and evict it from the cache.
+ if self._cache[value]:
+ yield self._cache[value].popleft()
+ # Otherwise we need to advance the parent iterator to search for
+ # a matching item, caching the rest.
+ else:
+ while True:
+ try:
+ item = next(self._it)
+ except StopIteration:
+ return
+ item_value = self._key(item)
+ if item_value == value:
+ yield item
+ break
+ elif self._validator(item_value):
+ self._cache[item_value].append(item)
+
+ def __iter__(self):
+ for item in self._it:
+ item_value = self._key(item)
+ if self._validator(item_value):
+ self._cache[item_value].append(item)
+
+ yield from self._cache.keys()
+
+ def __getitem__(self, value):
+ if not self._validator(value):
+ return iter(())
+
+ return self._get_values(value)
diff --git a/contrib/python/importlib-metadata/py3/ya.make b/contrib/python/importlib-metadata/py3/ya.make
index ad2a140f18c..8e7b3102b36 100644
--- a/contrib/python/importlib-metadata/py3/ya.make
+++ b/contrib/python/importlib-metadata/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(8.0.0)
+VERSION(8.1.0)
LICENSE(Apache-2.0)
diff --git a/contrib/python/importlib-resources/ya.make b/contrib/python/importlib-resources/ya.make
index 21d607abef0..c7ba0d70fcb 100644
--- a/contrib/python/importlib-resources/ya.make
+++ b/contrib/python/importlib-resources/ya.make
@@ -2,6 +2,9 @@ PY23_LIBRARY()
LICENSE(Apache-2.0)
+# The code is not taken from upstream. An analogue of version 1.0.2 has been implemented. See CONTRIB-1203
+VERSION(1.0.2)
+
PY_SRCS(
TOP_LEVEL
importlib_resources/__init__.py
diff --git a/contrib/python/mergedeep/.dist-info/METADATA b/contrib/python/mergedeep/.dist-info/METADATA
new file mode 100644
index 00000000000..23452d786be
--- /dev/null
+++ b/contrib/python/mergedeep/.dist-info/METADATA
@@ -0,0 +1,154 @@
+Metadata-Version: 2.1
+Name: mergedeep
+Version: 1.3.4
+Summary: A deep merge function for 🐍.
+Home-page: https://github.com/clarketm/mergedeep
+Author: Travis Clarke
+Author-email: [email protected]
+License: UNKNOWN
+Platform: UNKNOWN
+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: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Requires-Python: >=3.6
+Description-Content-Type: text/markdown
+
+# [mergedeep](https://mergedeep.readthedocs.io/en/latest/)
+
+[![PyPi release](https://img.shields.io/pypi/v/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![PyPi versions](https://img.shields.io/pypi/pyversions/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![Downloads](https://pepy.tech/badge/mergedeep)](https://pepy.tech/project/mergedeep)
+[![Conda Version](https://img.shields.io/conda/vn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Documentation Status](https://readthedocs.org/projects/mergedeep/badge/?version=latest)](https://mergedeep.readthedocs.io/en/latest/?badge=latest)
+
+A deep merge function for 🐍.
+
+[Check out the mergedeep docs](https://mergedeep.readthedocs.io/en/latest/)
+
+## Installation
+
+```bash
+$ pip install mergedeep
+```
+
+## Usage
+
+```text
+merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping
+```
+
+Deep merge without mutating the source dicts.
+
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merged = merge({}, a, b, c)
+
+print(merged)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+Deep merge into an existing dict.
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merge(a, b, c)
+
+print(a)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+### Merge strategies:
+
+1. Replace (*default*)
+
+> `Strategy.REPLACE`
+
+```python3
+# When `destination` and `source` keys are the same, replace the `destination` value with one from `source` (default).
+
+# Note: with multiple sources, the `last` (i.e. rightmost) source value will be what appears in the merged result.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": [3, 4]}
+
+merge(dst, src, strategy=Strategy.REPLACE)
+# same as: merge(dst, src)
+
+print(dst)
+# {"key": [3, 4]}
+```
+
+2. Additive
+
+> `Strategy.ADDITIVE`
+
+```python3
+# When `destination` and `source` values are both the same additive collection type, extend `destination` by adding values from `source`.
+# Additive collection types include: `list`, `tuple`, `set`, and `Counter`
+
+# Note: if the values are not additive collections of the same type, then fallback to a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2], "count": Counter({"a": 1, "b": 1})}
+src = {"key": [3, 4], "count": Counter({"a": 1, "c": 1})}
+
+merge(dst, src, strategy=Strategy.ADDITIVE)
+
+print(dst)
+# {"key": [1, 2, 3, 4], "count": Counter({"a": 2, "b": 1, "c": 1})}
+```
+
+3. Typesafe replace
+
+> `Strategy.TYPESAFE_REPLACE` or `Strategy.TYPESAFE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_REPLACE) # same as: `Strategy.TYPESAFE`
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+4. Typesafe additive
+
+> `Strategy.TYPESAFE_ADDITIVE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `ADDITIVE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_ADDITIVE)
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+## License
+
+MIT &copy; [**Travis Clarke**](https://blog.travismclarke.com/)
+
+
diff --git a/contrib/python/mergedeep/.dist-info/top_level.txt b/contrib/python/mergedeep/.dist-info/top_level.txt
new file mode 100644
index 00000000000..5413932b268
--- /dev/null
+++ b/contrib/python/mergedeep/.dist-info/top_level.txt
@@ -0,0 +1 @@
+mergedeep
diff --git a/contrib/python/mergedeep/.yandex_meta/yamaker.yaml b/contrib/python/mergedeep/.yandex_meta/yamaker.yaml
new file mode 100644
index 00000000000..b88229ef483
--- /dev/null
+++ b/contrib/python/mergedeep/.yandex_meta/yamaker.yaml
@@ -0,0 +1,2 @@
+mark_as_tests:
+ - mergedeep/test_mergedeep.py
diff --git a/contrib/python/mergedeep/LICENSE b/contrib/python/mergedeep/LICENSE
new file mode 100644
index 00000000000..45c64408162
--- /dev/null
+++ b/contrib/python/mergedeep/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Travis Clarke <[email protected]> (https://www.travismclarke.com/)
+
+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/contrib/python/mergedeep/README.md b/contrib/python/mergedeep/README.md
new file mode 100644
index 00000000000..86322730cd9
--- /dev/null
+++ b/contrib/python/mergedeep/README.md
@@ -0,0 +1,133 @@
+# [mergedeep](https://mergedeep.readthedocs.io/en/latest/)
+
+[![PyPi release](https://img.shields.io/pypi/v/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![PyPi versions](https://img.shields.io/pypi/pyversions/mergedeep.svg)](https://pypi.org/project/mergedeep/)
+[![Downloads](https://pepy.tech/badge/mergedeep)](https://pepy.tech/project/mergedeep)
+[![Conda Version](https://img.shields.io/conda/vn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/mergedeep.svg)](https://anaconda.org/conda-forge/mergedeep)
+[![Documentation Status](https://readthedocs.org/projects/mergedeep/badge/?version=latest)](https://mergedeep.readthedocs.io/en/latest/?badge=latest)
+
+A deep merge function for 🐍.
+
+[Check out the mergedeep docs](https://mergedeep.readthedocs.io/en/latest/)
+
+## Installation
+
+```bash
+$ pip install mergedeep
+```
+
+## Usage
+
+```text
+merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping
+```
+
+Deep merge without mutating the source dicts.
+
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merged = merge({}, a, b, c)
+
+print(merged)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+Deep merge into an existing dict.
+```python3
+from mergedeep import merge
+
+a = {"keyA": 1}
+b = {"keyB": {"sub1": 10}}
+c = {"keyB": {"sub2": 20}}
+
+merge(a, b, c)
+
+print(a)
+# {"keyA": 1, "keyB": {"sub1": 10, "sub2": 20}}
+```
+
+### Merge strategies:
+
+1. Replace (*default*)
+
+> `Strategy.REPLACE`
+
+```python3
+# When `destination` and `source` keys are the same, replace the `destination` value with one from `source` (default).
+
+# Note: with multiple sources, the `last` (i.e. rightmost) source value will be what appears in the merged result.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": [3, 4]}
+
+merge(dst, src, strategy=Strategy.REPLACE)
+# same as: merge(dst, src)
+
+print(dst)
+# {"key": [3, 4]}
+```
+
+2. Additive
+
+> `Strategy.ADDITIVE`
+
+```python3
+# When `destination` and `source` values are both the same additive collection type, extend `destination` by adding values from `source`.
+# Additive collection types include: `list`, `tuple`, `set`, and `Counter`
+
+# Note: if the values are not additive collections of the same type, then fallback to a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2], "count": Counter({"a": 1, "b": 1})}
+src = {"key": [3, 4], "count": Counter({"a": 1, "c": 1})}
+
+merge(dst, src, strategy=Strategy.ADDITIVE)
+
+print(dst)
+# {"key": [1, 2, 3, 4], "count": Counter({"a": 2, "b": 1, "c": 1})}
+```
+
+3. Typesafe replace
+
+> `Strategy.TYPESAFE_REPLACE` or `Strategy.TYPESAFE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `REPLACE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_REPLACE) # same as: `Strategy.TYPESAFE`
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+4. Typesafe additive
+
+> `Strategy.TYPESAFE_ADDITIVE`
+
+```python3
+# When `destination` and `source` values are of different types, raise `TypeError`. Otherwise, perform a `ADDITIVE` merge.
+
+from mergedeep import merge, Strategy
+
+dst = {"key": [1, 2]}
+src = {"key": {3, 4}}
+
+merge(dst, src, strategy=Strategy.TYPESAFE_ADDITIVE)
+# TypeError: destination type: <class 'list'> differs from source type: <class 'set'> for key: "key"
+```
+
+## License
+
+MIT &copy; [**Travis Clarke**](https://blog.travismclarke.com/)
diff --git a/contrib/python/mergedeep/mergedeep/__init__.py b/contrib/python/mergedeep/mergedeep/__init__.py
new file mode 100644
index 00000000000..92d12f6c949
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/__init__.py
@@ -0,0 +1,5 @@
+__version__ = "1.3.4"
+
+from mergedeep.mergedeep import merge, Strategy
+
+__all__ = ["merge", "Strategy"]
diff --git a/contrib/python/mergedeep/mergedeep/mergedeep.py b/contrib/python/mergedeep/mergedeep/mergedeep.py
new file mode 100644
index 00000000000..6dda8e82f38
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/mergedeep.py
@@ -0,0 +1,100 @@
+from collections import Counter
+from collections.abc import Mapping
+from copy import deepcopy
+from enum import Enum
+from functools import reduce, partial
+from typing import MutableMapping
+
+
+class Strategy(Enum):
+ # Replace `destination` item with one from `source` (default).
+ REPLACE = 0
+ # Combine `list`, `tuple`, `set`, or `Counter` types into one collection.
+ ADDITIVE = 1
+ # Alias to: `TYPESAFE_REPLACE`
+ TYPESAFE = 2
+ # Raise `TypeError` when `destination` and `source` types differ. Otherwise, perform a `REPLACE` merge.
+ TYPESAFE_REPLACE = 3
+ # Raise `TypeError` when `destination` and `source` types differ. Otherwise, perform a `ADDITIVE` merge.
+ TYPESAFE_ADDITIVE = 4
+
+
+def _handle_merge_replace(destination, source, key):
+ if isinstance(destination[key], Counter) and isinstance(source[key], Counter):
+ # Merge both destination and source `Counter` as if they were a standard dict.
+ _deepmerge(destination[key], source[key])
+ else:
+ # If a key exists in both objects and the values are `different`, the value from the `source` object will be used.
+ destination[key] = deepcopy(source[key])
+
+
+def _handle_merge_additive(destination, source, key):
+ # Values are combined into one long collection.
+ if isinstance(destination[key], list) and isinstance(source[key], list):
+ # Extend destination if both destination and source are `list` type.
+ destination[key].extend(deepcopy(source[key]))
+ elif isinstance(destination[key], set) and isinstance(source[key], set):
+ # Update destination if both destination and source are `set` type.
+ destination[key].update(deepcopy(source[key]))
+ elif isinstance(destination[key], tuple) and isinstance(source[key], tuple):
+ # Update destination if both destination and source are `tuple` type.
+ destination[key] = destination[key] + deepcopy(source[key])
+ elif isinstance(destination[key], Counter) and isinstance(source[key], Counter):
+ # Update destination if both destination and source are `Counter` type.
+ destination[key].update(deepcopy(source[key]))
+ else:
+ _handle_merge[Strategy.REPLACE](destination, source, key)
+
+
+def _handle_merge_typesafe(destination, source, key, strategy):
+ # Raise a TypeError if the destination and source types differ.
+ if type(destination[key]) is not type(source[key]):
+ raise TypeError(
+ f'destination type: {type(destination[key])} differs from source type: {type(source[key])} for key: "{key}"'
+ )
+ else:
+ _handle_merge[strategy](destination, source, key)
+
+
+_handle_merge = {
+ Strategy.REPLACE: _handle_merge_replace,
+ Strategy.ADDITIVE: _handle_merge_additive,
+ Strategy.TYPESAFE: partial(_handle_merge_typesafe, strategy=Strategy.REPLACE),
+ Strategy.TYPESAFE_REPLACE: partial(_handle_merge_typesafe, strategy=Strategy.REPLACE),
+ Strategy.TYPESAFE_ADDITIVE: partial(_handle_merge_typesafe, strategy=Strategy.ADDITIVE),
+}
+
+
+def _is_recursive_merge(a, b):
+ both_mapping = isinstance(a, Mapping) and isinstance(b, Mapping)
+ both_counter = isinstance(a, Counter) and isinstance(b, Counter)
+ return both_mapping and not both_counter
+
+
+def _deepmerge(dst, src, strategy=Strategy.REPLACE):
+ for key in src:
+ if key in dst:
+ if _is_recursive_merge(dst[key], src[key]):
+ # If the key for both `dst` and `src` are both Mapping types (e.g. dict), then recurse.
+ _deepmerge(dst[key], src[key], strategy)
+ elif dst[key] is src[key]:
+ # If a key exists in both objects and the values are `same`, the value from the `dst` object will be used.
+ pass
+ else:
+ _handle_merge.get(strategy)(dst, src, key)
+ else:
+ # If the key exists only in `src`, the value from the `src` object will be used.
+ dst[key] = deepcopy(src[key])
+ return dst
+
+
+def merge(destination: MutableMapping, *sources: Mapping, strategy: Strategy = Strategy.REPLACE) -> MutableMapping:
+ """
+ A deep merge function for 🐍.
+
+ :param destination: The destination mapping.
+ :param sources: The source mappings.
+ :param strategy: The merge strategy.
+ :return:
+ """
+ return reduce(partial(_deepmerge, strategy=strategy), sources, destination)
diff --git a/contrib/python/mergedeep/mergedeep/test_mergedeep.py b/contrib/python/mergedeep/mergedeep/test_mergedeep.py
new file mode 100644
index 00000000000..ef397288350
--- /dev/null
+++ b/contrib/python/mergedeep/mergedeep/test_mergedeep.py
@@ -0,0 +1,397 @@
+"""mergedeep test module"""
+import inspect
+import unittest
+from collections import Counter
+from copy import deepcopy
+
+from mergedeep import merge, Strategy
+
+
+class test_mergedeep(unittest.TestCase):
+ """mergedeep function tests."""
+
+ ##############################################################################################################################
+ # REPLACE
+ ##############################################################################################################################
+
+ def test_should_merge_3_dicts_into_new_dict_using_replace_strategy_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 5, "b": 1, "c": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": [1, 2, 3],
+ "g": (2, 4, 6),
+ "h": Counter({"a": 1, "b": 1}),
+ "j": 1,
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 5, "c": 1}),
+ "i": Counter({"a": 1}),
+ "z": Counter({"a": 2}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {
+ "a": {"b": {"_c": 15}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_2_dicts_into_existing_dict_using_replace_strategy_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 1, "b": 1, "c": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": [1, 2, 3],
+ "g": (2, 4, 6),
+ "h": Counter({"a": 1, "b": 1}),
+ "j": 1,
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": [4, 5, 6],
+ "g": (100, 200),
+ "h": Counter({"a": 1, "c": 1}),
+ "i": Counter({"a": 1}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}, "i": 2, "j": Counter({"z": 2})}
+ c_copy = deepcopy(c)
+
+ actual = merge(a, b, c, strategy=Strategy.REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(actual, a)
+ self.assertNotEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_have_default_strategy_of_replace(self):
+ func_spec = inspect.getfullargspec(merge)
+ default_strategy = Strategy.REPLACE
+
+ self.assertEqual(func_spec.kwonlydefaults.get("strategy"), default_strategy)
+
+ # mock_merge.method.assert_called_with(target, source, strategy=Strategy.REPLACE)
+
+ ##############################################################################################################################
+ # ADDITIVE
+ ##############################################################################################################################
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_lists_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": [1, 2, 3, 4, 5, 6],
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_sets_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": {1, 2, 3, 4, 5, 6},
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": {1, 2, 3}}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": {4, 5, 6}}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_tuples_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": (1, 2, 3, 4, 5, 6),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": (1, 2, 3)}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": (4, 5, 6)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_additive_strategy_on_counters_and_only_mutate_target(self,):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "f": Counter({"a": 2, "c": 1, "b": 1}),
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 4}),
+ }
+
+ a = {
+ "a": {"b": {"c": 5}},
+ "d": 1,
+ "e": {2: 3},
+ "f": Counter({"a": 1, "c": 1}),
+ "i": Counter({"f": 9}),
+ "j": Counter({"a": 1, "z": 4}),
+ }
+ a_copy = deepcopy(a)
+
+ b = {
+ "a": {"B": {"C": 10}},
+ "d": 2,
+ "e": 2,
+ "f": Counter({"a": 1, "b": 1}),
+ "j": [1, 2, 3],
+ "z": Counter({"a": 2}),
+ }
+ b_copy = deepcopy(b)
+
+ c = {
+ "a": {"b": {"_c": 15}},
+ "d": 3,
+ "e": {1: 2, "a": {"f": 2}},
+ "i": 2,
+ "j": Counter({"z": 2}),
+ "z": Counter({"a": 2}),
+ }
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_not_copy_references(self):
+ before = 1
+ after = 99
+
+ o1 = {"key1": before}
+ o2 = {"key2": before}
+
+ expected = {"list": deepcopy([o1, o2]), "tuple": deepcopy((o1, o2))}
+
+ a = {"list": [o1], "tuple": (o1,)}
+ b = {"list": [o2], "tuple": (o2,)}
+
+ actual = merge({}, a, b, strategy=Strategy.ADDITIVE)
+
+ o1["key1"] = after
+ o2["key2"] = after
+
+ self.assertEqual(actual, expected)
+
+ # Copied dicts should `not` mutate
+ self.assertEqual(actual["list"][0]["key1"], before)
+ self.assertEqual(actual["list"][1]["key2"], before)
+ self.assertEqual(actual["tuple"][0]["key1"], before)
+ self.assertEqual(actual["tuple"][1]["key2"], before)
+
+ # Non-copied dicts should mutate
+ self.assertEqual(a["list"][0]["key1"], after)
+ self.assertEqual(b["list"][0]["key2"], after)
+ self.assertEqual(a["tuple"][0]["key1"], after)
+ self.assertEqual(b["tuple"][0]["key2"], after)
+
+ ##############################################################################################################################
+ # TYPESAFE
+ # TYPESAFE_REPLACE
+ ##############################################################################################################################
+
+ def test_should_raise_TypeError_using_typesafe_strategy_if_types_differ(self):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE)
+
+ def test_should_raise_TypeError_using_typesafe_replace_strategy_if_types_differ(self,):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE_REPLACE)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [4, 5, 6],
+ "g": {2, 3, 4},
+ "h": (1, 3),
+ "z": Counter({"a": 1, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_replace_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [4, 5, 6],
+ "g": {2, 3, 4},
+ "h": (1, 3),
+ "z": Counter({"a": 1, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE_REPLACE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+ ##############################################################################################################################
+ # TYPESAFE_ADDITIVE
+ ##############################################################################################################################
+
+ def test_should_raise_TypeError_using_typesafe_additive_strategy_if_types_differ(self,):
+ a = {"a": {"b": {"c": 5}}, "d": 1, "e": {2: 3}, "f": [1, 2, 3]}
+ b = {"a": {"B": {"C": 10}}, "d": 2, "e": 2, "f": [4, 5, 6]}
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "e": {1: 2, "a": {"f": 2}}}
+
+ with self.assertRaises(TypeError):
+ merge({}, a, b, c, strategy=Strategy.TYPESAFE_ADDITIVE)
+
+ def test_should_merge_3_dicts_into_new_dict_using_typesafe_additive_strategy_and_only_mutate_target_if_types_are_compatible(
+ self,
+ ):
+ expected = {
+ "a": {"b": {"c": 5, "_c": 15}, "B": {"C": 10}},
+ "d": 3,
+ "f": [1, 2, 3, 4, 5, 6],
+ "g": {1, 2, 3, 4},
+ "h": (1, 1, 3),
+ "z": Counter({"a": 2, "b": 1, "c": 1}),
+ }
+
+ a = {"a": {"b": {"c": 5}}, "d": 1, "f": [1, 2, 3], "g": {1, 2, 3}, "z": Counter({"a": 1, "b": 1})}
+ a_copy = deepcopy(a)
+
+ b = {"a": {"B": {"C": 10}}, "d": 2, "f": [4, 5, 6], "g": {2, 3, 4}, "h": (1,)}
+ b_copy = deepcopy(b)
+
+ c = {"a": {"b": {"_c": 15}}, "d": 3, "h": (1, 3), "z": Counter({"a": 1, "c": 1})}
+ c_copy = deepcopy(c)
+
+ actual = merge({}, a, b, c, strategy=Strategy.TYPESAFE_ADDITIVE)
+
+ self.assertEqual(actual, expected)
+ self.assertEqual(a, a_copy)
+ self.assertEqual(b, b_copy)
+ self.assertEqual(c, c_copy)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/contrib/python/mergedeep/tests/ya.make b/contrib/python/mergedeep/tests/ya.make
new file mode 100644
index 00000000000..a0a227f788e
--- /dev/null
+++ b/contrib/python/mergedeep/tests/ya.make
@@ -0,0 +1,17 @@
+PY3TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/mergedeep
+)
+
+SRCDIR(contrib/python/mergedeep/mergedeep)
+
+TEST_SRCS(
+ test_mergedeep.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/mergedeep/ya.make b/contrib/python/mergedeep/ya.make
new file mode 100644
index 00000000000..ec7f03aaf4a
--- /dev/null
+++ b/contrib/python/mergedeep/ya.make
@@ -0,0 +1,29 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+SUBSCRIBER(g:python-contrib)
+
+VERSION(1.3.4)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ mergedeep/__init__.py
+ mergedeep/mergedeep.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/mergedeep/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)
diff --git a/contrib/python/pure-eval/.dist-info/METADATA b/contrib/python/pure-eval/.dist-info/METADATA
index 931f69c3484..1f086de3a8a 100644
--- a/contrib/python/pure-eval/.dist-info/METADATA
+++ b/contrib/python/pure-eval/.dist-info/METADATA
@@ -1,19 +1,19 @@
Metadata-Version: 2.1
-Name: pure-eval
-Version: 0.2.2
+Name: pure_eval
+Version: 0.2.3
Summary: Safely evaluate AST nodes without side effects
Home-page: http://github.com/alexmojaki/pure_eval
Author: Alex Hall
Author-email: [email protected]
License: MIT
-Platform: UNKNOWN
Classifier: Intended Audience :: Developers
-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: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Description-Content-Type: text/markdown
@@ -23,7 +23,7 @@ Requires-Dist: pytest ; extra == 'tests'
# `pure_eval`
-[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.5+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
+[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.7+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
This is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.
@@ -225,5 +225,3 @@ d = {1: 2}
y = 2
d[x] = 2
```
-
-
diff --git a/contrib/python/pure-eval/README.md b/contrib/python/pure-eval/README.md
index a4edbfc0a57..3506b333988 100644
--- a/contrib/python/pure-eval/README.md
+++ b/contrib/python/pure-eval/README.md
@@ -1,6 +1,6 @@
# `pure_eval`
-[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.5+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
+[![Build Status](https://travis-ci.org/alexmojaki/pure_eval.svg?branch=master)](https://travis-ci.org/alexmojaki/pure_eval) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/pure_eval/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/pure_eval?branch=master) [![Supports Python versions 3.7+](https://img.shields.io/pypi/pyversions/pure_eval.svg)](https://pypi.python.org/pypi/pure_eval)
This is a Python package that lets you safely evaluate certain AST nodes without triggering arbitrary code that may have unwanted side effects.
diff --git a/contrib/python/pure-eval/pure_eval/__init__.py b/contrib/python/pure-eval/pure_eval/__init__.py
index 0040e318a6e..ec4ba735030 100644
--- a/contrib/python/pure-eval/pure_eval/__init__.py
+++ b/contrib/python/pure-eval/pure_eval/__init__.py
@@ -6,3 +6,12 @@ try:
except ImportError:
# version.py is auto-generated with the git tag when building
__version__ = "???"
+
+__all__ = [
+ "Evaluator",
+ "CannotEval",
+ "group_expressions",
+ "is_expression_interesting",
+ "getattr_static",
+ "__version__",
+]
diff --git a/contrib/python/pure-eval/pure_eval/my_getattr_static.py b/contrib/python/pure-eval/pure_eval/my_getattr_static.py
index c750b1acc3f..5490b90adec 100644
--- a/contrib/python/pure-eval/pure_eval/my_getattr_static.py
+++ b/contrib/python/pure-eval/pure_eval/my_getattr_static.py
@@ -79,8 +79,10 @@ def getattr_static(obj, attr):
klass_result = _check_class(klass, attr)
if instance_result is not _sentinel and klass_result is not _sentinel:
- if (_check_class(type(klass_result), '__get__') is not _sentinel and
- _check_class(type(klass_result), '__set__') is not _sentinel):
+ if _check_class(type(klass_result), "__get__") is not _sentinel and (
+ _check_class(type(klass_result), "__set__") is not _sentinel
+ or _check_class(type(klass_result), "__delete__") is not _sentinel
+ ):
return _resolve_descriptor(klass_result, obj, klass)
if instance_result is not _sentinel:
diff --git a/contrib/python/pure-eval/pure_eval/utils.py b/contrib/python/pure-eval/pure_eval/utils.py
index a8a37302daa..19ead658506 100644
--- a/contrib/python/pure-eval/pure_eval/utils.py
+++ b/contrib/python/pure-eval/pure_eval/utils.py
@@ -184,7 +184,12 @@ def copy_ast_without_context(x):
if field != 'ctx'
if hasattr(x, field)
}
- return type(x)(**kwargs)
+ a = type(x)(**kwargs)
+ if hasattr(a, 'ctx'):
+ # Python 3.13.0b2+ defaults to Load when we don't pass ctx
+ # https://github.com/python/cpython/pull/118871
+ del a.ctx
+ return a
elif isinstance(x, list):
return list(map(copy_ast_without_context, x))
else:
diff --git a/contrib/python/pure-eval/pure_eval/version.py b/contrib/python/pure-eval/pure_eval/version.py
index 9dd16a34511..db9cf742db5 100644
--- a/contrib/python/pure-eval/pure_eval/version.py
+++ b/contrib/python/pure-eval/pure_eval/version.py
@@ -1 +1 @@
-__version__ = '0.2.2' \ No newline at end of file
+__version__ = '0.2.3' \ No newline at end of file
diff --git a/contrib/python/pure-eval/ya.make b/contrib/python/pure-eval/ya.make
index 9d3cdb4c6da..b42f4018d61 100644
--- a/contrib/python/pure-eval/ya.make
+++ b/contrib/python/pure-eval/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(0.2.2)
+VERSION(0.2.3)
LICENSE(MIT)
diff --git a/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA b/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA
index 260a9b18ccf..8b36c5866f6 100644
--- a/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA
+++ b/contrib/python/pytest-lazy-fixtures/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: pytest-lazy-fixtures
-Version: 1.0.7
+Version: 1.1.1
Summary: Allows you to use fixtures in @pytest.mark.parametrize.
Home-page: https://github.com/dev-petrov/pytest-lazy-fixtures
License: MIT
@@ -43,7 +43,7 @@ pip install pytest-lazy-fixtures
## Usage
-To use your fixtures inside `@pytest.mark.parametrize` you can use `lf` (`lazy_fixture`) or `pytest.lazy_fixtures`.
+To use your fixtures inside `@pytest.mark.parametrize` you can use `lf` (`lazy_fixture`).
```python
import pytest
@@ -56,10 +56,6 @@ def one():
@pytest.mark.parametrize('arg1,arg2', [('val1', lf('one'))])
def test_func(arg1, arg2):
assert arg2 == 1
-
[email protected]('arg1,arg2', [('val1', pytest.lazy_fixtures('one'))])
-def test_func(arg1, arg2):
- assert arg2 == 1
```
`lf` can be used with any data structures. For example, in the following example, `lf` is used in the dictionary:
@@ -96,7 +92,7 @@ def test_func(arg1, arg2):
assert arg2 == 1
```
-And there is some useful wrapper called `lfc` (`lazy_fixture_callable`) or `pytest.lazy_fixtures_callable`.
+And there is some useful wrapper called `lfc` (`lazy_fixture_callable`).
It can work with any callable and your fixtures, e.g.
```python
diff --git a/contrib/python/pytest-lazy-fixtures/README.md b/contrib/python/pytest-lazy-fixtures/README.md
index 6c13ec033a7..ba6ff870da3 100644
--- a/contrib/python/pytest-lazy-fixtures/README.md
+++ b/contrib/python/pytest-lazy-fixtures/README.md
@@ -22,7 +22,7 @@ pip install pytest-lazy-fixtures
## Usage
-To use your fixtures inside `@pytest.mark.parametrize` you can use `lf` (`lazy_fixture`) or `pytest.lazy_fixtures`.
+To use your fixtures inside `@pytest.mark.parametrize` you can use `lf` (`lazy_fixture`).
```python
import pytest
@@ -35,10 +35,6 @@ def one():
@pytest.mark.parametrize('arg1,arg2', [('val1', lf('one'))])
def test_func(arg1, arg2):
assert arg2 == 1
-
[email protected]('arg1,arg2', [('val1', pytest.lazy_fixtures('one'))])
-def test_func(arg1, arg2):
- assert arg2 == 1
```
`lf` can be used with any data structures. For example, in the following example, `lf` is used in the dictionary:
@@ -75,7 +71,7 @@ def test_func(arg1, arg2):
assert arg2 == 1
```
-And there is some useful wrapper called `lfc` (`lazy_fixture_callable`) or `pytest.lazy_fixtures_callable`.
+And there is some useful wrapper called `lfc` (`lazy_fixture_callable`).
It can work with any callable and your fixtures, e.g.
```python
diff --git a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/loader.py b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/loader.py
index 3c675c2aaf1..d2d39ff4f80 100644
--- a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/loader.py
+++ b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/loader.py
@@ -14,7 +14,7 @@ def load_lazy_fixtures(value, request: pytest.FixtureRequest):
return value.load_fixture(request)
# we need to check exact type
if type(value) is dict: # noqa: E721
- return {key: load_lazy_fixtures(value, request) for key, value in value.items()}
+ return {load_lazy_fixtures(key, request): load_lazy_fixtures(value, request) for key, value in value.items()}
# we need to check exact type
elif type(value) in {list, tuple, set}:
return type(value)([load_lazy_fixtures(value, request) for value in value])
diff --git a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/normalizer.py b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/normalizer.py
index fdbfe86402b..db4cfa8b00b 100644
--- a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/normalizer.py
+++ b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/normalizer.py
@@ -64,14 +64,15 @@ def _normalize_call(callspec, metafunc, used_keys):
fm = metafunc.config.pluginmanager.get_plugin("funcmanage")
used_keys = used_keys or set()
- valtype_keys = callspec.params.keys() - used_keys
+ params = callspec.params.copy() if pytest.version_tuple >= (8, 0, 0) else {**callspec.params, **callspec.funcargs}
+ valtype_keys = params.keys() - used_keys
for arg in valtype_keys:
- value = callspec.params[arg]
+ value = params[arg]
fixturenames_closure, arg2fixturedefs = _get_fixturenames_closure_and_arg2fixturedefs(fm, metafunc, value)
if fixturenames_closure and arg2fixturedefs:
- extra_fixturenames = [fname for fname in fixturenames_closure if fname not in callspec.params]
+ extra_fixturenames = [fname for fname in fixturenames_closure if fname not in params]
newmetafunc = _copy_metafunc(metafunc)
newmetafunc.fixturenames = extra_fixturenames
diff --git a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/plugin.py b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/plugin.py
index de7d75a4f79..f3475e199c8 100644
--- a/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/plugin.py
+++ b/contrib/python/pytest-lazy-fixtures/pytest_lazy_fixtures/plugin.py
@@ -1,16 +1,10 @@
import pytest
-from .lazy_fixture import LazyFixtureWrapper, lf
-from .lazy_fixture_callable import lfc
+from .lazy_fixture import LazyFixtureWrapper
from .loader import load_lazy_fixtures
from .normalizer import normalize_metafunc_calls
-def pytest_configure():
- pytest.lazy_fixtures = lf
- pytest.lazy_fixtures_callable = lfc
-
-
@pytest.hookimpl(tryfirst=True)
def pytest_fixture_setup(fixturedef, request):
val = getattr(request, "param", None)
diff --git a/contrib/python/pytest-lazy-fixtures/ya.make b/contrib/python/pytest-lazy-fixtures/ya.make
index b672f39330f..43c43a87f9d 100644
--- a/contrib/python/pytest-lazy-fixtures/ya.make
+++ b/contrib/python/pytest-lazy-fixtures/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(1.0.7)
+VERSION(1.1.1)
LICENSE(MIT)