├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── examples ├── kerfyn_playing.py ├── tour_case_classes.py └── tour_pattern_matching.py ├── pyfpm ├── __init__.py ├── matcher.py ├── parser.py └── pattern.py ├── requirements.txt ├── setup.py └── tests ├── test_matcher.py ├── test_parser.py └── test_pattern.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | #Translations 30 | *.mo 31 | 32 | #Mr Developer 33 | .mr.developer.cfg 34 | 35 | # Sphinx doc build 36 | _build 37 | 38 | # distutils MANIFEST 39 | MANIFEST 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.5" 4 | - "2.6" 5 | - "2.7" 6 | - "3.2" 7 | - "pypy" 8 | script: 9 | - nosetests --with-doctest 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Martin Blech 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyfpm 2 | 3 | `pyfpm` stands for PYthon Functional Pattern Matching. It's been heavily 4 | inspired by the Pattern Matching and Case Classes implementation in Scala. 5 | 6 | Build status at [Travis CI](http://travis-ci.org/): [![Build Status](https://secure.travis-ci.org/martinblech/pyfpm.png)](http://travis-ci.org/martinblech/pyfpm) 7 | 8 | ## Usage 9 | 10 | With `pyfpm` you can unpack objects using the `Unpacker` class: 11 | 12 | ```python 13 | unpacker = Unpacker() 14 | unpacker('head :: tail') << (1, 2, 3) 15 | unpacker.head # 1 16 | unpacker.tail # (2, 3) 17 | ``` 18 | 19 | or function parameters using the `match_args` decorator: 20 | 21 | ```python 22 | @match_args('[x:str, [y:int, z:int]]') 23 | def match(x, y, z): 24 | return (x, y, z) 25 | 26 | match('abc', (1, 2)) # ('abc', 1, 2) 27 | ``` 28 | 29 | You can also create simple matchers with lambda expressions using the `Matcher` 30 | class: 31 | 32 | ```python 33 | what_is_it = Matcher([ 34 | ('_:int', lambda: 'an int'), 35 | ('_:str', lambda: 'a string'), 36 | ('x', lambda x: 'something else: %s' % x), 37 | ]) 38 | 39 | what_is_it(10) # 'an int' 40 | what_is_it('abc') # 'a string' 41 | what_is_it({}) # 'something else: {}' 42 | ``` 43 | 44 | or more complex ones using the `Matcher.handler` decorator: 45 | 46 | ```python 47 | parse_options = Matcher() 48 | @parse_options.handler("['-h'|'--help', None]") 49 | def help(): 50 | return 'help' 51 | @parse_options.handler("['-o'|'--optim', level:int] if 1<=level<=5") 52 | def set_optimization(level): 53 | return 'optimization level set to %d' % level 54 | @parse_options.handler("['-o'|'--optim', bad_level]") 55 | def bad_optimization(bad_level): 56 | return 'bad optimization level: %s' % bad_level 57 | @parse_options.handler('x') 58 | def unknown_options(x): 59 | return 'unknown options: %s' % repr(x) 60 | 61 | parse_options(('-h', None)) # 'help' 62 | parse_options(('--help', None)) # 'help' 63 | parse_options(('-o', 3)) # 'optimization level set to 3' 64 | parse_options(('-o', 0)) # 'bad optimization level: 0' 65 | parse_options(('-v', 'x')) # "unknown options: ('-v', 'x')" 66 | ``` 67 | 68 | For more information, see the files in the `examples` directory alongside the 69 | links within them, or [read the docs](http://pyfpm.readthedocs.org/). 70 | 71 | ## Installation 72 | 73 | `pyfpm` is in [PyPi](http://pypi.python.org/pypi/pyfpm): 74 | 75 | ```bash 76 | $ pip install pyfpm 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyfpm.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyfpm.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyfpm" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyfpm" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyfpm documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Aug 4 20:39:29 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | import pyfpm 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'pyfpm' 46 | copyright = pyfpm.__copyright__ 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | # The short X.Y version. 53 | version = pyfpm.__version__ 54 | # The full version, including alpha/beta/rc tags. 55 | release = pyfpm.__version__ 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | #language = None 60 | 61 | # There are two options for replacing |today|: either, you set today to some 62 | # non-false value, then it is used: 63 | #today = '' 64 | # Else, today_fmt is used as the format for a strftime call. 65 | #today_fmt = '%B %d, %Y' 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | exclude_patterns = ['_build'] 70 | 71 | # The reST default role (used for this markup: `text`) to use for all documents. 72 | #default_role = None 73 | 74 | # If true, '()' will be appended to :func: etc. cross-reference text. 75 | #add_function_parentheses = True 76 | 77 | # If true, the current module name will be prepended to all description 78 | # unit titles (such as .. function::). 79 | #add_module_names = True 80 | 81 | # If true, sectionauthor and moduleauthor directives will be shown in the 82 | # output. They are ignored by default. 83 | #show_authors = False 84 | 85 | # The name of the Pygments (syntax highlighting) style to use. 86 | pygments_style = 'sphinx' 87 | 88 | # A list of ignored prefixes for module index sorting. 89 | #modindex_common_prefix = [] 90 | 91 | 92 | # -- Options for HTML output --------------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'default' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | #html_theme_options = {} 102 | 103 | # Add any paths that contain custom themes here, relative to this directory. 104 | #html_theme_path = [] 105 | 106 | # The name for this set of Sphinx documents. If None, it defaults to 107 | # " v documentation". 108 | #html_title = None 109 | 110 | # A shorter title for the navigation bar. Default is the same as html_title. 111 | #html_short_title = None 112 | 113 | # The name of an image file (relative to this directory) to place at the top 114 | # of the sidebar. 115 | #html_logo = None 116 | 117 | # The name of an image file (within the static path) to use as favicon of the 118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 119 | # pixels large. 120 | #html_favicon = None 121 | 122 | # Add any paths that contain custom static files (such as style sheets) here, 123 | # relative to this directory. They are copied after the builtin static files, 124 | # so a file named "default.css" will overwrite the builtin "default.css". 125 | html_static_path = ['_static'] 126 | 127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 128 | # using the given strftime format. 129 | #html_last_updated_fmt = '%b %d, %Y' 130 | 131 | # If true, SmartyPants will be used to convert quotes and dashes to 132 | # typographically correct entities. 133 | #html_use_smartypants = True 134 | 135 | # Custom sidebar templates, maps document names to template names. 136 | #html_sidebars = {} 137 | 138 | # Additional templates that should be rendered to pages, maps page names to 139 | # template names. 140 | #html_additional_pages = {} 141 | 142 | # If false, no module index is generated. 143 | #html_domain_indices = True 144 | 145 | # If false, no index is generated. 146 | #html_use_index = True 147 | 148 | # If true, the index is split into individual pages for each letter. 149 | #html_split_index = False 150 | 151 | # If true, links to the reST sources are added to the pages. 152 | #html_show_sourcelink = True 153 | 154 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 155 | #html_show_sphinx = True 156 | 157 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 158 | #html_show_copyright = True 159 | 160 | # If true, an OpenSearch description file will be output, and all pages will 161 | # contain a tag referring to it. The value of this option must be the 162 | # base URL from which the finished HTML is served. 163 | #html_use_opensearch = '' 164 | 165 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 166 | #html_file_suffix = None 167 | 168 | # Output file base name for HTML help builder. 169 | htmlhelp_basename = 'pyfpmdoc' 170 | 171 | 172 | # -- Options for LaTeX output -------------------------------------------------- 173 | 174 | latex_elements = { 175 | # The paper size ('letterpaper' or 'a4paper'). 176 | #'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | #'preamble': '', 183 | } 184 | 185 | # Grouping the document tree into LaTeX files. List of tuples 186 | # (source start file, target name, title, author, documentclass [howto/manual]). 187 | latex_documents = [ 188 | ('index', 'pyfpm.tex', u'pyfpm Documentation', 189 | pyfpm.__author__, 'manual'), 190 | ] 191 | 192 | # The name of an image file (relative to this directory) to place at the top of 193 | # the title page. 194 | #latex_logo = None 195 | 196 | # For "manual" documents, if this is true, then toplevel headings are parts, 197 | # not chapters. 198 | #latex_use_parts = False 199 | 200 | # If true, show page references after internal links. 201 | #latex_show_pagerefs = False 202 | 203 | # If true, show URL addresses after external links. 204 | #latex_show_urls = False 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'pyfpm', u'pyfpm Documentation', 219 | [pyfpm.__author__], 1) 220 | ] 221 | 222 | # If true, show URL addresses after external links. 223 | #man_show_urls = False 224 | 225 | 226 | # -- Options for Texinfo output ------------------------------------------------ 227 | 228 | # Grouping the document tree into Texinfo files. List of tuples 229 | # (source start file, target name, title, author, 230 | # dir menu entry, description, category) 231 | texinfo_documents = [ 232 | ('index', 'pyfpm', u'pyfpm Documentation', 233 | pyfpm.__author__, 'pyfpm', 'One line description of project.', 234 | 'Miscellaneous'), 235 | ] 236 | 237 | # Documents to append as an appendix to all manuals. 238 | #texinfo_appendices = [] 239 | 240 | # If false, no module index is generated. 241 | #texinfo_domain_indices = True 242 | 243 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 244 | #texinfo_show_urls = 'footnote' 245 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pyfpm documentation 2 | =================== 3 | 4 | The name :mod:`pyfpm` stands for PYthon Functional Pattern Matching. It's an 5 | attempt to bring Scala-like functional pattern matching to Python with a similar 6 | syntax. 7 | 8 | :mod:`pyfpm.matcher` 9 | -------------------- 10 | 11 | .. automodule:: pyfpm.matcher 12 | :members: Matcher, match_args, Unpacker, NoMatch 13 | 14 | :mod:`pyfpm.parser` 15 | ------------------- 16 | 17 | .. automodule:: pyfpm.parser 18 | :members: Parser 19 | 20 | :mod:`pyfpm.pattern` 21 | -------------------- 22 | 23 | .. automodule:: pyfpm.pattern 24 | :members: build, Match, Pattern, AnyPattern, EqualsPattern, InstanceOfPattern, 25 | RegexPattern, ListPattern, NamedTuplePattern, OrPattern 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | Indices and tables 31 | ================== 32 | 33 | * :ref:`genindex` 34 | * :ref:`modindex` 35 | * :ref:`search` 36 | 37 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyfpm.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyfpm.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /examples/kerfyn_playing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Loose port of the examples in `Kerflyn's Blog / Playing with Scala's 3 | pattern matching 4 | `_ 5 | """ 6 | from __future__ import print_function 7 | 8 | from pyfpm.matcher import Matcher 9 | 10 | # Traditional approach 11 | print('-'*80) 12 | toYesOrNo = Matcher([ 13 | ('1', lambda: 'yes'), 14 | ('0', lambda: 'no'), 15 | ('_', lambda: 'error'), 16 | ]) 17 | for x in (0, 1, 2): 18 | print(toYesOrNo(x)) 19 | 20 | print('-'*80) 21 | toYesOrNo = Matcher([ 22 | ('1 | 2 | 3', lambda: 'yes'), 23 | ('0', lambda: 'no'), 24 | ('_', lambda: 'error'), 25 | ]) 26 | for x in (0, 1, 2, 3, 4): 27 | print(toYesOrNo(x)) 28 | 29 | print('-'*80) 30 | def displayHelp(): 31 | print('HELP') 32 | def displayVersion(): 33 | print('V1.0') 34 | def unknownArgument(whatever): 35 | print('unknown argument: %s' % whatever) 36 | parseArgument = Matcher([ 37 | ('"-h" | "--help"', displayHelp), 38 | ('"-v" | "--version"', displayVersion), 39 | ('whatever', unknownArgument), 40 | ]) 41 | for x in ('-h', '--help', '-v', '--version', '-f', '--fdsa'): 42 | parseArgument(x) 43 | 44 | # Typed Pattern 45 | print('-'*80) 46 | f = Matcher([ 47 | ('i:int', lambda i: 'integer: %s' % i), 48 | ('_:float', lambda: 'a float'), 49 | ('s:str', lambda s: 'I want to say ' + s), 50 | ]) 51 | for x in (1, 1.0, 'hello'): 52 | print(f(x)) 53 | 54 | # Functional approach to pattern matching 55 | print('-'*80) 56 | fact = Matcher([ 57 | ('0', lambda: 1), 58 | ('n:int', lambda n: n * fact(n - 1)), 59 | ]) 60 | for x in range(10): 61 | print(fact(x)) 62 | 63 | # Pattern matching and collection: the look-alike approach 64 | print('-'*80) 65 | length = Matcher([ 66 | ('_ :: tail', lambda tail: 1 + length(tail)), 67 | ('[]', lambda: 0) 68 | ]) 69 | for x in range(10): 70 | print(length([None]*x)) 71 | 72 | print('-'*80) 73 | def setLanguageTo(lang): 74 | print('language set to:', lang) 75 | def setOptimizationLevel(n): 76 | print('optimization level set to:', n) 77 | def badOptimizationLevel(badLevel): 78 | print('bad optimization level:', badLevel) 79 | def displayHelp(): 80 | print('help!') 81 | def badArgument(bad): 82 | print('bad argument:', bad) 83 | parseArgument = Matcher([ 84 | ('["-l", lang]', setLanguageTo), 85 | ('["-o" | "--optim", n:int] if 0 < n <= 5', setOptimizationLevel), 86 | ('["-o" | "--optim", badLevel]', badOptimizationLevel), 87 | ('["-h" | "--help", None]', displayHelp), 88 | ('bad', badArgument), 89 | ]) 90 | for x in (('-l', 'eng'), 91 | ('-o', 1), ('--optim', 5), 92 | ('-o', 0), ('--optim', 6), 93 | ('-h', None), ('--help', None), 94 | ('-h', 1), ('--help', 'abc'), 95 | None, 'blabla' 96 | ): 97 | parseArgument(x) 98 | 99 | # Advanced pattern matching: case class 100 | 101 | print('-'*80) 102 | from collections import namedtuple 103 | X = namedtuple('X', []) 104 | Const = namedtuple('Const', 'value') 105 | Add = namedtuple('Add', 'left, right') 106 | Mult = namedtuple('Mult', 'left, right') 107 | Neg = namedtuple('Neg', 'expression') 108 | 109 | eval = Matcher([ 110 | ('X()', 111 | lambda xValue: xValue), 112 | ('Const(cst)', 113 | lambda xValue, cst: cst), 114 | ('Add(left, right)', 115 | lambda xValue, left, right: eval(left, xValue)+eval(right, xValue)), 116 | ('Mult(left, right)', 117 | lambda xValue, left, right: eval(left, xValue)*eval(right, xValue)), 118 | ('Neg(expr)', 119 | lambda xValue, expr: -eval(expr, xValue)), 120 | ]) 121 | 122 | expr = Add(Const(1), Mult(Const(2), Mult(X(), X()))) # 1 + 2 * X*X 123 | print('expression:', expr) 124 | result = eval(expr, 3) 125 | print('f(3):', result) 126 | assert result == 19 127 | 128 | deriv = Matcher([ 129 | ('X()', lambda: Const(1)), 130 | ('Const(_)', lambda: Const(0)), 131 | ('Add(left, right)', 132 | lambda left, right: Add(deriv(left), deriv(right))), 133 | ('Mult(left, right)', 134 | lambda left, right: Add(Mult(deriv(left), right), 135 | Mult(left, deriv(right)))), 136 | ('Neg(expr)', lambda: Neg(deriv(expr))), 137 | ]) 138 | 139 | df = deriv(expr) 140 | print('df:', df) 141 | result = eval(df, 3) 142 | print('df(3):', result) 143 | assert result == 12 144 | 145 | _simplify = Matcher() 146 | @_simplify.handler('Mult(Const(x), Const(y))') 147 | def _mult_consts(x, y): 148 | return Const(x * y) 149 | @_simplify.handler('Add(Const(x), Const(y))') 150 | def _add_consts(x, y): 151 | return Const(x + y) 152 | @_simplify.handler('Mult(Const(0), _)') 153 | def _mult_zero_left(): 154 | return Const(0) 155 | @_simplify.handler('Mult(_, Const(0))') 156 | def _mult_zero_right(): 157 | return Const(0) 158 | @_simplify.handler('Mult(Const(1), expr)') 159 | def _mult_one_left(expr): 160 | return simplify(expr) 161 | @_simplify.handler('Mult(expr, Const(1))') 162 | def _mult_one_right(expr): 163 | return simplify(expr) 164 | @_simplify.handler('Add(Const(0), expr)') 165 | def _add_zero_left(expr): 166 | return simplify(expr) 167 | @_simplify.handler('Add(expr, Const(0))') 168 | def _add_zero_right(expr): 169 | return simplify(expr) 170 | @_simplify.handler('Neg(Neg(expr))') 171 | def _double_negation(expr): 172 | return simplify(expr) 173 | @_simplify.handler('Add(left, right)') 174 | def _normal_add(left, right): 175 | return Add(simplify(left), simplify(right)) 176 | @_simplify.handler('Mult(left, right)') 177 | def _normal_mult(left, right): 178 | return Mult(simplify(left), simplify(right)) 179 | @_simplify.handler('Neg(expr)') 180 | def _normal_negation(expr): 181 | return simplify(expr) 182 | @_simplify.handler('expr') 183 | def _no_can_do(expr): 184 | return expr 185 | 186 | def simplify(expr): 187 | while True: 188 | simplified = _simplify(expr) 189 | if simplified == expr: 190 | return simplified 191 | expr = simplified 192 | 193 | df_simplified = simplify(df) 194 | print('df_simplified:', df_simplified) 195 | result = eval(df_simplified, 3) 196 | print('df_simplified(3)', result) 197 | assert result == 12 198 | 199 | for expr in ( 200 | Add(Const(5), Const(10)), 201 | Mult(Mult(Mult(Const(5), Add(Const(1), Const(0))), Const(.2)), X()), 202 | ): 203 | print('expr:', expr) 204 | print('simplified:', simplify(expr)) 205 | -------------------------------------------------------------------------------- /examples/tour_case_classes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Loose port of the examples at `A Tour of Scala: Case Classes `_ 4 | """ 5 | from __future__ import print_function 6 | from collections import namedtuple 7 | 8 | from pyfpm.matcher import Matcher 9 | 10 | Var = namedtuple('Var', 'name') 11 | Fun = namedtuple('Fun', 'arg, body') 12 | App = namedtuple('Term', 'f, v') 13 | 14 | print('-'*80) 15 | example = Fun('x', Fun('y', App(Var('x'), Var('y')))) 16 | print(example) 17 | 18 | print('-'*80) 19 | x = Var('x') 20 | print(x.name) 21 | 22 | print('-'*80) 23 | printTerm = Matcher() 24 | @printTerm.handler('Var(n:str)') 25 | def _var(n): 26 | print(n, end='') 27 | @printTerm.handler('Fun(x:str, b)') 28 | def _fun(x, b): 29 | print('^' + x + '.', end='') 30 | printTerm(b) 31 | @printTerm.handler('App(f, v)') 32 | def _app(f, v): 33 | print('(', end='') 34 | printTerm(f) 35 | print(' ', end='') 36 | printTerm(v) 37 | print(')', end='') 38 | 39 | isIdentityFun = Matcher() 40 | @isIdentityFun.handler('Fun(x, Var(y)) if x==y') 41 | def _identity(x, y): 42 | return True 43 | @isIdentityFun.handler('_') 44 | def _other(): 45 | return False 46 | 47 | id = Fun('x', Var('x')) 48 | t = Fun('x', Fun('y', App(Var('x'), Var('y')))) 49 | printTerm(t) 50 | print() 51 | print(isIdentityFun(id)) 52 | print(isIdentityFun(t)) 53 | -------------------------------------------------------------------------------- /examples/tour_pattern_matching.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Loose port of the examples at `A Tour of Scala: Pattern Matching `_ 4 | """ 5 | from __future__ import print_function 6 | 7 | from pyfpm.matcher import Matcher 8 | 9 | print('-'*80) 10 | matchTest = Matcher([ 11 | ('1', lambda: 'one'), 12 | ('2', lambda: 'two'), 13 | ('_', lambda: 'many') 14 | ]) 15 | print(matchTest(1)) 16 | print(matchTest(2)) 17 | print(matchTest(3)) 18 | 19 | print('-'*80) 20 | matchTest = Matcher([ 21 | ('1', lambda: 'one'), 22 | ('"two"', lambda: 2), 23 | ('y:int', lambda y: 'scala.Int') 24 | ]) 25 | print(matchTest(1)) 26 | print(matchTest("two")) 27 | print(matchTest(3)) 28 | -------------------------------------------------------------------------------- /pyfpm/__init__.py: -------------------------------------------------------------------------------- 1 | """This module provides Scala-like functional pattern matching in Python.""" 2 | 3 | __version__ = '0.1.3-dev' 4 | __author__ = 'Martin Blech' 5 | __copyright__ = '2012, ' + __author__ 6 | __license__ = 'MIT' 7 | -------------------------------------------------------------------------------- /pyfpm/matcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Matchers are the main user-facing API for `pyfpm`. 3 | 4 | This module lets you unpack objects: 5 | 6 | >>> unpacker = Unpacker() 7 | >>> unpacker('head :: tail') << (1, 2, 3) 8 | >>> unpacker.head 9 | 1 10 | >>> unpacker.tail 11 | (2, 3) 12 | 13 | or function parameters: 14 | 15 | >>> @match_args('[x:str, [y:int, z:int]]') 16 | ... def match(x, y, z): 17 | ... return (x, y, z) 18 | 19 | >>> match('abc', (1, 2)) 20 | ('abc', 1, 2) 21 | 22 | You can also create simple matchers with lambda expressions: 23 | 24 | >>> what_is_it = Matcher([ 25 | ... ('_:int', lambda: 'an int'), 26 | ... ('_:str', lambda: 'a string'), 27 | ... ('x', lambda x: 'something else: %s' % x), 28 | ... ]) 29 | 30 | >>> what_is_it(10) 31 | 'an int' 32 | >>> what_is_it('abc') 33 | 'a string' 34 | >>> what_is_it({}) 35 | 'something else: {}' 36 | 37 | or more complex ones using a decorator: 38 | 39 | >>> parse_options = Matcher() 40 | >>> @parse_options.handler("['-h'|'--help', None]") 41 | ... def help(): 42 | ... return 'help' 43 | >>> @parse_options.handler("['-o'|'--optim', level:int] if 1<=level<=5") 44 | ... def set_optimization(level): 45 | ... return 'optimization level set to %d' % level 46 | >>> @parse_options.handler("['-o'|'--optim', bad_level]") 47 | ... def bad_optimization(bad_level): 48 | ... return 'bad optimization level: %s' % bad_level 49 | >>> @parse_options.handler('x') 50 | ... def unknown_options(x): 51 | ... return 'unknown options: %s' % repr(x) 52 | 53 | >>> parse_options(('-h', None)) 54 | 'help' 55 | >>> parse_options(('--help', None)) 56 | 'help' 57 | >>> parse_options(('-o', 3)) 58 | 'optimization level set to 3' 59 | >>> parse_options(('-o', 0)) 60 | 'bad optimization level: 0' 61 | >>> parse_options(('-v', 'x')) 62 | "unknown options: ('-v', 'x')" 63 | 64 | """ 65 | from functools import wraps 66 | 67 | from pyfpm.parser import Parser, _get_caller_globals 68 | from pyfpm.pattern import _basestring 69 | 70 | class NoMatch(Exception): 71 | """ 72 | Thrown by matchers when no registered pattern could match the given object. 73 | 74 | """ 75 | 76 | class Matcher(object): 77 | """ 78 | Maps patterns to handler functions. 79 | 80 | :param bindings: an optional list of pattern-handler pairs. 81 | String patterns are automatically parsed. 82 | :type bindings: iterable 83 | :param context: an optional context for the :class:`Parser`. 84 | If absent, it uses the caller's `globals()` 85 | :type context: dict 86 | 87 | """ 88 | def __init__(self, bindings=[], context=None): 89 | self.bindings = [] 90 | if context is None: 91 | context = _get_caller_globals() 92 | self.parser = Parser(context) 93 | for pattern, handler in bindings: 94 | self.register(pattern, handler) 95 | 96 | def register(self, pattern, handler): 97 | """ 98 | Register a new pattern-handler pair. If the pattern is a string, it will 99 | be parsed automatically. 100 | 101 | :param pattern: Pattern or str -- the pattern 102 | :param handler: callable -- the handler function for the pattern 103 | 104 | """ 105 | if isinstance(pattern, _basestring): 106 | pattern = self.parser(pattern) 107 | self.bindings.append((pattern, handler)) 108 | 109 | def match(self, obj, *args): 110 | """ 111 | Match the given object against the registerd patterns until the first 112 | match. The corresponding handler gets called with `args` as 113 | positional arguments and the match context as keyword arguments. 114 | 115 | :param obj: the object to match the patterns with 116 | :param args: the extra positional arguments that the handler function 117 | will get called with 118 | :raises: NoMatch -- if none of the patterns can match de object 119 | 120 | Example: 121 | 122 | >>> m = Matcher([ 123 | ... ('head :: tail', lambda extra, head, tail: (extra, head, tail)), 124 | ... ('x', lambda extra, x: (extra, 'got something! %s' % x)), 125 | ... ]) 126 | >>> m.match('hello', 'yo!') 127 | ('yo!', 'got something! hello') 128 | >>> m.match((1, 2, 3), 'numbers') 129 | ('numbers', 1, (2, 3)) 130 | 131 | """ 132 | for pattern, handler in self.bindings: 133 | match = pattern << obj 134 | if match: 135 | return handler(*args, **match.ctx) 136 | raise NoMatch('no registered pattern could match %s' % repr(obj)) 137 | 138 | def __call__(self, obj, *args): 139 | """ 140 | Same as :func:`match`. Matcher instances can be called directly: 141 | 142 | >>> m = Matcher([('_', lambda: 'yes')]) 143 | >>> m(0) == m.match(0) 144 | True 145 | 146 | """ 147 | return self.match(obj, *args) 148 | 149 | def handler(self, pattern): 150 | """ 151 | Decorator for registering handlers. It's an alternate syntax with the 152 | same effect as :func:`register`: 153 | 154 | >>> m = Matcher() 155 | >>> @m.handler('x:int') 156 | ... def int_(x): 157 | ... return 'an int: %d' % x 158 | >>> @m.handler('_') 159 | ... def any(): 160 | ... return 'any' 161 | >>> m(1) 162 | 'an int: 1' 163 | >>> m(None) 164 | 'any' 165 | 166 | """ 167 | def _reg(function): 168 | self.register(pattern, function) 169 | return function 170 | return _reg 171 | 172 | def __eq__(self, other): 173 | return (self.__class__ == other.__class__ and 174 | self.bindings == other.bindings and 175 | self.parser.context == other.parser.context) 176 | 177 | def __repr__(self): 178 | return '%s(%s)' % (self.__class__.__name__, 179 | ','.join('='.join((str(k), repr(v))) 180 | for (k, v) in self.__dict__.items())) 181 | 182 | def match_args(pattern, context=None): 183 | """ 184 | Decorator for matching a function's arglist. 185 | 186 | :param pattern: Pattern or str -- the pattern 187 | :param context: dict -- an optional context for the pattern parser. If 188 | absent, it defaults to the caller's `globals()`. 189 | 190 | Usage: 191 | 192 | >>> @match_args('head::tail') 193 | ... def do_something(head, tail): 194 | ... return (head, tail) 195 | >>> do_something(1, 2, 3, 4) 196 | (1, (2, 3, 4)) 197 | 198 | """ 199 | if isinstance(pattern, _basestring): 200 | if context is None: 201 | context = _get_caller_globals() 202 | pattern = Parser(context)(pattern) 203 | def wrapper(function): 204 | @wraps(function) 205 | def f(*args): 206 | match = pattern.match(args) 207 | if not match: 208 | raise NoMatch("%s doesn't match %s" % (pattern, args)) 209 | return function(**match.ctx) 210 | return f 211 | return wrapper 212 | 213 | class _UnpackerHelper(object): 214 | def __init__(self, vars, pattern): 215 | self.vars = vars 216 | self.pattern = pattern 217 | 218 | def _do(self, other): 219 | match = self.pattern.match(other) 220 | if not match: 221 | raise NoMatch("%s doesn't match %s" % (self.pattern, other)) 222 | self.vars.update(match.ctx) 223 | 224 | def __lshift__(self, other): 225 | return self._do(other) 226 | 227 | class Unpacker(object): 228 | """ 229 | Inline object unpacker. Usage: 230 | 231 | >>> unpacker = Unpacker() 232 | >>> unpacker('[x, [y, z]]') << (1, (2, 3)) 233 | >>> unpacker.x 234 | 1 235 | >>> unpacker.y 236 | 2 237 | >>> unpacker.z 238 | 3 239 | 240 | """ 241 | def __call__(self, pattern, context=None): 242 | if isinstance(pattern, _basestring): 243 | if context is None: 244 | context = _get_caller_globals() 245 | pattern = Parser(context)(pattern) 246 | return _UnpackerHelper(self.__dict__, pattern) 247 | -------------------------------------------------------------------------------- /pyfpm/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scala-like pattern syntax parser. 3 | """ 4 | import re 5 | import inspect 6 | 7 | from pyparsing import Literal, Word, Group, Combine, Suppress,\ 8 | Forward, Optional, alphas, nums, alphanums, QuotedString,\ 9 | quotedString, dblQuotedString, removeQuotes, delimitedList,\ 10 | ParseException, Keyword, restOfLine, ParseFatalException 11 | 12 | from pyfpm.pattern import build as _ 13 | 14 | def _get_caller_globals(): 15 | frame = inspect.getouterframes(inspect.currentframe())[2][0] 16 | return frame.f_globals 17 | 18 | class _IfCondition(object): 19 | def __init__(self, code, context): 20 | self.code = code 21 | self.context = context 22 | 23 | def __call__(self, **kwargs): 24 | return eval(self.code, self.context, kwargs) 25 | 26 | def __eq__(self, other): 27 | return (isinstance(other, _IfCondition) and 28 | self.__dict__ == other.__dict__) 29 | 30 | def __str__(self): 31 | return '_IfCondition(code=%s, context=%s)' % ( 32 | self.code, 33 | self.context) 34 | 35 | def Parser(context=None): 36 | """ 37 | Create a parser. 38 | 39 | :param context: optional context, defaults to the caller's 40 | `globals()` 41 | :type context: dict 42 | 43 | .. warning:: creating a parser is expensive! 44 | 45 | Usage and syntax examples: 46 | 47 | >>> parser = Parser() 48 | 49 | match anything anonymously: 50 | 51 | >>> parser('_') << 'whatever' 52 | Match({}) 53 | 54 | match anything and bind to a name: 55 | 56 | >>> parser('x') << 1 57 | Match({'x': 1}) 58 | 59 | match instances of a specific type: 60 | 61 | >>> parser('_:str') << 1 62 | >>> parser('_:int') << 1 63 | Match({}) 64 | >>> parser('x:str') << 'abc' 65 | Match({'x': 'abc'}) 66 | 67 | match int, float, str and bool constants: 68 | 69 | >>> parser('1') << 1 70 | Match({}) 71 | >>> parser('1.618') << 1.618 72 | Match({}) 73 | >>> parser('"abc"') << 'abc' 74 | Match({}) 75 | >>> parser('True') << True 76 | Match({}) 77 | 78 | match lists: 79 | 80 | >>> parser('[]') << () 81 | Match({}) 82 | >>> parser('[x:int]') << [1] 83 | Match({'x': 1}) 84 | >>> parser('[a, b, _]') << [1, 2, 3] 85 | Match({'a': 1, 'b': 2}) 86 | 87 | split head vs. tail: 88 | 89 | >>> parser('a::b') << (1, 2, 3) 90 | Match({'a': 1, 'b': (2, 3)}) 91 | >>> parser('a::b::c') << (0, 1, 2, 3, 4) 92 | Match({'a': 0, 'c': (2, 3, 4), 'b': 1}) 93 | 94 | match named tuples (as if they were Scala case classes) 95 | 96 | >>> try: 97 | ... from collections import namedtuple 98 | ... Case3 = namedtuple('Case3', 'a b c') 99 | ... parser = Parser() # Case3 has to be in the context 100 | ... parser('Case3(x, y, z)') << Case3(1, 2, 3) 101 | ... except ImportError: 102 | ... from pyfpm.pattern import Match 103 | ... Match({'y': 2, 'x': 1, 'z': 3}) # no namedtuple in python < 2.6 104 | Match({'y': 2, 'x': 1, 'z': 3}) 105 | 106 | boolean or between expressions: 107 | 108 | >>> parser('a:int|b:str') << 1 109 | Match({'a': 1}) 110 | >>> parser('a:int|b:str') << 'hello' 111 | Match({'b': 'hello'}) 112 | 113 | nest expressions: 114 | 115 | >>> parser('[[[x:int]]]') << [[[1]]] 116 | Match({'x': 1}) 117 | >>> parser('[_:int|[], 2, 3]') << (1, 2, 3) 118 | Match({}) 119 | >>> parser('[_:int|[], 2, 3]') << ([], 2, 3) 120 | Match({}) 121 | >>> parser('[_:int|[], 2, 3]') << ([1], 2, 3) 122 | 123 | """ 124 | if context is None: 125 | context = _get_caller_globals() 126 | 127 | # parsing actions 128 | def get_type(type_name): 129 | try: 130 | t = eval(type_name, context) 131 | except NameError: 132 | raise ParseException('unknown type: %s' % type_name) 133 | if not isinstance(t, type): 134 | raise ParseException('not a type: %s' % type_name) 135 | return t 136 | 137 | def get_named_var(var_name): 138 | try: 139 | get_type(var_name) 140 | except ParseException: 141 | return _()%var_name 142 | raise ParseException('var name clashes with type: %s' % var_name) 143 | 144 | # begin grammar 145 | type_ = Word(alphas, alphanums + '._')('type_').setParseAction( 146 | lambda *args: get_type(args[-1].type_)) 147 | 148 | anon_var = Literal("_")('anon_var').setParseAction(lambda *args: _()) 149 | 150 | named_var = Word(alphas, alphanums + '_')('named_var').setParseAction( 151 | lambda *args: get_named_var(args[-1].named_var)) 152 | 153 | untyped_var = (named_var | anon_var)('untyped_var') 154 | 155 | typed_var = (untyped_var + Suppress(':') + type_)( 156 | 'typed_var').setParseAction( 157 | lambda *args: _(args[-1].type_)%args[-1].untyped_var.bound_name) 158 | 159 | var = (typed_var | untyped_var)('var') 160 | 161 | int_const = Combine(Optional('-') + Word(nums))( 162 | 'int_const').setParseAction(lambda *args: int(args[-1].int_const)) 163 | 164 | float_const = Combine(Optional('-') + Word(nums) + Literal('.') 165 | + Optional(Word(nums)) | 166 | Optional('-') + Literal('.') + Word(nums))( 167 | 'float_const').setParseAction( 168 | lambda *args: float(args[-1].float_const)) 169 | 170 | str_const = (quotedString | dblQuotedString)('str_const').setParseAction( 171 | removeQuotes) 172 | 173 | regex_const = QuotedString(quoteChar='/', escChar='\\')('regex_const' 174 | ).setParseAction(lambda *args: re.compile(args[-1].regex_const)) 175 | 176 | true = Keyword('True').setParseAction(lambda *args: _(True)) 177 | false = Keyword('False').setParseAction(lambda *args: _(False)) 178 | null = Keyword('None').setParseAction(lambda *args: _(None)) 179 | 180 | const = (float_const | int_const | str_const | regex_const | 181 | false | true | null)('const').setParseAction( 182 | lambda *args: _(args[-1].const)) 183 | 184 | scalar = Forward() 185 | 186 | pattern = Forward() 187 | 188 | head_tail = (scalar + Suppress('::') + pattern)( 189 | 'head_tail').setParseAction(lambda *args: args[-1][0] + args[-1][1]) 190 | 191 | list_item = (pattern | scalar)('list_item') 192 | 193 | list_contents = Optional(delimitedList(list_item))('list_contents') 194 | 195 | full_list = (Suppress('[') + list_contents + Suppress(']'))( 196 | 'full_list').setParseAction(lambda *args: _(list(args[-1]))) 197 | 198 | list_ = (head_tail | full_list)('list') 199 | 200 | case_class = Combine(type_ + Suppress('(') + list_contents + 201 | Suppress(')'))('case_class').setParseAction( 202 | lambda *args: _(args[-1][0].type_(*args[-1][0].list_contents))) 203 | 204 | scalar << (const | var | case_class | 205 | Suppress('(') + pattern + Suppress(')'))('scalar') 206 | 207 | or_clause = (list_ | scalar)('or_clause') 208 | 209 | or_expression = (or_clause + Suppress('|') + pattern)( 210 | 'or_expression').setParseAction( 211 | lambda *args: args[-1][0] | args[-1][1]) 212 | 213 | def conditional_pattern_action(*args): 214 | try: 215 | pattern, condition_string = args[-1] 216 | code = compile(condition_string.strip(), 217 | '', 'eval') 218 | pattern.if_(_IfCondition(code, context)) 219 | return pattern 220 | except ValueError: 221 | pass 222 | 223 | pattern << ((or_expression | or_clause) + 224 | Optional(Suppress(Keyword('if')) + restOfLine))( 225 | 'pattern').setParseAction(conditional_pattern_action) 226 | 227 | # end grammar 228 | 229 | def parse(expression): 230 | (p,) = pattern.parseString(expression, parseAll=True) 231 | return p 232 | 233 | parse.context = context 234 | parse.setDebug = pattern.setDebug 235 | return parse 236 | -------------------------------------------------------------------------------- /pyfpm/pattern.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds the actual pattern implementations. 3 | 4 | End users should not normally have to deal with it, except for constructing 5 | patterns programatically without making use of the pattern syntax parser. 6 | 7 | """ 8 | 9 | import re 10 | 11 | try: 12 | # python 2.x base string 13 | _basestring = basestring 14 | except NameError: 15 | # python 3.x base string 16 | _basestring = str 17 | 18 | class Match(object): 19 | """ 20 | Represents the result of matching successfully a pattern against an 21 | object. The `ctx` attribute is a :class:`dict` that contains the value for 22 | each bound name in the pattern, if any. 23 | 24 | """ 25 | def __init__(self, ctx=None, value=None): 26 | if ctx is None: 27 | ctx = {} 28 | self.ctx = ctx 29 | self.value = value 30 | 31 | def __eq__(self, other): 32 | return (isinstance(other, Match) and 33 | self.__dict__ == other.__dict__) 34 | 35 | def __repr__(self): 36 | return 'Match(%s)' % self.ctx 37 | 38 | class Pattern(object): 39 | """ 40 | Base Pattern class. Abstracts the behavior common to all pattern types, 41 | such as name bindings, conditionals and operator overloading for combining 42 | several patterns. 43 | 44 | """ 45 | 46 | def __init__(self): 47 | self.bound_name = None 48 | self.condition = None 49 | 50 | def match(self, other, ctx=None): 51 | """ 52 | Match this pattern against an object. Operator: `<<`. 53 | 54 | :param other: the object this pattern should be matched against. 55 | :param ctx: optional context. If none, an empty one will be 56 | automatically created. 57 | :type ctx: dict 58 | :returns: a :class:`Match` if successful, `None` otherwise. 59 | 60 | """ 61 | match = self._does_match(other, ctx) 62 | if match: 63 | ctx = match.ctx 64 | value = match.value or other 65 | if self.bound_name: 66 | if ctx is None: 67 | ctx = {} 68 | try: 69 | previous = ctx[self.bound_name] 70 | if previous != value: 71 | return None 72 | except KeyError: 73 | ctx[self.bound_name] = value 74 | if self.condition is None or self.condition(**ctx): 75 | return Match(ctx) 76 | return None 77 | def __lshift__(self, other): 78 | return self.match(other) 79 | 80 | def bind(self, name): 81 | """Bind this pattern to the given name. Operator: `%`.""" 82 | self.bound_name = name 83 | return self 84 | def __mod__(self, name): 85 | return self.bind(name) 86 | 87 | def if_(self, condition): 88 | """ 89 | Add a boolean condition to this pattern. Operator: `/`. 90 | 91 | :param condition: must accept the match context as keyword 92 | arguments and return a boolean-ish value. 93 | :type condition: callable 94 | 95 | """ 96 | self.condition = condition 97 | return self 98 | def __div__(self, condition): 99 | return self.if_(condition) 100 | def __truediv__(self, condition): 101 | return self.if_(condition) 102 | 103 | def multiply(self, n): 104 | """ 105 | Build a :class:`ListPattern` that matches `n` instances of this pattern. 106 | Operator: `*`. 107 | 108 | Example: 109 | 110 | >>> p = EqualsPattern(1).multiply(3) 111 | >>> p.match((1, 1, 1)) 112 | Match({}) 113 | 114 | """ 115 | return build(*([self]*n)) 116 | def __mul__(self, length): 117 | return self.multiply(length) 118 | def __rmul__(self, length): 119 | return self.multiply(length) 120 | 121 | def or_with(self, other): 122 | """ 123 | Build a new :class:`OrPattern` with this or the other pattern. 124 | Operator: `|`. 125 | 126 | Example: 127 | 128 | >>> p = EqualsPattern(1).or_with(InstanceOfPattern(str)) 129 | >>> p.match('hello') 130 | Match({}) 131 | >>> p.match(1) 132 | Match({}) 133 | >>> p.match(2) 134 | 135 | """ 136 | patterns = [] 137 | for pattern in (self, other): 138 | if isinstance(pattern, OrPattern): 139 | patterns.extend(pattern.patterns) 140 | else: 141 | patterns.append(pattern) 142 | return OrPattern(*patterns) 143 | def __or__(self, other): 144 | return self.or_with(other) 145 | 146 | def head_tail_with(self, other): 147 | """ 148 | Head-tail concatenate this pattern with the other. The lhs pattern will 149 | be the head and the other will be the tail. Operator: `+`. 150 | 151 | Example: 152 | 153 | >>> p = InstanceOfPattern(int).head_tail_with(ListPattern()) 154 | >>> p.match([1]) 155 | Match({}) 156 | >>> p.match([1, 2]) 157 | 158 | """ 159 | return ListPattern(self, other) 160 | def __add__(self, other): 161 | return self.head_tail_with(other) 162 | 163 | def __eq__(self, other): 164 | return (self.__class__ == other.__class__ and 165 | self.__dict__ == other.__dict__) 166 | 167 | def __repr__(self): 168 | return '%s(%s)' % (self.__class__.__name__, 169 | ', '.join('='.join((str(k), repr(v))) for (k, v) in 170 | self.__dict__.items() if v)) 171 | 172 | class AnyPattern(Pattern): 173 | """Pattern that matches anything.""" 174 | def _does_match(self, other, ctx): 175 | return Match(ctx) 176 | 177 | class EqualsPattern(Pattern): 178 | """Pattern that only matches objects that equal the given object.""" 179 | def __init__(self, obj): 180 | super(EqualsPattern, self).__init__() 181 | self.obj = obj 182 | 183 | def _does_match(self, other, ctx): 184 | if self.obj == other: 185 | return Match(ctx) 186 | else: 187 | return None 188 | 189 | class InstanceOfPattern(Pattern): 190 | """Pattern that only matches instances of the given class.""" 191 | def __init__(self, cls): 192 | super(InstanceOfPattern, self).__init__() 193 | self.cls = cls 194 | 195 | def _does_match(self, other, ctx): 196 | if isinstance(other, self.cls): 197 | return Match(ctx) 198 | else: 199 | return None 200 | 201 | _CompiledRegex = type(re.compile('')) 202 | class RegexPattern(Pattern): 203 | """Pattern that only matches strings that match the given regex.""" 204 | def __init__(self, regex): 205 | super(RegexPattern, self).__init__() 206 | if not isinstance(regex, _CompiledRegex): 207 | regex = re.compile(regex) 208 | self.regex = regex 209 | 210 | def _does_match(self, other, ctx): 211 | re_match = self.regex.match(other) 212 | if re_match: 213 | return Match(ctx, re_match.groups()) 214 | return None 215 | 216 | class ListPattern(Pattern): 217 | """Pattern that only matches iterables whose head matches `head_pattern` and 218 | whose tail matches `tail_pattern`""" 219 | def __init__(self, head_pattern=None, tail_pattern=None): 220 | super(ListPattern, self).__init__() 221 | if head_pattern is not None and tail_pattern is None: 222 | tail_pattern = ListPattern() 223 | self.head_pattern = head_pattern 224 | self.tail_pattern = tail_pattern 225 | 226 | def head_tail_with(self, other): 227 | return ListPattern(self.head_pattern, 228 | self.tail_pattern.head_tail_with(other)) 229 | 230 | def _does_match(self, other, ctx): 231 | try: 232 | if (self.head_pattern is None and 233 | self.tail_pattern is None and 234 | len(other) == 0): 235 | return Match(ctx) 236 | except TypeError: 237 | return None 238 | if isinstance(other, _basestring): 239 | return None 240 | try: 241 | head, tail = other[0], other[1:] 242 | except (IndexError, TypeError): 243 | return None 244 | if self.head_pattern is not None: 245 | match = self.head_pattern.match(head, ctx) 246 | if match: 247 | ctx = match.ctx 248 | match = self.tail_pattern.match(tail, ctx) 249 | if match: 250 | ctx = match.ctx 251 | else: 252 | return None 253 | else: 254 | return None 255 | else: 256 | if len(other): 257 | return None 258 | return Match(ctx) 259 | 260 | class NamedTuplePattern(Pattern): 261 | """Pattern that only matches named tuples of the given class and whose 262 | contents match the given patterns.""" 263 | def __init__(self, casecls, *initpatterns): 264 | super(NamedTuplePattern, self).__init__() 265 | self.casecls_pattern = InstanceOfPattern(casecls) 266 | if (len(initpatterns) == 1 and 267 | isinstance(initpatterns[0], ListPattern)): 268 | self.initargs_pattern = initpatterns[0] 269 | else: 270 | self.initargs_pattern = build(*initpatterns, **dict(is_list=True)) 271 | 272 | def _does_match(self, other, ctx): 273 | match = self.casecls_pattern.match(other, ctx) 274 | if not match: 275 | return None 276 | ctx = match.ctx 277 | return self.initargs_pattern.match(other, ctx) 278 | 279 | class OrPattern(Pattern): 280 | """Pattern that matches whenever any of the inner patterns match.""" 281 | def __init__(self, *patterns): 282 | if len(patterns) < 2: 283 | raise ValueError('need at least two patterns') 284 | super(OrPattern, self).__init__() 285 | self.patterns = patterns 286 | 287 | def _does_match(self, other, ctx): 288 | for pattern in self.patterns: 289 | if ctx is not None: 290 | ctx_ = ctx.copy() 291 | else: 292 | ctx_ = None 293 | match = pattern.match(other, ctx_) 294 | if match: 295 | return match 296 | return None 297 | 298 | def build(*args, **kwargs): 299 | """ 300 | Shorthand pattern factory. 301 | 302 | Examples: 303 | 304 | >>> build() == AnyPattern() 305 | True 306 | >>> build(1) == EqualsPattern(1) 307 | True 308 | >>> build('abc') == EqualsPattern('abc') 309 | True 310 | >>> build(str) == InstanceOfPattern(str) 311 | True 312 | >>> build(re.compile('.*')) == RegexPattern('.*') 313 | True 314 | >>> build(()) == build([]) == ListPattern() 315 | True 316 | >>> build([1]) == build((1,)) == ListPattern(EqualsPattern(1), 317 | ... ListPattern()) 318 | True 319 | >>> build(int, str, 'a') == ListPattern(InstanceOfPattern(int), 320 | ... ListPattern(InstanceOfPattern(str), 321 | ... ListPattern(EqualsPattern('a')))) 322 | True 323 | >>> try: 324 | ... from collections import namedtuple 325 | ... MyTuple = namedtuple('MyTuple', 'a b c') 326 | ... build(MyTuple(1, 2, 3)) == NamedTuplePattern(MyTuple, 1, 2, 3) 327 | ... except ImportError: 328 | ... True 329 | True 330 | 331 | """ 332 | arglen = len(args) 333 | if arglen > 1: 334 | head, tail = args[0], args[1:] 335 | return ListPattern(build(head), build(*tail, **(dict(is_list=True)))) 336 | if arglen == 0: 337 | return AnyPattern() 338 | (arg,) = args 339 | if kwargs.get('is_list', False): 340 | return ListPattern(build(arg)) 341 | if isinstance(arg, Pattern): 342 | return arg 343 | if isinstance(arg, _CompiledRegex): 344 | return RegexPattern(arg) 345 | if isinstance(arg, tuple) and hasattr(arg, '_fields'): 346 | return NamedTuplePattern(arg.__class__, *map(build, arg)) 347 | if isinstance(arg, type): 348 | return InstanceOfPattern(arg) 349 | if isinstance(arg, (tuple, list)): 350 | if len(arg) == 0: 351 | return ListPattern() 352 | return build(*arg, **(dict(is_list=True))) 353 | return EqualsPattern(arg) 354 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyparsing==1.5.6 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | import pyfpm 5 | 6 | setup(name='pyfpm', 7 | version=pyfpm.__version__, 8 | author=pyfpm.__author__, 9 | author_email='martinblech@gmail.com', 10 | url='https://github.com/martinblech/pyfpm', 11 | description='Scala-like functional pattern matching in Python.', 12 | long_description="""`pyfpm` stands for PYthon Functional Pattern 13 | Matching. It's been heavily inspired by the Pattern Matching and Case 14 | Classes implementation in Scala.""", 15 | classifiers=[ 16 | 'Development Status :: 4 - Beta', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Topic :: Software Development :: Libraries', 20 | ], 21 | packages=['pyfpm'], 22 | requires=['pyparsing'], 23 | ) 24 | -------------------------------------------------------------------------------- /tests/test_matcher.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyfpm.matcher import Matcher, NoMatch, match_args, Unpacker 4 | from pyfpm.pattern import build as _ 5 | 6 | class TestMatcher(unittest.TestCase): 7 | def test_constructor(self): 8 | f = lambda: None 9 | m1 = Matcher([(_(), f)]) 10 | m2 = Matcher() 11 | m2.register(_(), f) 12 | self.assertEquals(m1, m2) 13 | 14 | def test_equality(self): 15 | m1 = Matcher() 16 | m2 = Matcher() 17 | self.assertEquals(m1, m2) 18 | f = lambda: None 19 | m2.register(_(), f) 20 | self.assertNotEquals(m1, m2) 21 | m1.register(_(), f) 22 | self.assertEquals(m1, m2) 23 | 24 | def test_decorator(self): 25 | m1 = Matcher() 26 | m2 = Matcher() 27 | f = lambda: None 28 | m1.register(_(), f) 29 | decf = m2.handler(_())(f) 30 | self.assertEquals(decf, f) 31 | self.assertEquals(m1, m2) 32 | 33 | def test_emptymatcher(self): 34 | matcher = Matcher() 35 | try: 36 | matcher.match(None) 37 | self.fail('should fail with NoMatch') 38 | except NoMatch: 39 | pass 40 | 41 | def test_simplematch(self): 42 | m = Matcher() 43 | m.register(_(), lambda: 'test') 44 | self.assertEquals(m(None), 'test') 45 | 46 | def test_varbind(self): 47 | m = Matcher() 48 | m.register(_()%'x', lambda x: 'x=%s' % x) 49 | self.assertEquals(m(None), 'x=None') 50 | self.assertEquals(m(1), 'x=1') 51 | 52 | def test_handler_priority(self): 53 | m = Matcher() 54 | m.register(_(1), lambda: 'my precious, the one') 55 | m.register(_(int), lambda: 'just an int') 56 | m.register(_(), lambda: 'just an object? whatever') 57 | m.register(_(str), lambda: 'i wish i could find a string') 58 | self.assertNotEquals(m('hi'), 'i wish i could find a string') 59 | self.assertEquals(m(None), 'just an object? whatever') 60 | self.assertEquals(m(3), 'just an int') 61 | self.assertEquals(m(1), 'my precious, the one') 62 | 63 | def test_autoparse(self): 64 | m = Matcher([('1', lambda: None)]) 65 | self.assertEquals(m.bindings[0][0], _(1)) 66 | 67 | def test_autoparse_context(self): 68 | m = Matcher([('y:TestMatcher', lambda y: self.assertEquals(self, y))]) 69 | self.assertEquals(m.bindings[0][0], _(TestMatcher)%'y') 70 | m(self) 71 | 72 | class TestMatchArgsDecorator(unittest.TestCase): 73 | def test_decorator(self): 74 | @match_args('[]') 75 | def f(): 76 | return 1 77 | self.assertEquals(f(), 1) 78 | try: 79 | f(1) 80 | self.fail() 81 | except NoMatch: 82 | pass 83 | 84 | def test_head_tail(self): 85 | @match_args('head :: tail') 86 | def f(head, tail): 87 | return (head, tail) 88 | try: 89 | f() 90 | self.fail() 91 | except NoMatch: 92 | pass 93 | self.assertEquals(f(1), (1, ())) 94 | self.assertEquals(f(1, 2), (1, (2,))) 95 | self.assertEquals(f(1, 2, 3), (1, (2, 3))) 96 | 97 | class TestUnpacker(unittest.TestCase): 98 | def test_unpacker(self): 99 | unpacker = Unpacker() 100 | unpacker('head :: tail') << (1, 2, 3) 101 | self.assertEquals(unpacker.head, 1) 102 | self.assertEquals(unpacker.tail, (2, 3)) 103 | try: 104 | unpacker.fdsa 105 | self.fail('no var fdsa') 106 | except AttributeError: 107 | pass 108 | try: 109 | unpacker('x:str') << 1 110 | self.fail('1 is not a str') 111 | except NoMatch: 112 | pass 113 | try: 114 | unpacker.x 115 | self.fail('no var x') 116 | except AttributeError: 117 | pass 118 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | 4 | from pyfpm import parser 5 | from pyfpm.pattern import build as _ 6 | 7 | _has_named_tuple = False 8 | try: 9 | from collections import namedtuple 10 | _has_named_tuple = True 11 | Case3 = namedtuple('Case3', 'a b c') 12 | Case0 = namedtuple('Case0', '') 13 | except ImportError: 14 | pass 15 | 16 | class TestParser(unittest.TestCase): 17 | def setUp(self): 18 | self.parse = parser.Parser() 19 | 20 | def test_automatic_context(self): 21 | self.assertEquals(self.parse.context, globals()) 22 | 23 | def test_manual_context(self): 24 | obj = 'my local string' 25 | p = parser.Parser({'x': obj}) 26 | self.assertEquals(p.context['x'], obj) 27 | 28 | def test_anon_var(self): 29 | pattern = self.parse('_') 30 | self.assertEquals(pattern, _()) 31 | 32 | def test_named_var(self): 33 | pattern = self.parse('x') 34 | self.assertEquals(pattern, _()%'x') 35 | 36 | def test_type_annotation_space_irrelevance(self): 37 | self.assertEquals(self.parse('_:str'), self.parse('_ : str')) 38 | 39 | def test_typed_anon_var(self): 40 | pattern = self.parse('_:str') 41 | self.assertEquals(pattern, _(str)) 42 | 43 | def test_typed_named_var(self): 44 | pattern = self.parse('x:str') 45 | self.assertEquals(pattern, _(str)%'x') 46 | 47 | def test_nested_type_var(self): 48 | pattern = self.parse('x:unittest.TestCase') 49 | self.assertEquals(pattern, _(unittest.TestCase)%'x') 50 | 51 | def test_named_var_clash(self): 52 | for expr in ('str', 'object:str'): 53 | try: 54 | self.parse(expr) 55 | self.fail() 56 | except parser.ParseException: 57 | pass 58 | 59 | def test_fail_with_unknown_type(self): 60 | try: 61 | self.parse('_:fdsa') 62 | self.fail('fdsa is undefined') 63 | except parser.ParseException: 64 | pass 65 | 66 | def test_fail_with_known_nontype(self): 67 | try: 68 | self.parse('_:unittest') 69 | self.fail('unittest is not a type') 70 | except parser.ParseException: 71 | pass 72 | 73 | def test_int_constant(self): 74 | self.assertTrue(self.parse('1'), _(1)) 75 | self.assertTrue(self.parse('-1'), _(-1)) 76 | try: 77 | self.parse('- 1') 78 | self.fail('bad integer') 79 | except parser.ParseException: 80 | pass 81 | 82 | def test_float_constant(self): 83 | self.assertEquals(self.parse('1.'), _(1.)) 84 | self.assertEquals(self.parse('.5'), _(.5)) 85 | self.assertEquals(self.parse('1.5'), _(1.5)) 86 | self.assertEquals(self.parse('-1.'), _(-1.)) 87 | self.assertEquals(self.parse('-.5'), _(-.5)) 88 | self.assertEquals(self.parse('-1.5'), _(-1.5)) 89 | try: 90 | self.parse('1 . 0') 91 | self.fail('bad float') 92 | except parser.ParseException: 93 | pass 94 | 95 | def test_str_constant(self): 96 | self.assertEquals(self.parse('"abc"'), _('abc')) 97 | self.assertEquals(self.parse("'abc'"), _('abc')) 98 | 99 | def test_regex_constant(self): 100 | self.assertEquals(self.parse('/abc/'), _(re.compile('abc'))) 101 | self.assertEquals(self.parse(r'/\//'), _(re.compile('/'))) 102 | self.assertEquals(self.parse(r'/\\/'), _(re.compile(r'\\'))) 103 | 104 | def test_head_tail(self): 105 | self.assertEquals(self.parse('head :: tail'), _()%'head' + _()%'tail') 106 | self.assertEquals(self.parse('head :: []'), _()%'head' + _([])) 107 | self.assertEquals(self.parse('a :: b :: c'), 108 | _()%'a' + _()%'b' + _()%'c') 109 | self.assertEquals(self.parse('a :: b :: c :: d'), 110 | _()%'a' + _()%'b' + _()%'c' + _()%'d') 111 | 112 | def test_explicit_list(self): 113 | self.assertEquals(self.parse('[]'), _([])) 114 | self.assertEquals(self.parse('[x:int]'), _([_(int)%'x'])) 115 | self.assertEquals(self.parse('[_, x:int]'), _(_(), _(int)%'x')) 116 | self.assertEquals(self.parse('[_, []]'), _(_(), _([]))) 117 | self.assertEquals(self.parse('[[]]'), _([_([])])) 118 | self.assertEquals(self.parse('[[], _]'), _([_([]), _()])) 119 | 120 | def test_or(self): 121 | self.assertEquals(self.parse('x | y'), _()%'x' | _()%'y') 122 | 123 | def test_nested_or(self): 124 | self.assertEquals(self.parse('[x | y]'), _([_()%'x' | _()%'y'])) 125 | self.assertEquals(self.parse('[(x | y)]'), _([_()%'x' | _()%'y'])) 126 | 127 | if _has_named_tuple: 128 | def test_case_classes(self): 129 | self.assertEquals(self.parse('Case3(1, 2, 3)'), _(Case3(1, 2, 3))) 130 | self.assertEquals(self.parse('Case0()'), _(Case0())) 131 | 132 | def test_conditional_pattern(self): 133 | p = self.parse('_ if False') 134 | self.assertFalse(p << 1) 135 | 136 | p = self.parse('x:int if x > 1') 137 | self.assertFalse(p << 1) 138 | self.assertTrue(p << 2) 139 | 140 | p = self.parse('x if x==unittest') 141 | self.assertFalse(p << parser) 142 | self.assertTrue(p << unittest) 143 | 144 | p = self.parse('x if y==1') 145 | try: 146 | p << 1 147 | self.fail() 148 | except NameError: 149 | pass 150 | 151 | try: 152 | self.parse('x if TY*(*&^') 153 | self.fail() 154 | except SyntaxError: 155 | pass 156 | 157 | def test_conditional_pattern_equality(self): 158 | self.assertEquals(self.parse('x if x'), self.parse('x if x')) 159 | self.assertNotEquals(self.parse('x if not x'), self.parse('x if x')) 160 | self.assertTrue(str(self.parse('x if x').condition).startswith( 161 | '_IfCondition(code=')) 162 | -------------------------------------------------------------------------------- /tests/test_pattern.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | 4 | from pyfpm import pattern 5 | 6 | _m = pattern.Match 7 | 8 | _any = pattern.AnyPattern 9 | class TestAny(unittest.TestCase): 10 | def test_match_unbound(self): 11 | self.assertEquals(_any()<<1, _m()) 12 | 13 | def test_match_int(self): 14 | self.assertEquals(_any()%'x'<<1, _m({'x': 1})) 15 | 16 | _eq = pattern.EqualsPattern 17 | class TestEquals(unittest.TestCase): 18 | def test_match_unbound(self): 19 | self.assertEquals(_eq(1)<<1, _m()) 20 | 21 | def test_match_int(self): 22 | self.assertEquals(_eq(1)%'x'<<1, _m({'x': 1})) 23 | 24 | def test_not_match_different_int(self): 25 | self.assertFalse(_eq(1)%'x'<<2) 26 | 27 | def test_not_match_different_type(self): 28 | self.assertFalse(_eq(1)%'x'<<'abc') 29 | 30 | _iof = pattern.InstanceOfPattern 31 | class TestInstance(unittest.TestCase): 32 | def test_match_int(self): 33 | self.assertEquals(_iof(int)%'x'<<1, _m({'x': 1})) 34 | 35 | def test_match_different_int(self): 36 | self.assertEquals(_iof(int)%'x'<<2, _m({'x': 2})) 37 | 38 | def test_not_match_different_type(self): 39 | self.assertFalse(_iof(int)%'x'<<'abc') 40 | 41 | _regex = pattern.RegexPattern 42 | class TestRegex(unittest.TestCase): 43 | def test_match_simple(self): 44 | self.assertEquals(_regex('\d+.*')%'x' << '123abc', 45 | _m({'x': '123abc'})) 46 | 47 | def test_match_single_group(self): 48 | self.assertEquals(_regex('(\d+.*)')%'x' << '123abc', 49 | _m({'x': ('123abc',)})) 50 | 51 | def test_match_groups(self): 52 | self.assertEquals(_regex('(\d+)(.*)')%'x' << '123abc', 53 | _m({'x': ('123', 'abc')})) 54 | 55 | def test_match_nested_groups(self): 56 | self.assertEquals(_regex('((\d+)(.*))')%'x' << '123abc', 57 | _m({'x': ('123abc', '123', 'abc')})) 58 | 59 | _l = pattern.ListPattern 60 | class TestList(unittest.TestCase): 61 | def test_match_empty_list(self): 62 | self.assertEquals(_l()%'x'<<[], _m({'x': []})) 63 | 64 | def test_match_single_item(self): 65 | self.assertEquals(_l(_eq(1)%'x')<<[1], _m({'x': 1})) 66 | 67 | def test_match_multiple_items(self): 68 | self.assertEquals(_l(_eq(1)%'x', _l(_iof(str)%'y'))<<(1, 'a'), 69 | _m({'x': 1, 'y': 'a'})) 70 | self.assertEquals(_l(_eq(1)%'x', _l(_iof(str)%'y', _l(_any()%'z')))<<( 71 | 1, 'a', None), 72 | _m({'x': 1, 'y': 'a', 'z': None})) 73 | 74 | def test_no_match_extra_items(self): 75 | self.assertFalse(_l(_eq(1)%'x', _l(_iof(str)%'y'))<<(1, 'a', None)) 76 | 77 | def test_match_head_tail(self): 78 | self.assertEquals(_l(_any()%'head', _any()%'tail')<<(1, 2, 3), 79 | _m({'head': 1, 'tail': (2, 3)})) 80 | 81 | def test_not_match_scalar(self): 82 | scalars = (1, 'abc', .5, 'd', lambda: None) 83 | for x in scalars: 84 | self.assertFalse(_l() << x) 85 | self.assertFalse(_l(_any()) << x) 86 | 87 | # TODO: more tests 88 | 89 | _has_named_tuple = False 90 | try: 91 | from collections import namedtuple 92 | _has_named_tuple = True 93 | Case0 = namedtuple('Case0', '') 94 | Case1 = namedtuple('Case1', 'a') 95 | Case3 = namedtuple('Case3', 'a b c') 96 | Case4 = namedtuple('Case4', 'a b c d') 97 | except ImportError: 98 | pass 99 | 100 | if _has_named_tuple: 101 | _c = pattern.NamedTuplePattern 102 | 103 | class TestNamedTuplePattern(unittest.TestCase): 104 | def test_match_single_arg(self): 105 | self.assertEquals(_c(Case1, _eq(1)%'x')< 1) 198 | self.assertFalse(p << 1) 199 | self.assertTrue(p << 2) 200 | --------------------------------------------------------------------------------