├── .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 | `_ module. 12 | - ValueSortedDict - Dictionary with (key, value) item pairs sorted by value. 13 | - ItemSortedDict - Dictionary with key-function support for item pairs. 14 | - NearestDict - Dictionary with nearest-key lookup. 15 | - OrderedDict - Ordered dictionary with numeric indexing support. 16 | - OrderedSet - Ordered set with numeric indexing support. 17 | - IndexableDict - Dictionary with numeric indexing support. 18 | - IndexableSet - Set with numeric indexing support. 19 | - SegmentList - List with fast random access insertion and deletion. 20 | - 100% code coverage testing. 21 | - Developed on Python 3.9 22 | - Tested on CPython 3.6, 3.7, 3.8, and 3.9 23 | 24 | .. image:: https://github.com/grantjenks/python-sortedcollections/workflows/integration/badge.svg 25 | :target: https://github.com/grantjenks/python-sortedcollections/actions?query=workflow%3Aintegration 26 | 27 | .. image:: https://github.com/grantjenks/python-sortedcollections/workflows/release/badge.svg 28 | :target: https://github.com/grantjenks/python-sortedcollections/actions?query=workflow%3Arelease 29 | 30 | Quickstart 31 | ---------- 32 | 33 | Installing `Sorted Collections`_ is simple with `pip 34 | `_:: 35 | 36 | $ pip install sortedcollections 37 | 38 | You can access documentation in the interpreter with Python's built-in `help` 39 | function: 40 | 41 | .. code-block:: python 42 | 43 | >>> from sortedcollections import ValueSortedDict 44 | >>> help(ValueSortedDict) # doctest: +SKIP 45 | 46 | .. _`Sorted Collections`: http://www.grantjenks.com/docs/sortedcollections/ 47 | 48 | Recipes 49 | ------- 50 | 51 | - `Value Sorted Dictionary Recipe`_ 52 | - `Item Sorted Dictionary Recipe`_ 53 | - `Nearest Dictionary Recipe`_ 54 | - `Ordered Dictionary Recipe`_ 55 | - `Ordered Set Recipe`_ 56 | - `Indexable Dictionary Recipe`_ 57 | - `Indexable Set Recipe`_ 58 | - `Segment List Recipe`_ 59 | 60 | .. _`Value Sorted Dictionary Recipe`: http://www.grantjenks.com/docs/sortedcollections/valuesorteddict.html 61 | .. _`Item Sorted Dictionary Recipe`: http://www.grantjenks.com/docs/sortedcollections/itemsorteddict.html 62 | .. _`Nearest Dictionary Recipe`: http://www.grantjenks.com/docs/sortedcollections/nearestdict.html 63 | .. _`Ordered Dictionary Recipe`: http://www.grantjenks.com/docs/sortedcollections/ordereddict.html 64 | .. _`Ordered Set Recipe`: http://www.grantjenks.com/docs/sortedcollections/orderedset.html 65 | .. _`Indexable Dictionary Recipe`: http://www.grantjenks.com/docs/sortedcollections/indexabledict.html 66 | .. _`Indexable Set Recipe`: http://www.grantjenks.com/docs/sortedcollections/indexableset.html 67 | .. _`Segment List Recipe`: http://www.grantjenks.com/docs/sortedcollections/segmentlist.html 68 | 69 | Reference and Indices 70 | --------------------- 71 | 72 | - `Sorted Collections Documentation`_ 73 | - `Sorted Collections at PyPI`_ 74 | - `Sorted Collections at Github`_ 75 | - `Sorted Collections Issue Tracker`_ 76 | 77 | .. _`Sorted Collections Documentation`: http://www.grantjenks.com/docs/sortedcollections/ 78 | .. _`Sorted Collections at PyPI`: https://pypi.python.org/pypi/sortedcollections/ 79 | .. _`Sorted Collections at Github`: https://github.com/grantjenks/python-sortedcollections 80 | .. _`Sorted Collections Issue Tracker`: https://github.com/grantjenks/python-sortedcollections/issues 81 | 82 | Sorted Collections License 83 | -------------------------- 84 | 85 | Copyright 2015-2021 Grant Jenks 86 | 87 | Licensed under the Apache License, Version 2.0 (the "License"); 88 | you may not use this file except in compliance with the License. 89 | You may obtain a copy of the License at 90 | 91 | http://www.apache.org/licenses/LICENSE-2.0 92 | 93 | Unless required by applicable law or agreed to in writing, software 94 | distributed under the License is distributed on an "AS IS" BASIS, 95 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 96 | See the License for the specific language governing permissions and 97 | limitations under the License. 98 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/gj-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantjenks/python-sortedcollections/1a6a3457c94ee994d50514a59643ff7f55c7146b/docs/_static/gj-logo.png -------------------------------------------------------------------------------- /docs/_templates/gumroad.html: -------------------------------------------------------------------------------- 1 |

Give Support

2 |

3 | If you or your organization uses Sorted Collections, consider financial 4 | support: 5 |

6 |

7 | 8 | Give to Python Sorted Collections 9 | 10 |

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 | --------------------------------------------------------------------------------