├── .github
└── workflows
│ ├── integration.yml
│ └── release.yml
├── .gitignore
├── .pylintrc
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs
├── Makefile
├── _static
│ └── gj-logo.png
├── _templates
│ └── gumroad.html
├── conf.py
├── index.rst
├── indexabledict.rst
├── indexableset.rst
├── itemsorteddict.rst
├── make.bat
├── nearestdict.rst
├── ordereddict.rst
├── orderedset.rst
├── segmentlist.rst
└── valuesorteddict.rst
├── mypy.ini
├── requirements.txt
├── setup.py
├── sortedcollections
├── __init__.py
├── nearestdict.py
├── ordereddict.py
└── recipes.py
├── tests
├── __init__.py
├── test_doctest.py
├── test_itemsorteddict.py
├── test_nearestdict.py
├── test_ordereddict.py
├── test_orderedset.py
├── test_recipes.py
└── test_valuesorteddict.py
└── tox.ini
/.github/workflows/integration.yml:
--------------------------------------------------------------------------------
1 | name: integration
2 |
3 | on: [push]
4 |
5 | jobs:
6 |
7 | checks:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | max-parallel: 8
11 | matrix:
12 | check: [bluecheck, doc8, docs, flake8, isortcheck, mypy, pylint, rstcheck]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Set up Python
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: 3.9
20 | - name: Install dependencies
21 | run: |
22 | pip install --upgrade pip
23 | pip install tox
24 | - name: Run checks with tox
25 | run: |
26 | tox -e ${{ matrix.check }}
27 |
28 | tests:
29 | needs: checks
30 | runs-on: ${{ matrix.os }}
31 | strategy:
32 | max-parallel: 8
33 | matrix:
34 | os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-16.04]
35 | python-version: [3.6, 3.7, 3.8, 3.9]
36 |
37 | steps:
38 | - name: Set up Python ${{ matrix.python-version }} x64
39 | uses: actions/setup-python@v2
40 | with:
41 | python-version: ${{ matrix.python-version }}
42 | architecture: x64
43 |
44 | - uses: actions/checkout@v2
45 |
46 | - name: Install tox
47 | run: |
48 | pip install --upgrade pip
49 | pip install tox
50 |
51 | - name: Test with tox
52 | run: tox -e py
53 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 |
10 | upload:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v2
18 | with:
19 | python-version: 3.9
20 |
21 | - name: Install dependencies
22 | run: |
23 | pip install --upgrade pip
24 | pip install -r requirements.txt
25 |
26 | - name: Create source dist
27 | run: python setup.py sdist
28 |
29 | - name: Create wheel dist
30 | run: python setup.py bdist_wheel
31 |
32 | - name: Upload with twine
33 | env:
34 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
35 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
36 | run: |
37 | ls -l dist/*
38 | twine upload dist/*
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python byte-code
2 | *.py[co]
3 |
4 | # virutalenv directories
5 | /env*/
6 |
7 | # coverage files
8 | .coverage
9 |
10 | # setup sdist, test and upload directories
11 | /.tox/
12 | /build/
13 | /dist/
14 | /sortedcollections.egg-info/
15 | /docs/_build/
16 |
17 | .DS_Store
18 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # A comma-separated list of package or module names from where C extensions may
4 | # be loaded. Extensions are loading into the active Python interpreter and may
5 | # run arbitrary code.
6 | extension-pkg-whitelist=
7 |
8 | # Add files or directories to the blacklist. They should be base names, not
9 | # paths.
10 | ignore=CVS
11 |
12 | # Add files or directories matching the regex patterns to the blacklist. The
13 | # regex matches against base names, not paths.
14 | ignore-patterns=
15 |
16 | # Python code to execute, usually for sys.path manipulation such as
17 | # pygtk.require().
18 | #init-hook=
19 |
20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
21 | # number of processors available to use.
22 | jobs=1
23 |
24 | # Control the amount of potential inferred values when inferring a single
25 | # object. This can help the performance when dealing with large functions or
26 | # complex, nested conditions.
27 | limit-inference-results=100
28 |
29 | # List of plugins (as comma separated values of python modules names) to load,
30 | # usually to register additional checkers.
31 | load-plugins=
32 |
33 | # Pickle collected data for later comparisons.
34 | persistent=yes
35 |
36 | # Specify a configuration file.
37 | #rcfile=
38 |
39 | # When enabled, pylint would attempt to guess common misconfiguration and emit
40 | # user-friendly hints instead of false-positive error messages.
41 | suggestion-mode=yes
42 |
43 | # Allow loading of arbitrary C extensions. Extensions are imported into the
44 | # active Python interpreter and may run arbitrary code.
45 | unsafe-load-any-extension=no
46 |
47 |
48 | [MESSAGES CONTROL]
49 |
50 | # Only show warnings with the listed confidence levels. Leave empty to show
51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
52 | confidence=
53 |
54 | # Disable the message, report, category or checker with the given id(s). You
55 | # can either give multiple identifiers separated by comma (,) or put this
56 | # option multiple times (only on the command line, not in the configuration
57 | # file where it should appear only once). You can also use "--disable=all" to
58 | # disable everything first and then reenable specific checks. For example, if
59 | # you want to run only the similarities checker, you can use "--disable=all
60 | # --enable=similarities". If you want to run only the classes checker, but have
61 | # no Warning level messages displayed, use "--disable=all --enable=classes
62 | # --disable=W".
63 | disable=print-statement,
64 | parameter-unpacking,
65 | unpacking-in-except,
66 | old-raise-syntax,
67 | backtick,
68 | long-suffix,
69 | old-ne-operator,
70 | old-octal-literal,
71 | import-star-module-level,
72 | non-ascii-bytes-literal,
73 | raw-checker-failed,
74 | bad-inline-option,
75 | locally-disabled,
76 | file-ignored,
77 | suppressed-message,
78 | useless-suppression,
79 | deprecated-pragma,
80 | use-symbolic-message-instead,
81 | apply-builtin,
82 | basestring-builtin,
83 | buffer-builtin,
84 | cmp-builtin,
85 | coerce-builtin,
86 | execfile-builtin,
87 | file-builtin,
88 | long-builtin,
89 | raw_input-builtin,
90 | reduce-builtin,
91 | standarderror-builtin,
92 | unicode-builtin,
93 | xrange-builtin,
94 | coerce-method,
95 | delslice-method,
96 | getslice-method,
97 | setslice-method,
98 | no-absolute-import,
99 | old-division,
100 | dict-iter-method,
101 | dict-view-method,
102 | next-method-called,
103 | metaclass-assignment,
104 | indexing-exception,
105 | raising-string,
106 | reload-builtin,
107 | oct-method,
108 | hex-method,
109 | nonzero-method,
110 | cmp-method,
111 | input-builtin,
112 | round-builtin,
113 | intern-builtin,
114 | unichr-builtin,
115 | map-builtin-not-iterating,
116 | zip-builtin-not-iterating,
117 | range-builtin-not-iterating,
118 | filter-builtin-not-iterating,
119 | using-cmp-argument,
120 | eq-without-hash,
121 | div-method,
122 | idiv-method,
123 | rdiv-method,
124 | exception-message-attribute,
125 | invalid-str-codec,
126 | sys-max-int,
127 | bad-python3-import,
128 | deprecated-string-function,
129 | deprecated-str-translate-call,
130 | deprecated-itertools-function,
131 | deprecated-types-field,
132 | next-method-defined,
133 | dict-items-not-iterating,
134 | dict-keys-not-iterating,
135 | dict-values-not-iterating,
136 | deprecated-operator-function,
137 | deprecated-urllib-function,
138 | xreadlines-attribute,
139 | deprecated-sys-function,
140 | exception-escape,
141 | comprehension-escape,
142 | super-with-arguments,
143 | raise-missing-from
144 |
145 | # Enable the message, report, category or checker with the given id(s). You can
146 | # either give multiple identifier separated by comma (,) or put this option
147 | # multiple time (only on the command line, not in the configuration file where
148 | # it should appear only once). See also the "--disable" option for examples.
149 | enable=c-extension-no-member
150 |
151 |
152 | [REPORTS]
153 |
154 | # Python expression which should return a note less than 10 (10 is the highest
155 | # note). You have access to the variables errors warning, statement which
156 | # respectively contain the number of errors / warnings messages and the total
157 | # number of statements analyzed. This is used by the global evaluation report
158 | # (RP0004).
159 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
160 |
161 | # Template used to display messages. This is a python new-style format string
162 | # used to format the message information. See doc for all details.
163 | #msg-template=
164 |
165 | # Set the output format. Available formats are text, parseable, colorized, json
166 | # and msvs (visual studio). You can also give a reporter class, e.g.
167 | # mypackage.mymodule.MyReporterClass.
168 | output-format=text
169 |
170 | # Tells whether to display a full report or only the messages.
171 | reports=no
172 |
173 | # Activate the evaluation score.
174 | score=yes
175 |
176 |
177 | [REFACTORING]
178 |
179 | # Maximum number of nested blocks for function / method body
180 | max-nested-blocks=5
181 |
182 | # Complete name of functions that never returns. When checking for
183 | # inconsistent-return-statements if a never returning function is called then
184 | # it will be considered as an explicit return statement and no message will be
185 | # printed.
186 | never-returning-functions=sys.exit
187 |
188 |
189 | [LOGGING]
190 |
191 | # Format style used to check logging format string. `old` means using %
192 | # formatting, while `new` is for `{}` formatting.
193 | logging-format-style=old
194 |
195 | # Logging modules to check that the string format arguments are in logging
196 | # function parameter format.
197 | logging-modules=logging
198 |
199 |
200 | [SPELLING]
201 |
202 | # Limits count of emitted suggestions for spelling mistakes.
203 | max-spelling-suggestions=4
204 |
205 | # Spelling dictionary name. Available dictionaries: none. To make it working
206 | # install python-enchant package..
207 | spelling-dict=
208 |
209 | # List of comma separated words that should not be checked.
210 | spelling-ignore-words=
211 |
212 | # A path to a file that contains private dictionary; one word per line.
213 | spelling-private-dict-file=
214 |
215 | # Tells whether to store unknown words to indicated private dictionary in
216 | # --spelling-private-dict-file option instead of raising a message.
217 | spelling-store-unknown-words=no
218 |
219 |
220 | [MISCELLANEOUS]
221 |
222 | # List of note tags to take in consideration, separated by a comma.
223 | notes=FIXME,
224 | XXX,
225 | TODO
226 |
227 |
228 | [TYPECHECK]
229 |
230 | # List of decorators that produce context managers, such as
231 | # contextlib.contextmanager. Add to this list to register other decorators that
232 | # produce valid context managers.
233 | contextmanager-decorators=contextlib.contextmanager
234 |
235 | # List of members which are set dynamically and missed by pylint inference
236 | # system, and so shouldn't trigger E1101 when accessed. Python regular
237 | # expressions are accepted.
238 | generated-members=
239 |
240 | # Tells whether missing members accessed in mixin class should be ignored. A
241 | # mixin class is detected if its name ends with "mixin" (case insensitive).
242 | ignore-mixin-members=yes
243 |
244 | # Tells whether to warn about missing members when the owner of the attribute
245 | # is inferred to be None.
246 | ignore-none=yes
247 |
248 | # This flag controls whether pylint should warn about no-member and similar
249 | # checks whenever an opaque object is returned when inferring. The inference
250 | # can return multiple potential results while evaluating a Python object, but
251 | # some branches might not be evaluated, which results in partial inference. In
252 | # that case, it might be useful to still emit no-member and other checks for
253 | # the rest of the inferred objects.
254 | ignore-on-opaque-inference=yes
255 |
256 | # List of class names for which member attributes should not be checked (useful
257 | # for classes with dynamically set attributes). This supports the use of
258 | # qualified names.
259 | ignored-classes=optparse.Values,thread._local,_thread._local
260 |
261 | # List of module names for which member attributes should not be checked
262 | # (useful for modules/projects where namespaces are manipulated during runtime
263 | # and thus existing member attributes cannot be deduced by static analysis. It
264 | # supports qualified module names, as well as Unix pattern matching.
265 | ignored-modules=
266 |
267 | # Show a hint with possible names when a member name was not found. The aspect
268 | # of finding the hint is based on edit distance.
269 | missing-member-hint=yes
270 |
271 | # The minimum edit distance a name should have in order to be considered a
272 | # similar match for a missing member name.
273 | missing-member-hint-distance=1
274 |
275 | # The total number of similar names that should be taken in consideration when
276 | # showing a hint for a missing member.
277 | missing-member-max-choices=1
278 |
279 |
280 | [VARIABLES]
281 |
282 | # List of additional names supposed to be defined in builtins. Remember that
283 | # you should avoid defining new builtins when possible.
284 | additional-builtins=
285 |
286 | # Tells whether unused global variables should be treated as a violation.
287 | allow-global-unused-variables=yes
288 |
289 | # List of strings which can identify a callback function by name. A callback
290 | # name must start or end with one of those strings.
291 | callbacks=cb_,
292 | _cb
293 |
294 | # A regular expression matching the name of dummy variables (i.e. expected to
295 | # not be used).
296 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
297 |
298 | # Argument names that match this expression will be ignored. Default to name
299 | # with leading underscore.
300 | ignored-argument-names=_.*|^ignored_|^unused_
301 |
302 | # Tells whether we should check for unused import in __init__ files.
303 | init-import=no
304 |
305 | # List of qualified module names which can have objects that can redefine
306 | # builtins.
307 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
308 |
309 |
310 | [FORMAT]
311 |
312 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
313 | expected-line-ending-format=
314 |
315 | # Regexp for a line that is allowed to be longer than the limit.
316 | ignore-long-lines=^\s*(# )??$
317 |
318 | # Number of spaces of indent required inside a hanging or continued line.
319 | indent-after-paren=4
320 |
321 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
322 | # tab).
323 | indent-string=' '
324 |
325 | # Maximum number of characters on a single line.
326 | max-line-length=100
327 |
328 | # Maximum number of lines in a module.
329 | max-module-lines=1000
330 |
331 | # List of optional constructs for which whitespace checking is disabled. `dict-
332 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
333 | # `trailing-comma` allows a space between comma and closing bracket: (a, ).
334 | # `empty-line` allows space-only lines.
335 | no-space-check=trailing-comma,
336 | dict-separator
337 |
338 | # Allow the body of a class to be on the same line as the declaration if body
339 | # contains single statement.
340 | single-line-class-stmt=no
341 |
342 | # Allow the body of an if to be on the same line as the test if there is no
343 | # else.
344 | single-line-if-stmt=no
345 |
346 |
347 | [SIMILARITIES]
348 |
349 | # Ignore comments when computing similarities.
350 | ignore-comments=yes
351 |
352 | # Ignore docstrings when computing similarities.
353 | ignore-docstrings=yes
354 |
355 | # Ignore imports when computing similarities.
356 | ignore-imports=no
357 |
358 | # Minimum lines number of a similarity.
359 | min-similarity-lines=4
360 |
361 |
362 | [BASIC]
363 |
364 | # Naming style matching correct argument names.
365 | argument-naming-style=snake_case
366 |
367 | # Regular expression matching correct argument names. Overrides argument-
368 | # naming-style.
369 | #argument-rgx=
370 |
371 | # Naming style matching correct attribute names.
372 | attr-naming-style=snake_case
373 |
374 | # Regular expression matching correct attribute names. Overrides attr-naming-
375 | # style.
376 | #attr-rgx=
377 |
378 | # Bad variable names which should always be refused, separated by a comma.
379 | bad-names=foo,
380 | bar,
381 | baz,
382 | toto,
383 | tutu,
384 | tata
385 |
386 | # Naming style matching correct class attribute names.
387 | class-attribute-naming-style=any
388 |
389 | # Regular expression matching correct class attribute names. Overrides class-
390 | # attribute-naming-style.
391 | #class-attribute-rgx=
392 |
393 | # Naming style matching correct class names.
394 | class-naming-style=PascalCase
395 |
396 | # Regular expression matching correct class names. Overrides class-naming-
397 | # style.
398 | #class-rgx=
399 |
400 | # Naming style matching correct constant names.
401 | const-naming-style=UPPER_CASE
402 |
403 | # Regular expression matching correct constant names. Overrides const-naming-
404 | # style.
405 | #const-rgx=
406 |
407 | # Minimum line length for functions/classes that require docstrings, shorter
408 | # ones are exempt.
409 | docstring-min-length=-1
410 |
411 | # Naming style matching correct function names.
412 | function-naming-style=snake_case
413 |
414 | # Regular expression matching correct function names. Overrides function-
415 | # naming-style.
416 | #function-rgx=
417 |
418 | # Good variable names which should always be accepted, separated by a comma.
419 | good-names=i,
420 | j,
421 | k,
422 | ex,
423 | Run,
424 | _
425 |
426 | # Include a hint for the correct naming format with invalid-name.
427 | include-naming-hint=no
428 |
429 | # Naming style matching correct inline iteration names.
430 | inlinevar-naming-style=any
431 |
432 | # Regular expression matching correct inline iteration names. Overrides
433 | # inlinevar-naming-style.
434 | #inlinevar-rgx=
435 |
436 | # Naming style matching correct method names.
437 | method-naming-style=snake_case
438 |
439 | # Regular expression matching correct method names. Overrides method-naming-
440 | # style.
441 | #method-rgx=
442 |
443 | # Naming style matching correct module names.
444 | module-naming-style=snake_case
445 |
446 | # Regular expression matching correct module names. Overrides module-naming-
447 | # style.
448 | #module-rgx=
449 |
450 | # Colon-delimited sets of names that determine each other's naming style when
451 | # the name regexes allow several styles.
452 | name-group=
453 |
454 | # Regular expression which should only match function or class names that do
455 | # not require a docstring.
456 | no-docstring-rgx=^_
457 |
458 | # List of decorators that produce properties, such as abc.abstractproperty. Add
459 | # to this list to register other decorators that produce valid properties.
460 | # These decorators are taken in consideration only for invalid-name.
461 | property-classes=abc.abstractproperty
462 |
463 | # Naming style matching correct variable names.
464 | variable-naming-style=snake_case
465 |
466 | # Regular expression matching correct variable names. Overrides variable-
467 | # naming-style.
468 | #variable-rgx=
469 |
470 |
471 | [IMPORTS]
472 |
473 | # Allow wildcard imports from modules that define __all__.
474 | allow-wildcard-with-all=no
475 |
476 | # Analyse import fallback blocks. This can be used to support both Python 2 and
477 | # 3 compatible code, which means that the block might have code that exists
478 | # only in one or another interpreter, leading to false positives when analysed.
479 | analyse-fallback-blocks=no
480 |
481 | # Deprecated modules which should not be used, separated by a comma.
482 | deprecated-modules=optparse,tkinter.tix
483 |
484 | # Create a graph of external dependencies in the given file (report RP0402 must
485 | # not be disabled).
486 | ext-import-graph=
487 |
488 | # Create a graph of every (i.e. internal and external) dependencies in the
489 | # given file (report RP0402 must not be disabled).
490 | import-graph=
491 |
492 | # Create a graph of internal dependencies in the given file (report RP0402 must
493 | # not be disabled).
494 | int-import-graph=
495 |
496 | # Force import order to recognize a module as part of the standard
497 | # compatibility libraries.
498 | known-standard-library=
499 |
500 | # Force import order to recognize a module as part of a third party library.
501 | known-third-party=enchant
502 |
503 |
504 | [CLASSES]
505 |
506 | # List of method names used to declare (i.e. assign) instance attributes.
507 | defining-attr-methods=__init__,
508 | __new__,
509 | setUp
510 |
511 | # List of member names, which should be excluded from the protected access
512 | # warning.
513 | exclude-protected=_asdict,
514 | _fields,
515 | _replace,
516 | _source,
517 | _make
518 |
519 | # List of valid names for the first argument in a class method.
520 | valid-classmethod-first-arg=cls
521 |
522 | # List of valid names for the first argument in a metaclass class method.
523 | valid-metaclass-classmethod-first-arg=cls
524 |
525 |
526 | [DESIGN]
527 |
528 | # Maximum number of arguments for function / method.
529 | max-args=5
530 |
531 | # Maximum number of attributes for a class (see R0902).
532 | max-attributes=7
533 |
534 | # Maximum number of boolean expressions in an if statement.
535 | max-bool-expr=5
536 |
537 | # Maximum number of branch for function / method body.
538 | max-branches=12
539 |
540 | # Maximum number of locals for function / method body.
541 | max-locals=15
542 |
543 | # Maximum number of parents for a class (see R0901).
544 | max-parents=7
545 |
546 | # Maximum number of public methods for a class (see R0904).
547 | max-public-methods=20
548 |
549 | # Maximum number of return / yield for function / method body.
550 | max-returns=6
551 |
552 | # Maximum number of statements in function / method body.
553 | max-statements=50
554 |
555 | # Minimum number of public methods for a class (see R0903).
556 | min-public-methods=2
557 |
558 |
559 | [EXCEPTIONS]
560 |
561 | # Exceptions that will emit a warning when being caught. Defaults to
562 | # "Exception".
563 | overgeneral-exceptions=Exception
564 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015-2021 Grant Jenks
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst LICENSE
2 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Python Sorted Collections
2 | =========================
3 |
4 | `Sorted Collections`_ is an Apache2 licensed Python sorted collections library.
5 |
6 | Features
7 | --------
8 |
9 | - Pure-Python
10 | - Depends on the `Sorted Containers
11 |
3 | If you or your organization uses Sorted Collections, consider financial 4 | support: 5 |
6 | 11 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('..')) 18 | import sortedcollections 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Sorted Collections' 24 | copyright = 'Apache 2.0' 25 | author = 'Grant Jenks' 26 | 27 | # The short X.Y version 28 | version = sortedcollections.__version__ 29 | # The full version, including alpha/beta/rc tags 30 | release = sortedcollections.__version__ 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.viewcode', 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # The language for content autogenerated by Sphinx. Refer to documentation 61 | # for a list of supported languages. 62 | # 63 | # This is also used if you do content translation via gettext catalogs. 64 | # Usually you set "language" from the command line for these cases. 65 | language = None 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = None 74 | 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | html_theme = 'alabaster' 82 | 83 | # Theme options are theme-specific and customize the look and feel of a theme 84 | # further. For a list of options available for each theme, see the 85 | # documentation. 86 | html_theme_options = { 87 | 'logo': 'gj-logo.png', 88 | 'logo_name': True, 89 | 'logo_text_align': 'center', 90 | 'analytics_id': 'UA-19364636-2', 91 | 'show_powered_by': False, 92 | 'show_related': True, 93 | 'github_user': 'grantjenks', 94 | 'github_repo': 'python-sortedcollections', 95 | 'github_type': 'star', 96 | } 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | # Custom sidebar templates, must be a dictionary that maps document names 104 | # to template names. 105 | # 106 | # The default sidebars (for documents that don't match any pattern) are 107 | # defined by theme itself. Builtin themes are using these templates by 108 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 109 | # 'searchbox.html']``. 110 | html_sidebars = { 111 | '**': [ 112 | 'about.html', 113 | 'gumroad.html', 114 | 'localtoc.html', 115 | 'relations.html', 116 | 'searchbox.html', 117 | ] 118 | } 119 | 120 | 121 | # -- Options for HTMLHelp output --------------------------------------------- 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = 'SortedCollectionsDoc' 125 | 126 | 127 | # -- Options for LaTeX output ------------------------------------------------ 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | 134 | # The font size ('10pt', '11pt' or '12pt'). 135 | # 136 | # 'pointsize': '10pt', 137 | 138 | # Additional stuff for the LaTeX preamble. 139 | # 140 | # 'preamble': '', 141 | 142 | # Latex figure (float) alignment 143 | # 144 | # 'figure_align': 'htbp', 145 | } 146 | 147 | # Grouping the document tree into LaTeX files. List of tuples 148 | # (source start file, target name, title, 149 | # author, documentclass [howto, manual, or own class]). 150 | latex_documents = [ 151 | (master_doc, 'SortedCollections.tex', 'Sorted Collections Documentation', 152 | 'Grant Jenks', 'manual'), 153 | ] 154 | 155 | 156 | # -- Options for manual page output ------------------------------------------ 157 | 158 | # One entry per manual page. List of tuples 159 | # (source start file, name, description, authors, manual section). 160 | man_pages = [ 161 | (master_doc, 'sortedcollections', 'Sorted Collections Documentation', 162 | [author], 1) 163 | ] 164 | 165 | 166 | # -- Options for Texinfo output ---------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [ 172 | (master_doc, 'SortedCollections', 'Sorted Collections Documentation', 173 | author, 'SortedCollections', 'Python Sorted Collections library.', 174 | 'Miscellaneous'), 175 | ] 176 | 177 | 178 | # -- Options for Epub output ------------------------------------------------- 179 | 180 | # Bibliographic Dublin Core info. 181 | epub_title = project 182 | 183 | # The unique identifier of the text. This can be a ISBN number 184 | # or the project homepage. 185 | # 186 | # epub_identifier = '' 187 | 188 | # A unique identification for the text. 189 | # 190 | # epub_uid = '' 191 | 192 | # A list of files that should not be packed into the epub file. 193 | epub_exclude_files = ['search.html'] 194 | 195 | 196 | # -- Extension configuration ------------------------------------------------- 197 | 198 | # -- Options for todo extension ---------------------------------------------- 199 | 200 | # If true, `todo` and `todoList` produce output, else they produce nothing. 201 | todo_include_todos = True 202 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | .. toctree:: 4 | :hidden: 5 | 6 | valuesorteddict 7 | itemsorteddict 8 | nearestdict 9 | ordereddict 10 | orderedset 11 | indexabledict 12 | indexableset 13 | segmentlist 14 | -------------------------------------------------------------------------------- /docs/indexabledict.rst: -------------------------------------------------------------------------------- 1 | Indexable Dictionary Recipe 2 | =========================== 3 | 4 | .. autoclass:: sortedcollections.IndexableDict 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/indexableset.rst: -------------------------------------------------------------------------------- 1 | Indexable Set Recipe 2 | ==================== 3 | 4 | .. autoclass:: sortedcollections.IndexableSet 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/itemsorteddict.rst: -------------------------------------------------------------------------------- 1 | Item Sorted Dictionary Recipe 2 | ============================= 3 | 4 | .. autoclass:: sortedcollections.ItemSortedDict 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/nearestdict.rst: -------------------------------------------------------------------------------- 1 | Nearest Dictionary Recipe 2 | ========================= 3 | 4 | .. autoclass:: sortedcollections.NearestDict 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/ordereddict.rst: -------------------------------------------------------------------------------- 1 | Ordered Dictionary Recipe 2 | ========================= 3 | 4 | .. autoclass:: sortedcollections.OrderedDict 5 | :special-members: 6 | :members: 7 | 8 | .. autoclass:: sortedcollections.ordereddict.KeysView 9 | :special-members: 10 | :members: 11 | 12 | .. autoclass:: sortedcollections.ordereddict.ItemsView 13 | :special-members: 14 | :members: 15 | 16 | .. autoclass:: sortedcollections.ordereddict.ValuesView 17 | :special-members: 18 | :members: 19 | -------------------------------------------------------------------------------- /docs/orderedset.rst: -------------------------------------------------------------------------------- 1 | Ordered Set Recipe 2 | ================== 3 | 4 | .. autoclass:: sortedcollections.OrderedSet 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/segmentlist.rst: -------------------------------------------------------------------------------- 1 | Segment List Recipe 2 | =================== 3 | 4 | .. autoclass:: sortedcollections.SegmentList 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /docs/valuesorteddict.rst: -------------------------------------------------------------------------------- 1 | Value Sorted Dictionary Recipe 2 | ============================== 3 | 4 | .. autoclass:: sortedcollections.ValueSortedDict 5 | :special-members: 6 | :members: 7 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | [mypy-sortedcontainers.*] 4 | ignore_missing_imports = True 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | blue 3 | coverage 4 | doc8 5 | mypy 6 | pylint 7 | pytest 8 | pytest-cov 9 | rstcheck 10 | sphinx 11 | tox 12 | twine 13 | wheel 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import re 3 | 4 | from setuptools import setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | 8 | class Tox(TestCommand): 9 | def finalize_options(self): 10 | TestCommand.finalize_options(self) 11 | self.test_args = [] 12 | self.test_suite = True 13 | 14 | def run_tests(self): 15 | import tox 16 | 17 | errno = tox.cmdline(self.test_args) 18 | exit(errno) 19 | 20 | 21 | with open('README.rst') as reader: 22 | readme = reader.read() 23 | 24 | init_text = (pathlib.Path('sortedcollections') / '__init__.py').read_text() 25 | match = re.search(r"^__version__ = '(.+)'$", init_text, re.MULTILINE) 26 | version = match.group(1) 27 | 28 | setup( 29 | name='sortedcollections', 30 | version=version, 31 | description='Python Sorted Collections', 32 | long_description=readme, 33 | author='Grant Jenks', 34 | author_email='contact@grantjenks.com', 35 | url='http://www.grantjenks.com/docs/sortedcollections/', 36 | license='Apache 2.0', 37 | packages=['sortedcollections'], 38 | install_requires=['sortedcontainers'], 39 | tests_require=['tox'], 40 | cmdclass={'test': Tox}, 41 | classifiers=( 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: Apache Software License', 45 | 'Natural Language :: English', 46 | 'Programming Language :: Python', 47 | 'Programming Language :: Python :: 3.6', 48 | 'Programming Language :: Python :: 3.7', 49 | 'Programming Language :: Python :: 3.8', 50 | 'Programming Language :: Python :: 3.9', 51 | 'Programming Language :: Python :: Implementation :: CPython', 52 | 'Programming Language :: Python :: Implementation :: PyPy', 53 | ), 54 | ) 55 | -------------------------------------------------------------------------------- /sortedcollections/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Sorted Collections 2 | 3 | SortedCollections is an Apache2 licensed Python sorted collections library. 4 | 5 | >>> from sortedcollections import ValueSortedDict 6 | >>> vsd = ValueSortedDict({'a': 2, 'b': 1, 'c': 3}) 7 | >>> list(vsd.keys()) 8 | ['b', 'a', 'c'] 9 | 10 | :copyright: (c) 2015-2021 by Grant Jenks. 11 | :license: Apache 2.0, see LICENSE for more details. 12 | 13 | """ 14 | 15 | from sortedcontainers import ( 16 | SortedDict, 17 | SortedList, 18 | SortedListWithKey, 19 | SortedSet, 20 | ) 21 | 22 | from .nearestdict import NearestDict 23 | from .ordereddict import OrderedDict 24 | from .recipes import ( 25 | IndexableDict, 26 | IndexableSet, 27 | ItemSortedDict, 28 | OrderedSet, 29 | SegmentList, 30 | ValueSortedDict, 31 | ) 32 | 33 | __all__ = [ 34 | 'IndexableDict', 35 | 'IndexableSet', 36 | 'ItemSortedDict', 37 | 'NearestDict', 38 | 'OrderedDict', 39 | 'OrderedSet', 40 | 'SegmentList', 41 | 'SortedDict', 42 | 'SortedList', 43 | 'SortedListWithKey', 44 | 'SortedSet', 45 | 'ValueSortedDict', 46 | ] 47 | 48 | __version__ = '2.1.0' 49 | -------------------------------------------------------------------------------- /sortedcollections/nearestdict.py: -------------------------------------------------------------------------------- 1 | """NearestDict implementation. 2 | 3 | One primary use case for this data structure is storing data by a 4 | `datetime.datetime` or `float` key. 5 | """ 6 | 7 | from sortedcontainers import SortedDict 8 | 9 | 10 | class NearestDict(SortedDict): 11 | """A dict using nearest-key lookup. 12 | 13 | A :class:`SortedDict` subclass that uses nearest-key lookup instead of 14 | exact-key lookup. Optionally, you can specify a rounding mode to return the 15 | nearest key less than or equal to or greater than or equal to the provided 16 | key. 17 | 18 | When using :attr:`NearestDict.NEAREST` the keys must support subtraction to 19 | allow finding the nearest key (by find the key with the smallest difference 20 | to the given one). 21 | 22 | Additional methods: 23 | 24 | * :meth:`NearestDict.nearest_key` 25 | 26 | Example usage: 27 | 28 | >>> d = NearestDict({1.0: 'foo'}) 29 | >>> d[1.0] 30 | 'foo' 31 | >>> d[0.0] 32 | 'foo' 33 | >>> d[2.0] 34 | 'foo' 35 | """ 36 | 37 | NEAREST_PREV = -1 38 | NEAREST = 0 39 | NEAREST_NEXT = 1 40 | 41 | def __init__(self, *args, **kwargs): 42 | """Initialize a NearestDict instance. 43 | 44 | Optional `rounding` argument dictates how 45 | :meth:`NearestDict.nearest_key` rounds. It must be one of 46 | :attr:`NearestDict.NEAREST_NEXT`, :attr:`NearestDict.NEAREST`, or 47 | :attr:`NearestDict.NEAREST_PREV`. (Default: 48 | :attr:`NearestDict.NEAREST`) 49 | 50 | :params rounding: how to round on nearest-key lookup (optional) 51 | :params args: positional arguments for :class:`SortedDict`. 52 | :params kwargs: keyword arguments for :class:`SortedDict`. 53 | """ 54 | self.rounding = kwargs.pop('rounding', self.NEAREST) 55 | super().__init__(*args, **kwargs) 56 | 57 | def nearest_key(self, request): 58 | """Return nearest-key to `request`, respecting `self.rounding`. 59 | 60 | >>> d = NearestDict({1.0: 'foo'}) 61 | >>> d.nearest_key(0.0) 62 | 1.0 63 | >>> d.nearest_key(2.0) 64 | 1.0 65 | 66 | >>> d = NearestDict({1.0: 'foo'}, rounding=NearestDict.NEAREST_PREV) 67 | >>> d.nearest_key(0.0) 68 | Traceback (most recent call last): 69 | ... 70 | KeyError: 'No key below 0.0 found' 71 | >>> d.nearest_key(2.0) 72 | 1.0 73 | 74 | :param request: nearest-key lookup value 75 | :return: key nearest to `request`, respecting `rounding` 76 | :raises KeyError: if no appropriate key can be found 77 | """ 78 | key_list = self.keys() 79 | 80 | if not key_list: 81 | raise KeyError('NearestDict is empty') 82 | 83 | index = self.bisect_left(request) 84 | 85 | if index >= len(key_list): 86 | if self.rounding == self.NEAREST_NEXT: 87 | raise KeyError(f'No key above {request!r} found') 88 | return key_list[index - 1] 89 | if key_list[index] == request: 90 | return key_list[index] 91 | if index == 0 and self.rounding == self.NEAREST_PREV: 92 | raise KeyError(f'No key below {request!r} found') 93 | if self.rounding == self.NEAREST_PREV: 94 | return key_list[index - 1] 95 | if self.rounding == self.NEAREST_NEXT: 96 | return key_list[index] 97 | if abs(key_list[index - 1] - request) < abs(key_list[index] - request): 98 | return key_list[index - 1] 99 | return key_list[index] 100 | 101 | def __getitem__(self, request): 102 | """Return item corresponding to :meth:`.nearest_key`. 103 | 104 | :param request: nearest-key lookup value 105 | :return: item corresponding to key nearest `request` 106 | :raises KeyError: if no appropriate item can be found 107 | 108 | >>> d = NearestDict({1.0: 'foo'}) 109 | >>> d[0.0] 110 | 'foo' 111 | >>> d[2.0] 112 | 'foo' 113 | 114 | >>> d = NearestDict({1.0: 'foo'}, rounding=NearestDict.NEAREST_NEXT) 115 | >>> d[0.0] 116 | 'foo' 117 | >>> d[2.0] 118 | Traceback (most recent call last): 119 | ... 120 | KeyError: 'No key above 2.0 found' 121 | """ 122 | key = self.nearest_key(request) 123 | return super().__getitem__(key) 124 | -------------------------------------------------------------------------------- /sortedcollections/ordereddict.py: -------------------------------------------------------------------------------- 1 | """Ordered dictionary implementation. 2 | 3 | """ 4 | 5 | from itertools import count 6 | from operator import eq 7 | 8 | from sortedcontainers import SortedDict 9 | from sortedcontainers.sortedlist import recursive_repr 10 | 11 | from .recipes import abc 12 | 13 | NONE = object() 14 | 15 | 16 | class KeysView(abc.KeysView, abc.Sequence): 17 | "Read-only view of mapping keys." 18 | # noqa pylint: disable=too-few-public-methods,protected-access,too-many-ancestors 19 | def __getitem__(self, index): 20 | "``keys_view[index]``" 21 | _nums = self._mapping._nums 22 | if isinstance(index, slice): 23 | nums = _nums._list[index] 24 | return [_nums[num] for num in nums] 25 | return _nums[_nums._list[index]] 26 | 27 | 28 | class ItemsView(abc.ItemsView, abc.Sequence): 29 | "Read-only view of mapping items." 30 | # noqa pylint: disable=too-few-public-methods,protected-access,too-many-ancestors 31 | def __getitem__(self, index): 32 | "``items_view[index]``" 33 | _mapping = self._mapping 34 | _nums = _mapping._nums 35 | if isinstance(index, slice): 36 | nums = _nums._list[index] 37 | keys = [_nums[num] for num in nums] 38 | return [(key, _mapping[key]) for key in keys] 39 | num = _nums._list[index] 40 | key = _nums[num] 41 | return key, _mapping[key] 42 | 43 | 44 | class ValuesView(abc.ValuesView, abc.Sequence): 45 | "Read-only view of mapping values." 46 | # noqa pylint: disable=too-few-public-methods,protected-access,too-many-ancestors 47 | def __getitem__(self, index): 48 | "``items_view[index]``" 49 | _mapping = self._mapping 50 | _nums = _mapping._nums 51 | if isinstance(index, slice): 52 | nums = _nums._list[index] 53 | keys = [_nums[num] for num in nums] 54 | return [_mapping[key] for key in keys] 55 | num = _nums._list[index] 56 | key = _nums[num] 57 | return _mapping[key] 58 | 59 | 60 | class OrderedDict(dict): 61 | """Dictionary that remembers insertion order and is numerically indexable. 62 | 63 | Keys are numerically indexable using dict views. For example:: 64 | 65 | >>> ordered_dict = OrderedDict.fromkeys('abcde') 66 | >>> keys = ordered_dict.keys() 67 | >>> keys[0] 68 | 'a' 69 | >>> keys[-2:] 70 | ['d', 'e'] 71 | 72 | The dict views support the sequence abstract base class. 73 | 74 | """ 75 | 76 | # pylint: disable=super-init-not-called 77 | def __init__(self, *args, **kwargs): 78 | self._keys = {} 79 | self._nums = SortedDict() 80 | self._keys_view = self._nums.keys() 81 | self._count = count() 82 | self.update(*args, **kwargs) 83 | 84 | def __setitem__(self, key, value, dict_setitem=dict.__setitem__): 85 | "``ordered_dict[key] = value``" 86 | if key not in self: 87 | num = next(self._count) 88 | self._keys[key] = num 89 | self._nums[num] = key 90 | dict_setitem(self, key, value) 91 | 92 | def __delitem__(self, key, dict_delitem=dict.__delitem__): 93 | "``del ordered_dict[key]``" 94 | dict_delitem(self, key) 95 | num = self._keys.pop(key) 96 | del self._nums[num] 97 | 98 | def __iter__(self): 99 | "``iter(ordered_dict)``" 100 | return iter(self._nums.values()) 101 | 102 | def __reversed__(self): 103 | "``reversed(ordered_dict)``" 104 | nums = self._nums 105 | for key in reversed(nums): 106 | yield nums[key] 107 | 108 | def clear(self, dict_clear=dict.clear): 109 | "Remove all items from mapping." 110 | dict_clear(self) 111 | self._keys.clear() 112 | self._nums.clear() 113 | 114 | def popitem(self, last=True): 115 | """Remove and return (key, value) item pair. 116 | 117 | Pairs are returned in LIFO order if last is True or FIFO order if 118 | False. 119 | 120 | """ 121 | index = -1 if last else 0 122 | num = self._keys_view[index] 123 | key = self._nums[num] 124 | value = self.pop(key) 125 | return key, value 126 | 127 | update = __update = abc.MutableMapping.update 128 | 129 | def keys(self): 130 | "Return set-like and sequence-like view of mapping keys." 131 | return KeysView(self) 132 | 133 | def items(self): 134 | "Return set-like and sequence-like view of mapping items." 135 | return ItemsView(self) 136 | 137 | def values(self): 138 | "Return set-like and sequence-like view of mapping values." 139 | return ValuesView(self) 140 | 141 | def pop(self, key, default=NONE): 142 | """Remove given key and return corresponding value. 143 | 144 | If key is not found, default is returned if given, otherwise raise 145 | KeyError. 146 | 147 | """ 148 | if key in self: 149 | value = self[key] 150 | del self[key] 151 | return value 152 | if default is NONE: 153 | raise KeyError(key) 154 | return default 155 | 156 | def setdefault(self, key, default=None): 157 | """Return ``mapping.get(key, default)``, also set ``mapping[key] = default`` if 158 | key not in mapping. 159 | 160 | """ 161 | if key in self: 162 | return self[key] 163 | self[key] = default 164 | return default 165 | 166 | @recursive_repr() 167 | def __repr__(self): 168 | "Text representation of mapping." 169 | return f'{self.__class__.__name__}({list(self.items())!r})' 170 | 171 | __str__ = __repr__ 172 | 173 | def __reduce__(self): 174 | "Support for pickling serialization." 175 | return (self.__class__, (list(self.items()),)) 176 | 177 | def copy(self): 178 | "Return shallow copy of mapping." 179 | return self.__class__(self) 180 | 181 | @classmethod 182 | def fromkeys(cls, iterable, value=None): 183 | """Return new mapping with keys from iterable. 184 | 185 | If not specified, value defaults to None. 186 | 187 | """ 188 | return cls((key, value) for key in iterable) 189 | 190 | def __eq__(self, other): 191 | "Test self and other mapping for equality." 192 | if isinstance(other, OrderedDict): 193 | return dict.__eq__(self, other) and all(map(eq, self, other)) 194 | return dict.__eq__(self, other) 195 | 196 | __ne__ = abc.MutableMapping.__ne__ 197 | 198 | def _check(self): 199 | "Check consistency of internal member variables." 200 | # pylint: disable=protected-access 201 | keys = self._keys 202 | nums = self._nums 203 | 204 | for key, value in keys.items(): 205 | assert nums[value] == key 206 | 207 | nums._check() 208 | -------------------------------------------------------------------------------- /sortedcollections/recipes.py: -------------------------------------------------------------------------------- 1 | """Sorted collections recipes implementations. 2 | 3 | """ 4 | 5 | from collections import abc 6 | from copy import deepcopy 7 | from itertools import count 8 | 9 | from sortedcontainers import SortedDict, SortedKeyList, SortedSet 10 | from sortedcontainers.sortedlist import recursive_repr 11 | 12 | 13 | class IndexableDict(SortedDict): 14 | """Dictionary that supports numerical indexing. 15 | 16 | Keys are numerically indexable using dict views. For example:: 17 | 18 | >>> indexable_dict = IndexableDict.fromkeys('abcde') 19 | >>> keys = indexable_dict.keys() 20 | >>> sorted(keys[:]) == ['a', 'b', 'c', 'd', 'e'] 21 | True 22 | 23 | The dict views support the sequence abstract base class. 24 | 25 | """ 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(hash, *args, **kwargs) 29 | 30 | 31 | class IndexableSet(SortedSet): 32 | """Set that supports numerical indexing. 33 | 34 | Values are numerically indexable. For example:: 35 | 36 | >>> indexable_set = IndexableSet('abcde') 37 | >>> sorted(indexable_set[:]) == ['a', 'b', 'c', 'd', 'e'] 38 | True 39 | 40 | `IndexableSet` implements the sequence abstract base class. 41 | 42 | """ 43 | 44 | # pylint: disable=too-many-ancestors 45 | def __init__(self, *args, **kwargs): 46 | super().__init__(*args, key=hash, **kwargs) 47 | 48 | def __reduce__(self): 49 | return self.__class__, (set(self),) 50 | 51 | 52 | class ItemSortedDict(SortedDict): 53 | """Sorted dictionary with key-function support for item pairs. 54 | 55 | Requires key function callable specified as the first argument. The 56 | callable must accept two arguments, key and value, and return a value used 57 | to determine the sort order. For example:: 58 | 59 | def multiply(key, value): 60 | return key * value 61 | mapping = ItemSortedDict(multiply, [(3, 2), (4, 1), (2, 5)]) 62 | list(mapping) == [4, 3, 2] 63 | 64 | Above, the key/value item pairs are ordered by ``key * value`` according to 65 | the callable given as the first argument. 66 | 67 | """ 68 | 69 | def __init__(self, *args, **kwargs): 70 | assert args and callable(args[0]) 71 | args = list(args) 72 | func = self._func = args[0] 73 | 74 | def key_func(key): 75 | "Apply key function to (key, value) item pair." 76 | return func(key, self[key]) 77 | 78 | args[0] = key_func 79 | super().__init__(*args, **kwargs) 80 | 81 | def __delitem__(self, key): 82 | "``del mapping[key]``" 83 | if key not in self: 84 | raise KeyError(key) 85 | self._list_remove(key) 86 | dict.__delitem__(self, key) 87 | 88 | def __setitem__(self, key, value): 89 | "``mapping[key] = value``" 90 | if key in self: 91 | self._list_remove(key) 92 | dict.__delitem__(self, key) 93 | dict.__setitem__(self, key, value) 94 | self._list_add(key) 95 | 96 | _setitem = __setitem__ 97 | 98 | def copy(self): 99 | "Return shallow copy of the mapping." 100 | return self.__class__(self._func, iter(self.items())) 101 | 102 | __copy__ = copy 103 | 104 | def __deepcopy__(self, memo): 105 | items = (deepcopy(item, memo) for item in self.items()) 106 | return self.__class__(self._func, items) 107 | 108 | 109 | class ValueSortedDict(SortedDict): 110 | """Sorted dictionary that maintains (key, value) item pairs sorted by value. 111 | 112 | - ``ValueSortedDict()`` -> new empty dictionary. 113 | 114 | - ``ValueSortedDict(mapping)`` -> new dictionary initialized from a mapping 115 | object's (key, value) pairs. 116 | 117 | - ``ValueSortedDict(iterable)`` -> new dictionary initialized as if via:: 118 | 119 | d = ValueSortedDict() 120 | for k, v in iterable: 121 | d[k] = v 122 | 123 | - ``ValueSortedDict(**kwargs)`` -> new dictionary initialized with the 124 | name=value pairs in the keyword argument list. For example:: 125 | 126 | ValueSortedDict(one=1, two=2) 127 | 128 | An optional key function callable may be specified as the first 129 | argument. When so, the callable will be applied to the value of each item 130 | pair to determine the comparable for sort order as with Python's builtin 131 | ``sorted`` function. 132 | 133 | """ 134 | 135 | def __init__(self, *args, **kwargs): 136 | args = list(args) 137 | if args and callable(args[0]): 138 | func = self._func = args[0] 139 | 140 | def key_func(key): 141 | "Apply key function to ``mapping[value]``." 142 | return func(self[key]) 143 | 144 | args[0] = key_func 145 | else: 146 | self._func = None 147 | 148 | def key_func(key): 149 | "Return mapping value for key." 150 | return self[key] 151 | 152 | if args and args[0] is None: 153 | args[0] = key_func 154 | else: 155 | args.insert(0, key_func) 156 | super().__init__(*args, **kwargs) 157 | 158 | def __delitem__(self, key): 159 | "``del mapping[key]``" 160 | if key not in self: 161 | raise KeyError(key) 162 | self._list_remove(key) 163 | dict.__delitem__(self, key) 164 | 165 | def __setitem__(self, key, value): 166 | "``mapping[key] = value``" 167 | if key in self: 168 | self._list_remove(key) 169 | dict.__delitem__(self, key) 170 | dict.__setitem__(self, key, value) 171 | self._list_add(key) 172 | 173 | _setitem = __setitem__ 174 | 175 | def copy(self): 176 | "Return shallow copy of the mapping." 177 | return self.__class__(self._func, iter(self.items())) 178 | 179 | __copy__ = copy 180 | 181 | def __reduce__(self): 182 | items = [(key, self[key]) for key in self._list] 183 | args = (self._func, items) 184 | return (self.__class__, args) 185 | 186 | @recursive_repr() 187 | def __repr__(self): 188 | items = ', '.join(f'{key!r}: {self[key]!r}' for key in self._list) 189 | return f'{self.__class__.__name__}({self._func!r}, {{{items}}})' 190 | 191 | 192 | class OrderedSet(abc.MutableSet, abc.Sequence): 193 | """Like OrderedDict, OrderedSet maintains the insertion order of elements. 194 | 195 | For example:: 196 | 197 | >>> ordered_set = OrderedSet('abcde') 198 | >>> list(ordered_set) == list('abcde') 199 | True 200 | >>> ordered_set = OrderedSet('edcba') 201 | >>> list(ordered_set) == list('edcba') 202 | True 203 | 204 | OrderedSet also implements the collections.Sequence interface. 205 | 206 | """ 207 | 208 | # pylint: disable=too-many-ancestors 209 | def __init__(self, iterable=()): 210 | # pylint: disable=super-init-not-called 211 | self._keys = {} 212 | self._nums = SortedDict() 213 | self._keys_view = self._nums.keys() 214 | self._count = count() 215 | self |= iterable 216 | 217 | def __contains__(self, key): 218 | "``key in ordered_set``" 219 | return key in self._keys 220 | 221 | count = __contains__ 222 | 223 | def __iter__(self): 224 | "``iter(ordered_set)``" 225 | return iter(self._nums.values()) 226 | 227 | def __reversed__(self): 228 | "``reversed(ordered_set)``" 229 | _nums = self._nums 230 | for key in reversed(_nums): 231 | yield _nums[key] 232 | 233 | def __getitem__(self, index): 234 | "``ordered_set[index]`` -> element; lookup element at index." 235 | num = self._keys_view[index] 236 | return self._nums[num] 237 | 238 | def __len__(self): 239 | "``len(ordered_set)``" 240 | return len(self._keys) 241 | 242 | def index(self, value): 243 | "Return index of value." 244 | # pylint: disable=arguments-differ 245 | try: 246 | return self._keys[value] 247 | except KeyError: 248 | raise ValueError(f'{value!r} is not in {type(self).__name__}') 249 | 250 | def add(self, value): 251 | "Add element, value, to set." 252 | if value not in self._keys: 253 | num = next(self._count) 254 | self._keys[value] = num 255 | self._nums[num] = value 256 | 257 | def discard(self, value): 258 | "Remove element, value, from set if it is a member." 259 | num = self._keys.pop(value, None) 260 | if num is not None: 261 | del self._nums[num] 262 | 263 | def __repr__(self): 264 | "Text representation of set." 265 | return f'{type(self).__name__}({list(self)!r})' 266 | 267 | __str__ = __repr__ 268 | 269 | 270 | class SegmentList(SortedKeyList): 271 | """List that supports fast random insertion and deletion of elements. 272 | 273 | SegmentList is a special case of a SortedList initialized with a key 274 | function that always returns 0. As such, several SortedList methods are not 275 | implemented for SegmentList. 276 | 277 | """ 278 | 279 | # pylint: disable=too-many-ancestors 280 | def __init__(self, iterable=()): 281 | super().__init__(iterable, self.zero) 282 | 283 | @staticmethod 284 | def zero(_): 285 | "Return 0." 286 | return 0 287 | 288 | def __setitem__(self, index, value): 289 | if isinstance(index, slice): 290 | raise NotImplementedError 291 | pos, idx = self._pos(index) 292 | self._lists[pos][idx] = value 293 | 294 | def append(self, value): 295 | if self._len: 296 | pos = len(self._lists) - 1 297 | self._lists[pos].append(value) 298 | self._keys[pos].append(0) 299 | self._expand(pos) 300 | else: 301 | self._lists.append([value]) 302 | self._keys.append([0]) 303 | self._maxes.append(0) 304 | self._len += 1 305 | 306 | def extend(self, values): 307 | for value in values: 308 | self.append(value) 309 | 310 | def insert(self, index, value): 311 | if index == self._len: 312 | self.append(value) 313 | return 314 | pos, idx = self._pos(index) 315 | self._lists[pos].insert(idx, value) 316 | self._keys[pos].insert(idx, 0) 317 | self._expand(pos) 318 | self._len += 1 319 | 320 | def reverse(self): 321 | values = list(self) 322 | values.reverse() 323 | self.clear() 324 | self.extend(values) 325 | 326 | def sort(self, key=None, reverse=False): 327 | "Stable sort in place." 328 | values = sorted(self, key=key, reverse=reverse) 329 | self.clear() 330 | self.extend(values) 331 | 332 | def _not_implemented(self, *args, **kwargs): 333 | "Not implemented." 334 | raise NotImplementedError 335 | 336 | add = _not_implemented 337 | bisect = _not_implemented 338 | bisect_left = _not_implemented 339 | bisect_right = _not_implemented 340 | bisect_key = _not_implemented 341 | bisect_key_left = _not_implemented 342 | bisect_key_right = _not_implemented 343 | irange = _not_implemented 344 | irange_key = _not_implemented 345 | update = _not_implemented 346 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantjenks/python-sortedcollections/1a6a3457c94ee994d50514a59643ff7f55c7146b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_doctest.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | 3 | import sortedcollections 4 | import sortedcollections.ordereddict 5 | import sortedcollections.recipes 6 | 7 | 8 | def test_sortedcollections(): 9 | failed, attempted = doctest.testmod(sortedcollections) 10 | assert attempted > 0 11 | assert failed == 0 12 | 13 | 14 | def test_sortedcollections_ordereddict(): 15 | failed, attempted = doctest.testmod(sortedcollections.ordereddict) 16 | assert attempted > 0 17 | assert failed == 0 18 | 19 | 20 | def test_sortedcollections_recipes(): 21 | failed, attempted = doctest.testmod(sortedcollections.recipes) 22 | assert attempted > 0 23 | assert failed == 0 24 | -------------------------------------------------------------------------------- /tests/test_itemsorteddict.py: -------------------------------------------------------------------------------- 1 | "Test sortedcollections.ItemSortedDict" 2 | 3 | import copy 4 | 5 | import pytest 6 | 7 | from sortedcollections import ItemSortedDict 8 | 9 | 10 | def key_func(key, value): 11 | return key 12 | 13 | 14 | def value_func(key, value): 15 | return value 16 | 17 | 18 | alphabet = 'abcdefghijklmnopqrstuvwxyz' 19 | 20 | 21 | def test_init(): 22 | temp = ItemSortedDict(key_func) 23 | temp._check() 24 | 25 | 26 | def test_init_args(): 27 | temp = ItemSortedDict(key_func, enumerate(alphabet)) 28 | assert len(temp) == 26 29 | assert temp[0] == 'a' 30 | assert temp[25] == 'z' 31 | assert temp.keys()[4] == 4 32 | temp._check() 33 | 34 | 35 | def test_init_kwargs(): 36 | temp = ItemSortedDict(key_func, a=0, b=1, c=2) 37 | assert len(temp) == 3 38 | assert temp['a'] == 0 39 | assert temp.keys()[0] == 'a' 40 | temp._check() 41 | 42 | 43 | def test_getitem(): 44 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 45 | assert temp[0] == 'z' 46 | assert temp.keys()[0] == 25 47 | assert list(temp) == list(reversed(range(26))) 48 | 49 | 50 | def test_delitem(): 51 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 52 | del temp[25] 53 | assert temp.keys()[0] == 24 54 | 55 | 56 | def test_delitem_error(): 57 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 58 | with pytest.raises(KeyError): 59 | del temp[-1] 60 | 61 | 62 | def test_setitem(): 63 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 64 | temp[25] = '!' 65 | del temp[25] 66 | iloc = temp.keys() 67 | assert iloc[0] == 24 68 | temp[25] = 'a' 69 | assert iloc[0] == 25 70 | 71 | 72 | def test_copy(): 73 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 74 | that = temp.copy() 75 | assert temp == that 76 | assert temp._key != that._key 77 | 78 | 79 | def test_deepcopy(): 80 | temp = ItemSortedDict(value_func, enumerate(reversed(alphabet))) 81 | that = copy.deepcopy(temp) 82 | assert temp == that 83 | assert temp._key != that._key 84 | 85 | 86 | def test_update(): 87 | temp = ItemSortedDict(lambda key, value: value) 88 | for index, letter in enumerate(alphabet): 89 | pair = {index: letter} 90 | temp.update(pair) 91 | -------------------------------------------------------------------------------- /tests/test_nearestdict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from sortedcollections import NearestDict 4 | 5 | 6 | def test_basic(): 7 | d = NearestDict() 8 | 9 | with pytest.raises(KeyError): 10 | d[0] 11 | 12 | d[0] = 'a' 13 | assert d[0] == 'a' 14 | d[0] = 'b' 15 | assert d[0] == 'b' 16 | 17 | 18 | def test_iteration(): 19 | # In sorted order by key 20 | exp_items = ( 21 | (0, 'a'), 22 | (1, 'b'), 23 | (2, 'c'), 24 | ) 25 | 26 | d = NearestDict() 27 | for k, v in exp_items: 28 | d[k] = v 29 | 30 | for act, exp in zip(d.items(), exp_items): 31 | assert act == exp 32 | 33 | 34 | def test_nearest(): 35 | d = NearestDict(rounding=NearestDict.NEAREST) 36 | 37 | d[0] = 'a' 38 | d[3] = 'b' 39 | assert d[-1] == 'a' 40 | assert d[0] == 'a' 41 | assert d[1] == 'a' 42 | assert d[2] == 'b' 43 | assert d[3] == 'b' 44 | assert d[4] == 'b' 45 | 46 | 47 | def test_nearest_prev(): 48 | d = NearestDict(rounding=NearestDict.NEAREST_PREV) 49 | 50 | d[0] = 'a' 51 | d[3] = 'b' 52 | with pytest.raises(KeyError): 53 | d[-1] 54 | assert d[0] == 'a' 55 | assert d[1] == 'a' 56 | assert d[2] == 'a' 57 | assert d[3] == 'b' 58 | assert d[4] == 'b' 59 | 60 | 61 | def test_nearest_next(): 62 | d = NearestDict(rounding=NearestDict.NEAREST_NEXT) 63 | 64 | d[0] = 'a' 65 | d[3] = 'b' 66 | assert d[-1] == 'a' 67 | assert d[0] == 'a' 68 | assert d[1] == 'b' 69 | assert d[2] == 'b' 70 | assert d[3] == 'b' 71 | with pytest.raises(KeyError): 72 | d[4] 73 | -------------------------------------------------------------------------------- /tests/test_ordereddict.py: -------------------------------------------------------------------------------- 1 | "Test sortedcollections.OrderedDict" 2 | 3 | import pickle 4 | 5 | import pytest 6 | 7 | from sortedcollections import OrderedDict 8 | 9 | pairs = dict(enumerate(range(10))) 10 | 11 | 12 | def test_init(): 13 | od = OrderedDict() 14 | assert len(od) == 0 15 | od._check() 16 | od = OrderedDict(enumerate(range(10))) 17 | assert len(od) == 10 18 | od._check() 19 | od = OrderedDict(a=0, b=1, c=2) 20 | assert len(od) == 3 21 | od._check() 22 | od = OrderedDict(pairs) 23 | assert len(od) == 10 24 | od._check() 25 | 26 | 27 | def test_setitem(): 28 | od = OrderedDict() 29 | od['alice'] = 0 30 | od['bob'] = 1 31 | od['carol'] = 2 32 | assert len(od) == 3 33 | od['carol'] = 2 34 | assert len(od) == 3 35 | od._check() 36 | 37 | 38 | def test_delitem(): 39 | od = OrderedDict(pairs) 40 | assert len(od) == 10 41 | for value in range(10): 42 | del od[value] 43 | assert len(od) == 0 44 | od._check() 45 | 46 | 47 | def test_iter_reversed(): 48 | od = OrderedDict([('b', 0), ('a', 1), ('c', 2)]) 49 | assert list(od) == ['b', 'a', 'c'] 50 | assert list(reversed(od)) == ['c', 'a', 'b'] 51 | od._check() 52 | 53 | 54 | def test_clear(): 55 | od = OrderedDict(pairs) 56 | assert len(od) == 10 57 | od.clear() 58 | assert len(od) == 0 59 | od._check() 60 | 61 | 62 | def test_popitem(): 63 | od = OrderedDict(enumerate(range(10))) 64 | for num in reversed(range(10)): 65 | key, value = od.popitem() 66 | assert num == key == value 67 | od._check() 68 | 69 | od = OrderedDict(enumerate(range(10))) 70 | for num in range(10): 71 | key, value = od.popitem(last=False) 72 | assert num == key == value 73 | od._check() 74 | 75 | 76 | def test_keys(): 77 | od = OrderedDict(enumerate(range(10))) 78 | assert list(reversed(od.keys())) == list(reversed(range(10))) 79 | assert od.keys()[:3] == [0, 1, 2] 80 | od._check() 81 | 82 | 83 | def test_items(): 84 | items = list(enumerate(range(10))) 85 | od = OrderedDict(enumerate(range(10))) 86 | assert list(reversed(od.items())) == list(reversed(items)) 87 | assert od.items()[:3] == [(0, 0), (1, 1), (2, 2)] 88 | od._check() 89 | 90 | 91 | def test_values(): 92 | od = OrderedDict(enumerate(range(10))) 93 | assert list(reversed(od.values())) == list(reversed(range(10))) 94 | assert od.values()[:3] == [0, 1, 2] 95 | od._check() 96 | 97 | 98 | def test_iloc(): 99 | od = OrderedDict(enumerate(range(10))) 100 | iloc = od.keys() 101 | for num in range(10): 102 | assert iloc[num] == num 103 | iloc[-1] == 9 104 | assert len(iloc) == 10 105 | od._check() 106 | 107 | 108 | def test_pop(): 109 | od = OrderedDict(enumerate(range(10))) 110 | for num in range(10): 111 | assert od.pop(num) == num 112 | od._check() 113 | assert od.pop(0, 'thing') == 'thing' 114 | assert od.pop(1, default='thing') == 'thing' 115 | od._check() 116 | 117 | 118 | def test_pop_error(): 119 | od = OrderedDict() 120 | with pytest.raises(KeyError): 121 | od.pop(0) 122 | 123 | 124 | def test_setdefault(): 125 | od = OrderedDict() 126 | od.setdefault(0, False) 127 | assert od[0] is False 128 | od.setdefault(1, default=True) 129 | assert od[1] is True 130 | od.setdefault(2) 131 | assert od[2] is None 132 | assert od.setdefault(0) is False 133 | assert od.setdefault(1) is True 134 | 135 | 136 | def test_repr(): 137 | od = OrderedDict() 138 | assert repr(od) == 'OrderedDict([])' 139 | assert str(od) == 'OrderedDict([])' 140 | 141 | 142 | def test_reduce(): 143 | od = OrderedDict(enumerate(range(10))) 144 | data = pickle.dumps(od) 145 | copy = pickle.loads(data) 146 | assert od == copy 147 | 148 | 149 | def test_copy(): 150 | od = OrderedDict(enumerate(range(10))) 151 | copy = od.copy() 152 | assert od == copy 153 | 154 | 155 | def test_fromkeys(): 156 | od = OrderedDict.fromkeys('abc') 157 | assert od == {'a': None, 'b': None, 'c': None} 158 | od._check() 159 | 160 | 161 | def test_equality(): 162 | od = OrderedDict.fromkeys('abc') 163 | assert od == {'a': None, 'b': None, 'c': None} 164 | assert od != {} 165 | assert od != OrderedDict() 166 | od._check() 167 | -------------------------------------------------------------------------------- /tests/test_orderedset.py: -------------------------------------------------------------------------------- 1 | "Test sortedcollections.OrderedSet." 2 | 3 | import random 4 | 5 | import pytest 6 | 7 | from sortedcollections import OrderedSet 8 | 9 | 10 | def test_init(): 11 | os = OrderedSet() 12 | assert len(os) == 0 13 | 14 | 15 | def test_contains(): 16 | os = OrderedSet(range(100)) 17 | assert len(os) == 100 18 | for value in range(100): 19 | assert value in os 20 | assert os.count(value) == 1 21 | assert -1 not in os 22 | assert 100 not in os 23 | 24 | 25 | def test_iter(): 26 | os = OrderedSet(range(100)) 27 | assert list(os) == list(range(100)) 28 | names = ['eve', 'carol', 'alice', 'dave', 'bob'] 29 | os = OrderedSet(names) 30 | assert list(os) == names 31 | 32 | 33 | def test_reversed(): 34 | os = OrderedSet(range(100)) 35 | assert list(reversed(os)) == list(reversed(range(100))) 36 | names = ['eve', 'carol', 'alice', 'dave', 'bob'] 37 | os = OrderedSet(names) 38 | assert list(reversed(os)) == list(reversed(names)) 39 | 40 | 41 | def test_getitem(): 42 | values = list(range(100)) 43 | random.shuffle(values) 44 | os = OrderedSet(values) 45 | assert len(os) == len(values) 46 | for index in range(len(os)): 47 | assert os[index] == values[index] 48 | 49 | 50 | def test_index(): 51 | values = list(range(100)) 52 | random.shuffle(values) 53 | os = OrderedSet(values) 54 | assert len(os) == len(values) 55 | for value in values: 56 | assert values.index(value) == os.index(value) 57 | 58 | 59 | def test_index_error(): 60 | os = OrderedSet(range(10)) 61 | with pytest.raises(ValueError): 62 | os.index(10) 63 | 64 | 65 | def test_add(): 66 | os = OrderedSet() 67 | for value in range(100): 68 | os.add(value) 69 | assert len(os) == 100 70 | os.add(0) 71 | assert len(os) == 100 72 | for value in range(100): 73 | assert value in os 74 | 75 | 76 | def test_discard(): 77 | os = OrderedSet(range(100)) 78 | for value in range(200): 79 | os.discard(value) 80 | assert len(os) == 0 81 | 82 | 83 | def test_repr(): 84 | os = OrderedSet() 85 | assert repr(os) == 'OrderedSet([])' 86 | -------------------------------------------------------------------------------- /tests/test_recipes.py: -------------------------------------------------------------------------------- 1 | "Test sortedcollections.recipes" 2 | 3 | import pickle 4 | 5 | import pytest 6 | 7 | from sortedcollections import IndexableDict, IndexableSet, SegmentList 8 | 9 | 10 | def test_index_dict(): 11 | mapping = IndexableDict(enumerate(range(10))) 12 | iloc = mapping.keys() 13 | for value in range(10): 14 | assert iloc[value] == value 15 | 16 | 17 | def test_index_set(): 18 | set_values = IndexableSet(range(10)) 19 | for index in range(10): 20 | assert set_values[index] == index 21 | 22 | 23 | def test_index_set_pickle(): 24 | set_values1 = IndexableSet(range(10)) 25 | data = pickle.dumps(set_values1) 26 | set_values2 = pickle.loads(data) 27 | assert set_values1 == set_values2 28 | 29 | 30 | def test_segment_list(): 31 | values = [5, 1, 3, 2, 4, 8, 6, 7, 9, 0] 32 | sl = SegmentList(values) 33 | assert list(sl) == values 34 | sl.sort() 35 | assert list(sl) == list(range(10)) 36 | sl.reverse() 37 | assert list(sl) == list(reversed(range(10))) 38 | sl.reverse() 39 | sl.append(10) 40 | assert list(sl) == list(range(11)) 41 | sl.extend(range(11, 15)) 42 | assert list(sl) == list(range(15)) 43 | del sl[5:] 44 | assert list(sl) == list(range(5)) 45 | sl[2] = 'c' 46 | sl.insert(3, 'd') 47 | sl.insert(6, 'e') 48 | assert list(sl) == [0, 1, 'c', 'd', 3, 4, 'e'] 49 | 50 | 51 | def test_segment_list_bisect(): 52 | sl = SegmentList() 53 | with pytest.raises(NotImplementedError): 54 | sl.bisect(0) 55 | 56 | 57 | def test_segment_list_setitem_slice(): 58 | sl = SegmentList() 59 | with pytest.raises(NotImplementedError): 60 | sl[:] = [0] 61 | -------------------------------------------------------------------------------- /tests/test_valuesorteddict.py: -------------------------------------------------------------------------------- 1 | "Test sortedcollections.ValueSortedDict" 2 | 3 | import pickle 4 | 5 | import pytest 6 | 7 | from sortedcollections import ValueSortedDict 8 | 9 | 10 | def identity(value): 11 | return value 12 | 13 | 14 | alphabet = 'abcdefghijklmnopqrstuvwxyz' 15 | 16 | 17 | def test_init(): 18 | temp = ValueSortedDict() 19 | temp._check() 20 | 21 | 22 | def test_init_args(): 23 | temp = ValueSortedDict(enumerate(alphabet)) 24 | assert len(temp) == 26 25 | assert temp[0] == 'a' 26 | assert temp[25] == 'z' 27 | assert temp.keys()[4] == 4 28 | temp._check() 29 | 30 | 31 | def test_init_kwargs(): 32 | temp = ValueSortedDict(None, a=0, b=1, c=2) 33 | assert len(temp) == 3 34 | assert temp['a'] == 0 35 | assert temp.keys()[0] == 'a' 36 | temp._check() 37 | 38 | 39 | def test_getitem(): 40 | temp = ValueSortedDict(identity, enumerate(reversed(alphabet))) 41 | assert temp[0] == 'z' 42 | assert temp.keys()[0] == 25 43 | assert list(temp) == list(reversed(range(26))) 44 | 45 | 46 | def test_delitem(): 47 | temp = ValueSortedDict(identity, enumerate(reversed(alphabet))) 48 | del temp[25] 49 | assert temp.keys()[0] == 24 50 | 51 | 52 | def test_delitem_error(): 53 | temp = ValueSortedDict(identity, enumerate(reversed(alphabet))) 54 | with pytest.raises(KeyError): 55 | del temp[-1] 56 | 57 | 58 | def test_setitem(): 59 | temp = ValueSortedDict(identity, enumerate(reversed(alphabet))) 60 | temp[25] = '!' 61 | del temp[25] 62 | assert temp.keys()[0] == 24 63 | temp[25] = 'a' 64 | assert temp.keys()[0] == 25 65 | 66 | 67 | def test_copy(): 68 | temp = ValueSortedDict(identity, enumerate(reversed(alphabet))) 69 | that = temp.copy() 70 | assert temp == that 71 | assert temp._key != that._key 72 | 73 | 74 | def test_pickle(): 75 | original = ValueSortedDict(identity, enumerate(reversed(alphabet))) 76 | data = pickle.dumps(original) 77 | duplicate = pickle.loads(data) 78 | assert original == duplicate 79 | 80 | 81 | class Negater: 82 | def __call__(self, value): 83 | return -value 84 | 85 | def __repr__(self): 86 | return 'negate' 87 | 88 | 89 | def test_repr(): 90 | temp = ValueSortedDict(Negater()) 91 | assert repr(temp) == 'ValueSortedDict(negate, {})' 92 | 93 | 94 | def test_update(): 95 | temp = ValueSortedDict() 96 | for index, letter in enumerate(alphabet): 97 | pair = {index: letter} 98 | temp.update(pair) 99 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py36,py37,py38,py39 3 | skip_missing_interpreters=True 4 | 5 | [testenv] 6 | commands=pytest 7 | deps= 8 | pytest 9 | pytest-cov 10 | 11 | [testenv:blue] 12 | commands=blue {toxinidir}/setup.py {toxinidir}/sortedcollections {toxinidir}/tests 13 | deps=blue 14 | 15 | [testenv:bluecheck] 16 | commands=blue --check {toxinidir}/setup.py {toxinidir}/sortedcollections {toxinidir}/tests 17 | deps=blue 18 | 19 | [testenv:doc8] 20 | deps=doc8 21 | commands=doc8 docs 22 | 23 | [testenv:docs] 24 | allowlist_externals=make 25 | changedir=docs 26 | commands=make html 27 | deps=sphinx 28 | 29 | [testenv:flake8] 30 | commands=flake8 {toxinidir}/setup.py {toxinidir}/sortedcollections {toxinidir}/tests 31 | deps=flake8 32 | 33 | [testenv:isort] 34 | commands=isort {toxinidir}/setup.py {toxinidir}/sortedcollections {toxinidir}/tests 35 | deps=isort 36 | 37 | [testenv:isortcheck] 38 | commands=isort --check {toxinidir}/setup.py {toxinidir}/sortedcollections {toxinidir}/tests 39 | deps=isort 40 | 41 | [testenv:mypy] 42 | commands=mypy {toxinidir}/sortedcollections 43 | deps=mypy 44 | 45 | [testenv:pylint] 46 | commands=pylint {toxinidir}/sortedcollections 47 | deps=pylint 48 | 49 | [testenv:rstcheck] 50 | commands=rstcheck {toxinidir}/README.rst 51 | deps=rstcheck 52 | 53 | [testenv:uploaddocs] 54 | allowlist_externals=rsync 55 | changedir=docs 56 | commands= 57 | rsync -azP --stats --delete _build/html/ \ 58 | grantjenks.com:/srv/www/www.grantjenks.com/public/docs/sortedcollections/ 59 | 60 | [isort] 61 | multi_line_output = 3 62 | include_trailing_comma = True 63 | force_grid_wrap = 0 64 | use_parentheses = True 65 | ensure_newline_before_comments = True 66 | line_length = 79 67 | 68 | [pytest] 69 | addopts= 70 | --cov-branch 71 | --cov-fail-under=100 72 | --cov-report=term-missing 73 | --cov=sortedcollections 74 | --doctest-glob="*.rst" 75 | --------------------------------------------------------------------------------