pax_global_header00006660000000000000000000000064144361224310014512gustar00rootroot0000000000000052 comment=fa2c806bef659c3e8f6824512835715cbaada71c jaraco.collections-4.2.0/000077500000000000000000000000001443612243100152715ustar00rootroot00000000000000jaraco.collections-4.2.0/.coveragerc000066400000000000000000000002051443612243100174070ustar00rootroot00000000000000[run] omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* disable_warnings = couldnt-parse [report] show_missing = True jaraco.collections-4.2.0/.editorconfig000066400000000000000000000003661443612243100177530ustar00rootroot00000000000000root = true [*] charset = utf-8 indent_style = tab indent_size = 4 insert_final_newline = true end_of_line = lf [*.py] indent_style = space max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 [*.rst] indent_style = space jaraco.collections-4.2.0/.github/000077500000000000000000000000001443612243100166315ustar00rootroot00000000000000jaraco.collections-4.2.0/.github/FUNDING.yml000066400000000000000000000000421443612243100204420ustar00rootroot00000000000000tidelift: pypi/jaraco.collections jaraco.collections-4.2.0/.github/dependabot.yml000066400000000000000000000002241443612243100214570ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" allow: - dependency-type: "all" jaraco.collections-4.2.0/.github/workflows/000077500000000000000000000000001443612243100206665ustar00rootroot00000000000000jaraco.collections-4.2.0/.github/workflows/main.yml000066400000000000000000000064441443612243100223450ustar00rootroot00000000000000name: tests on: [push, pull_request] permissions: contents: read env: # Environment variables to support color support (jaraco/skeleton#66): # Request colored output from CLI tools supporting it. Different tools # interpret the value differently. For some, just being set is sufficient. # For others, it must be a non-zero integer. For yet others, being set # to a non-empty value is sufficient. For tox, it must be one of # , 0, 1, false, no, off, on, true, yes. The only enabling value # in common is "1". FORCE_COLOR: 1 # MyPy's color enforcement (must be a non-zero number) MYPY_FORCE_COLOR: -42 # Recognized by the `py` package, dependency of `pytest` (must be "1") PY_COLORS: 1 # Make tox-wrapped tools see color requests TOX_TESTENV_PASSENV: >- FORCE_COLOR MYPY_FORCE_COLOR NO_COLOR PY_COLORS PYTEST_THEME PYTEST_THEME_MODE # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream # Must be "1". TOX_PARALLEL_NO_SPINNER: 1 jobs: test: strategy: matrix: python: - "3.7" - "3.11" - "3.12" # Workaround for actions/setup-python#508 dev: - -dev platform: - ubuntu-latest - macos-latest - windows-latest include: - python: "3.8" platform: ubuntu-latest - python: "3.9" platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.12' }} steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }}${{ matrix.dev }} - name: Install tox run: | python -m pip install tox - name: Run tests run: tox docs: runs-on: ubuntu-latest env: TOXENV: docs steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }}${{ matrix.dev }} - name: Install tox run: | python -m pip install tox - name: Run tests run: tox check: # This job does nothing and is only used for the branch protection if: always() needs: - test - docs runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} release: permissions: contents: write needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: python-version: 3.11-dev - name: Install tox run: | python -m pip install tox - name: Release run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jaraco.collections-4.2.0/.pre-commit-config.yaml000066400000000000000000000001211443612243100215440ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 22.6.0 hooks: - id: black jaraco.collections-4.2.0/.readthedocs.yaml000066400000000000000000000003511443612243100205170ustar00rootroot00000000000000version: 2 python: install: - path: . extra_requirements: - docs # workaround for readthedocs/readthedocs.org#9623 build: # workaround for readthedocs/readthedocs.org#9635 os: ubuntu-22.04 tools: python: "3" jaraco.collections-4.2.0/CHANGES.rst000066400000000000000000000062201443612243100170730ustar00rootroot00000000000000v4.2.0 ====== Added ``Mask``, the inverse of a ``Projection``. v4.1.0 ====== ``Projection`` now accepts an iterable or callable or pattern for matching keys. ``Projection`` now retains order of keys from the underlying mapping. ``DictFilter`` is now deprecated in favor of ``Projection``. v4.0.0 ====== ``DictFilter`` no longer accepts ``include_keys`` and requires ``include_pattern`` as a keyword argument. v3.11.0 ======= In ``DictFilter``, deprecated ``include_keys`` parameter and usage without ``include_pattern``. Future versions will honor ``include_pattern`` as a required keyword argument. All other uses are deprecated. For uses that currently rely on ``include_keys``, use ``Projection`` instead/in addition. For example, instead of:: filtered = DictFilter(orig, include_keys=['a'], include_pattern='b+') Use:: filtered = DictFilter(Projection(['a'], orig), include_pattern='b+') v3.10.0 ======= In ``Projection``, harmonize the implementation and optimize using ``set`` instead of ``tuple``. v3.9.0 ====== ``DictFilter.__len__`` no longer relies on the iterable. Improves efficiency and fixes ``RecursionError`` on PyPy (#12). v3.8.0 ====== Made ``DictStack`` mutable. v3.7.0 ====== Added ``RangeMap.left``. v3.6.0 ====== Revised ``DictFilter``: - Fixed issue where ``DictFilter.__contains__`` would raise a ``KeyError``. - Relies heavily now on ``collections.abc.Mapping`` base class. v3.5.2 ====== Packaging refresh. Enrolled with Tidelift. v3.5.1 ====== Fixed ``DictStack.__len__`` and addressed recursion error on PyPy in ``__getitem__``. v3.5.0 ====== ``DictStack`` now supports the following Mapping behaviors: - ``.items()`` - casting to a dict - ``__contains__`` (i.e. "x in stack") Require Python 3.7 or later. v3.4.0 ====== Add ``WeightedLookup``. v3.3.0 ====== Add ``FreezableDefaultDict``. v3.2.0 ====== Rely on PEP 420 for namespace package. v3.1.0 ====== Refreshed packaging. Dropped dependency on six. v3.0.0 ====== Require Python 3.6 or later. 2.1 === Added ``pop_all`` function. 2.0 === Switch to `pkgutil namespace technique `_ for the ``jaraco`` namespace. 1.6.0 ===== Fix DeprecationWarnings when referencing abstract base classes from collections module. 1.5.3 ===== Refresh package metadata. 1.5.2 ===== Fixed KeyError in BijectiveMap when a new value matched an existing key (but not the reverse). Now a ValueError is raised as intended. 1.5.1 ===== Refresh packaging. 1.5 === Added a ``Projection`` class providing a much simpler interface than DictFilter. 1.4.1 ===== #3: Fixed less-than-equal and greater-than-equal comparisons in ``Least`` and ``Greatest``. 1.4 === Added ``Least`` and ``Greatest`` classes, instances of which always compare lesser or greater than all other objects. 1.3.2 ===== Fixed failure of KeyTransformingDict to transform keys on calls to ``.get``. 1.3 === Moved hosting to Github. 1.2.2 ===== Restore Python 2.7 compatibility. 1.2 === Add InstrumentedDict. 1.1 === Conditionally require setup requirements. 1.0 === Initial functionality taken from jaraco.util 10.8. jaraco.collections-4.2.0/LICENSE000066400000000000000000000017771443612243100163120ustar00rootroot00000000000000Permission 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. jaraco.collections-4.2.0/README.rst000066400000000000000000000052211443612243100167600ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/v/jaraco.collections.svg :target: https://pypi.org/project/jaraco.collections .. image:: https://img.shields.io/pypi/pyversions/jaraco.collections.svg .. image:: https://github.com/jaraco/jaraco.collections/workflows/tests/badge.svg :target: https://github.com/jaraco/jaraco.collections/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black .. image:: https://readthedocs.org/projects/jaracocollections/badge/?version=latest :target: https://jaracocollections.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/jaraco.collections :target: https://tidelift.com/subscription/pkg/pypi-jaraco.collections?utm_source=pypi-jaraco.collections&utm_medium=readme Models and classes to supplement the stdlib 'collections' module. See the docs, linked above, for descriptions and usage examples. Highlights include: - RangeMap: A mapping that accepts a range of values for keys. - Projection: A subset over an existing mapping. - KeyTransformingDict: Generalized mapping with keys transformed by a function. - FoldedCaseKeyedDict: A dict whose string keys are case-insensitive. - BijectiveMap: A map where keys map to values and values back to their keys. - ItemsAsAttributes: A mapping mix-in exposing items as attributes. - IdentityOverrideMap: A map whose keys map by default to themselves unless overridden. - FrozenDict: A hashable, immutable map. - Enumeration: An object whose keys are enumerated. - Everything: A container that contains all things. - Least, Greatest: Objects that are always less than or greater than any other. - pop_all: Return all items from the mutable sequence and remove them from that sequence. - DictStack: A stack of dicts, great for sharing scopes. - WeightedLookup: A specialized RangeMap for selecting an item by weights. For Enterprise ============== Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. Security Contact ================ To report a security vulnerability, please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. jaraco.collections-4.2.0/docs/000077500000000000000000000000001443612243100162215ustar00rootroot00000000000000jaraco.collections-4.2.0/docs/conf.py000066400000000000000000000027351443612243100175270ustar00rootroot00000000000000extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', ] master_doc = "index" html_theme = "furo" # Link dates and other references in the changelog extensions += ['rst.linker'] link_files = { '../CHANGES.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( pattern=r'(Issue #|\B#)(?P\d+)', url='{package_url}/issues/{issue}', ), dict( pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), dict( pattern=r'PEP[- ](?P\d+)', url='https://peps.python.org/pep-{pep_number:0>4}/', ), ], ) } # Be strict about any broken references nitpicky = True nitpick_ignore = [] # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 extensions += ['sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } # Preserve authored syntax for defaults autodoc_preserve_defaults = True extensions += ['jaraco.tidelift'] # jaraco/jaraco.collections#11 nitpick_ignore += [ ('py:class', 'v, remove specified key and return the corresponding value.'), ('py:class', 'None. Update D from dict/iterable E and F.'), ('py:class', 'D[k] if k in D, else d. d defaults to None.'), ] nitpick_ignore += [ ('py:class', 're.Pattern'), ] jaraco.collections-4.2.0/docs/history.rst000066400000000000000000000001211443612243100204460ustar00rootroot00000000000000:tocdepth: 2 .. _changes: History ******* .. include:: ../CHANGES (links).rst jaraco.collections-4.2.0/docs/index.rst000066400000000000000000000005141443612243100200620ustar00rootroot00000000000000Welcome to |project| documentation! =================================== .. toctree:: :maxdepth: 1 history .. tidelift-referral-banner:: .. automodule:: jaraco.collections :members: :undoc-members: :show-inheritance: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` jaraco.collections-4.2.0/jaraco/000077500000000000000000000000001443612243100165305ustar00rootroot00000000000000jaraco.collections-4.2.0/jaraco/collections.py000066400000000000000000000644601443612243100214320ustar00rootroot00000000000000import re import operator import collections.abc import itertools import copy import functools import random import warnings from collections.abc import Container, Iterable, Mapping from typing import Callable, Union import jaraco.text _Matchable = Union[Callable, Container, Iterable, re.Pattern] def _dispatch(obj: _Matchable) -> Callable: # can't rely on singledispatch for Union[Container, Iterable] # due to ambiguity # (https://peps.python.org/pep-0443/#abstract-base-classes). if isinstance(obj, re.Pattern): return obj.fullmatch if not isinstance(obj, Callable): # type: ignore if not isinstance(obj, Container): obj = set(obj) # type: ignore obj = obj.__contains__ return obj # type: ignore class Projection(collections.abc.Mapping): """ Project a set of keys over a mapping >>> sample = {'a': 1, 'b': 2, 'c': 3} >>> prj = Projection(['a', 'c', 'd'], sample) >>> dict(prj) {'a': 1, 'c': 3} Projection also accepts an iterable or callable or pattern. >>> iter_prj = Projection(iter('acd'), sample) >>> call_prj = Projection(lambda k: ord(k) in (97, 99, 100), sample) >>> pat_prj = Projection(re.compile(r'[acd]'), sample) >>> prj == iter_prj == call_prj == pat_prj True Keys should only appear if they were specified and exist in the space. Order is retained. >>> list(prj) ['a', 'c'] Attempting to access a key not in the projection results in a KeyError. >>> prj['b'] Traceback (most recent call last): ... KeyError: 'b' Use the projection to update another dict. >>> target = {'a': 2, 'b': 2} >>> target.update(prj) >>> target {'a': 1, 'b': 2, 'c': 3} Projection keeps a reference to the original dict, so modifying the original dict may modify the Projection. >>> del sample['a'] >>> dict(prj) {'c': 3} """ def __init__(self, keys: _Matchable, space: Mapping): self._match = _dispatch(keys) self._space = space def __getitem__(self, key): if not self._match(key): raise KeyError(key) return self._space[key] def _keys_resolved(self): return filter(self._match, self._space) def __iter__(self): return self._keys_resolved() def __len__(self): return len(tuple(self._keys_resolved())) class Mask(Projection): """ The inverse of a projection, masking out keys. >>> sample = {'a': 1, 'b': 2, 'c': 3} >>> msk = Mask(['a', 'c', 'd'], sample) >>> dict(msk) {'b': 2} """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # self._match = compose(operator.not_, self._match) self._match = lambda key, orig=self._match: not orig(key) class DictFilter(Projection): """ *Deprecated* Takes a dict and simulates a sub-dict based on a pattern. >>> sample = dict(a=1, b=2, c=3, d=4, ef=5) Filter for only single-character keys: >>> filtered = DictFilter(sample, include_pattern='.$') >>> filtered == dict(a=1, b=2, c=3, d=4) True >>> filtered['e'] Traceback (most recent call last): ... KeyError: 'e' >>> 'e' in filtered False Pattern is useful for excluding keys with a prefix. >>> filtered = DictFilter(sample, include_pattern=r'(?![ace])') >>> filtered == dict(b=2, d=4) True DictFilter keeps a reference to the original dict, so modifying the original dict may modify the filtered dict. >>> del sample['d'] >>> dict(filtered) {'b': 2} """ def __init__(self, dict, *, include_pattern): warnings.warn( "DictFilter is deprecated. Pass re.Pattern to Projection instead.", DeprecationWarning, stacklevel=2, ) super().__init__(re.compile(include_pattern).match, dict) def dict_map(function, dictionary): """ dict_map is much like the built-in function map. It takes a dictionary and applys a function to the values of that dictionary, returning a new dictionary with the mapped values in the original keys. >>> d = dict_map(lambda x:x+1, dict(a=1, b=2)) >>> d == dict(a=2,b=3) True """ return dict((key, function(value)) for key, value in dictionary.items()) class RangeMap(dict): """ A dictionary-like object that uses the keys as bounds for a range. Inclusion of the value for that range is determined by the key_match_comparator, which defaults to less-than-or-equal. A value is returned for a key if it is the first key that matches in the sorted list of keys. One may supply keyword parameters to be passed to the sort function used to sort keys (i.e. key, reverse) as sort_params. Create a map that maps 1-3 -> 'a', 4-6 -> 'b' >>> r = RangeMap({3: 'a', 6: 'b'}) # boy, that was easy >>> r[1], r[2], r[3], r[4], r[5], r[6] ('a', 'a', 'a', 'b', 'b', 'b') Even float values should work so long as the comparison operator supports it. >>> r[4.5] 'b' Notice that the way rangemap is defined, it must be open-ended on one side. >>> r[0] 'a' >>> r[-1] 'a' One can close the open-end of the RangeMap by using undefined_value >>> r = RangeMap({0: RangeMap.undefined_value, 3: 'a', 6: 'b'}) >>> r[0] Traceback (most recent call last): ... KeyError: 0 One can get the first or last elements in the range by using RangeMap.Item >>> last_item = RangeMap.Item(-1) >>> r[last_item] 'b' .last_item is a shortcut for Item(-1) >>> r[RangeMap.last_item] 'b' Sometimes it's useful to find the bounds for a RangeMap >>> r.bounds() (0, 6) RangeMap supports .get(key, default) >>> r.get(0, 'not found') 'not found' >>> r.get(7, 'not found') 'not found' One often wishes to define the ranges by their left-most values, which requires use of sort params and a key_match_comparator. >>> r = RangeMap({1: 'a', 4: 'b'}, ... sort_params=dict(reverse=True), ... key_match_comparator=operator.ge) >>> r[1], r[2], r[3], r[4], r[5], r[6] ('a', 'a', 'a', 'b', 'b', 'b') That wasn't nearly as easy as before, so an alternate constructor is provided: >>> r = RangeMap.left({1: 'a', 4: 'b', 7: RangeMap.undefined_value}) >>> r[1], r[2], r[3], r[4], r[5], r[6] ('a', 'a', 'a', 'b', 'b', 'b') """ def __init__(self, source, sort_params={}, key_match_comparator=operator.le): dict.__init__(self, source) self.sort_params = sort_params self.match = key_match_comparator @classmethod def left(cls, source): return cls( source, sort_params=dict(reverse=True), key_match_comparator=operator.ge ) def __getitem__(self, item): sorted_keys = sorted(self.keys(), **self.sort_params) if isinstance(item, RangeMap.Item): result = self.__getitem__(sorted_keys[item]) else: key = self._find_first_match_(sorted_keys, item) result = dict.__getitem__(self, key) if result is RangeMap.undefined_value: raise KeyError(key) return result def get(self, key, default=None): """ Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method never raises a KeyError. """ try: return self[key] except KeyError: return default def _find_first_match_(self, keys, item): is_match = functools.partial(self.match, item) matches = list(filter(is_match, keys)) if matches: return matches[0] raise KeyError(item) def bounds(self): sorted_keys = sorted(self.keys(), **self.sort_params) return (sorted_keys[RangeMap.first_item], sorted_keys[RangeMap.last_item]) # some special values for the RangeMap undefined_value = type('RangeValueUndefined', (), {})() class Item(int): "RangeMap Item" first_item = Item(0) last_item = Item(-1) def __identity(x): return x def sorted_items(d, key=__identity, reverse=False): """ Return the items of the dictionary sorted by the keys. >>> sample = dict(foo=20, bar=42, baz=10) >>> tuple(sorted_items(sample)) (('bar', 42), ('baz', 10), ('foo', 20)) >>> reverse_string = lambda s: ''.join(reversed(s)) >>> tuple(sorted_items(sample, key=reverse_string)) (('foo', 20), ('bar', 42), ('baz', 10)) >>> tuple(sorted_items(sample, reverse=True)) (('foo', 20), ('baz', 10), ('bar', 42)) """ # wrap the key func so it operates on the first element of each item def pairkey_key(item): return key(item[0]) return sorted(d.items(), key=pairkey_key, reverse=reverse) class KeyTransformingDict(dict): """ A dict subclass that transforms the keys before they're used. Subclasses may override the default transform_key to customize behavior. """ @staticmethod def transform_key(key): # pragma: nocover return key def __init__(self, *args, **kargs): super(KeyTransformingDict, self).__init__() # build a dictionary using the default constructs d = dict(*args, **kargs) # build this dictionary using transformed keys. for item in d.items(): self.__setitem__(*item) def __setitem__(self, key, val): key = self.transform_key(key) super(KeyTransformingDict, self).__setitem__(key, val) def __getitem__(self, key): key = self.transform_key(key) return super(KeyTransformingDict, self).__getitem__(key) def __contains__(self, key): key = self.transform_key(key) return super(KeyTransformingDict, self).__contains__(key) def __delitem__(self, key): key = self.transform_key(key) return super(KeyTransformingDict, self).__delitem__(key) def get(self, key, *args, **kwargs): key = self.transform_key(key) return super(KeyTransformingDict, self).get(key, *args, **kwargs) def setdefault(self, key, *args, **kwargs): key = self.transform_key(key) return super(KeyTransformingDict, self).setdefault(key, *args, **kwargs) def pop(self, key, *args, **kwargs): key = self.transform_key(key) return super(KeyTransformingDict, self).pop(key, *args, **kwargs) def matching_key_for(self, key): """ Given a key, return the actual key stored in self that matches. Raise KeyError if the key isn't found. """ try: return next(e_key for e_key in self.keys() if e_key == key) except StopIteration: raise KeyError(key) class FoldedCaseKeyedDict(KeyTransformingDict): """ A case-insensitive dictionary (keys are compared as insensitive if they are strings). >>> d = FoldedCaseKeyedDict() >>> d['heLlo'] = 'world' >>> list(d.keys()) == ['heLlo'] True >>> list(d.values()) == ['world'] True >>> d['hello'] == 'world' True >>> 'hello' in d True >>> 'HELLO' in d True >>> print(repr(FoldedCaseKeyedDict({'heLlo': 'world'}))) {'heLlo': 'world'} >>> d = FoldedCaseKeyedDict({'heLlo': 'world'}) >>> print(d['hello']) world >>> print(d['Hello']) world >>> list(d.keys()) ['heLlo'] >>> d = FoldedCaseKeyedDict({'heLlo': 'world', 'Hello': 'world'}) >>> list(d.values()) ['world'] >>> key, = d.keys() >>> key in ['heLlo', 'Hello'] True >>> del d['HELLO'] >>> d {} get should work >>> d['Sumthin'] = 'else' >>> d.get('SUMTHIN') 'else' >>> d.get('OTHER', 'thing') 'thing' >>> del d['sumthin'] setdefault should also work >>> d['This'] = 'that' >>> print(d.setdefault('this', 'other')) that >>> len(d) 1 >>> print(d['this']) that >>> print(d.setdefault('That', 'other')) other >>> print(d['THAT']) other Make it pop! >>> print(d.pop('THAT')) other To retrieve the key in its originally-supplied form, use matching_key_for >>> print(d.matching_key_for('this')) This >>> d.matching_key_for('missing') Traceback (most recent call last): ... KeyError: 'missing' """ @staticmethod def transform_key(key): return jaraco.text.FoldedCase(key) class DictAdapter: """ Provide a getitem interface for attributes of an object. Let's say you want to get at the string.lowercase property in a formatted string. It's easy with DictAdapter. >>> import string >>> print("lowercase is %(ascii_lowercase)s" % DictAdapter(string)) lowercase is abcdefghijklmnopqrstuvwxyz """ def __init__(self, wrapped_ob): self.object = wrapped_ob def __getitem__(self, name): return getattr(self.object, name) class ItemsAsAttributes: """ Mix-in class to enable a mapping object to provide items as attributes. >>> C = type('C', (dict, ItemsAsAttributes), dict()) >>> i = C() >>> i['foo'] = 'bar' >>> i.foo 'bar' Natural attribute access takes precedence >>> i.foo = 'henry' >>> i.foo 'henry' But as you might expect, the mapping functionality is preserved. >>> i['foo'] 'bar' A normal attribute error should be raised if an attribute is requested that doesn't exist. >>> i.missing Traceback (most recent call last): ... AttributeError: 'C' object has no attribute 'missing' It also works on dicts that customize __getitem__ >>> missing_func = lambda self, key: 'missing item' >>> C = type( ... 'C', ... (dict, ItemsAsAttributes), ... dict(__missing__ = missing_func), ... ) >>> i = C() >>> i.missing 'missing item' >>> i.foo 'missing item' """ def __getattr__(self, key): try: return getattr(super(ItemsAsAttributes, self), key) except AttributeError as e: # attempt to get the value from the mapping (return self[key]) # but be careful not to lose the original exception context. noval = object() def _safe_getitem(cont, key, missing_result): try: return cont[key] except KeyError: return missing_result result = _safe_getitem(self, key, noval) if result is not noval: return result # raise the original exception, but use the original class # name, not 'super'. (message,) = e.args message = message.replace('super', self.__class__.__name__, 1) e.args = (message,) raise def invert_map(map): """ Given a dictionary, return another dictionary with keys and values switched. If any of the values resolve to the same key, raises a ValueError. >>> numbers = dict(a=1, b=2, c=3) >>> letters = invert_map(numbers) >>> letters[1] 'a' >>> numbers['d'] = 3 >>> invert_map(numbers) Traceback (most recent call last): ... ValueError: Key conflict in inverted mapping """ res = dict((v, k) for k, v in map.items()) if not len(res) == len(map): raise ValueError('Key conflict in inverted mapping') return res class IdentityOverrideMap(dict): """ A dictionary that by default maps each key to itself, but otherwise acts like a normal dictionary. >>> d = IdentityOverrideMap() >>> d[42] 42 >>> d['speed'] = 'speedo' >>> print(d['speed']) speedo """ def __missing__(self, key): return key class DictStack(list, collections.abc.MutableMapping): """ A stack of dictionaries that behaves as a view on those dictionaries, giving preference to the last. >>> stack = DictStack([dict(a=1, c=2), dict(b=2, a=2)]) >>> stack['a'] 2 >>> stack['b'] 2 >>> stack['c'] 2 >>> len(stack) 3 >>> stack.push(dict(a=3)) >>> stack['a'] 3 >>> stack['a'] = 4 >>> set(stack.keys()) == set(['a', 'b', 'c']) True >>> set(stack.items()) == set([('a', 4), ('b', 2), ('c', 2)]) True >>> dict(**stack) == dict(stack) == dict(a=4, c=2, b=2) True >>> d = stack.pop() >>> stack['a'] 2 >>> d = stack.pop() >>> stack['a'] 1 >>> stack.get('b', None) >>> 'c' in stack True >>> del stack['c'] >>> dict(stack) {'a': 1} """ def __iter__(self): dicts = list.__iter__(self) return iter(set(itertools.chain.from_iterable(c.keys() for c in dicts))) def __getitem__(self, key): for scope in reversed(tuple(list.__iter__(self))): if key in scope: return scope[key] raise KeyError(key) push = list.append def __contains__(self, other): return collections.abc.Mapping.__contains__(self, other) def __len__(self): return len(list(iter(self))) def __setitem__(self, key, item): last = list.__getitem__(self, -1) return last.__setitem__(key, item) def __delitem__(self, key): last = list.__getitem__(self, -1) return last.__delitem__(key) # workaround for mypy confusion def pop(self, *args, **kwargs): return list.pop(self, *args, **kwargs) class BijectiveMap(dict): """ A Bijective Map (two-way mapping). Implemented as a simple dictionary of 2x the size, mapping values back to keys. Note, this implementation may be incomplete. If there's not a test for your use case below, it's likely to fail, so please test and send pull requests or patches for additional functionality needed. >>> m = BijectiveMap() >>> m['a'] = 'b' >>> m == {'a': 'b', 'b': 'a'} True >>> print(m['b']) a >>> m['c'] = 'd' >>> len(m) 2 Some weird things happen if you map an item to itself or overwrite a single key of a pair, so it's disallowed. >>> m['e'] = 'e' Traceback (most recent call last): ValueError: Key cannot map to itself >>> m['d'] = 'e' Traceback (most recent call last): ValueError: Key/Value pairs may not overlap >>> m['e'] = 'd' Traceback (most recent call last): ValueError: Key/Value pairs may not overlap >>> print(m.pop('d')) c >>> 'c' in m False >>> m = BijectiveMap(dict(a='b')) >>> len(m) 1 >>> print(m['b']) a >>> m = BijectiveMap() >>> m.update(a='b') >>> m['b'] 'a' >>> del m['b'] >>> len(m) 0 >>> 'a' in m False """ def __init__(self, *args, **kwargs): super(BijectiveMap, self).__init__() self.update(*args, **kwargs) def __setitem__(self, item, value): if item == value: raise ValueError("Key cannot map to itself") overlap = ( item in self and self[item] != value or value in self and self[value] != item ) if overlap: raise ValueError("Key/Value pairs may not overlap") super(BijectiveMap, self).__setitem__(item, value) super(BijectiveMap, self).__setitem__(value, item) def __delitem__(self, item): self.pop(item) def __len__(self): return super(BijectiveMap, self).__len__() // 2 def pop(self, key, *args, **kwargs): mirror = self[key] super(BijectiveMap, self).__delitem__(mirror) return super(BijectiveMap, self).pop(key, *args, **kwargs) def update(self, *args, **kwargs): # build a dictionary using the default constructs d = dict(*args, **kwargs) # build this dictionary using transformed keys. for item in d.items(): self.__setitem__(*item) class FrozenDict(collections.abc.Mapping, collections.abc.Hashable): """ An immutable mapping. >>> a = FrozenDict(a=1, b=2) >>> b = FrozenDict(a=1, b=2) >>> a == b True >>> a == dict(a=1, b=2) True >>> dict(a=1, b=2) == a True >>> 'a' in a True >>> type(hash(a)) is type(0) True >>> set(iter(a)) == {'a', 'b'} True >>> len(a) 2 >>> a['a'] == a.get('a') == 1 True >>> a['c'] = 3 Traceback (most recent call last): ... TypeError: 'FrozenDict' object does not support item assignment >>> a.update(y=3) Traceback (most recent call last): ... AttributeError: 'FrozenDict' object has no attribute 'update' Copies should compare equal >>> copy.copy(a) == a True Copies should be the same type >>> isinstance(copy.copy(a), FrozenDict) True FrozenDict supplies .copy(), even though collections.abc.Mapping doesn't demand it. >>> a.copy() == a True >>> a.copy() is not a True """ __slots__ = ['__data'] def __new__(cls, *args, **kwargs): self = super(FrozenDict, cls).__new__(cls) self.__data = dict(*args, **kwargs) return self # Container def __contains__(self, key): return key in self.__data # Hashable def __hash__(self): return hash(tuple(sorted(self.__data.items()))) # Mapping def __iter__(self): return iter(self.__data) def __len__(self): return len(self.__data) def __getitem__(self, key): return self.__data[key] # override get for efficiency provided by dict def get(self, *args, **kwargs): return self.__data.get(*args, **kwargs) # override eq to recognize underlying implementation def __eq__(self, other): if isinstance(other, FrozenDict): other = other.__data return self.__data.__eq__(other) def copy(self): "Return a shallow copy of self" return copy.copy(self) class Enumeration(ItemsAsAttributes, BijectiveMap): """ A convenient way to provide enumerated values >>> e = Enumeration('a b c') >>> e['a'] 0 >>> e.a 0 >>> e[1] 'b' >>> set(e.names) == set('abc') True >>> set(e.codes) == set(range(3)) True >>> e.get('d') is None True Codes need not start with 0 >>> e = Enumeration('a b c', range(1, 4)) >>> e['a'] 1 >>> e[3] 'c' """ def __init__(self, names, codes=None): if isinstance(names, str): names = names.split() if codes is None: codes = itertools.count() super(Enumeration, self).__init__(zip(names, codes)) @property def names(self): return (key for key in self if isinstance(key, str)) @property def codes(self): return (self[name] for name in self.names) class Everything: """ A collection "containing" every possible thing. >>> 'foo' in Everything() True >>> import random >>> random.randint(1, 999) in Everything() True >>> random.choice([None, 'foo', 42, ('a', 'b', 'c')]) in Everything() True """ def __contains__(self, other): return True class InstrumentedDict(collections.UserDict): # type: ignore # buggy mypy """ Instrument an existing dictionary with additional functionality, but always reference and mutate the original dictionary. >>> orig = {'a': 1, 'b': 2} >>> inst = InstrumentedDict(orig) >>> inst['a'] 1 >>> inst['c'] = 3 >>> orig['c'] 3 >>> inst.keys() == orig.keys() True """ def __init__(self, data): super().__init__() self.data = data class Least: """ A value that is always lesser than any other >>> least = Least() >>> 3 < least False >>> 3 > least True >>> least < 3 True >>> least <= 3 True >>> least > 3 False >>> 'x' > least True >>> None > least True """ def __le__(self, other): return True __lt__ = __le__ def __ge__(self, other): return False __gt__ = __ge__ class Greatest: """ A value that is always greater than any other >>> greatest = Greatest() >>> 3 < greatest True >>> 3 > greatest False >>> greatest < 3 False >>> greatest > 3 True >>> greatest >= 3 True >>> 'x' > greatest False >>> None > greatest False """ def __ge__(self, other): return True __gt__ = __ge__ def __le__(self, other): return False __lt__ = __le__ def pop_all(items): """ Clear items in place and return a copy of items. >>> items = [1, 2, 3] >>> popped = pop_all(items) >>> popped is items False >>> popped [1, 2, 3] >>> items [] """ result, items[:] = items[:], [] return result # mypy disabled for pytest-dev/pytest#8332 class FreezableDefaultDict(collections.defaultdict): # type: ignore """ Often it is desirable to prevent the mutation of a default dict after its initial construction, such as to prevent mutation during iteration. >>> dd = FreezableDefaultDict(list) >>> dd[0].append('1') >>> dd.freeze() >>> dd[1] [] >>> len(dd) 1 """ def __missing__(self, key): return getattr(self, '_frozen', super().__missing__)(key) def freeze(self): self._frozen = lambda key: self.default_factory() class Accumulator: def __init__(self, initial=0): self.val = initial def __call__(self, val): self.val += val return self.val class WeightedLookup(RangeMap): """ Given parameters suitable for a dict representing keys and a weighted proportion, return a RangeMap representing spans of values proportial to the weights: >>> even = WeightedLookup(a=1, b=1) [0, 1) -> a [1, 2) -> b >>> lk = WeightedLookup(a=1, b=2) [0, 1) -> a [1, 3) -> b >>> lk[.5] 'a' >>> lk[1.5] 'b' Adds ``.random()`` to select a random weighted value: >>> lk.random() in ['a', 'b'] True >>> choices = [lk.random() for x in range(1000)] Statistically speaking, choices should be .5 a:b >>> ratio = choices.count('a') / choices.count('b') >>> .4 < ratio < .6 True """ def __init__(self, *args, **kwargs): raw = dict(*args, **kwargs) # allocate keys by weight indexes = map(Accumulator(), raw.values()) super().__init__(zip(indexes, raw.keys()), key_match_comparator=operator.lt) def random(self): lower, upper = self.bounds() selector = random.random() * upper return self[selector] jaraco.collections-4.2.0/mypy.ini000066400000000000000000000002321443612243100167650ustar00rootroot00000000000000[mypy] ignore_missing_imports = True # required to support namespace packages # https://github.com/python/mypy/issues/14057 explicit_package_bases = True jaraco.collections-4.2.0/pyproject.toml000066400000000000000000000005661443612243100202140ustar00rootroot00000000000000[build-system] requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" [tool.black] skip-string-normalization = true [tool.setuptools_scm] [tool.pytest-enabler.black] addopts = "--black" [tool.pytest-enabler.mypy] addopts = "--mypy" [tool.pytest-enabler.cov] addopts = "--cov" [tool.pytest-enabler.ruff] addopts = "--ruff" jaraco.collections-4.2.0/pytest.ini000066400000000000000000000016041443612243100173230ustar00rootroot00000000000000[pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules --import-mode=importlib filterwarnings= ## upstream # Ensure ResourceWarnings are emitted default::ResourceWarning # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy # python/cpython#100750 ignore:'encoding' argument not specified::platform # pypa/build#615 ignore:'encoding' argument not specified::build.env ## end upstream ignore:DictFilter is deprecated jaraco.collections-4.2.0/setup.cfg000066400000000000000000000022061443612243100171120ustar00rootroot00000000000000[metadata] name = jaraco.collections author = Jason R. Coombs author_email = jaraco@jaraco.com description = Collection objects similar to those in stdlib by jaraco long_description = file:README.rst url = https://github.com/jaraco/jaraco.collections classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only [options] packages = find_namespace: include_package_data = true python_requires = >=3.7 install_requires = jaraco.text [options.packages.find] exclude = build* dist* docs* tests* [options.extras_require] testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.3 pytest-ruff # local docs = # upstream sphinx >= 3.5 jaraco.packaging >= 9 rst.linker >= 1.9 furo sphinx-lint # tidelift jaraco.tidelift >= 1.4 # local [options.entry_points] jaraco.collections-4.2.0/tests/000077500000000000000000000000001443612243100164335ustar00rootroot00000000000000jaraco.collections-4.2.0/tests/test_collections.py000066400000000000000000000012401443612243100223570ustar00rootroot00000000000000from jaraco import collections class AlwaysStringKeysDict(collections.KeyTransformingDict): """ An implementation of a KeyTransformingDict subclass that always converts the keys to strings. """ @staticmethod def transform_key(key): return str(key) def test_always_lower_keys_dict(): """ Tests AlwaysLowerKeysDict to ensure KeyTransformingDict works. """ d = AlwaysStringKeysDict() d['farther'] = 'closer' d['Lasting'] = 'fleeting' d[3] = 'three' d[{'a': 1}] = 'a is one' assert all(isinstance(key, str) for key in d) assert "{'a': 1}" in d assert 3 in d assert d[3] == d['3'] == 'three' jaraco.collections-4.2.0/tox.ini000066400000000000000000000014331443612243100166050ustar00rootroot00000000000000[tox] envlist = python minversion = 3.2 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = setenv = PYTHONWARNDEFAULTENCODING = 1 commands = pytest {posargs} usedevelop = True extras = testing [testenv:docs] extras = docs testing changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html python -m sphinxlint [testenv:release] skip_install = True deps = build twine>=3 jaraco.develop>=7.1 passenv = TWINE_PASSWORD GITHUB_TOKEN setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" python -m build python -m twine upload dist/* python -m jaraco.develop.create-github-release