├── .gitignore ├── .pylintrc ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── gj-logo.png ├── _templates │ └── gumroad.html ├── conf.py ├── index.rst ├── make.bat ├── reference.rst └── tutorial.rst ├── patternmatching └── __init__.py ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── test_funcs.py └── test_regex.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Python byte-code 2 | *.py[co] 3 | 4 | # virutalenv directories 5 | /env*/ 6 | 7 | # test files/directories 8 | /.cache/ 9 | .coverage* 10 | .pytest_cache/ 11 | /.tox/ 12 | 13 | # setup and upload directories 14 | /build/ 15 | /dist/ 16 | /patternmatching.egg-info/ 17 | /docs/_build/ 18 | 19 | # macOS metadata 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /.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 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=1 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 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 | too-few-public-methods, 143 | too-many-ancestors, 144 | unused-argument, 145 | redefined-outer-name, 146 | no-member, 147 | no-else-return, 148 | 149 | # Enable the message, report, category or checker with the given id(s). You can 150 | # either give multiple identifier separated by comma (,) or put this option 151 | # multiple time (only on the command line, not in the configuration file where 152 | # it should appear only once). See also the "--disable" option for examples. 153 | enable=c-extension-no-member 154 | 155 | 156 | [REPORTS] 157 | 158 | # Python expression which should return a score less than or equal to 10. You 159 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 160 | # which contain the number of messages in each category, as well as 'statement' 161 | # which is the total number of statements analyzed. This score is used by the 162 | # global evaluation report (RP0004). 163 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 164 | 165 | # Template used to display messages. This is a python new-style format string 166 | # used to format the message information. See doc for all details. 167 | #msg-template= 168 | 169 | # Set the output format. Available formats are text, parseable, colorized, json 170 | # and msvs (visual studio). You can also give a reporter class, e.g. 171 | # mypackage.mymodule.MyReporterClass. 172 | output-format=text 173 | 174 | # Tells whether to display a full report or only the messages. 175 | reports=no 176 | 177 | # Activate the evaluation score. 178 | score=yes 179 | 180 | 181 | [REFACTORING] 182 | 183 | # Maximum number of nested blocks for function / method body 184 | max-nested-blocks=5 185 | 186 | # Complete name of functions that never returns. When checking for 187 | # inconsistent-return-statements if a never returning function is called then 188 | # it will be considered as an explicit return statement and no message will be 189 | # printed. 190 | never-returning-functions=sys.exit 191 | 192 | 193 | [LOGGING] 194 | 195 | # The type of string formatting that logging methods do. `old` means using % 196 | # formatting, `new` is for `{}` formatting. 197 | logging-format-style=old 198 | 199 | # Logging modules to check that the string format arguments are in logging 200 | # function parameter format. 201 | logging-modules=logging 202 | 203 | 204 | [SPELLING] 205 | 206 | # Limits count of emitted suggestions for spelling mistakes. 207 | max-spelling-suggestions=4 208 | 209 | # Spelling dictionary name. Available dictionaries: none. To make it work, 210 | # install the python-enchant package. 211 | spelling-dict= 212 | 213 | # List of comma separated words that should not be checked. 214 | spelling-ignore-words= 215 | 216 | # A path to a file that contains the private dictionary; one word per line. 217 | spelling-private-dict-file= 218 | 219 | # Tells whether to store unknown words to the private dictionary (see the 220 | # --spelling-private-dict-file option) instead of raising a message. 221 | spelling-store-unknown-words=no 222 | 223 | 224 | [MISCELLANEOUS] 225 | 226 | # List of note tags to take in consideration, separated by a comma. 227 | notes=FIXME, 228 | XXX, 229 | TODO 230 | 231 | # Regular expression of note tags to take in consideration. 232 | #notes-rgx= 233 | 234 | 235 | [TYPECHECK] 236 | 237 | # List of decorators that produce context managers, such as 238 | # contextlib.contextmanager. Add to this list to register other decorators that 239 | # produce valid context managers. 240 | contextmanager-decorators=contextlib.contextmanager 241 | 242 | # List of members which are set dynamically and missed by pylint inference 243 | # system, and so shouldn't trigger E1101 when accessed. Python regular 244 | # expressions are accepted. 245 | generated-members= 246 | 247 | # Tells whether missing members accessed in mixin class should be ignored. A 248 | # mixin class is detected if its name ends with "mixin" (case insensitive). 249 | ignore-mixin-members=yes 250 | 251 | # Tells whether to warn about missing members when the owner of the attribute 252 | # is inferred to be None. 253 | ignore-none=yes 254 | 255 | # This flag controls whether pylint should warn about no-member and similar 256 | # checks whenever an opaque object is returned when inferring. The inference 257 | # can return multiple potential results while evaluating a Python object, but 258 | # some branches might not be evaluated, which results in partial inference. In 259 | # that case, it might be useful to still emit no-member and other checks for 260 | # the rest of the inferred objects. 261 | ignore-on-opaque-inference=yes 262 | 263 | # List of class names for which member attributes should not be checked (useful 264 | # for classes with dynamically set attributes). This supports the use of 265 | # qualified names. 266 | ignored-classes=optparse.Values,thread._local,_thread._local 267 | 268 | # List of module names for which member attributes should not be checked 269 | # (useful for modules/projects where namespaces are manipulated during runtime 270 | # and thus existing member attributes cannot be deduced by static analysis). It 271 | # supports qualified module names, as well as Unix pattern matching. 272 | ignored-modules= 273 | 274 | # Show a hint with possible names when a member name was not found. The aspect 275 | # of finding the hint is based on edit distance. 276 | missing-member-hint=yes 277 | 278 | # The minimum edit distance a name should have in order to be considered a 279 | # similar match for a missing member name. 280 | missing-member-hint-distance=1 281 | 282 | # The total number of similar names that should be taken in consideration when 283 | # showing a hint for a missing member. 284 | missing-member-max-choices=1 285 | 286 | # List of decorators that change the signature of a decorated function. 287 | signature-mutators= 288 | 289 | 290 | [VARIABLES] 291 | 292 | # List of additional names supposed to be defined in builtins. Remember that 293 | # you should avoid defining new builtins when possible. 294 | additional-builtins= 295 | 296 | # Tells whether unused global variables should be treated as a violation. 297 | allow-global-unused-variables=yes 298 | 299 | # List of strings which can identify a callback function by name. A callback 300 | # name must start or end with one of those strings. 301 | callbacks=cb_, 302 | _cb 303 | 304 | # A regular expression matching the name of dummy variables (i.e. expected to 305 | # not be used). 306 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 307 | 308 | # Argument names that match this expression will be ignored. Default to name 309 | # with leading underscore. 310 | ignored-argument-names=_.*|^ignored_|^unused_ 311 | 312 | # Tells whether we should check for unused import in __init__ files. 313 | init-import=no 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 318 | 319 | 320 | [FORMAT] 321 | 322 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 323 | expected-line-ending-format= 324 | 325 | # Regexp for a line that is allowed to be longer than the limit. 326 | ignore-long-lines=^\s*(# )??$ 327 | 328 | # Number of spaces of indent required inside a hanging or continued line. 329 | indent-after-paren=4 330 | 331 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 332 | # tab). 333 | indent-string=' ' 334 | 335 | # Maximum number of characters on a single line. 336 | max-line-length=100 337 | 338 | # Maximum number of lines in a module. 339 | max-module-lines=2000 340 | 341 | # Allow the body of a class to be on the same line as the declaration if body 342 | # contains single statement. 343 | single-line-class-stmt=no 344 | 345 | # Allow the body of an if to be on the same line as the test if there is no 346 | # else. 347 | single-line-if-stmt=no 348 | 349 | 350 | [SIMILARITIES] 351 | 352 | # Ignore comments when computing similarities. 353 | ignore-comments=yes 354 | 355 | # Ignore docstrings when computing similarities. 356 | ignore-docstrings=yes 357 | 358 | # Ignore imports when computing similarities. 359 | ignore-imports=no 360 | 361 | # Minimum lines number of a similarity. 362 | min-similarity-lines=4 363 | 364 | 365 | [BASIC] 366 | 367 | # Naming style matching correct argument names. 368 | argument-naming-style=snake_case 369 | 370 | # Regular expression matching correct argument names. Overrides argument- 371 | # naming-style. 372 | #argument-rgx= 373 | 374 | # Naming style matching correct attribute names. 375 | attr-naming-style=snake_case 376 | 377 | # Regular expression matching correct attribute names. Overrides attr-naming- 378 | # style. 379 | #attr-rgx= 380 | 381 | # Bad variable names which should always be refused, separated by a comma. 382 | bad-names=foo, 383 | bar, 384 | baz, 385 | toto, 386 | tutu, 387 | tata 388 | 389 | # Bad variable names regexes, separated by a comma. If names match any regex, 390 | # they will always be refused 391 | bad-names-rgxs= 392 | 393 | # Naming style matching correct class attribute names. 394 | class-attribute-naming-style=any 395 | 396 | # Regular expression matching correct class attribute names. Overrides class- 397 | # attribute-naming-style. 398 | #class-attribute-rgx= 399 | 400 | # Naming style matching correct class names. 401 | class-naming-style=PascalCase 402 | 403 | # Regular expression matching correct class names. Overrides class-naming- 404 | # style. 405 | #class-rgx= 406 | 407 | # Naming style matching correct constant names. 408 | const-naming-style=UPPER_CASE 409 | 410 | # Regular expression matching correct constant names. Overrides const-naming- 411 | # style. 412 | #const-rgx= 413 | 414 | # Minimum line length for functions/classes that require docstrings, shorter 415 | # ones are exempt. 416 | docstring-min-length=-1 417 | 418 | # Naming style matching correct function names. 419 | function-naming-style=snake_case 420 | 421 | # Regular expression matching correct function names. Overrides function- 422 | # naming-style. 423 | #function-rgx= 424 | 425 | # Good variable names which should always be accepted, separated by a comma. 426 | good-names=i, 427 | j, 428 | k, 429 | ex, 430 | Run, 431 | _ 432 | 433 | # Good variable names regexes, separated by a comma. If names match any regex, 434 | # they will always be accepted 435 | good-names-rgxs= 436 | 437 | # Include a hint for the correct naming format with invalid-name. 438 | include-naming-hint=no 439 | 440 | # Naming style matching correct inline iteration names. 441 | inlinevar-naming-style=any 442 | 443 | # Regular expression matching correct inline iteration names. Overrides 444 | # inlinevar-naming-style. 445 | #inlinevar-rgx= 446 | 447 | # Naming style matching correct method names. 448 | method-naming-style=snake_case 449 | 450 | # Regular expression matching correct method names. Overrides method-naming- 451 | # style. 452 | #method-rgx= 453 | 454 | # Naming style matching correct module names. 455 | module-naming-style=snake_case 456 | 457 | # Regular expression matching correct module names. Overrides module-naming- 458 | # style. 459 | #module-rgx= 460 | 461 | # Colon-delimited sets of names that determine each other's naming style when 462 | # the name regexes allow several styles. 463 | name-group= 464 | 465 | # Regular expression which should only match function or class names that do 466 | # not require a docstring. 467 | no-docstring-rgx=^_ 468 | 469 | # List of decorators that produce properties, such as abc.abstractproperty. Add 470 | # to this list to register other decorators that produce valid properties. 471 | # These decorators are taken in consideration only for invalid-name. 472 | property-classes=abc.abstractproperty 473 | 474 | # Naming style matching correct variable names. 475 | variable-naming-style=snake_case 476 | 477 | # Regular expression matching correct variable names. Overrides variable- 478 | # naming-style. 479 | #variable-rgx= 480 | 481 | 482 | [STRING] 483 | 484 | # This flag controls whether inconsistent-quotes generates a warning when the 485 | # character used as a quote delimiter is used inconsistently within a module. 486 | check-quote-consistency=no 487 | 488 | # This flag controls whether the implicit-str-concat should generate a warning 489 | # on implicit string concatenation in sequences defined over several lines. 490 | check-str-concat-over-line-jumps=no 491 | 492 | 493 | [IMPORTS] 494 | 495 | # List of modules that can be imported at any level, not just the top level 496 | # one. 497 | allow-any-import-level= 498 | 499 | # Allow wildcard imports from modules that define __all__. 500 | allow-wildcard-with-all=no 501 | 502 | # Analyse import fallback blocks. This can be used to support both Python 2 and 503 | # 3 compatible code, which means that the block might have code that exists 504 | # only in one or another interpreter, leading to false positives when analysed. 505 | analyse-fallback-blocks=no 506 | 507 | # Deprecated modules which should not be used, separated by a comma. 508 | deprecated-modules=optparse,tkinter.tix 509 | 510 | # Create a graph of external dependencies in the given file (report RP0402 must 511 | # not be disabled). 512 | ext-import-graph= 513 | 514 | # Create a graph of every (i.e. internal and external) dependencies in the 515 | # given file (report RP0402 must not be disabled). 516 | import-graph= 517 | 518 | # Create a graph of internal dependencies in the given file (report RP0402 must 519 | # not be disabled). 520 | int-import-graph= 521 | 522 | # Force import order to recognize a module as part of the standard 523 | # compatibility libraries. 524 | known-standard-library= 525 | 526 | # Force import order to recognize a module as part of a third party library. 527 | known-third-party=enchant 528 | 529 | # Couples of modules and preferred modules, separated by a comma. 530 | preferred-modules= 531 | 532 | 533 | [CLASSES] 534 | 535 | # List of method names used to declare (i.e. assign) instance attributes. 536 | defining-attr-methods=__init__, 537 | __new__, 538 | setUp, 539 | __post_init__ 540 | 541 | # List of member names, which should be excluded from the protected access 542 | # warning. 543 | exclude-protected=_asdict, 544 | _fields, 545 | _replace, 546 | _source, 547 | _make 548 | 549 | # List of valid names for the first argument in a class method. 550 | valid-classmethod-first-arg=cls 551 | 552 | # List of valid names for the first argument in a metaclass class method. 553 | valid-metaclass-classmethod-first-arg=cls 554 | 555 | 556 | [DESIGN] 557 | 558 | # Maximum number of arguments for function / method. 559 | max-args=5 560 | 561 | # Maximum number of attributes for a class (see R0902). 562 | max-attributes=7 563 | 564 | # Maximum number of boolean expressions in an if statement (see R0916). 565 | max-bool-expr=5 566 | 567 | # Maximum number of branch for function / method body. 568 | max-branches=12 569 | 570 | # Maximum number of locals for function / method body. 571 | max-locals=15 572 | 573 | # Maximum number of parents for a class (see R0901). 574 | max-parents=7 575 | 576 | # Maximum number of public methods for a class (see R0904). 577 | max-public-methods=20 578 | 579 | # Maximum number of return / yield for function / method body. 580 | max-returns=6 581 | 582 | # Maximum number of statements in function / method body. 583 | max-statements=50 584 | 585 | # Minimum number of public methods for a class (see R0903). 586 | min-public-methods=2 587 | 588 | 589 | [EXCEPTIONS] 590 | 591 | # Exceptions that will emit a warning when being caught. Defaults to 592 | # "BaseException, Exception". 593 | overgeneral-exceptions=BaseException, 594 | Exception 595 | -------------------------------------------------------------------------------- /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 Pattern Matching 2 | ======================= 3 | 4 | Python, I love you. But I'd like you to change. It's not you, it's me. Really. 5 | See, you don't have pattern matching. But, that's not the root of it. Macros 6 | are the root of it. You don't have macros but that's OK. Right now, I want 7 | pattern matching. I know you offer me ``if``/``elif``/``else`` statements but I 8 | need more. I'm going to abuse your functions. Guido, et al, I hope you can 9 | forgive me. This will only hurt a little. 10 | 11 | `Python Pattern Matching`_ is an Apache2 licensed Python module for `pattern 12 | matching`_ like that found in functional programming languages. Most projects 13 | that address Python pattern matching focus on syntax and simple cases. Operator 14 | overloading is often used to change the semantics of operators to support 15 | pattern matching. In other cases, function decorators are used to implement 16 | multiple dispatch, sometimes known as function overloading. Each of these 17 | syntaxes, operators, and decorators, is really more of a detail in the 18 | application of pattern matching. 19 | 20 | A lot of people have tried to make this work before. Somehow it didn't take. I 21 | should probably call this yet-another-python-pattern-matching-module but 22 | "yappmm" doesn't roll off the tongue. Other people have tried overloading 23 | operators and changing codecs. This module started as a codec hack but those are 24 | hard because they need an ecosystem of emacs-modes, vim-modes and the like to 25 | really be convenient. 26 | 27 | Python Pattern Matching focuses instead on the semantics of pattern matching in 28 | Python. The dynamic duck-typing behavior in Python is distinct from the tagged 29 | unions found in functional programming languages. Rather than trying to emulate 30 | the behavior of functional pattern matching, this project attempts to implement 31 | pattern matching that looks and feels native to Python. In doing so the 32 | traditional function call is used as syntax. There are no import hooks, no 33 | codecs, no AST transforms. 34 | 35 | .. todo:: 36 | 37 | Python ``match`` function example. 38 | 39 | Finally, pythonic pattern matching! If you've experienced the feature before in 40 | "functional" languages like Erlang, Haskell, Clojure, F#, OCaml, etc. then you 41 | can guess at the semantics. 42 | 43 | .. todo:: 44 | 45 | Show the same code without ``patternmatching``. 46 | 47 | 48 | Features 49 | -------- 50 | 51 | - Pure-Python 52 | - Developed on Python 3.9 53 | - Tested on CPython 3.6, 3.7, 3.8, and 3.9 54 | 55 | .. todo:: 56 | 57 | - Fully Documented 58 | - 100% test coverage 59 | - Hours of stress testing 60 | 61 | 62 | Quickstart 63 | ---------- 64 | 65 | Installing `Python Pattern Matching`_ is simple with `pip 66 | `_:: 67 | 68 | $ pip install patternmatching 69 | 70 | You can access documentation in the interpreter with Python's built-in `help` 71 | function. The `help` works on modules, classes, and functions in `pattern 72 | matching`_. 73 | 74 | .. code-block:: python 75 | 76 | >>> from patternmatching import match, bind, bound, like 77 | >>> help(match) # doctest: +SKIP 78 | 79 | 80 | Alternative Packages 81 | -------------------- 82 | 83 | - https://github.com/lihaoyi/macropy 84 | - module import, but similar design 85 | - https://github.com/Suor/patterns 86 | - decorator with funky syntax 87 | - Shared at Python Brazil 2013 88 | - https://github.com/mariusae/match 89 | - http://monkey.org/~marius/pattern-matching-in-python.html 90 | - operator overloading 91 | - http://blog.chadselph.com/adding-functional-style-pattern-matching-to-python.html 92 | - multi-methods 93 | - http://svn.colorstudy.com/home/ianb/recipes/patmatch.py 94 | - multi-methods 95 | - http://www.artima.com/weblogs/viewpost.jsp?thread=101605 96 | - the original multi-methods 97 | - http://speak.codebunk.com/post/77084204957/pattern-matching-in-python 98 | - multi-methods supporting callables 99 | - http://www.aclevername.com/projects/splarnektity/ 100 | - not sure how it works but the syntax leaves a lot to be desired 101 | - https://github.com/martinblech/pyfpm 102 | - multi-dispatch with string parsing 103 | - https://github.com/jldupont/pyfnc 104 | - multi-dispatch 105 | - http://www.pyret.org/ 106 | - It's own language 107 | - https://pypi.python.org/pypi/PEAK-Rules 108 | - generic multi-dispatch style for business rules 109 | - http://home.in.tum.de/~bayerj/patternmatch.py 110 | - Pattern-object idea (no binding) 111 | - https://github.com/admk/patmat 112 | - multi-dispatch style 113 | 114 | 115 | Other Languages 116 | --------------- 117 | 118 | - https://msdn.microsoft.com/en-us/library/dd547125.aspx F# 119 | - https://doc.rust-lang.org/book/patterns.html Rust 120 | - https://www.haskell.org/tutorial/patterns.html Haskell 121 | - http://erlang.org/doc/reference_manual/expressions.html#pattern Erlang 122 | - https://ocaml.org/learn/tutorials/data_types_and_matching.html Ocaml 123 | 124 | 125 | Developer Guide 126 | --------------- 127 | 128 | * `Python Pattern Matching Tutorial`_ 129 | * `Python Pattern Matching Reference`_ 130 | * `Python Pattern Matching Search`_ 131 | * `Python Pattern Matching Index`_ 132 | 133 | .. _`Python Pattern Matching Tutorial`: http://www.grantjenks.com/docs/patternmatching/tutorial.html 134 | .. _`Python Pattern Matching Reference`: http://www.grantjenks.com/docs/patternmatching/reference.html 135 | .. _`Python Pattern Matching Search`: http://www.grantjenks.com/docs/patternmatching/search.html 136 | .. _`Python Pattern Matching Index`: http://www.grantjenks.com/docs/patternmatching/genindex.html 137 | 138 | 139 | Project Links 140 | ------------- 141 | 142 | * `Python Pattern Matching`_ 143 | * `Python Pattern Matching at PyPI`_ 144 | * `Python Pattern Matching at GitHub`_ 145 | * `Python Pattern Matching Issue Tracker`_ 146 | 147 | .. _`Python Pattern Matching`: http://www.grantjenks.com/docs/patternmatching/ 148 | .. _`Python Pattern Matching at PyPI`: https://pypi.python.org/pypi/patternmatching/ 149 | .. _`Python Pattern Matching at GitHub`: https://github.com/grantjenks/python-pattern-matching 150 | .. _`Python Pattern Matching Issue Tracker`: https://github.com/grantjenks/python-pattern-matching/issues 151 | 152 | 153 | Python Pattern Matching License 154 | ------------------------------- 155 | 156 | Copyright 2015-2021, Grant Jenks 157 | 158 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 159 | this file except in compliance with the License. You may obtain a copy of the 160 | License at 161 | 162 | http://www.apache.org/licenses/LICENSE-2.0 163 | 164 | Unless required by applicable law or agreed to in writing, software distributed 165 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 166 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 167 | specific language governing permissions and limitations under the License. 168 | 169 | .. _`pattern matching`: http://www.grantjenks.com/docs/patternmatching/ 170 | -------------------------------------------------------------------------------- /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-pattern-matching/7235c491b22183e8cfbd87b39dcda6e97437a268/docs/_static/gj-logo.png -------------------------------------------------------------------------------- /docs/_templates/gumroad.html: -------------------------------------------------------------------------------- 1 |

Give Support

2 |

3 | If you or your organization uses Python Pattern Matching, please consider 4 | financial support: 5 |

6 |

7 | 8 | Give to Python Pattern Matching 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 patternmatching 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'Pattern Matching' 24 | copyright = patternmatching.__copyright__ 25 | author = patternmatching.__author__ 26 | 27 | # The short X.Y version 28 | version = patternmatching.__version__ 29 | # The full version, including alpha/beta/rc tags 30 | release = patternmatching.__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-pattern-matching', 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 = 'PatternMatchingdoc' 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, 'PatternMatching.tex', 'Pattern Matching 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, 'patternmatching', 'Pattern Matching 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, 'PatternMatching', 'Pattern Matching Documentation', 173 | author, 'PatternMatching', 'Python Pattern Matching 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 | tutorial 7 | reference 8 | -------------------------------------------------------------------------------- /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/reference.rst: -------------------------------------------------------------------------------- 1 | Python Pattern Matching Reference 2 | ================================= 3 | 4 | .. todo:: 5 | 6 | autodoc api 7 | 8 | 9 | Future Work 10 | ----------- 11 | 12 | * Rather than adding `when` clause, just use `and` 13 | match(value, (bind.first, bind.second)) and bound.first < bound.second 14 | * Should ``anyof(*patterns)`` be added? 15 | * "match(value, anyof('foo', 'bar'))" 16 | * Should the short-form of this just override any.__call__? 17 | * Should ``allof(*patterns)`` be added? 18 | * "match(value, allof((0, _, _), (_, _, 1)))" 19 | * "match(value, (allof(anyof('grant', 'shannon'), bind.name), bind.name))" 20 | * todo: bind.many 21 | 22 | .. code-block:: python 23 | 24 | class Many(object): 25 | def __init__(self, name, count=slice(None), values=(Anything,)): 26 | self.count = count 27 | self.values = values 28 | self.name = name 29 | def __getitem__(self, index): 30 | """Index may be any of: 31 | name 32 | twople ~ (name, slice) 33 | threeple ~ (name, slice, value-spec) 34 | """ 35 | # todo 36 | 37 | many = Many() 38 | 39 | * Many should have special significance in _sequence_rule which permits 40 | binding to multiple values. 41 | * To be really powerful, this should support backtracking in the style of 42 | regular expressions. 43 | * Maybe the third argument to slice could be True / False to indicate greedy 44 | or non-greedy matching. 45 | * If second argument is a tuple, does that work like a character class and 46 | match any of the stated objects? How then to match a tuple? Put it in 47 | another tuple! 48 | * This is close to supporting the full power of regular languages. If it were 49 | possible to `or` expressions (as done with `(A|B)`) then it would be 50 | feature-complete. 51 | Actually, that's possible with: 52 | 53 | .. code-block:: python 54 | 55 | bind.many(1, (0, 1), 'zeroorone') 56 | 57 | * But could you put a `bind.many` within a `bind.many` like: 58 | 59 | .. code-block:: python 60 | 61 | bind.many(1, (bind.many(:, int), bind.many(:, float)), 'intsorfloats') 62 | 63 | * And does that recurse? So you could have bind.many nested three times over? 64 | That sounds pretty difficult to achieve. How does naming of the nested 65 | nested groups sound? Lol, Python's regex package has this same difficulty. 66 | It's hard to imagine why anyone would create such complex data structure 67 | queries and want to express them in this way. 68 | 69 | * Why not instead map patterns to letters and do traditional regex-matching? 70 | 71 | * Support ellipsis-like syntax to match anything in the rest of the list or 72 | tuple. 73 | 74 | * Match ``set`` expression? 75 | 76 | * Add "when" clause like match(expr, when(predicate)) 77 | 78 | * Add ``or``/``and`` pattern-matching 79 | 80 | * Match ``dict`` expression? 81 | 82 | * Match regex? 83 | 84 | * API for matching: __match__ and Matcher object for state 85 | * Method of binding values to names 86 | * Algorithm for patterns (generic regex) 87 | * New match rule for "types" 88 | 89 | * Add __match__ predicate and refactor cases. 90 | * I wonder, can Pattern cases be refactored? Maybe "visit" should allow case 91 | action generators? isinstance(result, types.GeneratorType) 92 | * Add Start and End to patterns. 93 | * Add Set predicate and action? 94 | 95 | .. code-block:: python 96 | 97 | def set_predicate(matcher, value, pattern): 98 | return isinstance(pattern, Set) 99 | 100 | def set_action(matcher, value, pattern): 101 | value_sequence = tuple(value) 102 | for permutation in itertools.permutations(pattern): 103 | try: 104 | matcher.names.push() 105 | matcher.visit(value_sequence, permutation) 106 | matcher.names.pull() 107 | return 108 | except Mismatch: 109 | matcher.names.undo() 110 | else: 111 | raise Mismatch 112 | 113 | * Add Mapping predicate and action? 114 | * Improve docstrings with examples. 115 | 116 | .. todo:: 117 | 118 | Examples. 119 | 120 | .. code-block:: python 121 | 122 | import operator 123 | from collections import Sequence 124 | 125 | def make_operators(attrs): 126 | "Add operators to attributes dictionary." 127 | def method(function): 128 | return lambda self, that: BinaryOperator(self, function, that) 129 | def rmethod(function): 130 | return lambda self, that: BinaryOperator(that, function, self) 131 | for term in ['add', 'sub', 'mul', 'div']: 132 | function = getattr(operator, term) 133 | attrs['__%s__' % term] = method(function) 134 | attrs['__r%s__' % term] = rmethod(function) 135 | 136 | class MetaTypeOperators(type): 137 | "Metaclass to add operators to type of types." 138 | def __new__(cls, name, base, attrs): 139 | make_operators(attrs) 140 | return super(MetaTypeOperators, cls).__new__(cls, name, base, attrs) 141 | 142 | class MetaOperators(type): 143 | "Metaclass to add operators to types." 144 | __metaclass__ = MetaTypeOperators 145 | def __new__(cls, name, base, attrs): 146 | make_operators(attrs) 147 | return super(MetaOperators, cls).__new__(cls, name, base, attrs) 148 | def __repr__(self): 149 | return self.__name__ 150 | 151 | class Record(object): 152 | __metaclass__ = MetaOperators 153 | __slots__ = () 154 | def __init__(self, *args): 155 | assert len(self.__slots__) == len(args) 156 | for field, value in zip(self.__slots__, args): 157 | setattr(self, field, value) 158 | def __getitem__(self, index): 159 | return getattr(self, self.__slots__[index]) 160 | def __len__(self): 161 | return len(self.__slots__) 162 | def __eq__(self, that): 163 | if not isinstance(that, type(self)): 164 | return NotImplemented 165 | return all(item == iota for item, iota in zip(self, that)) 166 | def __repr__(self): 167 | args = ', '.join(repr(item) for item in self) 168 | return '%s(%s)' % (type(self).__name__, args) 169 | # pickle support 170 | def __getstate__(self): 171 | return tuple(self) 172 | def __setstate__(self, state): 173 | self.__init__(*state) 174 | 175 | Sequence.register(Record) 176 | 177 | class BinaryOperator(Record): 178 | __slots__ = 'left', 'operator', 'right' 179 | 180 | class Constant(Record): 181 | __slots__ = 'value', 182 | 183 | class Variable(Record): 184 | __slots__ = 'name', 185 | 186 | class Term(Record): 187 | __slots__ = 'value', 188 | def __match__(self, matcher, value): 189 | return matcher.visit(value, self.value) 190 | 191 | zero = Constant(0) 192 | one = Constant(1) 193 | x = Variable('x') 194 | 195 | from patternmatching import * 196 | 197 | assert match(zero + one, Constant + Constant) 198 | assert match(zero * Variable, zero * anyone) 199 | 200 | alpha = Term(bind.alpha) 201 | 202 | assert match(zero + zero, alpha + alpha) 203 | 204 | TODO 205 | ---- 206 | 207 | - Should this module just be a function like: 208 | 209 | :: 210 | 211 | def bind(object, expression): 212 | """Attempt to bind object to expression. 213 | Expression may contain `bind.name`-style attributes which will bind the 214 | `name` in the callers context. 215 | """ 216 | pass # todo 217 | 218 | What if just returned a mapping with the bindings and something 219 | like bind.result was available to capture the latest expression. 220 | For nested calls, bind.results could be a stack. Then the `like` function 221 | call could just return a Like object which `bind` recognized specially. 222 | Alternately `bind.results` could work using `with` statement to create 223 | the nested scope. 224 | 225 | :: 226 | 227 | if bind(r'', text): 228 | match = bind.result 229 | print match.groups(1) 230 | elif bind([bind.name, 0], [5, 0]): 231 | pass 232 | 233 | Change signature to `bind(object, pattern)` and make a Pattern object. If 234 | the second argument is not a pattern object, then it is made into one 235 | (if necessary). Pattern objects should support `__contains__`. 236 | 237 | `bind` could also be a decorator in the style of oh-so-many multi-dispatch 238 | style pattern matchers. 239 | 240 | To bind anything, use bind.any or bind.__ as a place-filler that does not 241 | actually bind to values. 242 | 243 | - Add like(...) function-call-like thing and support the following: 244 | like(type(obj)) check isinstance 245 | like('string') checks regex 246 | like(... callable ...) applies callable, binds truthy 247 | - Also make `like` composable with `and` and `or` 248 | - Add `when` support somehow and somewhere 249 | - Add __ (two dunders) for place-holder 250 | - Add match(..., fall_through=False) to prevent fall_through 251 | - Use bind.name rather than quote(name) 252 | - Improve debug-ability: write source to temporary file and modify code object 253 | accordingly. Change co_filename and co_firstlineno to temporary file? 254 | - Support/test Python 2.6, Python 3 and PyPy 2 / 3 255 | - Good paper worth referencing on patterns in Thorn: 256 | http://hirzels.com/martin/papers/dls12-thorn-patterns.pdf 257 | - Support ellipsis-like syntax to match anything in the rest of the list or 258 | tuple. Consider using ``quote(*args)`` to mean zero or more elements. Elements 259 | are bound to args: 260 | 261 | :: 262 | 263 | match [1, 2, 3, 4]: 264 | like [1, 2, quote(*args)]: 265 | print 'args == [3, 4]' 266 | 267 | - Match ``set`` expression. Only allow one ``quote`` variable. If present the 268 | quoted variable must come last. 269 | 270 | :: 271 | 272 | with match({3, 1, 4, 2}): 273 | with {1, 2, 4, quote(value)}: 274 | print 'value == 3' 275 | with {3, 4, quote(*args)}: 276 | print 'args = {1, 2}' 277 | 278 | - Add "when" clause like: 279 | 280 | :: 281 | 282 | with match(list_item): 283 | with like([first, second], first < second): 284 | print 'ascending' 285 | with like([first, second], first > second): 286 | print 'descending' 287 | 288 | - Add ``or``/``and`` pattern-matching like: 289 | 290 | :: 291 | 292 | with match(value): 293 | with [alpha] or [alpha, beta]: 294 | pass 295 | with [1, _, _] and [_, _, 2]: 296 | pass 297 | 298 | - Match ``dict`` expression? 299 | - Match regexp? 300 | 301 | Future? 302 | ------- 303 | 304 | - Provide more generic macro-expansion facilities. Consider if this module 305 | could instead be written as the following: 306 | 307 | :: 308 | 309 | def assign(var, value, _globals, _locals): 310 | exec '{var} = value'.format(var) in _globals, _locals 311 | 312 | @patternmatching.macro 313 | def match(expr, statements): 314 | """with match(expr): ... expansion 315 | with match(value / 5): 316 | ... statements ... 317 | -> 318 | patternmatching.store['temp0'] = value / 5 319 | try: 320 | ... statements ... 321 | except patternmatching.PatternmatchingBreak: 322 | pass 323 | """ 324 | symbol[temp] = expand[expr] 325 | try: 326 | expand[statements] 327 | except patternmatching.PatternMatchingBreak: 328 | pass 329 | 330 | @patternmatching.macro 331 | def like(expr, statements): 332 | """with like(expr): ... expansion 333 | with like(3 + value): 334 | ... statements ... 335 | -> 336 | patternmatching.store['temp1'] = patternmatching.bind(expr, patternmatching.store['temp0'], globals(), locals()) 337 | if patternmatching.store['temp1']: 338 | for var in patternmatching.store['temp1'][1]: 339 | assign(var, patternmatching.store['temp1'][1][var], globals(), locals()) 340 | ... statements ... 341 | raise patternmatching.PatternmatchingBreak 342 | """ 343 | symbol[result] = patternmatching.bind(expr, symbol[match.temp], globals(), locals()) 344 | if symbol[result]: 345 | for var in symbol[result][1]: 346 | assign(var, symbol[result][1][var], globals(), locals()) 347 | expand[statements] 348 | raise patternmatching.PatternmatchingBreak 349 | 350 | @patternmatching.expand(match, like) 351 | def test(): 352 | with match('hello' + ' world'): 353 | with like(1): 354 | print 'fail' 355 | with like(False): 356 | print 'fail' 357 | with like('hello world'): 358 | print 'succeed' 359 | with like(_): 360 | print 'fail' 361 | 362 | I'm not convinced this is better. But it's interesting. I think you could do 363 | nearly this in ``macropy`` if you were willing to organize your code for the 364 | import hook to work. 365 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Python Pattern Matching Tutorial 2 | ================================ 3 | 4 | .. todo:: 5 | 6 | Demonstrate what's possible! 7 | 8 | - https://xmonader.github.io/prolog/2018/12/21/solving-murder-prolog.html 9 | - Optimizer for expressions with namedtuples. 10 | -------------------------------------------------------------------------------- /patternmatching/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Pattern Matching 2 | 3 | Python pattern matching library. 4 | 5 | """ 6 | 7 | from collections.abc import Mapping, Sequence 8 | from functools import wraps 9 | 10 | infinity = float('inf') 11 | 12 | 13 | class Record: 14 | """Mutable "named tuple"-like base class.""" 15 | 16 | __slots__ = () 17 | 18 | def __init__(self, *args): 19 | assert len(self.__slots__) == len(args) 20 | for field, value in zip(self.__slots__, args): 21 | setattr(self, field, value) 22 | 23 | def __getitem__(self, index): 24 | return getattr(self, self.__slots__[index]) 25 | 26 | def __eq__(self, that): 27 | if not isinstance(that, type(self)): 28 | return NotImplemented 29 | if self.__slots__ != that.__slots__: 30 | return False 31 | return all(item == iota for item, iota in zip(self, that)) 32 | 33 | def __repr__(self): 34 | args = ', '.join(repr(item) for item in self) 35 | return '%s(%s)' % (type(self).__name__, args) 36 | 37 | def __getstate__(self): 38 | return tuple(self) 39 | 40 | def __setstate__(self, state): 41 | self.__init__(*state) 42 | 43 | 44 | class Case(Record): 45 | """Three-ple of `name`, `predicate`, and `action`. 46 | 47 | `Matcher` objects successively try a sequence of `Case` predicates. When a 48 | match is found, the `Case` action is applied. 49 | 50 | """ 51 | 52 | __slots__ = 'name', 'predicate', 'action' 53 | 54 | 55 | default_cases = [] 56 | 57 | 58 | class Mismatch(Exception): 59 | "Raised by `action` functions of `Case` records to abort on mismatch." 60 | 61 | 62 | ############################################################################### 63 | # Match Case: __match__ 64 | ############################################################################### 65 | 66 | 67 | def match_predicate(matcher, value, pattern): 68 | "Return True if `pattern` has `__match__` attribute." 69 | return hasattr(pattern, '__match__') 70 | 71 | 72 | def match_action(matcher, value, pattern): 73 | "Match `value` by calling `__match__` attribute of `pattern`." 74 | attr = getattr(pattern, '__match__') 75 | return attr(matcher, value) 76 | 77 | 78 | default_cases.append(Case('__match__', match_predicate, match_action)) 79 | 80 | 81 | ############################################################################### 82 | # Match Case: patterns 83 | ############################################################################### 84 | 85 | 86 | class APattern(Sequence): 87 | """Abstract base class extending `Sequence` to define equality and hashing. 88 | 89 | Defines one slot, `_details`, for comparison and hashing. 90 | 91 | Used by `Pattern` and `PatternMixin` types. 92 | 93 | """ 94 | 95 | # pylint: disable=abstract-method 96 | __slots__ = ('_details',) 97 | 98 | def __eq__(self, that): 99 | return self._details == that._details 100 | 101 | def __ne__(self, that): 102 | return self._details != that._details 103 | 104 | def __hash__(self): 105 | return hash(self._details) 106 | 107 | def __match__(self, matcher, value): 108 | """Match `pattern` to `value` with `Pattern` semantics. 109 | 110 | The `Pattern` type is used to define semantics like regular expressions. 111 | 112 | >>> match([0, 1, 2], [0, 1] + anyone) 113 | True 114 | >>> match([0, 0, 0, 0], 0 * repeat) 115 | True 116 | >>> match('blue', either('red', 'blue', 'yellow')) 117 | True 118 | >>> match([2, 4, 6], exclude(like(lambda num: num % 2)) * repeat(min=3)) 119 | True 120 | 121 | """ 122 | # pylint: disable=too-many-return-statements 123 | names = matcher.names 124 | len_value = len(value) 125 | 126 | # Life is easier with generators. I tried twice to write "visit" 127 | # recursively without success. Consider: 128 | # 129 | # match('abc', 'a' + 'b' * repeat * group + 'bc') 130 | # 131 | # Notice the "'b' * repeat" clause is nested within a group 132 | # clause. When recursing, the "'b' * repeat" clause will match greedily 133 | # against 'abc' at offset 1 but then the whole pattern will fail as 134 | # 'bc' does not match at offset 2. So backtracking of the nested clause 135 | # is required. Generators communicate multiple end offsets and support 136 | # the needed backtracking. 137 | 138 | def visit(pattern, index, offset, count): 139 | # pylint: disable=too-many-branches 140 | len_pattern = len(pattern) 141 | 142 | if index == len_pattern: 143 | yield offset 144 | return 145 | 146 | item = pattern[index] 147 | 148 | if isinstance(item, Repeat): 149 | if count > item.max: 150 | return 151 | 152 | if item.greedy: 153 | if offset < len_value: 154 | for end in visit(item.pattern, 0, offset, count): 155 | for stop in visit(pattern, index, end, count + 1): 156 | yield stop 157 | 158 | if count >= item.min: 159 | for stop in visit(pattern, index + 1, offset, 0): 160 | yield stop 161 | else: 162 | if count >= item.min: 163 | for stop in visit(pattern, index + 1, offset, 0): 164 | yield stop 165 | 166 | if offset < len_value: 167 | for end in visit(item.pattern, 0, offset, count): 168 | for stop in visit(pattern, index, end, count + 1): 169 | yield stop 170 | 171 | return 172 | 173 | elif isinstance(item, Group): 174 | for end in visit(item.pattern, 0, offset, 0): 175 | if item.name is None: 176 | for stop in visit(pattern, index + 1, end, 0): 177 | yield stop 178 | else: 179 | segment = value[offset:end] 180 | names.push() 181 | 182 | try: 183 | name_store(names, item.name, segment) 184 | except Mismatch: 185 | names.undo() 186 | else: 187 | for stop in visit(pattern, index + 1, end, 0): 188 | yield stop 189 | 190 | names.undo() 191 | 192 | return 193 | 194 | elif isinstance(item, Either): 195 | for option in item.options: 196 | for end in visit(option, 0, offset, 0): 197 | for stop in visit(pattern, index + 1, end, 0): 198 | yield stop 199 | return 200 | 201 | elif isinstance(item, Exclude): 202 | for option in item.options: 203 | for end in visit(option, 0, offset, 0): 204 | return 205 | 206 | for end in visit(pattern, index + 1, offset + 1, 0): 207 | yield end 208 | 209 | else: 210 | if offset >= len_value: 211 | return 212 | 213 | names.push() 214 | 215 | try: 216 | matcher.visit(value[offset], item) 217 | except Mismatch: 218 | pass 219 | else: 220 | for end in visit(pattern, index + 1, offset + 1, 0): 221 | yield end 222 | 223 | names.undo() 224 | 225 | return 226 | 227 | for end in visit(self, 0, 0, 0): 228 | return value[:end] 229 | 230 | raise Mismatch 231 | 232 | 233 | def make_tuple(value): 234 | """Return value as tuple. 235 | 236 | >>> make_tuple((1, 2, 3)) 237 | (1, 2, 3) 238 | >>> make_tuple('abc') 239 | ('a', 'b', 'c') 240 | >>> make_tuple([4, 5, 6]) 241 | (4, 5, 6) 242 | >>> make_tuple(None) 243 | (None,) 244 | 245 | """ 246 | if isinstance(value, tuple): 247 | return value 248 | if isinstance(value, Sequence): 249 | return tuple(value) 250 | return (value,) 251 | 252 | 253 | class Pattern(APattern): 254 | """Wrap tuple to extend addition operator. 255 | 256 | >>> Pattern() 257 | Pattern() 258 | >>> Pattern([1, 2, 3]) 259 | Pattern(1, 2, 3) 260 | >>> Pattern() + [1, 2, 3] 261 | Pattern(1, 2, 3) 262 | >>> None + Pattern() 263 | Pattern(None) 264 | >>> list(Pattern(4, 5, 6)) 265 | [4, 5, 6] 266 | 267 | """ 268 | 269 | def __init__(self, *args): 270 | self._details = make_tuple(args[0] if len(args) == 1 else args) 271 | 272 | def __getitem__(self, index): 273 | return self._details[index] 274 | 275 | def __len__(self): 276 | return len(self._details) 277 | 278 | def __add__(self, that): 279 | return Pattern(self._details + make_tuple(that)) 280 | 281 | def __radd__(self, that): 282 | return Pattern(make_tuple(that) + self._details) 283 | 284 | def __repr__(self): 285 | args = ', '.join(repr(value) for value in self._details) 286 | return '%s(%s)' % (type(self).__name__, args) 287 | 288 | 289 | class PatternMixin(APattern): 290 | """Abstract base class to wrap a tuple to extend multiplication and 291 | addition. 292 | 293 | """ 294 | 295 | def __getitem__(self, index): 296 | if index == 0: 297 | return self 298 | raise IndexError 299 | 300 | def __len__(self): 301 | return 1 302 | 303 | def __add__(self, that): 304 | return Pattern(self) + that 305 | 306 | def __radd__(self, that): 307 | return that + Pattern(self) 308 | 309 | def __mul__(self, that): 310 | return that.__rmul__(self) 311 | 312 | def __getattr__(self, name): 313 | return getattr(self._details, name) 314 | 315 | def __repr__(self): 316 | pairs = zip(self._details.__slots__, self._details) 317 | tokens = ('%s=%s' % (name, repr(value)) for name, value in pairs) 318 | return '%s(%s)' % (type(self).__name__, ', '.join(tokens)) 319 | 320 | 321 | ############################################################################### 322 | # Match Case: anyone 323 | ############################################################################### 324 | 325 | 326 | class Anyone(PatternMixin): 327 | """Match any one thing. 328 | 329 | >>> Anyone() 330 | anyone 331 | >>> match('blah', Anyone()) 332 | True 333 | >>> anyone + [1, 2, 3] 334 | Pattern(anyone, 1, 2, 3) 335 | >>> (4, 5) + anyone + None 336 | Pattern(4, 5, anyone, None) 337 | 338 | """ 339 | 340 | def __init__(self): 341 | self._details = () 342 | 343 | def __match__(self, matcher, value): 344 | "Pass because `anyone` matches any one thing." 345 | 346 | def __repr__(self): 347 | return 'anyone' 348 | 349 | 350 | anyone = Anyone() 351 | 352 | 353 | ############################################################################### 354 | # Match Case: repeat 355 | ############################################################################### 356 | 357 | 358 | def sequence(value): 359 | """Return value as sequence. 360 | 361 | >>> sequence('abc') 362 | 'abc' 363 | >>> sequence(1) 364 | (1,) 365 | >>> sequence([1]) 366 | [1] 367 | 368 | """ 369 | return value if isinstance(value, Sequence) else (value,) 370 | 371 | 372 | class _Repeat(Record): 373 | __slots__ = 'pattern', 'min', 'max', 'greedy' 374 | 375 | 376 | class Repeat(PatternMixin): 377 | """Pattern specifying repetition with min/max count and greedy parameters. 378 | 379 | Inherits from `PatternMixin` which defines multiplication operators to 380 | capture patterns. 381 | 382 | >>> Repeat() 383 | Repeat(pattern=(), min=0, max=inf, greedy=True) 384 | >>> repeat = Repeat() 385 | >>> repeat(max=1) 386 | Repeat(pattern=(), min=0, max=1, greedy=True) 387 | >>> maybe = repeat(max=1) 388 | >>> Repeat(anyone) 389 | Repeat(pattern=anyone, min=0, max=inf, greedy=True) 390 | >>> anyone * repeat 391 | Repeat(pattern=anyone, min=0, max=inf, greedy=True) 392 | >>> anything = anyone * repeat 393 | >>> anyone * repeat(min=1) 394 | Repeat(pattern=anyone, min=1, max=inf, greedy=True) 395 | >>> something = anyone * repeat(min=1) 396 | >>> padding = anyone * repeat(greedy=False) 397 | 398 | """ 399 | 400 | def __init__(self, pattern=(), min=0, max=infinity, greedy=True): 401 | # pylint: disable=redefined-builtin 402 | self._details = _Repeat(pattern, min, max, greedy) 403 | 404 | def __rmul__(self, that): 405 | return type(self)(sequence(that), *tuple(self._details)[1:]) 406 | 407 | def __call__(self, min=0, max=infinity, greedy=True, pattern=()): 408 | # pylint: disable=redefined-builtin 409 | return type(self)(pattern, min, max, greedy) 410 | 411 | 412 | repeat = Repeat() 413 | maybe = repeat(max=1) 414 | anything = anyone * repeat 415 | something = anyone * repeat(min=1) 416 | padding = anyone * repeat(greedy=False) 417 | 418 | 419 | ############################################################################### 420 | # Match Case: groups 421 | ############################################################################### 422 | 423 | 424 | class _Group(Record): 425 | __slots__ = 'pattern', 'name' 426 | 427 | 428 | class Group(PatternMixin): 429 | """Pattern specifying a group with name parameter. 430 | 431 | Inherits from `PatternMixin` which defines multiplication operators to 432 | capture patterns. 433 | 434 | >>> Group() 435 | Group(pattern=(), name=None) 436 | >>> Group(['red', 'blue', 'yellow'], 'color') 437 | Group(pattern=['red', 'blue', 'yellow'], name='color') 438 | >>> group = Group() 439 | >>> ['red', 'blue', 'yellow'] * group('color') 440 | Group(pattern=['red', 'blue', 'yellow'], name='color') 441 | 442 | """ 443 | 444 | def __init__(self, pattern=(), name=None): 445 | self._details = _Group(pattern, name) 446 | 447 | def __rmul__(self, that): 448 | return type(self)(sequence(that), *tuple(self._details)[1:]) 449 | 450 | def __call__(self, name=None, pattern=()): 451 | return type(self)(pattern, name) 452 | 453 | 454 | group = Group() 455 | 456 | 457 | ############################################################################### 458 | # Match Case: options 459 | ############################################################################### 460 | 461 | 462 | class _Options(Record): 463 | __slots__ = ('options',) 464 | 465 | 466 | class Options(PatternMixin): 467 | "Pattern specifying a sequence of options to match." 468 | 469 | def __init__(self, *options): 470 | self._details = _Options(tuple(map(sequence, options))) 471 | 472 | def __call__(self, *options): 473 | return type(self)(*options) 474 | 475 | def __rmul__(self, that): 476 | return type(self)(*sequence(that)) 477 | 478 | def __repr__(self): 479 | args = ', '.join(map(repr, self._details.options)) 480 | return '%s(%s)' % (type(self).__name__, args) 481 | 482 | 483 | class Either(Options): 484 | "Pattern specifying that any of options may match." 485 | 486 | 487 | either = Either() 488 | 489 | 490 | class Exclude(Options): 491 | "Pattern specifying that none of options may match." 492 | 493 | 494 | exclude = Exclude() 495 | 496 | 497 | ############################################################################### 498 | # Match Case: names 499 | ############################################################################### 500 | 501 | 502 | class Name(Record): 503 | """Name objects simply wrap a `value` to be used as a name. 504 | 505 | >>> match([1, 2, 3], [Name('head'), 2, 3]) 506 | True 507 | >>> bound.head == 1 508 | True 509 | 510 | """ 511 | 512 | __slots__ = ('value',) 513 | 514 | def __match__(self, matcher, value): 515 | "Store `value` in `matcher` with `name` and return `value`." 516 | name_store(matcher.names, self.value, value) 517 | 518 | 519 | def name_store(names, name, value): 520 | """Store `value` in `names` with given `name`. 521 | 522 | If `name` is already present in `names` then raise `Mismatch` on inequality 523 | between `value` and stored value. 524 | 525 | """ 526 | if name in names: 527 | if value == names[name]: 528 | pass # Prefer equality comparison to inequality. 529 | else: 530 | raise Mismatch 531 | names[name] = value 532 | 533 | 534 | class Binder: 535 | """Binder objects return Name objects on attribute lookup. 536 | 537 | A few attributes behave specially: 538 | 539 | * `bind.any` returns an `Anyone` object. 540 | * `bind.push`, `bind.pop`, and `bind.reset` raise an AttributeError 541 | because the names would conflict with `Bounder` attributes. 542 | 543 | >>> bind = Binder() 544 | >>> bind.head 545 | Name('head') 546 | >>> bind.tail 547 | Name('tail') 548 | >>> bind.any 549 | anyone 550 | >>> bind.push 551 | Traceback (most recent call last): 552 | ... 553 | AttributeError 554 | 555 | """ 556 | 557 | def __getattr__(self, name): 558 | if name == 'any': 559 | return anyone 560 | if name in ('push', 'pop', 'reset'): 561 | raise AttributeError 562 | return Name(name) 563 | 564 | 565 | bind = Binder() 566 | 567 | 568 | ############################################################################### 569 | # Match Case: likes 570 | ############################################################################### 571 | 572 | import functools # noqa: E402 # pylint: disable=wrong-import-position 573 | import re # noqa: E402 # pylint: disable=wrong-import-position 574 | 575 | like_errors = ( 576 | AttributeError, 577 | LookupError, 578 | NotImplementedError, 579 | TypeError, 580 | ValueError, 581 | ) 582 | 583 | 584 | class Like(Record): 585 | # pylint: disable=missing-docstring 586 | __slots__ = 'pattern', 'name' 587 | 588 | def __match__(self, matcher, value): 589 | """Apply `pattern` to `value` and store result in `matcher`. 590 | 591 | Given `pattern` is expected as `Like` instance and deconstructed by 592 | attribute into `pattern` and `name`. 593 | 594 | When `pattern` is text then it is used as a regular expression. 595 | 596 | When `name` is None then the result is not stored in `matcher.names`. 597 | 598 | Raises `Mismatch` if callable raises exception in `like_errors` or 599 | result is falsy. 600 | 601 | >>> match('abcdef', like('abc.*')) 602 | True 603 | >>> match(123, like(lambda num: num % 2 == 0)) 604 | False 605 | 606 | """ 607 | pattern = self.pattern 608 | name = self.name 609 | 610 | if isinstance(pattern, str): 611 | if not isinstance(value, str): 612 | raise Mismatch 613 | func = functools.partial(re.match, pattern) 614 | else: 615 | func = pattern 616 | 617 | try: 618 | result = func(value) 619 | except like_errors: 620 | raise Mismatch from None 621 | 622 | if not result: 623 | raise Mismatch from None 624 | 625 | if name is not None: 626 | name_store(matcher.names, name, result) 627 | 628 | 629 | def like(pattern, name='match'): 630 | """Return `Like` object with given `pattern` and `name`, default "match". 631 | 632 | >>> like('abc.*') 633 | Like('abc.*', 'match') 634 | >>> like('abc.*', 'prefix') 635 | Like('abc.*', 'prefix') 636 | 637 | """ 638 | return Like(pattern, name) 639 | 640 | 641 | ############################################################################### 642 | # Match Case: types 643 | ############################################################################### 644 | 645 | 646 | def type_predicate(matcher, value, pattern): 647 | "Return True if `pattern` is an instance of `type`." 648 | return isinstance(pattern, type) 649 | 650 | 651 | def type_action(matcher, value, pattern): 652 | """Match `value` as subclass or instance of `pattern`. 653 | 654 | >>> match(1, int) 655 | True 656 | >>> match(True, bool) 657 | True 658 | >>> match(True, int) 659 | True 660 | >>> match(bool, int) 661 | True 662 | >>> match(0.0, int) 663 | False 664 | >>> match(float, int) 665 | False 666 | 667 | """ 668 | if isinstance(value, type) and issubclass(value, pattern): 669 | return value 670 | if isinstance(value, pattern): 671 | return value 672 | raise Mismatch 673 | 674 | 675 | default_cases.append(Case('types', type_predicate, type_action)) 676 | 677 | 678 | ############################################################################### 679 | # Match Case: literals 680 | ############################################################################### 681 | 682 | literal_types = (type(None), bool, int, float, complex, str, bytes) 683 | 684 | 685 | def literal_predicate(matcher, value, pattern): 686 | "Return True if `value` and `pattern` instance of `literal_types`." 687 | literal_pattern = isinstance(pattern, literal_types) 688 | return literal_pattern and isinstance(value, literal_types) 689 | 690 | 691 | def literal_action(matcher, value, pattern): 692 | """Match `value` as equal to `pattern`. 693 | 694 | >>> match(1, 1) 695 | True 696 | >>> match('abc', 'abc') 697 | True 698 | >>> match(1, 1.0) 699 | True 700 | >>> match(1, True) 701 | True 702 | 703 | """ 704 | if value == pattern: 705 | return value 706 | raise Mismatch 707 | 708 | 709 | default_cases.append(Case('literals', literal_predicate, literal_action)) 710 | 711 | 712 | ############################################################################### 713 | # Match Case: equality 714 | ############################################################################### 715 | 716 | 717 | def equality_predicate(matcher, value, pattern): 718 | "Return True if `value` equals `pattern`." 719 | try: 720 | return value == pattern 721 | except Exception: # pylint: disable=broad-except 722 | return False 723 | 724 | 725 | def equality_action(matcher, value, pattern): 726 | """Match `value` as equal to `pattern`. 727 | 728 | >>> identity = lambda value: value 729 | >>> match(identity, identity) 730 | True 731 | >>> match('abc', 'abc') 732 | True 733 | >>> match(1, 1.0) 734 | True 735 | >>> match(1, True) 736 | True 737 | 738 | """ 739 | return value 740 | 741 | 742 | default_cases.append(Case('equality', equality_predicate, equality_action)) 743 | 744 | 745 | ############################################################################### 746 | # Match Case: sequences 747 | ############################################################################### 748 | 749 | 750 | def sequence_predicate(matcher, value, pattern): 751 | """Return True if `pattern` is instance of Sequence and `value` is instance of 752 | type of `pattern`. 753 | 754 | """ 755 | return isinstance(pattern, Sequence) and isinstance(value, type(pattern)) 756 | 757 | 758 | def sequence_action(matcher, value, pattern): 759 | """Iteratively match items of `pattern` with `value` in sequence. 760 | 761 | Return tuple of results of matches. 762 | 763 | >>> match([0, 'abc', {}], [int, str, dict]) 764 | True 765 | >>> match((0, True, bool), (0.0, 1, int)) 766 | True 767 | >>> match([], ()) 768 | False 769 | 770 | """ 771 | if len(value) != len(pattern): 772 | raise Mismatch 773 | 774 | pairs = zip(value, pattern) 775 | return tuple(matcher.visit(item, iota) for item, iota in pairs) 776 | 777 | 778 | default_cases.append(Case('sequences', sequence_predicate, sequence_action)) 779 | 780 | 781 | ############################################################################### 782 | # Store bound names in a stack. 783 | ############################################################################### 784 | 785 | 786 | class Bounder: 787 | """Stack for storing names bound to values for `Matcher`. 788 | 789 | >>> Bounder() 790 | Bounder([]) 791 | >>> bound = Bounder([{'foo': 0}]) 792 | >>> bound.foo 793 | 0 794 | >>> len(bound) 795 | 1 796 | >>> bound.pop() 797 | {'foo': 0} 798 | >>> len(bound) 799 | 0 800 | >>> bound.push({'bar': 1}) 801 | >>> len(bound) 802 | 1 803 | 804 | """ 805 | 806 | def __init__(self, maps=()): 807 | self._maps = list(maps) 808 | 809 | def __getattr__(self, attr): 810 | try: 811 | return self._maps[-1][attr] 812 | except (IndexError, KeyError): 813 | raise AttributeError(attr) from None 814 | 815 | def __getitem__(self, key): 816 | try: 817 | return self._maps[-1][key] 818 | except IndexError: 819 | raise KeyError(key) from None 820 | 821 | def __eq__(self, that): 822 | return self._maps[-1] == that 823 | 824 | def __ne__(self, that): 825 | return self._maps[-1] != that 826 | 827 | def __iter__(self): 828 | return iter(self._maps[-1]) 829 | 830 | def __len__(self): 831 | return len(self._maps) 832 | 833 | def push(self, mapping): 834 | """Push mapping.""" 835 | self._maps.append(mapping) 836 | 837 | def pop(self): 838 | """Pop last mapping.""" 839 | return self._maps.pop() 840 | 841 | def reset(self, func=None): # pylint: disable=R1710 842 | """Remove all mappings. 843 | 844 | Works also as a decorator. 845 | 846 | """ 847 | if func is None: 848 | del self._maps[:] 849 | else: 850 | 851 | @wraps(func) 852 | def wrapper(*args, **kwargs): 853 | start = len(self._maps) 854 | try: 855 | return func(*args, **kwargs) 856 | finally: 857 | while len(self._maps) > start: 858 | self.pop() 859 | 860 | return wrapper 861 | 862 | def __repr__(self): 863 | return '%s(%r)' % (type(self).__name__, self._maps) 864 | 865 | 866 | ############################################################################### 867 | # Stack of mappings. 868 | ############################################################################### 869 | 870 | 871 | class MapStack(Mapping): 872 | # pylint: disable=missing-docstring 873 | def __init__(self, maps=()): 874 | self._maps = list(maps) or [{}] 875 | 876 | def push(self): 877 | # pylint: disable=missing-docstring 878 | self._maps.append({}) 879 | 880 | def pull(self): 881 | # pylint: disable=missing-docstring 882 | _maps = self._maps 883 | mapping = _maps.pop() 884 | accumulator = _maps[-1] 885 | accumulator.update(mapping) 886 | 887 | def undo(self): 888 | # pylint: disable=missing-docstring 889 | return self._maps.pop() 890 | 891 | def __getitem__(self, key): 892 | for mapping in reversed(self._maps): 893 | if key in mapping: 894 | return mapping[key] 895 | raise KeyError(key) 896 | 897 | def __setitem__(self, key, value): 898 | self._maps[-1][key] = value 899 | 900 | def __delitem__(self, key): 901 | del self._maps[-1][key] 902 | 903 | def pop(self, key, default=None): 904 | # pylint: disable=missing-docstring 905 | return self._maps[-1].pop(key, default) 906 | 907 | def __iter__(self): 908 | return iter(set().union(*self._maps)) 909 | 910 | def __len__(self): 911 | return len(set().union(*self._maps)) 912 | 913 | def __repr__(self): 914 | return '%s(%r)' % (type(self).__name__, self._maps) 915 | 916 | def get(self, key, default=None): 917 | # pylint: disable=missing-docstring 918 | return self[key] if key in self else default 919 | 920 | def __contains__(self, key): 921 | return any(key in mapping for mapping in reversed(self._maps)) 922 | 923 | def __bool__(self): 924 | return any(self._maps) 925 | 926 | def copy(self): 927 | # pylint: disable=missing-docstring 928 | return dict(self) 929 | 930 | def reset(self): 931 | # pylint: disable=missing-docstring 932 | del self._maps[1:] 933 | self._maps[0].clear() 934 | 935 | 936 | ############################################################################### 937 | # Matcher objects put it all together. 938 | ############################################################################### 939 | 940 | 941 | class Matcher: 942 | """Container for match function state with list of pattern cases. 943 | 944 | >>> matcher = Matcher() 945 | >>> matcher.match(None, None) 946 | True 947 | >>> matcher.match(0, int) 948 | True 949 | >>> match = matcher.match 950 | >>> match([1, 2, 3], [1, bind.middle, 3]) 951 | True 952 | >>> matcher.bound.middle 953 | 2 954 | >>> bound = matcher.bound 955 | >>> match([(1, 2, 3), 4, 5], [bind.any, 4, bind.tail]) 956 | True 957 | >>> bound.tail 958 | 5 959 | 960 | """ 961 | 962 | def __init__(self, cases=None): 963 | cases = default_cases if cases is None else cases 964 | self.cases = cases 965 | self.bound = Bounder() 966 | self.names = MapStack() 967 | 968 | def match(self, value, pattern): 969 | # pylint: disable=missing-docstring 970 | names = self.names 971 | try: 972 | self.visit(value, pattern) 973 | except Mismatch: 974 | return False 975 | else: 976 | self.bound.push(names.copy()) 977 | finally: 978 | names.reset() 979 | return True 980 | 981 | def visit(self, value, pattern): 982 | # pylint: disable=missing-docstring 983 | for _, predicate, action in self.cases: 984 | if predicate(self, value, pattern): 985 | return action(self, value, pattern) 986 | raise Mismatch 987 | 988 | 989 | matcher = Matcher() 990 | match = matcher.match 991 | bound = matcher.bound 992 | 993 | 994 | ############################################################################### 995 | # Pattern Matching 996 | ############################################################################### 997 | 998 | # fmt: off 999 | __all__ = [ 1000 | 'Matcher', 'match', 1001 | 'Name', 'Binder', 'bind', 'Bounder', 'bound', 1002 | 'Like', 'like', 'like_errors', 1003 | 'literal_types', 1004 | 'Anyone', 'anyone', 1005 | 'Pattern', 'Exclude', 'exclude', 'Either', 'either', 'Group', 'group', 1006 | 'Repeat', 'repeat', 'maybe', 'anything', 'something', 'padding', 1007 | ] 1008 | # fmt: on 1009 | 1010 | __title__ = 'patternmatching' 1011 | __version__ = '3.0.1' 1012 | __build__ = 0x030001 1013 | __author__ = 'Grant Jenks' 1014 | __license__ = 'Apache 2.0' 1015 | __copyright__ = '2015-2021, Grant Jenks' 1016 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | blue 3 | coverage 4 | doc8 5 | flake8 6 | ipython 7 | jedi==0.17.* # Remove after IPython bug fixed. 8 | pylint 9 | pytest 10 | pytest-cov 11 | rstcheck 12 | sphinx 13 | tox 14 | twine 15 | wheel 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools.command.test import test as TestCommand 3 | 4 | import patternmatching 5 | 6 | 7 | class Tox(TestCommand): 8 | def finalize_options(self): 9 | TestCommand.finalize_options(self) 10 | self.test_args = [] 11 | self.test_suite = True 12 | 13 | def run_tests(self): 14 | import tox 15 | 16 | errno = tox.cmdline(self.test_args) 17 | exit(errno) 18 | 19 | 20 | with open('README.rst') as reader: 21 | readme = reader.read() 22 | 23 | setup( 24 | name=patternmatching.__title__, 25 | version=patternmatching.__version__, 26 | description='Python Pattern Matching', 27 | long_description=readme, 28 | author='Grant Jenks', 29 | author_email='contact@grantjenks.com', 30 | url='http://www.grantjenks.com/docs/patternmatching/', 31 | license='Apache 2.0', 32 | packages=['patternmatching'], 33 | tests_require=['tox'], 34 | cmdclass={'test': Tox}, 35 | classifiers=( 36 | 'Development Status :: 5 - Production/Stable', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: Apache Software License', 39 | 'Natural Language :: English', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.6', 43 | 'Programming Language :: Python :: 3.7', 44 | 'Programming Language :: Python :: 3.8', 45 | 'Programming Language :: Python :: 3.9', 46 | 'Programming Language :: Python :: Implementation :: CPython', 47 | ), 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantjenks/python-pattern-matching/7235c491b22183e8cfbd87b39dcda6e97437a268/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_funcs.py: -------------------------------------------------------------------------------- 1 | import random 2 | from collections import namedtuple 3 | 4 | import pytest 5 | 6 | from patternmatching import bind, bound, group, like, match, padding, repeat 7 | 8 | Point = namedtuple('Point', 'x y z t') 9 | 10 | 11 | def match_basic(value): 12 | if match(value, None): 13 | return 'case-1' 14 | elif match(value, True): 15 | return 'case-2' 16 | elif match(value, False): 17 | return 'case-3' 18 | elif match(value, -100): 19 | return 'case-4' 20 | elif match(value, 1.234): 21 | return 'case-5' 22 | elif match(value, 12345678901234567890): 23 | return 'case-6' 24 | elif match(value, complex(1, 2)): 25 | return 'case-7' 26 | elif match(value, str('alpha')): 27 | return 'case-8' 28 | elif match(value, bytes(b'beta')): 29 | return 'case-9' 30 | elif match(value, (1, 2, 3, 4)): 31 | return 'case-15' 32 | elif match(value, [bind.first, bind.second, bind.third]): 33 | return 'case-11' 34 | elif match(value, like('^abc..abc$')): 35 | return 'case-12' 36 | elif match(value, like(lambda val: val % 17 == 0)): 37 | return 'case-13' 38 | elif match(value, Point(0, 0, 0, 0)): 39 | return 'case-14' 40 | elif match(value, [1, 2, 3, 4]): 41 | return 'case-16' 42 | elif match(value, (0, [1, (2, [3, (4, [5])])])): 43 | return 'case-17' 44 | elif match(value, tuple): 45 | return 'case-10' 46 | elif match(value, like(lambda val: val % 19 == 0)): 47 | return 'case-18' 48 | elif match(value, object): 49 | return 'case-19' 50 | else: 51 | raise Exception('no match') 52 | 53 | 54 | def test_basic(): 55 | values = [ 56 | None, 57 | True, 58 | False, 59 | -100, 60 | 1.234, 61 | 12345678901234567890, 62 | complex(1, 2), 63 | str('alpha'), 64 | bytes(b'beta'), 65 | Point, 66 | [5, 6, 7], 67 | 'abc01abc', 68 | 119, 69 | Point(0, 0, 0, 0), 70 | Point(1, 2, 3, 4), 71 | [1, 2, 3, 4], 72 | (0, [1, (2, [3, (4, [5])])]), 73 | 114, 74 | list, 75 | ] 76 | 77 | cases = ['case-%s' % (num + 1) for num in range(len(values))] 78 | 79 | results = list(zip(values, cases)) 80 | 81 | random.seed(0) 82 | 83 | for _ in range(1000): 84 | random.shuffle(results) 85 | for value, result in results: 86 | assert match_basic(value) == result 87 | 88 | 89 | def test_bind_result(): 90 | with pytest.raises(AttributeError): 91 | if match(0, bind.push): 92 | return 'zero' 93 | else: 94 | return 'nonzero' 95 | 96 | 97 | def test_bind_any(): 98 | assert match([0, 1, 2], [bind.any, bind.any, bind.any]) 99 | 100 | 101 | def test_bind_repeated(): 102 | assert match((0, 0, 1, 1), (bind.zero, bind.zero, bind.one, bind.one)) 103 | assert not match((0, 1), (bind.value, bind.value)) 104 | 105 | 106 | def test_bound(): 107 | assert match(5, bind.value) 108 | assert bound.value == 5 109 | 110 | 111 | def test_bind_repeat(): 112 | assert match([1, 1, 1, 2, 2, 3], 1 * repeat + bind.value * repeat + 3) 113 | assert bound.value == 2 114 | 115 | 116 | def test_bind_repeat_alternate(): 117 | pattern = bind.any * repeat + bind.any * group('value') + [2, 1] + bind.any 118 | assert match([0, 1, 2, 1, 2], pattern) 119 | assert bound.value == [1] 120 | 121 | 122 | def test_bind_padding_name(): 123 | pattern = padding + [bind.value, bind.other, bind.value, 3] 124 | assert match([1, 2, 1, 2, 1, 2, 3], pattern) 125 | assert bound.value == 2 126 | assert bound.other == 1 127 | 128 | 129 | def test_bind_padding_like(): 130 | def odd_num(value): 131 | return value % 2 and value 132 | 133 | pattern = padding + [ 134 | like(odd_num, 'value'), 135 | like(odd_num, 'other'), 136 | like(odd_num, 'value'), 137 | 2, 138 | ] 139 | assert match([3, 5, 3, 5, 3, 5, 2], pattern) 140 | assert bound.value == 5 141 | assert bound.other == 3 142 | -------------------------------------------------------------------------------- /tests/test_regex.py: -------------------------------------------------------------------------------- 1 | """Regular expression module tests. 2 | 3 | A large number of these were ported from: 4 | https://github.com/python/cpython/blob/master/Lib/test/re_tests.py 5 | 6 | """ 7 | 8 | import patternmatching as pm 9 | 10 | a_foo = 'a' * pm.group(1) 11 | abc_e = 'abc' * pm.either 12 | term = 'multiple words' 13 | b_s = 'b' * pm.repeat 14 | 15 | 16 | def test_pattern_add_tuple(): 17 | pattern = pm.Pattern((1, 2, 3)) 18 | pattern = pattern + (4, 5, 6) 19 | assert pattern == pm.Pattern((1, 2, 3, 4, 5, 6)) 20 | 21 | 22 | def test_pattern_radd_tuple(): 23 | pattern = pm.Pattern((4, 5, 6)) 24 | pattern = (1, 2, 3) + pattern 25 | assert pattern == pm.Pattern((1, 2, 3, 4, 5, 6)) 26 | 27 | 28 | def test_pattern_add_single(): 29 | pattern = pm.Pattern((1, 2, 3)) 30 | pattern = pattern + None 31 | assert pattern == pm.Pattern((1, 2, 3, None)) 32 | 33 | 34 | def test_pattern_radd_single(): 35 | pattern = pm.Pattern((4, 5, 6)) 36 | pattern = None + pattern 37 | assert pattern == pm.Pattern((None, 4, 5, 6)) 38 | 39 | 40 | def test_pattern_ne(): 41 | assert pm.Pattern((1, 2, 3)) != pm.Pattern((4, 5, 6)) 42 | 43 | 44 | def test_pattern_hash(): 45 | assert hash(pm.Pattern((1, 2, 3))) == hash((1, 2, 3)) 46 | 47 | 48 | def test_pattern_repr(): 49 | assert repr(pm.Pattern((1, 2, 3))) == 'Pattern(1, 2, 3)' 50 | 51 | 52 | def test_anyone_repr(): 53 | assert repr(pm.anyone) == 'anyone' 54 | 55 | 56 | def test_repeat_rmul_single(): 57 | assert None * pm.repeat == pm.Repeat((None,), 0, pm.infinity, True) 58 | 59 | 60 | def test_repeat_repr(): 61 | assert repr(pm.repeat) == 'Repeat(pattern=(), min=0, max=inf, greedy=True)' 62 | 63 | 64 | def test_either_rmul_single(): 65 | assert None * pm.either == pm.Either(None) 66 | 67 | 68 | def test_either_repr(): 69 | assert repr(pm.either) == 'Either()' 70 | 71 | 72 | @pm.bound.reset 73 | def run(*args): 74 | pattern, value, result = args 75 | if result is None: 76 | assert not pm.match(value, pattern) 77 | else: 78 | assert pm.match(value, pattern) 79 | del result['_'] 80 | assert pm.bound == result 81 | 82 | 83 | def test_basic(): 84 | run('', '', {'_': ''}) 85 | run('abc', 'abc', {'_': 'abc'}) 86 | run('abc', 'xbc', None) 87 | run('abc', 'axc', None) 88 | run('abc', 'abx', None) 89 | run('abc', 'xabc', None) 90 | run('abc', 'xabcy', None) 91 | run('abc', '', None) 92 | run(term + ' of text', 'uh-oh', None) 93 | 94 | 95 | def test_groups(): 96 | run(a_foo, 'a', {'_': 'a', 1: 'a'}) 97 | run(a_foo, 'aa', {'_': 'a', 1: 'a'}) 98 | run(a_foo * pm.group(2), 'a', {'_': 'a', 1: 'a', 2: 'a'}) 99 | run( 100 | a_foo * pm.group(2) * pm.group(3), 101 | 'a', 102 | {'_': 'a', 1: 'a', 2: 'a', 3: 'a'}, 103 | ) 104 | run(a_foo + 'b' + 'c' * pm.group(2), 'abc', {'_': 'abc', 1: 'a', 2: 'c'}) 105 | run( 106 | abc_e * pm.repeat + pm.Group(abc_e, name=1) + 'd', 107 | 'abbbcd', 108 | {'_': 'abbbcd', 1: 'c'}, 109 | ) 110 | run( 111 | pm.Group(abc_e, name=1) * pm.repeat + 'bcd', 112 | 'abcd', 113 | {'_': 'abcd', 1: 'a'}, 114 | ) 115 | 116 | 117 | def test_anyone(): 118 | run('a' + pm.anyone + 'c', 'abc', {'_': 'abc'}) 119 | run('a' + pm.anything + 'c', 'axyzc', {'_': 'axyzc'}) 120 | run('a' + pm.anything + 'c', 'axyzd', None) 121 | run('a' + pm.anything + 'c', 'ac', {'_': 'ac'}) 122 | run('a' + pm.anyone * pm.repeat(min=2, max=3) + 'c', 'abbc', {'_': 'abbc'}) 123 | 124 | 125 | def test_repeat(): 126 | run('a' + b_s + 'c', 'abc', {'_': 'abc'}) 127 | run('a' + b_s + 'bc', 'abc', {'_': 'abc'}) 128 | run('a' + b_s + 'bc', 'abbc', {'_': 'abbc'}) 129 | run('a' + b_s + 'bc', 'abbbbc', {'_': 'abbbbc'}) 130 | run('a' * pm.repeat, '', {'_': ''}) 131 | run('a' * pm.repeat, 'a', {'_': 'a'}) 132 | run('a' * pm.repeat, 'aaa', {'_': 'aaa'}) 133 | run( 134 | pm.padding + 'a' * pm.repeat(1) + 'b' + 'c' * pm.repeat(1), 135 | 'aabbabc', 136 | {'_': 'aabbabc'}, 137 | ) 138 | 139 | 140 | def test_repeat_range(): 141 | run( 142 | 'a' 143 | + 'b' 144 | * pm.repeat( 145 | 1, 146 | ) 147 | + 'bc', 148 | 'abbbbc', 149 | {'_': 'abbbbc'}, 150 | ) 151 | run('a' + 'b' * pm.repeat(1, 3) + 'bc', 'abbbbc', {'_': 'abbbbc'}) 152 | run('a' + 'b' * pm.repeat(3, 4) + 'bc', 'abbbbc', {'_': 'abbbbc'}) 153 | run('a' + 'b' * pm.repeat(4, 5) + 'bc', 'abbbbc', None) 154 | 155 | 156 | def test_some(): 157 | run('a' + 'b' * pm.something + 'bc', 'abbc', {'_': 'abbc'}) 158 | run('a' + 'b' * pm.something + 'bc', 'abc', None) 159 | run('a' + 'b' * pm.something + 'bc', 'abq', None) 160 | run('a' + 'b' * pm.something + 'bc', 'abbbbc', {'_': 'abbbbc'}) 161 | 162 | 163 | def test_maybe(): 164 | run('a' * pm.maybe, '', {'_': ''}) 165 | run('a' * pm.maybe, 'a', {'_': 'a'}) 166 | run('a' * pm.maybe, 'aa', {'_': 'a'}) 167 | run('a' + 'a' * pm.maybe, 'aa', {'_': 'aa'}) 168 | run('a' + pm.anyone + 'c' + 'd' * pm.maybe, 'abc', {'_': 'abc'}) 169 | run('a' + 'b' * pm.maybe + 'bc', 'abbc', {'_': 'abbc'}) 170 | run('a' + 'b' * pm.maybe + 'bc', 'abc', {'_': 'abc'}) 171 | run('a' + 'b' * pm.repeat(0, 1) + 'bc', 'abc', {'_': 'abc'}) 172 | run('a' + 'b' * pm.maybe + 'bc', 'abbbbc', None) 173 | run('a' + 'b' * pm.maybe + 'c', 'abc', {'_': 'abc'}) 174 | run('a' + 'b' * pm.repeat(0, 1) + 'c', 'abc', {'_': 'abc'}) 175 | 176 | 177 | def test_either(): 178 | run('a' + pm.either('bc') + 'd', 'abc', None) 179 | run('a' + 'bc' * pm.either + 'd', 'abd', {'_': 'abd'}) 180 | run(('ab', 'cd') * pm.either, 'abc', {'_': 'ab'}) 181 | run(('ab', 'cd') * pm.either, 'abcd', {'_': 'ab'}) 182 | run(pm.Either('a' * pm.repeat(min=1), 'b') * pm.repeat, 'ab', {'_': 'ab'}) 183 | run( 184 | pm.Either('a' * pm.repeat(min=1), 'b') * pm.repeat(min=1), 185 | 'ab', 186 | {'_': 'ab'}, 187 | ) 188 | run(pm.Either('a' * pm.repeat(min=1), 'b') * pm.maybe, 'ab', {'_': 'a'}) 189 | run('abcde' * pm.either, 'e', {'_': 'e'}) 190 | run('abcde' * pm.either + 'f', 'ef', {'_': 'ef'}) 191 | run(pm.padding + pm.Either('ab', 'cd') + 'e', 'abcde', {'_': 'abcde'}) 192 | run('abhgefdc' * pm.either + 'ij', 'hij', {'_': 'hij'}) 193 | 194 | 195 | def test_exclude(): 196 | run('a' + 'bc' * pm.exclude + 'd', 'aed', {'_': 'aed'}) 197 | run('a' + 'bc' * pm.exclude + 'd', 'abd', None) 198 | run('ab' * pm.exclude * pm.repeat, 'cde', {'_': 'cde'}) 199 | 200 | 201 | def test_misc(): 202 | bc_e_r_g_1 = 'bc' * pm.either * pm.repeat * pm.group(1) 203 | bc_e_r_1_g_1 = 'bc' * pm.either * pm.repeat(min=1) * pm.group(1) 204 | 205 | run( 206 | pm.padding + 'ab' * pm.either + 'c' * pm.repeat + 'd', 207 | 'abcd', 208 | {'_': 'abcd'}, 209 | ) 210 | run(('ab', 'a' + b_s) * pm.either + 'bc', 'abc', {'_': 'abc'}) 211 | run('a' + bc_e_r_g_1 + 'c' * pm.repeat, 'abc', {'_': 'abc', 1: 'bc'}) 212 | run( 213 | 'a' + bc_e_r_g_1 + ('c' * pm.repeat + 'd') * pm.group(2), 214 | 'abcd', 215 | {'_': 'abcd', 1: 'bc', 2: 'd'}, 216 | ) 217 | run( 218 | 'a' + bc_e_r_1_g_1 + ('c' * pm.repeat + 'd') * pm.group(2), 219 | 'abcd', 220 | {'_': 'abcd', 1: 'bc', 2: 'd'}, 221 | ) 222 | run( 223 | 'a' + bc_e_r_g_1 + ('c' * pm.repeat(min=1) + 'd') * pm.group(2), 224 | 'abcd', 225 | {'_': 'abcd', 1: 'b', 2: 'cd'}, 226 | ) 227 | run( 228 | 'a' + 'bcd' * pm.either * pm.repeat + 'dcdcde', 229 | 'adcdcde', 230 | {'_': 'adcdcde'}, 231 | ) 232 | run('a' + 'bcd' * pm.either * pm.repeat(min=1) + 'dcdcde', 'adcdcde', None) 233 | run(('ab', 'a') * pm.either + b_s + 'c', 'abc', {'_': 'abc'}) 234 | run( 235 | pm.Group(pm.Group('a') + pm.Group('b') + pm.Group('c')) 236 | + pm.Group('d'), 237 | 'abcd', 238 | {'_': 'abcd'}, 239 | ) 240 | run( 241 | pm.anything * pm.group(1) + 'c' + pm.anything * pm.group(2), 242 | 'abcde', 243 | {'_': 'abcde', 1: 'ab', 2: 'de'}, 244 | ) 245 | run('k' * pm.either, 'ab', None) 246 | run('a' + '-' * pm.maybe + 'c', 'ac', {'_': 'ac'}) 247 | run( 248 | pm.Either(pm.Group('a') + pm.Group('b') + 'c', 'ab'), 'ab', {'_': 'ab'} 249 | ) 250 | run('a' * pm.group * pm.repeat(min=1) + 'x', 'aaax', {'_': 'aaax'}) 251 | run( 252 | 'ac' * pm.either * pm.group * pm.repeat(min=1) 253 | + 'ac' * pm.either * pm.group(1) 254 | + 'x', 255 | 'aacx', 256 | {'_': 'aacx', 1: 'c'}, 257 | ) 258 | run( 259 | pm.padding + pm.Group('/' * pm.exclude * pm.repeat + '/', 1) + 'sub1/', 260 | 'd:msgs/tdir/sub1/trial/away.cpp', 261 | {'_': 'd:msgs/tdir/sub1/', 1: 'tdir/'}, 262 | ) 263 | run( 264 | ('N' * pm.exclude * pm.repeat + 'N') * pm.group() * pm.repeat(min=1) 265 | + ('N' * pm.exclude * pm.repeat + 'N') * pm.group(1), 266 | 'abNNxyzN', 267 | {'_': 'abNNxyzN', 1: 'xyzN'}, 268 | ) 269 | run( 270 | ('N' * pm.exclude * pm.repeat + 'N') * pm.group() * pm.repeat(min=1) 271 | + ('N' * pm.exclude * pm.repeat + 'N') * pm.group(1), 272 | 'abNNxyz', 273 | {'_': 'abNN', 1: 'N'}, 274 | ) 275 | run(abc_e * pm.repeat * pm.group(1) + 'x', 'abcx', {'_': 'abcx', 1: 'abc'}) 276 | run(abc_e * pm.repeat * pm.group(1) + 'x', 'abc', None) 277 | run( 278 | pm.padding + 'xyz' * pm.either * pm.repeat * pm.group(1) + 'x', 279 | 'abcx', 280 | {'_': 'abcx', 1: ''}, 281 | ) 282 | run( 283 | pm.Either(pm.Group('a') * pm.repeat(min=1) + 'b', 'aac'), 284 | 'aac', 285 | {'_': 'aac'}, 286 | ) 287 | 288 | 289 | def test_not_greedy(): 290 | run( 291 | 'a' + pm.anyone * pm.repeat(min=1, greedy=False) + 'c', 292 | 'abcabc', 293 | {'_': 'abc'}, 294 | ) 295 | 296 | 297 | def test_something(): 298 | run(pm.Either('a' * pm.something, 'b') * pm.repeat, 'ab', {'_': 'ab'}) 299 | run( 300 | pm.Either('a' * pm.something, 'b') 301 | * pm.repeat( 302 | 0, 303 | ), 304 | 'ab', 305 | {'_': 'ab'}, 306 | ) 307 | run(pm.Either('a' * pm.something, 'b') * pm.something, 'ab', {'_': 'ab'}) 308 | run( 309 | pm.Either('a' * pm.something, 'b') 310 | * pm.repeat( 311 | 1, 312 | ), 313 | 'ab', 314 | {'_': 'ab'}, 315 | ) 316 | run(pm.Either('a' * pm.something, 'b') * pm.maybe, 'ab', {'_': 'a'}) 317 | run(pm.Either('a' * pm.something, 'b') * pm.repeat(0, 1), 'ab', {'_': 'a'}) 318 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=bluecheck,doc8,docs,isortcheck,flake8,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}/patternmatching {toxinidir}/tests 13 | deps=blue 14 | 15 | [testenv:bluecheck] 16 | commands=blue --check {toxinidir}/setup.py {toxinidir}/patternmatching {toxinidir}/tests 17 | deps=blue 18 | 19 | [testenv:doc8] 20 | deps=doc8 21 | commands=doc8 docs --ignore-path docs/_build 22 | 23 | [testenv:docs] 24 | allowlist_externals=make 25 | changedir=docs 26 | commands=make html 27 | deps= 28 | django==2.2.* 29 | sphinx 30 | 31 | [testenv:flake8] 32 | commands=flake8 {toxinidir}/setup.py {toxinidir}/patternmatching {toxinidir}/tests 33 | deps=flake8 34 | 35 | [testenv:isort] 36 | commands=isort {toxinidir}/setup.py {toxinidir}/patternmatching {toxinidir}/tests 37 | deps=isort 38 | 39 | [testenv:isortcheck] 40 | commands=isort --check {toxinidir}/setup.py {toxinidir}/patternmatching {toxinidir}/tests 41 | deps=isort 42 | 43 | [testenv:pylint] 44 | commands=pylint {toxinidir}/patternmatching 45 | deps= 46 | django==2.2.* 47 | pylint 48 | 49 | [testenv:rstcheck] 50 | commands=rstcheck --report warning {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/patternmatching/ 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=90 72 | --cov-report=term-missing 73 | --cov=patternmatching 74 | --doctest-glob="*.rst" 75 | 76 | [doc8] 77 | # ignore=D000 78 | 79 | [flake8] 80 | exclude= 81 | extend-ignore=E203 82 | max-line-length=120 83 | --------------------------------------------------------------------------------