├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── MANIFEST.in ├── docs ├── Makefile ├── _static │ └── media │ │ ├── Diagrams.grl │ │ ├── Magic.grl │ │ ├── Magic.png │ │ ├── Transforms.grl │ │ ├── Transforms.png │ │ └── Workflow.png ├── case_classes.rst ├── conf.py ├── demo.rst ├── discussion.rst ├── enums.rst ├── examples │ ├── __init__.py │ ├── first_macro │ │ ├── __init__.py │ │ ├── full │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ │ ├── nop │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ │ └── quasiquote │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ ├── hygiene │ │ ├── __init__.py │ │ ├── gen_sym │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ │ ├── hygiene_failures │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ │ ├── hygienic_quasiquotes │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ │ └── unhygienic │ │ │ ├── __init__.py │ │ │ ├── macro_module.py │ │ │ ├── run.py │ │ │ └── target.py │ └── using_macros │ │ ├── __init__.py │ │ ├── run.py │ │ └── target.py ├── experimental.rst ├── export_code.rst ├── first_macro.rst ├── hygienic_macro.rst ├── index.rst ├── interned.rst ├── interpolation.rst ├── js.rst ├── lazy.rst ├── overview.rst ├── pattern.rst ├── peg.rst ├── pinq.rst ├── pyxl.rst ├── quick_lambda.rst ├── reference.rst ├── tco.rst ├── tracing.rst └── tutorials.rst ├── macropy ├── __init__.py ├── activate.py ├── case_classes.py ├── console.py ├── core │ ├── __init__.py │ ├── analysis.py │ ├── cleanup.py │ ├── compat.py │ ├── console.py │ ├── exact_src.py │ ├── exporters.py │ ├── failure.py │ ├── gen_sym.py │ ├── hquotes.py │ ├── import_hooks.py │ ├── macros.py │ ├── quotes.py │ ├── test │ │ ├── __init__.py │ │ ├── analysis.py │ │ ├── exact_src.py │ │ ├── exact_src_macro.py │ │ ├── exporters │ │ │ ├── __init__.py │ │ │ ├── pyc_cache.py │ │ │ ├── pyc_cache_macro.py │ │ │ ├── save.py │ │ │ └── save_macro.py │ │ ├── failure.py │ │ ├── failure_macro.py │ │ ├── gen_sym.py │ │ ├── gen_sym_macro.py │ │ ├── hquotes │ │ │ ├── __init__.py │ │ │ ├── hq.py │ │ │ ├── hq2.py │ │ │ ├── hq_macro.py │ │ │ └── hq_macro2.py │ │ ├── macros │ │ │ ├── __init__.py │ │ │ ├── added_decorator.py │ │ │ ├── added_decorator_macro.py │ │ │ ├── aliases.py │ │ │ ├── aliases_macro.py │ │ │ ├── argument.py │ │ │ ├── argument_macros.py │ │ │ ├── basic_block.py │ │ │ ├── basic_block_macro.py │ │ │ ├── basic_decorator.py │ │ │ ├── basic_decorator_macro.py │ │ │ ├── basic_expr.py │ │ │ ├── basic_expr_macro.py │ │ │ ├── line_number_error_source.py │ │ │ ├── line_number_macro.py │ │ │ ├── line_number_source.py │ │ │ ├── not_imported.py │ │ │ ├── not_imported_macro.py │ │ │ ├── quote_macro.py │ │ │ └── quote_source.py │ │ ├── quotes.py │ │ ├── unparse.py │ │ └── walkers.py │ ├── util.py │ └── walkers.py ├── experimental │ ├── __init__.py │ ├── js_snippets.py │ ├── pattern.py │ ├── pinq.py │ ├── pyxl_strings.py │ ├── tco.py │ └── test │ │ ├── __init__.py │ │ ├── js_snippets.py │ │ ├── pattern.py │ │ ├── pinq.py │ │ ├── pyxl_snippets.py │ │ ├── tco.py │ │ └── world.sql ├── logging.py ├── peg.py ├── quick_lambda.py ├── string_interp.py ├── test │ ├── __init__.py │ ├── case_classes.py │ ├── peg.py │ ├── peg_json │ │ ├── fail1.json │ │ ├── fail10.json │ │ ├── fail11.json │ │ ├── fail12.json │ │ ├── fail13.json │ │ ├── fail14.json │ │ ├── fail15.json │ │ ├── fail16.json │ │ ├── fail17.json │ │ ├── fail18.json │ │ ├── fail19.json │ │ ├── fail2.json │ │ ├── fail20.json │ │ ├── fail21.json │ │ ├── fail22.json │ │ ├── fail23.json │ │ ├── fail24.json │ │ ├── fail25.json │ │ ├── fail26.json │ │ ├── fail27.json │ │ ├── fail28.json │ │ ├── fail29.json │ │ ├── fail3.json │ │ ├── fail30.json │ │ ├── fail31.json │ │ ├── fail32.json │ │ ├── fail33.json │ │ ├── fail4.json │ │ ├── fail5.json │ │ ├── fail6.json │ │ ├── fail7.json │ │ ├── fail8.json │ │ ├── fail9.json │ │ ├── pass1.json │ │ ├── pass2.json │ │ └── pass3.json │ ├── quick_lambda.py │ ├── string_interp.py │ └── tracing.py └── tracing.py ├── readme.rst ├── run_tests.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | .idea_modules/ 3 | *.iml 4 | *.pyc 5 | *.swp 6 | MANIFEST 7 | *.egg-info 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | sudo: false 4 | python: 5 | - '3.4' 6 | - '3.5' 7 | - '3.6' 8 | - 'pypy3.5' 9 | # Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs 10 | matrix: 11 | include: 12 | - python: 3.7 13 | dist: xenial 14 | sudo: true 15 | 16 | install: pip install .[pinq,pyxl] 17 | 18 | script: python run_tests.py 19 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.1.0 (unreleased) 5 | ------------------ 6 | 7 | 1.1.0b2 (2018-05-12) 8 | -------------------- 9 | 10 | - Fix readme. 11 | 12 | 1.1.0b1 (2018-05-12) 13 | -------------------- 14 | 15 | - Fix and re-enable the ``SaveExporter()`` class. 16 | 17 | - Fix some issues with pattern matching module requiring non obvious 18 | imports from user's side. 19 | 20 | - Fix pattern matching macro need to import ``_matching`` symbol 21 | 22 | - Add an ``OptionalMatcher`` to pattern matching macro to support 23 | exclusive or conditions. 24 | 25 | - Refactor documentation using sphinx. 26 | 27 | - Refactor the macro expansion core code. Now the macro expansion 28 | order is from inside-out. This allows to use macros inside other 29 | macro's body. 30 | 31 | - Added support for Python 3.4. 32 | 33 | - Added ``SELECT * FROM country`` emulation in `pinq` macro. 34 | 35 | - Update the examples. 36 | 37 | - Update tco and pyxl macros. 38 | 39 | - Prevent the import hooks from raising errors. 40 | 41 | 42 | 1.0.4.dev2 (2017-09-08) 43 | ----------------------- 44 | 45 | - Add MANIFEST.in; 46 | 47 | 1.0.4.dev1 (2017-09-08) 48 | ----------------------- 49 | 50 | - Updated the import machinery and macro detection to be compatible 51 | with Python 3.5+. 52 | 53 | - Removed support for Python 2, supporting it would require a way to 54 | manage differences at the ast level, but i don't use Python2 anymore. 55 | 56 | - Added support for Python 3.5+ in the form of new call arguments form 57 | and new ``AsyncFunctionDef``. 58 | 59 | - Basic scope analysis now available in the form of the ``@Scoped`` 60 | decorator, to be used in conjunction with ``@Walker``. 61 | 62 | 1.0.3 63 | ----- 64 | 65 | - Error messages are now raised at run-time rather than at import 66 | time, with other improvements (double stack traces, catchability). 67 | 68 | - ``@enum`` macro now has much better error messages 69 | 70 | - Improved error messages for mis-use of stub functions outside their 71 | related macro (e.g. the ``u``, ``name``, ``ast`` stubs for the ``q``/``hq`` 72 | macros) 73 | 74 | - Improved error messages for invalid case class signatures 75 | 76 | - Hygienic Quasiquotes now allow lexical capture of module objects 77 | 78 | 1.0.2 79 | ----- 80 | 81 | - Removed unit test from PyPI distribution 82 | 83 | 1.0.1 84 | ----- 85 | - Fixed a bug in ``ast_ctx_fixer`` 86 | - ``gen_sym()`` is now ``gen_sym(name="sym")``, allowing you to override the base name 87 | - Implemented ``macropy.case_classes.enum`` macro 88 | - Implemented ``macropy.quick_lambda.lazy`` and ``macropy.quick_lambda.interned`` macros 89 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude .git .travis.yml 2 | include *.cfg *.py *.rst *.txt 3 | -------------------------------------------------------------------------------- /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 | SPHINXPROJ = MacroPy3 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/media/Diagrams.grl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Diagrams.grl -------------------------------------------------------------------------------- /docs/_static/media/Magic.grl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Magic.grl -------------------------------------------------------------------------------- /docs/_static/media/Magic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Magic.png -------------------------------------------------------------------------------- /docs/_static/media/Transforms.grl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Transforms.grl -------------------------------------------------------------------------------- /docs/_static/media/Transforms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Transforms.png -------------------------------------------------------------------------------- /docs/_static/media/Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/_static/media/Workflow.png -------------------------------------------------------------------------------- /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/stable/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 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'MacroPy3' 23 | copyright = '2013-2018 Li Haoyi, Justin Holmgren, Alberto Berti' 24 | author = 'Alberto Berti' 25 | 26 | # The short X.Y version 27 | version = '1.1.0' 28 | # The full version, including alpha/beta/rc tags 29 | release = '1.1.0' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.extlinks', 45 | ] 46 | 47 | gitter = 'lihaoyi' 48 | 49 | extlinks = {'repo': ('https://github.com/' + gitter + '/macropy/tree/master/%s', 50 | '')} 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path . 74 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'sphinxdoc' 86 | 87 | # import sphinx_py3doc_enhanced_theme 88 | # html_theme = "sphinx_py3doc_enhanced_theme" 89 | # html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | # html_theme_options = {} 96 | 97 | # Add any paths that contain custom static files (such as style sheets) here, 98 | # relative to this directory. They are copied after the builtin static files, 99 | # so a file named "default.css" will overwrite the builtin "default.css". 100 | html_static_path = ['_static'] 101 | 102 | # Custom sidebar templates, must be a dictionary that maps document names 103 | # to template names. 104 | # 105 | # The default sidebars (for documents that don't match any pattern) are 106 | # defined by theme itself. Builtin themes are using these templates by 107 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 108 | # 'searchbox.html']``. 109 | # 110 | # html_sidebars = {} 111 | 112 | html_sidebars = { 113 | '**': ['relations.html', 'globaltoc.html', 'searchbox.html'] 114 | } 115 | 116 | default_role = 'py:obj' 117 | 118 | # -- Options for HTMLHelp output --------------------------------------------- 119 | 120 | # Output file base name for HTML help builder. 121 | htmlhelp_basename = 'MacroPy3doc' 122 | 123 | 124 | # -- Options for LaTeX output ------------------------------------------------ 125 | 126 | latex_elements = { 127 | # The paper size ('letterpaper' or 'a4paper'). 128 | # 129 | # 'papersize': 'letterpaper', 130 | 131 | # The font size ('10pt', '11pt' or '12pt'). 132 | # 133 | # 'pointsize': '10pt', 134 | 135 | # Additional stuff for the LaTeX preamble. 136 | # 137 | # 'preamble': '', 138 | 139 | # Latex figure (float) alignment 140 | # 141 | # 'figure_align': 'htbp', 142 | } 143 | 144 | # Grouping the document tree into LaTeX files. List of tuples 145 | # (source start file, target name, title, 146 | # author, documentclass [howto, manual, or own class]). 147 | latex_documents = [ 148 | (master_doc, 'MacroPy3.tex', 'MacroPy3 Documentation', 149 | 'Alberto Berti', 'manual'), 150 | ] 151 | 152 | 153 | # -- Options for manual page output ------------------------------------------ 154 | 155 | # One entry per manual page. List of tuples 156 | # (source start file, name, description, authors, manual section). 157 | man_pages = [ 158 | (master_doc, 'macropy3', 'MacroPy3 Documentation', 159 | [author], 1) 160 | ] 161 | 162 | 163 | # -- Options for Texinfo output ---------------------------------------------- 164 | 165 | # Grouping the document tree into Texinfo files. List of tuples 166 | # (source start file, target name, title, author, 167 | # dir menu entry, description, category) 168 | texinfo_documents = [ 169 | (master_doc, 'MacroPy3', 'MacroPy3 Documentation', 170 | author, 'MacroPy3', 'One line description of project.', 171 | 'Miscellaneous'), 172 | ] 173 | 174 | 175 | # -- Extension configuration ------------------------------------------------- 176 | -------------------------------------------------------------------------------- /docs/demo.rst: -------------------------------------------------------------------------------- 1 | .. _demo: 2 | 3 | Demo Macros 4 | =========== 5 | 6 | Below are a few example uses of macros that are implemented (together 7 | with test cases!) in the `macropy `:repo: and 8 | `macropy/experimental `:repo: folders. These are 9 | also the ideal places to go look at to learn to write your own macros: 10 | check out the source code of the `String Interpolation 11 | `:repo: or `Quick Lambda 12 | `:repo: macros for some small (<30 lines), 13 | self contained examples. Their `unit 14 | `:repo:` `tests 15 | `:repo: demonstrate how these macros are 16 | used. 17 | 18 | Feel free to open up a REPL and try out the examples in the console; 19 | simply ``import macropy.console``, and most of the examples should 20 | work right off the bat when pasted in! Macros in this section are also 21 | relatively stable and well-tested, and you can rely on them to work 22 | and not to suddenly change from version to version. 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | :caption: Contents: 27 | 28 | case_classes 29 | enums 30 | quick_lambda 31 | lazy 32 | interned 33 | interpolation 34 | tracing 35 | peg 36 | -------------------------------------------------------------------------------- /docs/enums.rst: -------------------------------------------------------------------------------- 1 | .. _enums: 2 | 3 | Enums 4 | ----- 5 | 6 | .. code:: python 7 | 8 | from macropy.case_classes import macros, enum 9 | 10 | @enum 11 | class Direction: 12 | North, South, East, West 13 | 14 | print(Direction(name="North")) # Direction.North 15 | 16 | print(Direction.South.name) # South 17 | 18 | print(Direction(id=2)) # Direction.East 19 | 20 | print(Direction.West.id) # 3 21 | 22 | print(Direction.North.next) # Direction.South 23 | print(Direction.West.prev) # Direction.East 24 | 25 | print(Direction.all) 26 | # [Direction.North, Direction.East, Direction.South, Direction.West] 27 | 28 | 29 | MacroPy also provides an implementation of `Enumerations`__ , heavily 30 | inspired by the `Java implementation`__ and built upon `Case Classes 31 | `:ref:. These are effectively case classes with: 32 | 33 | __ http://en.wikipedia.org/wiki/Enumerated_type 34 | __ http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html 35 | 36 | - A fixed set of instances; 37 | - Auto-generated ``name``, ``id``, ``next`` and ``prev`` fields; 38 | - Auto-generated ``all`` list, which enumerates all instances; 39 | 40 | - A ``__new__`` method that retrieves an existing instance, rather than 41 | creating new ones 42 | 43 | Note that instances of an Enum cannot be created manually: calls such 44 | as ``Direction(name="North")`` or ``Direction(id=2)`` attempt to retrieve 45 | an existing Enum with that property, throwing an exception if there is 46 | none. This means that reference equality is always used to compare 47 | instances of Enums for equality, allowing for much faster equality 48 | checks than if you had used `Case Classes `:ref:. 49 | 50 | Definition of Instances 51 | ~~~~~~~~~~~~~~~~~~~~~~~ 52 | 53 | The instances of an Enum can be declared on a single line, as in the 54 | example above, or they can be declared on subsequent lines: 55 | 56 | .. code:: python 57 | 58 | @enum 59 | class Direction: 60 | North 61 | South 62 | East 63 | West 64 | 65 | 66 | or in a mix of the two styles: 67 | 68 | .. code:: python 69 | 70 | @enum 71 | class Direction: 72 | North, South 73 | East, West 74 | 75 | 76 | The basic rule here is that the body of an Enum can only contain bare 77 | names, function calls (show below), tuples of these, or function defs: 78 | no other statements are allowed. In turn the bare names and function 79 | calls are turned into instances of the Enum, while function defs 80 | (shown later) are turned into their methods. This also means that 81 | unlike `Case Classes `:ref:, Enums cannot have a `body 82 | initializer `:ref:. 83 | 84 | Complex Enums 85 | ~~~~~~~~~~~~~ 86 | 87 | .. code:: python 88 | 89 | @enum 90 | class Direction(alignment, continents): 91 | North("Vertical", ["Northrend"]) 92 | East("Horizontal", ["Azeroth", "Khaz Modan", "Lordaeron"]) 93 | South("Vertical", ["Pandaria"]) 94 | West("Horizontal", ["Kalimdor"]) 95 | 96 | @property 97 | def opposite(self): 98 | return Direction(id=(self.id + 2) % 4) 99 | 100 | def padded_name(self, n): 101 | return ("<" * n) + self.name + (">" * n) 102 | 103 | # members 104 | print(Direction.North.alignment) # Vertical 105 | print(Direction.East.continent) # ["Azeroth", "Khaz Modan", "Lordaeron"] 106 | 107 | # properties 108 | print(Direction.North.opposite) # Direction.South 109 | 110 | # methods 111 | print(Direction.South.padded_name(2)) # <> 112 | 113 | Enums are not limited to the auto-generated members shown above. Apart 114 | from the fact that Enums have no constructor, and no body initializer, 115 | they can contain fields, methods and properties just like :ref:`Case 116 | Classes ` do. This allows you to associate arbitrary 117 | data with each instance of the Enum, and have them perform as 118 | full-fledged objects rather than fancy integers. 119 | -------------------------------------------------------------------------------- /docs/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/examples/__init__.py -------------------------------------------------------------------------------- /docs/examples/first_macro/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/first_macro/full/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/first_macro/full/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | 4 | from macropy.core.macros import Macros 5 | from macropy.core.quotes import macros, q, ast_literal 6 | from macropy.core.walkers import Walker 7 | 8 | _ = None # makes IDE happy 9 | 10 | 11 | macros = Macros() # noqa: F811 12 | 13 | 14 | @macros.expr 15 | def f(tree, **kw): 16 | names = ('arg' + str(i) for i in range(100)) 17 | 18 | @Walker 19 | def underscore_search(tree, collect, **kw): 20 | if isinstance(tree, ast.Name) and tree.id == "_": 21 | name = next(names) 22 | tree.id = name 23 | collect(name) 24 | return tree 25 | 26 | tree, used_names = underscore_search.recurse_collect(tree) 27 | 28 | new_tree = q[lambda: ast_literal[tree]] 29 | new_tree.args.args = [ast.arg(arg=x) for x in used_names] 30 | return new_tree 31 | -------------------------------------------------------------------------------- /docs/examples/first_macro/full/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/first_macro/full/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import reduce 3 | 4 | from macro_module import macros, f, _ # noqa: F401 5 | 6 | my_func = f[_ + (1 * _)] 7 | print(my_func(10, 20)) # 30 8 | 9 | print(reduce(f[_ + _], [1, 2, 3])) # 6 10 | print(list(filter(f[_ % 2 != 0], [1, 2, 3]))) # [1, 3] 11 | print(list(map(f[_ * 10], [1, 2, 3]))) # [10, 20, 30] 12 | -------------------------------------------------------------------------------- /docs/examples/first_macro/nop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyi/macropy/a815f5a58231d8fa65386cd71ff0d15d09fe9fa3/docs/examples/first_macro/nop/__init__.py -------------------------------------------------------------------------------- /docs/examples/first_macro/nop/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.macros import Macros 3 | 4 | macros = Macros() 5 | 6 | 7 | @macros.expr 8 | def expand(tree, **kw): 9 | return tree 10 | -------------------------------------------------------------------------------- /docs/examples/first_macro/nop/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/first_macro/nop/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, expand # noqa: F401 3 | 4 | print(expand[1 + 2]) 5 | -------------------------------------------------------------------------------- /docs/examples/first_macro/quasiquote/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/first_macro/quasiquote/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.macros import Macros 3 | from macropy.core.quotes import macros, q, ast_literal, u 4 | 5 | macros = Macros() # noqa: F811 6 | 7 | 8 | @macros.expr 9 | def expand(tree, **kw): 10 | addition = 10 11 | return q[lambda x: x * ast_literal[tree] + u[addition]] 12 | -------------------------------------------------------------------------------- /docs/examples/first_macro/quasiquote/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/first_macro/quasiquote/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, expand # noqa: F401 3 | 4 | func = expand[1 + 2] 5 | print(func(5)) 6 | -------------------------------------------------------------------------------- /docs/examples/hygiene/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/hygiene/gen_sym/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/hygiene/gen_sym/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | 4 | from macropy.core.macros import Macros 5 | from macropy.core.quotes import macros, q, ast_literal 6 | from macropy.core.walkers import Walker 7 | 8 | _ = None # makes IDE happy 9 | 10 | macros = Macros() # noqa: F811 11 | 12 | 13 | @macros.expr 14 | def f(tree, gen_sym, **kw): 15 | 16 | @Walker 17 | def underscore_search(tree, collect, **kw): 18 | if isinstance(tree, ast.Name) and tree.id == "_": 19 | name = gen_sym() 20 | tree.id = name 21 | collect(name) 22 | return tree 23 | 24 | tree, used_names = underscore_search.recurse_collect(tree) 25 | 26 | new_tree = q[lambda: ast_literal[tree]] 27 | new_tree.args.args = [ast.arg(arg=x) for x in used_names] 28 | return new_tree 29 | -------------------------------------------------------------------------------- /docs/examples/hygiene/gen_sym/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/hygiene/gen_sym/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, f, _ # noqa: F401 3 | 4 | arg0 = 10 5 | 6 | func = f[_ + arg0] 7 | 8 | # prints 11, using `gen_sym`. Otherwise it would print `2` 9 | print(func(1)) 10 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygiene_failures/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygiene_failures/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | 4 | from macropy.core.macros import Macros 5 | from macropy.core.quotes import macros, q, ast_literal 6 | from macropy.core.walkers import Walker 7 | 8 | _ = None # makes IDE happy 9 | 10 | macros = Macros() # noqa: F811 11 | 12 | 13 | @macros.expr 14 | def f(tree, **kw): 15 | names = ('arg' + str(i) for i in range(100)) 16 | 17 | @Walker 18 | def underscore_search(tree, collect, **kw): 19 | if isinstance(tree, ast.Name) and tree.id == "_": 20 | name = next(names) 21 | tree.id = name 22 | collect(name) 23 | return tree 24 | 25 | tree, used_names = underscore_search.recurse_collect(tree) 26 | 27 | new_tree = q[lambda: ast_literal[tree]] 28 | new_tree.args.args = [ast.arg(arg=x) for x in used_names] 29 | return new_tree 30 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygiene_failures/run.py: -------------------------------------------------------------------------------- 1 | import macropy.activate 2 | import target 3 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygiene_failures/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, f, _ # noqa: F401 3 | 4 | arg0 = 10 5 | 6 | func = f[_ + arg0] 7 | 8 | print(func(1)) 9 | # 2 10 | # should print 11 11 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygienic_quasiquotes/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygienic_quasiquotes/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.macros import Macros 3 | from macropy.core.hquotes import macros, hq, u, ast_literal 4 | 5 | macros = Macros() # noqa: F811 6 | 7 | 8 | @macros.expr 9 | def log(tree, exact_src, **kw): 10 | new_tree = hq[wrap(u[exact_src(tree)], ast_literal[tree])] 11 | return new_tree 12 | 13 | 14 | def wrap(txt, x): 15 | print(txt + " -> " + repr(x)) 16 | return x 17 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygienic_quasiquotes/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/hygiene/hygienic_quasiquotes/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, log # noqa: F401 3 | 4 | wrap = 3 # try to confuse it 5 | 6 | log[1 + 2 + 3] 7 | # 1 + 2 + 3 -> 6 8 | # it still works despite trying to confuse it with `wraps` 9 | -------------------------------------------------------------------------------- /docs/examples/hygiene/unhygienic/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/hygiene/unhygienic/macro_module.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.macros import Macros 3 | from macropy.core.hquotes import macros, hq, u, unhygienic, ast_literal 4 | 5 | macros = Macros() # noqa: F811 6 | 7 | 8 | @macros.expr 9 | def log(tree, exact_src, **kw): 10 | new_tree = hq[wrap(unhygienic[log_func], u[exact_src(tree)], 11 | ast_literal[tree])] 12 | return new_tree 13 | 14 | 15 | def wrap(printer, txt, x): 16 | printer(txt + " -> " + repr(x)) 17 | return x 18 | 19 | 20 | @macros.expose_unhygienic 21 | def log_func(txt): 22 | print(txt) 23 | -------------------------------------------------------------------------------- /docs/examples/hygiene/unhygienic/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/hygiene/unhygienic/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macro_module import macros, log # noqa: F401 3 | 4 | buffer = [] 5 | 6 | 7 | def log_func(txt): 8 | buffer.append(txt) 9 | 10 | 11 | log[1 + 2 + 3] 12 | log[1 + 2] 13 | # doesn't print anything 14 | 15 | print(buffer) 16 | # ['1 + 2 + 3 -> 6', '1 + 2 -> 3'] 17 | -------------------------------------------------------------------------------- /docs/examples/using_macros/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Haoyi' 2 | -------------------------------------------------------------------------------- /docs/examples/using_macros/run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.activate 3 | import target 4 | -------------------------------------------------------------------------------- /docs/examples/using_macros/target.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.case_classes import macros, case # noqa: F401 3 | 4 | 5 | @case 6 | class Point(x, y): # noqa: F821 7 | pass 8 | 9 | 10 | p = Point(1, 2) 11 | 12 | print(str(p)) # Point(1, 2) 13 | print(p.x) # 1 14 | print(p.y) # 2 15 | print(Point(1, 2) == Point(1, 2)) # True 16 | x, y = p 17 | print(x, y) # (1, 2) 18 | -------------------------------------------------------------------------------- /docs/experimental.rst: -------------------------------------------------------------------------------- 1 | .. _experimental: 2 | 3 | Experimental Macros 4 | =================== 5 | 6 | Below are a selection of macros which demonstrate the cooler aspects 7 | of MacroPy, but are not currently stable or tested enough that we 8 | would be comfortable using them in production code. 9 | 10 | .. warning:: 11 | 12 | Be aware that for what concerns MacroPy3 :ref:`js` hadn't been 13 | updated due to the external dependency lacking compatibility with 14 | Python 3. 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | :caption: Contents: 19 | 20 | pattern 21 | tco 22 | pinq 23 | pyxl 24 | js 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. MacroPy3 documentation master file, created by 2 | sphinx-quickstart on Wed Feb 28 23:22:39 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to MacroPy3's documentation! 7 | ==================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | overview 14 | tutorials 15 | demo 16 | experimental 17 | discussion 18 | reference 19 | 20 | 21 | MacroPy3 is a port of the original MacroPy to Python 3. If you look for 22 | the Python 2 version see the `python2 branch`__. 23 | 24 | __ https://github.com/lihaoyi/macropy/tree/python2 25 | 26 | **MacroPy** is an implementation of `Syntactic Macros`__ in the 27 | `Python Programming Language `_. MacroPy provides 28 | a mechanism for user-defined functions (macros) to perform 29 | transformations on the `abstract syntax tree`__ (AST) of a 30 | Python program at *import time*. This is an easy way to enhance the 31 | semantics of a Python program in ways which are otherwise impossible, 32 | for example providing an extremely concise way of declaring classes: 33 | 34 | __ //en.wikipedia.org/wiki/Macro_(computer_science)#Syntactic_macros 35 | __ //en.wikipedia.org/wiki/Abstract_syntax_tree 36 | 37 | .. code:: python 38 | 39 | >>> import macropy.console 40 | 0=[]=====> MacroPy Enabled <=====[]=0 41 | >>> from macropy.case_classes import macros, case 42 | 43 | >>> @case 44 | class Point(x, y): pass 45 | 46 | >>> p = Point(1, 2) 47 | >>> print(p.x) 48 | 1 49 | >>> print(p) 50 | Point(1, 2) 51 | 52 | 53 | Try it out in the REPL, it should just work! You can also see the 54 | :repo:`docs/examples/using_macros` folder for a minimal example of using 55 | MacroPy's existing macros. 56 | 57 | MacroPy has been used to implement features such as: 58 | 59 | - `case_classes`:ref:, easy Algebraic Data Types from Scala, and `enums`:ref:; 60 | - `quicklambda`:ref: from Scala and Groovy, and the `lazy`:ref: and 61 | `interned`:ref: utility macros; 62 | - `interpolation`:ref:, a common feature in many programming 63 | languages; 64 | - `tracing`:ref: and `asserts`:ref:, and `show_expanded`:ref:, to help 65 | in the debugging effort; 66 | - `peg`:ref:, Parser Combinators inspired by Scala's, 67 | 68 | 69 | As well as a number of more experimental macros such as: 70 | 71 | - `pattern`:ref: from the Functional Programming world 72 | - `tco`:ref:, preventing unnecessary stack overflows 73 | - `pinq`:ref:, a shameless clone of LINQ to SQL from C# 74 | - `pyxl_snippets`:ref:, XML interpolation within your Python code 75 | - `js`:ref:, cross compiling snippets of Python into equivalent 76 | JavaScript 77 | 78 | Browse the :ref:`high-level overview `, or look at the 79 | `Tutorials`:ref: will go into greater detail and walk you through 80 | 81 | - `first_macro`:ref: 82 | - `hygienic_macro`:ref: 83 | - `exporting`:ref: 84 | 85 | Or just skip ahead to the `discussion`:ref: and 86 | `conclusion`:ref:. We're open to contributions, so send us your 87 | ideas/questions/issues/pull-requests and we'll do our best to 88 | accommodate you! You can ask questions on the `Google Group 89 | `_ and on the 90 | `Gitter channel `_ or file bugs on 91 | thee `issues`__ page. See the `changelist `:repo: to 92 | see what's changed recently. 93 | 94 | __ https://github.com/lihaoyi/macropy/issues 95 | 96 | Requirements 97 | ============ 98 | 99 | MacroPy3 is tested to run on `CPython 3.4 100 | `_ or newer and `PyPy 101 | `_ 3.5. I has no current support for `Jython 102 | `_. MacroPy3 is also available on `PyPI 103 | `_. 104 | 105 | Installation 106 | ============ 107 | 108 | Just execute a: 109 | 110 | .. code:: console 111 | 112 | $ pip install macropy3 113 | 114 | if you want to use macros that require external libraries in order to 115 | work, you can automatically install those dependencies by installing 116 | one of the ``pinq`` or ``pyxl`` extras like this: 117 | 118 | .. code:: console 119 | 120 | $ pip install macropy3[pinq,pyxl] 121 | 122 | 123 | then have a look at the docs at ``_. 124 | 125 | .. _conclusion: 126 | 127 | MacroPy: Bringing Macros to Python 128 | ================================== 129 | 130 | Macros are always a contentious issue. On one hand, we have the 131 | LISP community, which seems to using macros for everything. On the 132 | other hand, most mainstream programmers shy away from them, believing 133 | them to be extremely powerful and potentially confusing, not to 134 | mention extremely difficult to execute. 135 | 136 | With MacroPy, we believe that we have a powerful, flexible tool that 137 | makes it trivially easy to write AST-transforming macros with any 138 | level of complexity. We have a `compelling suite of use cases 139 | `_ demonstrating the utility of such transforms. 140 | 141 | .. and all of it runs perfectly fine on alternative implementations of 142 | .. Python such as PyPy. 143 | 144 | Credits 145 | ======= 146 | 147 | MacroPy was initially created as a final project for the `MIT 148 | `_ class `6.945: Adventures in Advanced Symbolic 149 | Programming `_, 150 | taught by `Gerald Jay Sussman 151 | `_ and `Pavel Panchekha 152 | `_. Inspiration was taken from project such 153 | as `Scala Macros `_, `Karnickel 154 | `_ and `Pyxl 155 | `_. 156 | 157 | License 158 | ======= 159 | 160 | MIT 161 | 162 | Indices and tables 163 | ================== 164 | 165 | * :ref:`genindex` 166 | * :ref:`modindex` 167 | * :ref:`search` 168 | -------------------------------------------------------------------------------- /docs/interned.rst: -------------------------------------------------------------------------------- 1 | .. _interned: 2 | 3 | Interned 4 | -------- 5 | 6 | .. code:: python 7 | 8 | from macropy.quick_lambda import macros, interned 9 | 10 | # count how many times expensive_func runs 11 | count = [0] 12 | def expensive_func(): 13 | count[0] += 1 14 | 15 | def func(): 16 | return interned[expensive_func()] 17 | 18 | print(count[0] # 0) 19 | func() 20 | print(count[0] # 1) 21 | func() 22 | print(count[0] # 1) 23 | 24 | The ``interned`` macro is similar to the :ref:`Lazy ` macro in 25 | that the code within the ``interned[...]`` block is wrapped in a thunk 26 | and evaluated at most once. Unlike the ``lazy`` macro, however, 27 | ``interned`` does not created a memoizing thunk that you can pass 28 | around your program; instead, the memoization is done on a 29 | *per-use-site* basis. 30 | 31 | As you can see in the example above, although ``func`` is called 32 | repeatedly, the ``expensive_func()`` call within the ``interned`` 33 | block is only ever evaluated once. This is handy in that it gives you 34 | a mechanism for memoizing a particular computation without worrying 35 | about finding a place to store the memoized values. It's just memoized 36 | globally (often what you want) while being scoped locally, which 37 | avoids polluting the global namespace with names only relevant to a 38 | single function (also often what you want). 39 | -------------------------------------------------------------------------------- /docs/interpolation.rst: -------------------------------------------------------------------------------- 1 | .. _interpolation: 2 | 3 | String Interpolation 4 | -------------------- 5 | 6 | .. note:: 7 | 8 | While this is somewhat of just historical interest as of Python 3.6, 9 | it was in the original set of macros developed for Python 2.x and 10 | it's a good example of MacroPy capabilities. 11 | 12 | .. code:: python 13 | 14 | from macropy.string_interp import macros, s 15 | 16 | a, b = 1, 2 17 | print(s["{a} apple and {b} bananas"]) 18 | # 1 apple and 2 bananas 19 | 20 | 21 | Unlike the normal string interpolation in Python, MacroPy's string 22 | interpolation allows the programmer to specify the variables to be 23 | interpolated _inline_ inside the string. The macro ``s`` then takes 24 | the string literal: 25 | 26 | .. code:: python 27 | 28 | "{a} apple and {b} bananas" 29 | 30 | 31 | and expands it into the expression: 32 | 33 | .. code:: python 34 | 35 | "%s apple and %s bananas" % (a, b) 36 | 37 | 38 | Which is evaluated at run-time in the local scope, using whatever the 39 | values ``a`` and `b` happen to hold at the time. The contents of the 40 | ``{...}`` can be any arbitrary python expression, and is not limited to 41 | variable names: 42 | 43 | .. code:: python 44 | 45 | from macropy.string_interp import macros, s 46 | A = 10 47 | B = 5 48 | print(s["{A} + {B} = {A + B}"]) 49 | # 10 + 5 = 15 50 | -------------------------------------------------------------------------------- /docs/js.rst: -------------------------------------------------------------------------------- 1 | .. warning:: 2 | 3 | Be aware that for what concerns MacroPy3 this macro hadn't been 4 | updated due to the external dependency lacking compatibility with 5 | Python 3. 6 | 7 | .. _js: 8 | 9 | JS Snippets 10 | ------------ 11 | 12 | .. code:: python 13 | 14 | from macropy.experimental.javascript import macros, pyjs 15 | 16 | code, javascript = pyjs[lambda x: x > 5 and x % 2 == 0] 17 | 18 | print(code) 19 | # at 0x0000000003515C18> 20 | 21 | print(javascript) 22 | # $def(function $_lambda(x) {return $b.bool($b.do_ops(x, '>', 5)) && $b.bool($b.do_ops($b.mod(x, 2), '==', 0));}) 23 | 24 | for i in range(10): 25 | print(i, code(i), self.exec_js_func(javascript, i)) 26 | 27 | # 0 False False 28 | # 1 False False 29 | # 2 False False 30 | # 3 False False 31 | # 4 False False 32 | # 5 False False 33 | # 6 True True 34 | # 7 False False 35 | # 8 True True 36 | # 9 False False 37 | 38 | 39 | JS Snippets is a macro that allows you to mark out sections of code 40 | that will be cross-compiled into Javascript at module-import 41 | time. This cross-compilation is done using `PJs 42 | `_. The generated Javascript is 43 | incredibly ugly, thanks in part to the fact that in order to preserve 44 | semantics in the presence of features that Python has but JS lacks 45 | (such as `operator overloading 46 | `_), basically 47 | every operation in the Javascript program has to be virtualized into a 48 | method call. The translation also breaks down around the fringes of 49 | the Python language. 50 | 51 | Nonetheless, as the abov`_ 69 | of `ad-hoc `_ `mini-DSLs 70 | `_, 71 | this lets you write your validation logic in plain Python. 72 | 73 | As mentioned earlier, JS Snippets isn't very robust, and the 74 | translation is full of bugs: 75 | 76 | .. code:: python 77 | 78 | # these work 79 | assert self.exec_js(js[10]) == 10 80 | assert self.exec_js(js["i am a cow"]) == "i am a cow" 81 | 82 | # these literals are buggy, and it seems to be PJs' fault 83 | # ??? all the results seem to turn into strings ??? 84 | assert self.exec_js(js(3.14)) == 3.14 # Fails 85 | assert self.exec_js(js[[1, 2, 'lol']]) == [1, 2, 'lol'] # Fails 86 | assert self.exec_js(js[{"moo": 2, "cow": 1}]) == {"moo": 2, "cow": 1} # Fails 87 | 88 | # set literals aren't supported so this throws an exception at 89 | # macro-expansion time 90 | # self.exec_js(js[{1, 2, 'lol'}]) 91 | 92 | 93 | Even as such basic things fail, other, more complex operations work 94 | flawlessly: 95 | 96 | .. code:: python 97 | 98 | script = js[sum([x for x in range(10) if x > 5])] 99 | print(script) 100 | # "$b.sum($b.listcomp([$b.range(10)], function (x) {return x;}, [function (x) { return $b.do_ops(x, '>', 5); }]))" 101 | print(self.exec_js(script)) 102 | # 30 103 | 104 | 105 | Here's another, less trivial use case: cross compiling a function that 106 | searches for the `prime numbers 107 | `_: 108 | 109 | .. code:: python 110 | 111 | code, javascript = pyjs[lambda n: [ 112 | x for x in range(n) 113 | if 0 == len([ 114 | y for y in range(2, x-2) 115 | if x % y == 0 116 | ]) 117 | ]] 118 | print(code(20)) 119 | # [0, 1, 2, 3, 4, 5, 7, 11, 13, 17, 19] 120 | print(self.exec_js_func(javascript, 20))) 121 | # [0, 1, 2, 3, 4, 5, 7, 11, 13, 17, 19] 122 | 123 | 124 | These examples are all taken from the `unit tests`__. 125 | 126 | __ macropy/experimental/test/js_snippets.py 127 | 128 | Like `pinq`:ref:, JS Snippets 129 | demonstrates the feasibility, the convenience of being able to mark 130 | out sections of code using macros, to be cross-compiled into another 131 | language and run remotely. Unlike PINQ, which is built on top of the 132 | stable, battle-tested and widely used `SQLAlchemy 133 | `_ library, JS Snippets is built on top of 134 | an relatively unknown and untested Python to Javascript 135 | cross-compiler, making it far from production ready. 136 | 137 | Nonetheless, JS Snippets demonstrate the promise of being able to 138 | cross-compile bits of your program and being able to run parts of it 139 | remotely. The code which performs the integration of PJs and MacroPy 140 | is a scant :repo:`25 lines long `. If 141 | a better, more robust Python to Javascript cross-compiler appears some 142 | day, we could easily make use of it to provide a stable, seamless 143 | developer experience of sharing code between (web) client and server. 144 | -------------------------------------------------------------------------------- /docs/lazy.rst: -------------------------------------------------------------------------------- 1 | .. _lazy: 2 | 3 | Lazy 4 | ---- 5 | 6 | .. code:: python 7 | 8 | from macropy.quick_lambda import macros, lazy 9 | 10 | # count how many times expensive_func runs 11 | count = [0] 12 | def expensive_func(): 13 | count[0] += 1 14 | 15 | thunk = lazy[expensive_func()] 16 | 17 | print(count[0] # 0) 18 | 19 | thunk() 20 | print(count[0]) # 1 21 | thunk() 22 | print(count[0]) # 1 23 | 24 | The ``lazy`` macro is used to create a memoizing thunk. Wrapping an 25 | expression with ``lazy`` creates a thunk which needs to be applied 26 | (e.g. ``thunk()``) in order to get the value of the expression 27 | out. This macro then memoizes the result of that expression, such that 28 | subsequent calls to ``thunk()`` will not cause re-computation. 29 | 30 | This macro is a tradeoff between declaring the value as a variable: 31 | 32 | .. code:: python 33 | 34 | var = expensive_func() 35 | 36 | 37 | Which evaluates exactly once, even when not used, and declaring it as 38 | a function: 39 | 40 | .. code:: python 41 | 42 | thunk = lambda: expensive_func() 43 | 44 | 45 | Which no longer evaluates when not used, but now re-evaluates every 46 | single time. With ``lazy``, you get an expression that evaluates 0 or 1 47 | times. This way, you don't have to pay the cost of computation if it 48 | is not used at all (the problems with variables) or the cost of 49 | needlessly evaluating it more than once (the problem with lambdas). 50 | 51 | This is handy to have if you know how to compute an expression in a 52 | local scope that may be used repeatedly later. It may depend on many 53 | local variables, for example, which would be inconvenient to pass 54 | along to the point at which you know whether the computation is 55 | necessary. This way, you can simply "compute" the lazy value and pass 56 | it along, just as you would compute the value normally, but with the 57 | benefit of only-if-necessary evaluation. 58 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | .. :Project: macropy3 -- overview 3 | .. :Created: gio 01 mar 2018 00:36:26 CET 4 | .. :Author: Alberto Berti 5 | .. :License: GNU General Public License version 3 or later 6 | .. :Copyright: © 2018 Alberto Berti 7 | .. 8 | 9 | .. _overview: 10 | 11 | 30,000ft Overview 12 | ================= 13 | 14 | Macro functions are defined in three ways: 15 | 16 | .. code:: python 17 | 18 | from macropy.core.macros import Macros 19 | 20 | macros = Macros() 21 | 22 | @macros.expr 23 | def my_expr_macro(tree, **kw): 24 | ... 25 | return new_tree 26 | 27 | @macros.block 28 | def my_block_macro(tree, **kw): 29 | ... 30 | return new_tree 31 | 32 | @macros.decorator 33 | def my_decorator_macro(tree, **kw): 34 | ... 35 | return new_tree 36 | 37 | 38 | The line ``macros = Macros()`` is required to mark the file as 39 | providing macros, and the ``macros`` object then provides the methods 40 | ``expr``, ``block`` and ``decorator`` which can be used to decorate 41 | functions to mark them out as the three different kinds of macros. 42 | 43 | Each macro function is passed a ``tree``. The ``tree`` is an ``AST`` 44 | object, the sort provided by Python's `ast module`__. The macro is 45 | able to do whatever transformations it wants, and it returns a 46 | modified (or even an entirely new) ``AST`` object which MacroPy will 47 | use to replace the original macro invocation. The macro also takes 48 | ``**kw``, which contains :ref:`other useful things` which you may 49 | need. 50 | 51 | __ http://docs.python.org/3/library/ast.html 52 | 53 | These three types of macros are called via: 54 | 55 | .. code:: python 56 | 57 | from my_macro_module import macros, my_expr_macro, my_block_macro, my_decorator_macro 58 | 59 | val = my_expr_macro[...] 60 | 61 | with my_block_macro: 62 | ... 63 | 64 | @my_decorator_macro 65 | class X(): 66 | ... 67 | 68 | 69 | Where the line ``from my_macro_module import macros, ...`` is 70 | necessary to tell MacroPy which macros these module relies 71 | on. Multiple things can be imported from each module, but ``macros`` 72 | must come first for macros from that module to be used. 73 | 74 | Any time any of these syntactic forms is seen, if a matching macro 75 | exists in any of the packages from which ``macros`` has been imported 76 | from, the abstract syntax tree captured by these forms (the ``...`` in 77 | the code above) is given to the respective macro to handle. The tree 78 | (new, modified, or even unchanged) which the macro returns is 79 | substituted into the original code in-place. 80 | 81 | MacroPy intercepts the module-loading workflow, via the functionality 82 | provided by `PEP 302: New Import Hooks`__. The workflow is roughly: 83 | 84 | __ http://www.python.org/dev/peps/pep-0302/ 85 | 86 | - Intercept an import 87 | - Parse the contents of the file into an AST 88 | - Walk the AST and expand any macros that it finds 89 | - Compile the modified AST and resume loading it as a module 90 | 91 | .. image:: _static/media/Workflow.png 92 | 93 | Note that this means **you cannot use macros in a file that is run 94 | directly**, as it will not be passed through the import hooks. Hence 95 | the minimum viable setup is: 96 | 97 | .. code:: python 98 | 99 | # run.py 100 | import macropy.activate # sets up macro import hooks 101 | import other # imports other.py and passes it through import hooks 102 | 103 | 104 | # my_macro_module.py 105 | from macropy.core.macros import Macros 106 | 107 | macros = Macros() 108 | 109 | ... define some macros ... 110 | 111 | 112 | # other.py 113 | from macropy.macros.my_macro_module import macros, ... 114 | 115 | ... do stuff with macros ... 116 | 117 | 118 | Where you run ``run.py`` instead of ``other.py``. For the same 119 | reason, you cannot directly run MacroPy's own unit tests directly 120 | using ``unittest`` or ``nose``: you need to run the 121 | :repo:`run_tests.py` file from the project root for the tests to 122 | run. See the `runnable, self-contained no-op example 123 | `:repo: to see exactly what this looks 124 | like, or the example for `using existing macros 125 | `:repo:. 126 | 127 | MacroPy also works in the REPL: 128 | 129 | .. code:: python 130 | 131 | ~/wip/macropy$ python 132 | Python 3.6.4 (default, Jan 5 2018, 02:13:53) 133 | [GCC 7.2.0] on linux 134 | Type "help", "copyright", "credits" or "license" for more information. 135 | >>> import macropy.console 136 | 0=[]=====> MacroPy Enabled <=====[]=0 137 | >>> from macropy.tracing import macros, trace 138 | >>> trace[[x*2 for x in range(3)]] 139 | range(3) -> range(0, 3) 140 | x*2 -> 0 141 | x*2 -> 2 142 | x*2 -> 4 143 | [x*2 for x in range(3)] -> [0, 2, 4] 144 | [0, 2, 4] 145 | 146 | 147 | This example demonstrates the usage of the `tracing`:ref: macro, which 148 | helps trace the evaluation of a Python expression. Although support 149 | for the REPL is still experimental, most examples on this page will 150 | work when copied and pasted into the REPL verbatim. 151 | 152 | .. warning:: 153 | 154 | As of MacroPy3 the following statement is untested 155 | 156 | MacroPy also works in the PyPy and `IPython `_ 157 | REPLs. 158 | -------------------------------------------------------------------------------- /docs/pattern.rst: -------------------------------------------------------------------------------- 1 | .. _pattern: 2 | 3 | Pattern Matching 4 | ---------------- 5 | 6 | .. code:: python 7 | 8 | from macropy.case_classes import macros, case 9 | from macropy.experimental.pattern import macros, switch 10 | 11 | @case 12 | class Nil(): 13 | pass 14 | 15 | @case 16 | class Cons(x, xs): 17 | pass 18 | 19 | def reduce(op, my_list): 20 | with switch(my_list): 21 | if Cons(x, Nil()): 22 | return x 23 | elif Cons(x, xs): 24 | return op(x, reduce(op, xs)) 25 | 26 | print(reduce(lambda a, b: a + b, Cons(1, Cons(2, Cons(4, Nil()))))) 27 | # 7 28 | print(reduce(lambda a, b: a * b, Cons(1, Cons(3, Cons(5, Nil()))))) 29 | # 15 30 | print(reduce(Nil(), lambda a, b: a * b)) 31 | # None 32 | 33 | 34 | Pattern matching allows you to quickly check a variable against a 35 | series of possibilities, sort of like a `switch statement 36 | `_ on steroids. Unlike 37 | a switch statement in other languages (Java, C++), the ``switch`` macro 38 | allows you to match against the *inside* of a pattern: in this case, 39 | not just that ``my_list`` is a ``Cons`` object, but also that the ``xs`` 40 | member of ``my_list`` is a ``Nil`` object. This can be nested arbitrarily 41 | deep, and allows you to easily check if a data-structure has a 42 | particular "shape" that you are expecting. Out of convenience, the 43 | value of the leaf nodes in the pattern are bound to local variables, 44 | so you can immediately use ``x`` and ``xs`` inside the body of the 45 | if-statement without having to extract it (again) from ``my_list``. 46 | 47 | The ``reduce`` function above (an simple, cons-list specific 48 | implementation of `reduce 49 | `_) takes a 50 | Cons list (defined using `case_classes`:ref:) and quickly 51 | checks if it either a ``Cons`` with a ``Nil`` right hand side, or a ``Cons`` 52 | with something else. This is converted (roughly) into: 53 | 54 | .. code:: python 55 | 56 | def reduce(my_list, op): 57 | if isinstance(my_list, Cons) and isinstance(my_list.xs, Nil): 58 | x = my_list.x 59 | return x 60 | elif isinstance(my_list, Cons): 61 | x = my_list.x 62 | xs = my_list.xs 63 | return op(x, reduce(xs, op)) 64 | 65 | 66 | Which is significantly messier to write, with all the ``isinstance`` 67 | checks cluttering up the code and having to manually extract the 68 | values you need from ``my_list`` after the ``isinstance`` checks have 69 | passed. 70 | 71 | Another common use case for pattern matching is working with tree 72 | structures, like ASTs. This macro is a stylized version of the MacroPy 73 | code to identify ``with ...:`` macros: 74 | 75 | .. code:: python 76 | 77 | def expand_macros(node): 78 | with switch(node): 79 | if With(Name(name)): 80 | return handle(name) 81 | else: 82 | return node 83 | 84 | 85 | Compare it to the same code written manually using if-elses: 86 | 87 | .. code:: python 88 | 89 | def expand_macros(node): 90 | if isinstance(node, With) \ 91 | and isinstance(node.context_expr, Name) \ 92 | and node.context_expr.id in macros.block_registry: 93 | name = node.context_expr.id 94 | 95 | return handle(name) 96 | else: 97 | return node 98 | 99 | 100 | As you can see, matching against ``With(Name(name))`` is a quick and 101 | easy way of checking that the value in ``node`` matches a particular 102 | shape, and is much less cumbersome than a series of conditionals. 103 | 104 | It is also possible to use pattern matching outside of a ``switch``, by 105 | using the ``patterns`` macro. Within ``patterns``, any left shift (``<<``) 106 | statement attempts to match the value on the right to the pattern on 107 | the left, allowing nested matches and binding variables as described 108 | earlier. 109 | 110 | .. code:: python 111 | 112 | from macropy.experimental.pattern import macros, patterns 113 | from macropy.case_classes import macros, case 114 | 115 | @case 116 | class Rect(p1, p2): pass 117 | 118 | @case 119 | class Line(p1, p2): pass 120 | 121 | @case 122 | class Point(x, y): pass 123 | 124 | def area(rect): 125 | with patterns: 126 | Rect(Point(x1, y1), Point(x2, y2)) << rect 127 | return (x2 - x1) * (y2 - y1) 128 | 129 | print(area(Rect(Point(1, 1), Point(3, 3)))) # 4 130 | 131 | 132 | If the match fails, a ``PatternMatchException`` will be thrown. 133 | 134 | .. code:: python 135 | 136 | print(area(Line(Point(1, 1), Point(3, 3)))) 137 | # macropy.macros.pattern.PatternMatchException: Matchee should be of type 138 | 139 | 140 | Class Matching Details 141 | ~~~~~~~~~~~~~~~~~~~~~~ 142 | 143 | When you pattern match ``Foo(x, y)`` against a value ``Foo(3, 4)``, what 144 | happens behind the scenes is that the constructor of ``Foo`` is 145 | inspected. We may find that it takes two parameters ``a`` and ``b``. We 146 | assume that the constructor then contains lines like: ```python self.a 147 | = a self.b = b ``` We don't have access to the source of Foo, so this 148 | is the best we can do. Then ``Foo(x, y) << Foo(3, 4)`` is transformed 149 | roughly into 150 | 151 | .. code:: python 152 | 153 | tmp = Foo(3,4) 154 | tmp_matcher = ClassMatcher(Foo, [NameMatcher('x'), NameMatcher('y')]) 155 | tmp_matcher.match(tmp) 156 | x = tmp_matcher.getVar('x') 157 | y = tmp_matcher.getVar('y') 158 | 159 | 160 | In some cases, constructors will not be so standard. In this case, we 161 | can use keyword arguments to pattern match against named fields. For 162 | example, an equivalent to the above which doesn't rely on the specific 163 | implementation of th constructor is ``Foo(a=x, b=y) << Foo(3, 4)``. 164 | Here the semantics are that the field ``a`` is extracted from ``Foo(3,4)`` 165 | to be matched against the simple pattern ``x``. We could also replace 166 | ``x`` with a more complex pattern, as in ``Foo(a=Bar(z), b=y) << 167 | Foo(Bar(2), 4)``. 168 | 169 | 170 | Custom Patterns 171 | ~~~~~~~~~~~~~~~ 172 | 173 | It is also possible to completely override the way in which a pattern 174 | is matched by defining an ``__unapply__`` class method of the class 175 | which you are pattern matching. The 'class' need not actually be the 176 | type of the matched object, as in the following example borrowed from 177 | Scala. The ``__unapply__`` method takes as arguments the value being 178 | matched, as well as a list of keywords. 179 | 180 | The method should then return a tuple of a list of positional matches, 181 | and a dictionary of the keyword matches. 182 | 183 | .. code:: python 184 | 185 | class Twice(object): 186 | @classmethod 187 | def __unapply__(clazz, x, kw_keys): 188 | if not isinstance(x, int) or x % 2 != 0: 189 | raise PatternMatchException() 190 | else: 191 | return ([x/2], {}) 192 | 193 | with patterns: 194 | Twice(n) << 8 195 | print(n) # 4 196 | -------------------------------------------------------------------------------- /docs/pyxl.rst: -------------------------------------------------------------------------------- 1 | .. _pyxl_snippets: 2 | 3 | Pyxl Snippets 4 | ------------- 5 | 6 | .. code:: python 7 | 8 | from macropy.experimental.pyxl_strings import macros, p 9 | 10 | image_name = "bolton.png" 11 | image = p[''] 12 | 13 | text = "Michael Bolton" 14 | block = p['
{image}{text}
'] 15 | 16 | element_list = [image, text] 17 | block2 = p['
{element_list}
'] 18 | 19 | assert block2.to_string() == '
Michael Bolton
' 20 | 21 | 22 | `Pyxl `_ is a way of integrating XML 23 | markup into your Python code. By default, pyxl hooks into the python 24 | UTF-8 decoder in order to transform the source files at load-time. In 25 | this, it is similar to how MacroPy transforms source files at import 26 | time. 27 | 28 | A major difference is that Pyxl by default leaves the HTML fragments 29 | directly in the source code: 30 | 31 | .. code:: python 32 | 33 | image_name = "bolton.png" 34 | image = 35 | 36 | text = "Michael Bolton" 37 | block =
{image}{text}
38 | 39 | element_list = [image, text] 40 | block2 =
{element_list}
41 | 42 | 43 | While the MacroPy version requires each snippet to be wrapped in a 44 | ``p["..."]`` wrapper. This :repo:`three-line-of-code macro 45 | ` simply uses pyxl as a macro 46 | (operating on string literals), rather than hooking into the UTF-8 47 | decoder. In general, this demonstrates how easy it is to integrate an 48 | "external" DSL into your python program: MacroPy handles all the 49 | intricacies of hooking into the interpreter and intercepting the 50 | import workflow. The programmer simply needs to provide the 51 | source-to-source transformation, which in this case was already 52 | provided. 53 | -------------------------------------------------------------------------------- /docs/quick_lambda.rst: -------------------------------------------------------------------------------- 1 | .. _quicklambda: 2 | 3 | Quick Lambdas 4 | ------------- 5 | 6 | .. code:: python 7 | 8 | from macropy.quick_lambda import macros, f, _ 9 | 10 | print(map(f[_ + 1], [1, 2, 3])) # [2, 3, 4] 11 | print(reduce(f[_ * _], [1, 2, 3])) # 6 12 | 13 | 14 | Macropy provides a syntax for lambda expressions similar to Scala's 15 | `anonymous functions`__. Essentially, the transformation is: 16 | 17 | __ http://www.codecommit.com/blog/scala/quick-explanation-of-scalas-syntax 18 | 19 | .. code:: python 20 | 21 | f[_ * _] -> lambda a, b: a * b 22 | 23 | 24 | where the underscores get replaced by identifiers, which are then set 25 | to be the parameters of the enclosing ``lambda``. This works too: 26 | 27 | .. code:: python 28 | 29 | print(map(f[_.split(' ')[0]], ["i am cow", "hear me moo"])) 30 | # ['i', 'hear'] 31 | 32 | 33 | Quick Lambdas can be also used as a concise, lightweight, 34 | more-readable substitute for ``functools.partial`` 35 | 36 | .. code:: python 37 | 38 | from macropy.quick_lambda import macros, f 39 | basetwo = f[int(_, base=2)] 40 | print(basetwo('10010')) # 18 41 | 42 | 43 | is equivalent to 44 | 45 | .. code:: python 46 | 47 | import functools 48 | basetwo = functools.partial(int, base=2) 49 | print(basetwo('10010')) # 18 50 | 51 | 52 | Quick Lambdas can also be used entirely without the `_` placeholders, 53 | in which case they wrap the target in a no argument ``lambda: ...`` 54 | thunk: 55 | 56 | .. code:: python 57 | 58 | from random import random 59 | thunk = f[random() * 2 + 3] 60 | print(thunk()) # 4.522011062548173 61 | print(thunk()) # 4.894243231792029 62 | 63 | 64 | This cuts out reduces the number of characters needed to make a thunk 65 | from 7 (using ``lambda``) to 2, making it much easier to use thunks to 66 | do things like emulating `by name parameters`__. The implementation of 67 | quicklambda is about :repo:`30 lines of code 68 | `, and is worth a look if you want to see how 69 | a simple (but extremely useful!) macro can be written. 70 | 71 | __ http://locrianmode.blogspot.com/2011/07/scala-by-name-parameter.html 72 | -------------------------------------------------------------------------------- /docs/tco.rst: -------------------------------------------------------------------------------- 1 | .. _tco: 2 | 3 | Tail-call Optimization 4 | ---------------------- 5 | 6 | .. code:: python 7 | 8 | from macropy.experimental.tco import macros, tco 9 | 10 | @tco 11 | def fact(n, acc=1): 12 | if n == 0: 13 | return acc 14 | else: 15 | return fact(n-1, n * acc) 16 | 17 | print(fact(10000)) # doesn't stack overflow 18 | # 28462596809170545189064132121198688901... 19 | 20 | 21 | `Tail-call Optimization`__ is a technique which will optimize away the 22 | stack usage of functions calls which are in a tail 23 | position. Intuitively, if a function **A** calls another function 24 | **B**, but does not do any computation after **B** returns (i.e. **A** 25 | returns immediately when **B** returns), we don't need to keep around 26 | the `stack frame `_ for 27 | **A**, which is normally used to store where to resume the computation 28 | after **B** returns. By optimizing this, we can prevent really deep 29 | tail-recursive functions (like the factorial example above) from 30 | `overflowing the stack 31 | `_. 32 | 33 | __ http://en.wikipedia.org/wiki/Tail_call 34 | 35 | 36 | The ``@tco`` decorator macro doesn't just work with tail-recursive 37 | functions, but also with any generic tail-calls (of either a function 38 | or a method) via `trampolining`_, such this mutually 39 | recursive example: 40 | 41 | .. code:: python 42 | 43 | from macropy.experimental.tco import macros, tco 44 | 45 | class Example(object): 46 | 47 | @tco 48 | def odd(n): 49 | if n < 0: 50 | return odd(-n) 51 | elif n == 0: 52 | return False 53 | else: 54 | return even(n - 1) 55 | 56 | @tco 57 | def even(n): 58 | if n == 0: 59 | return True 60 | else: 61 | return odd(n-1) 62 | 63 | print(Example().even(100000)) # No stack overflow 64 | # True 65 | 66 | 67 | Note that both ``odd`` and ``even`` were both decorated with ``@tco``. All 68 | functions which would ordinarily use too many stack frames must be 69 | decorated. 70 | 71 | Trampolining 72 | ~~~~~~~~~~~~ 73 | 74 | How is tail recursion implemented? The idea is that if a function ``f`` 75 | would return the result of a recursive call to some function ``g``, it 76 | could instead return ``g``, along with whatever arguments it would have 77 | passed to ``g``. Then instead of running ``f`` directly, we run 78 | ``trampoline(f)``, which will call ``f``, call the result of ``f``, call the 79 | result of that ``f``, etc. until finally some call returns an actual 80 | value. 81 | 82 | A transformed (and simplified) version of the tail-call optimized 83 | factorial would look like this 84 | 85 | .. code:: python 86 | 87 | def trampoline_decorator(func): 88 | def trampolined(*args): 89 | if not in_trampoline(): 90 | return trampoline(func, args) 91 | return func(*args) 92 | return trampolined 93 | 94 | def trampoline(func, args): 95 | _enter_trampoline() 96 | while True: 97 | result = func(*args) 98 | with patterns: 99 | if ('macropy-tco-call', func, args) << result: 100 | pass 101 | else: 102 | if ignoring: 103 | _exit_trampoline() 104 | return None 105 | else: 106 | _exit_trampoline() 107 | return result 108 | 109 | @trampoline_decorator 110 | def fact(n, acc): 111 | if n == 0: 112 | return 1 113 | else: 114 | return ('macropy-tco-call', fact, [n-1, n * acc]) 115 | -------------------------------------------------------------------------------- /docs/tracing.rst: -------------------------------------------------------------------------------- 1 | .. _tracing: 2 | 3 | Tracing 4 | ------- 5 | 6 | .. code:: python 7 | 8 | from macropy.tracing import macros, log 9 | log[1 + 2] 10 | # 1 + 2 -> 3 11 | # 3 12 | 13 | log["omg" * 3] 14 | # ('omg' * 3) -> 'omgomgomg' 15 | # 'omgomgomg' 16 | 17 | 18 | Tracing allows you to easily see what is happening inside your 19 | code. Many a time programmers have written code like 20 | 21 | .. code:: python 22 | 23 | print("value", value) 24 | print("sqrt(x)", sqrt(x)) 25 | 26 | 27 | and the ``log()`` macro (shown above) helps remove this duplication by 28 | automatically expanding ``log(1 + 2)`` into ``wrap("(1 + 2)", (1 + 29 | 2))``. ``wrap`` then evaluates the expression, printing out the source 30 | code and final value of the computation. 31 | 32 | In addition to simple logging, MacroPy provides the ``trace()`` 33 | macro. This macro not only logs the source and result of the given 34 | expression, but also the source and result of all sub-expressions 35 | nested within it: 36 | 37 | .. code:: python 38 | 39 | from macropy.tracing import macros, trace 40 | trace[[len(x)*3 for x in ["omg", "wtf", "b" * 2 + "q", "lo" * 3 + "l"]]] 41 | # "b" * 2 -> 'bb' 42 | # "b" * 2 + "q" -> 'bbq' 43 | # "lo" * 3 -> 'lololo' 44 | # "lo" * 3 + "l" -> 'lololol' 45 | # ["omg", "wtf", "b" * 2 + "q", "lo" * 3 + "l"] -> ['omg', 'wtf', 'bbq', 'lololol'] 46 | # len(x) -> 3 47 | # len(x)*3 -> 9 48 | # len(x) -> 3 49 | # len(x)*3 -> 9 50 | # len(x) -> 3 51 | # len(x)*3 -> 9 52 | # len(x) -> 7 53 | # len(x)*3 -> 21 54 | # [len(x)*3 for x in ["omg", "wtf", "b" * 2 + "q", "lo" * 3 + "l"]] -> [9, 9, 9, 21] 55 | # [9, 9, 9, 21] 56 | 57 | 58 | As you can see, ``trace`` logs the source and value of all 59 | sub-expressions that get evaluated in the course of evaluating the 60 | list comprehension. 61 | 62 | Lastly, ``trace`` can be used as a block macro: 63 | 64 | 65 | .. code:: python 66 | 67 | from macropy.tracing import macros, trace 68 | with trace: 69 | sum = 0 70 | for i in range(0, 5): 71 | sum = sum + 5 72 | 73 | # sum = 0 74 | # for i in range(0, 5): 75 | # sum = sum + 5 76 | # range(0, 5) -> [0, 1, 2, 3, 4] 77 | # sum = sum + 5 78 | # sum + 5 -> 5 79 | # sum = sum + 5 80 | # sum + 5 -> 10 81 | # sum = sum + 5 82 | # sum + 5 -> 15 83 | # sum = sum + 5 84 | # sum + 5 -> 20 85 | # sum = sum + 5 86 | # sum + 5 -> 25 87 | 88 | 89 | Used this way, ``trace`` will print out the source code of every 90 | *statement* that gets executed, in addition to tracing the evaluation 91 | of any expressions within those statements. 92 | 93 | Apart from simply printing out the traces, you can also redirect the 94 | traces wherever you want by having a ``log()`` function in scope: 95 | 96 | .. code:: python 97 | 98 | result = [] 99 | 100 | def log(x): 101 | result.append(x) 102 | 103 | 104 | The tracer uses whatever ``log()`` function it finds, falling back on 105 | printing only if none exists. Instead of printing, this ``log()`` 106 | function appends the traces to a list, and is used in our unit tests. 107 | 108 | We think that tracing is an extremely useful macro. For debugging what 109 | is happening, for teaching newbies how evaluation of expressions 110 | works, or for a myriad of other purposes, it is a powerful tool. The 111 | fact that it can be written as a `100 line macro 112 | `:repo: is a bonus. 113 | 114 | .. _asserts: 115 | 116 | Smart Asserts 117 | ~~~~~~~~~~~~~ 118 | 119 | 120 | .. code:: python 121 | 122 | from macropy.tracing import macros, require 123 | require[3**2 + 4**2 != 5**2] 124 | # Traceback (most recent call last): 125 | # File "", line 1, in 126 | # File "macropy.tracing.py", line 67, in handle 127 | # raise AssertionError("Require Failed\n" + "\n".join(out)) 128 | # AssertionError: Require Failed 129 | # 3**2 -> 9 130 | # 4**2 -> 16 131 | # 3**2 + 4**2 -> 25 132 | # 5**2 -> 25 133 | # 3**2 + 4**2 != 5**2 -> False 134 | 135 | 136 | MacroPy provides a variant on the ``assert`` keyword called 137 | ``require``. Like ``assert``, ``require`` throws an ``AssertionError`` if the 138 | condition is false. 139 | 140 | Unlike ``assert``, ``require`` automatically tells you what code failed 141 | the condition, and traces all the sub-expressions within the code so 142 | you can more easily see what went wrong. Pretty handy! 143 | 144 | ``require`` can also be used in block form: 145 | 146 | .. code:: python 147 | 148 | from macropy.tracing import macros, require 149 | with require: 150 | a > 5 151 | a * b == 20 152 | a < 2 153 | 154 | # Traceback (most recent call last): 155 | # File "", line 4, in 156 | # File "macropy.tracing.py", line 67, in handle 157 | # raise AssertionError("Require Failed\n" + "\n".join(out)) 158 | # AssertionError: Require Failed 159 | # a < 2 -> False 160 | 161 | 162 | This requires every statement in the block to be a boolean 163 | expression. Each expression will then be wrapped in a ``require()``, 164 | throwing an ``AssertionError`` with a nice trace when a condition fails. 165 | 166 | .. _show_expanded: 167 | 168 | show_expanded 169 | ~~~~~~~~~~~~~ 170 | 171 | .. code:: python 172 | 173 | from ast import * 174 | from macropy.core.quotes import macros, q 175 | from macropy.tracing import macros, show_expanded 176 | 177 | print(show_expanded[q[1 + 2]]) 178 | # BinOp(left=Num(n=1), op=Add(), right=Num(n=2)) 179 | 180 | 181 | ``show_expanded`` is a macro which is similar to the simple ``log`` macro 182 | shown above, but prints out what the wrapped code looks like *after 183 | all macros have been expanded*. This makes it extremely useful for 184 | debugging macros, where you need to figure out exactly what your code 185 | is being expanded into. ``show_expanded`` also works in block form: 186 | 187 | .. code:: python 188 | 189 | from macropy.core.quotes import macros, q 190 | from macropy.tracing import macros, show_expanded, trace 191 | 192 | with show_expanded: 193 | a = 1 194 | b = q[1 + 2] 195 | with q as code: 196 | print(a) 197 | 198 | # a = 1 199 | # b = BinOp(left=Num(n=1), op=Add(), right=Num(n=2)) 200 | # code = [Print(dest=None, values=[Name(id='a', ctx=Load())], nl=True)] 201 | 202 | 203 | These examples show how the `quasiquotes`:ref: macro works: it turns 204 | an expression or block of code into its AST, assigning the AST to a 205 | variable at runtime for other code to use. 206 | 207 | Here is a less trivial example: `case_classes`:ref: are a pretty 208 | useful macro, which saves us the hassle of writing a pile of 209 | boilerplate ourselves. By using ``show_expanded``, we can see what the 210 | case class definition expands into: 211 | 212 | .. code:: python 213 | 214 | from macropy.case_classes import macros, case 215 | from macropy.tracing import macros, show_expanded 216 | 217 | with show_expanded: 218 | @case 219 | class Point(x, y): 220 | pass 221 | 222 | # class Point(CaseClass): 223 | # def __init__(self, x, y): 224 | # self.x = x 225 | # self.y = y 226 | # pass 227 | # _fields = ['x', 'y'] 228 | # _varargs = None 229 | # _kwargs = None 230 | # __slots__ = ['x', 'y'] 231 | 232 | 233 | Pretty neat! 234 | 235 | --------------------------------- 236 | 237 | If you want to write your own custom logging, tracing or debugging 238 | macros, take a look at the `100 lines of code 239 | `:repo: that implements all the functionality 240 | shown above. 241 | -------------------------------------------------------------------------------- /docs/tutorials.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials: 2 | 3 | Tutorials 4 | ========= 5 | 6 | This section contains step-by-step guides to get started writing 7 | macros using MacroPy: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | first_macro 14 | hygienic_macro 15 | export_code 16 | 17 | These tutorials proceed through a serious of examples, many of which 18 | are available in the `docs/examples`:repo: folder. 19 | -------------------------------------------------------------------------------- /macropy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This is the root directory of the project, and directly contains a 3 | bunch of stable, useful and well-tested macros. It also sets up the 4 | import hooks that are required for macros to run properly. 5 | """ 6 | 7 | 8 | def activate(): 9 | from .core import macros # noqa 10 | from .core import cleanup # noqa 11 | from .core import exact_src # noqa 12 | from .core import gen_sym # noqa 13 | 14 | from .core import import_hooks 15 | import sys 16 | sys.meta_path.insert(0, import_hooks.MacroFinder) 17 | import macropy # noqa 18 | from .core import hquotes # noqa 19 | from .core import failure # noqa 20 | 21 | 22 | def console(): 23 | from macropy.core.console import MacroConsole 24 | MacroConsole().interact("0=[]=====> MacroPy Enabled <=====[]=0") 25 | 26 | 27 | from .core import exporters # noqa 28 | 29 | __version__ = "1.1.0b2" 30 | exporter = exporters.NullExporter() 31 | -------------------------------------------------------------------------------- /macropy/activate.py: -------------------------------------------------------------------------------- 1 | """Shorthand import to initialize MacroPy""" 2 | import macropy 3 | 4 | macropy.activate() 5 | -------------------------------------------------------------------------------- /macropy/console.py: -------------------------------------------------------------------------------- 1 | """Shorthand import to initialize the MacroPy console""" 2 | import macropy.activate 3 | macropy.console() -------------------------------------------------------------------------------- /macropy/core/analysis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Walker that performs simple name-binding analysis as it traverses the AST""" 3 | 4 | import ast 5 | 6 | from .util import merge_dicts 7 | from .walkers import Walker 8 | from . import compat 9 | 10 | 11 | __all__ = ['Scoped'] 12 | 13 | 14 | @Walker 15 | def find_names(tree, collect, stop, **kw): 16 | if isinstance(tree, (ast.Attribute, ast.Subscript)): 17 | stop() 18 | if isinstance(tree, ast.Name): 19 | collect((tree.id, tree)) 20 | 21 | 22 | @Walker 23 | def find_assignments(tree, collect, stop, **kw): 24 | if isinstance(tree, compat.scope_nodes): 25 | collect((tree.name, tree)) 26 | stop() 27 | if isinstance(tree, ast.Assign): 28 | for x in find_names.collect(tree.targets): 29 | collect(x) 30 | 31 | 32 | def extract_arg_names(args): 33 | return dict( 34 | ([(args.vararg.arg, args.vararg)] if args.vararg else []) + 35 | ([(args.kwarg.arg, args.kwarg)] if args.kwarg else []) + 36 | [(arg.arg, arg) for arg in args.args] + 37 | [(arg.arg, arg) for arg in args.kwonlyargs] 38 | ) 39 | 40 | 41 | class Scoped(Walker): 42 | """Used in conjunction with `@Walker`, via 43 | 44 | @Scoped 45 | @Walker 46 | def my_func(tree, scope, **kw): 47 | ... 48 | 49 | This decorator wraps the `Walker` and injects in a `scope` argument into 50 | the function. This argument is a dictionary of names which are in-scope 51 | in the present `tree`s environment, starting from the `tree` on which the 52 | recursion was start. 53 | 54 | This can be used to track the usage of a name binding through the AST 55 | snippet, and detecting when the name gets shadowed by a more tightly scoped 56 | name binding. 57 | """ 58 | 59 | def __init__(self, walker): 60 | self.walker = walker 61 | 62 | def recurse_collect(self, tree, sub_kw=[], **kw): 63 | 64 | kw['scope'] = kw.get('scope', dict(find_assignments.collect(tree))) 65 | return Walker.recurse_collect(self, tree, sub_kw, **kw) 66 | 67 | def func(self, tree, set_ctx_for, scope, **kw): 68 | 69 | def extend_scope(tree, *dicts, **kw): 70 | new_scope = merge_dicts(*([scope] + list(dicts))) 71 | if "remove" in kw: 72 | for rem in kw['remove']: 73 | del new_scope[rem] 74 | 75 | set_ctx_for(tree, scope=new_scope) 76 | if isinstance(tree, ast.Lambda): 77 | extend_scope(tree.body, extract_arg_names(tree.args)) 78 | 79 | if isinstance(tree, (ast.GeneratorExp, ast.ListComp, ast.SetComp, 80 | ast.DictComp)): 81 | iterator_vars = {} 82 | for gen in tree.generators: 83 | extend_scope(gen.target, iterator_vars) 84 | extend_scope(gen.iter, iterator_vars) 85 | iterator_vars.update(dict(find_names.collect(gen.target))) 86 | extend_scope(gen.ifs, iterator_vars) 87 | 88 | if isinstance(tree, ast.DictComp): 89 | extend_scope(tree.key, iterator_vars) 90 | extend_scope(tree.value, iterator_vars) 91 | else: 92 | extend_scope(tree.elt, iterator_vars) 93 | 94 | if isinstance(tree, compat.function_nodes): 95 | 96 | extend_scope(tree.args, {tree.name: tree}) 97 | extend_scope( 98 | tree.body, 99 | {tree.name: tree}, 100 | extract_arg_names(tree.args), 101 | dict(find_assignments.collect(tree.body)), 102 | ) 103 | 104 | if isinstance(tree, ast.ClassDef): 105 | extend_scope(tree.bases, remove=[tree.name]) 106 | extend_scope(tree.body, dict(find_assignments.collect(tree.body)), 107 | remove=[tree.name]) 108 | 109 | if isinstance(tree, ast.ExceptHandler): 110 | extend_scope(tree.body, {tree.name: tree.name}) 111 | 112 | if isinstance(tree, ast.For): 113 | extend_scope(tree.body, dict(find_names.collect(tree.target))) 114 | 115 | if isinstance(tree, ast.With): 116 | extend_scope(tree.body, dict(find_names.collect(tree.items))) 117 | 118 | return self.walker.func( 119 | tree, 120 | set_ctx_for=set_ctx_for, 121 | scope=scope, 122 | **kw 123 | ) 124 | -------------------------------------------------------------------------------- /macropy/core/cleanup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Filters used to touch up the not-quite-perfect ASTs that we allow macros 3 | to return.""" 4 | 5 | import ast 6 | 7 | from .util import register 8 | from .macros import filters 9 | from .walkers import Walker 10 | 11 | 12 | @register(filters) 13 | def fix_ctx(tree, **kw): 14 | return ast_ctx_fixer.recurse(tree, ctx=ast.Load()) 15 | 16 | 17 | @Walker 18 | def ast_ctx_fixer(tree, stop, set_ctx, set_ctx_for, **kw): 19 | ctx = kw.get("ctx", None) 20 | """Fix any missing `ctx` attributes within an AST; allows you to build 21 | your ASTs without caring about that stuff and just filling it in later.""" 22 | if ("ctx" in type(tree)._fields and 23 | (not hasattr(tree, "ctx") or tree.ctx is None)): 24 | tree.ctx = ctx 25 | 26 | if type(tree) is ast.AugAssign: 27 | set_ctx_for(tree.target, ctx=ast.AugStore()) 28 | 29 | if type(tree) is ast.Attribute: 30 | set_ctx_for(tree.value, ctx=ast.Load()) 31 | 32 | if type(tree) is ast.Assign: 33 | set_ctx_for(tree.targets, ctx=ast.Store()) 34 | set_ctx_for(tree.value, ctx=ast.Load()) 35 | 36 | if type(tree) is ast.Delete: 37 | set_ctx_for(tree.targets, ctx=ast.Del()) 38 | 39 | 40 | @register(filters) 41 | def fill_line_numbers(tree, lineno, col_offset, **kw): 42 | """Fill in line numbers somewhat more cleverly than the 43 | ast.fix_missing_locations method, which doesn't take into account the 44 | fact that line numbers are monotonically increasing down lists of AST 45 | nodes.""" 46 | if type(tree) is list: 47 | for sub in tree: 48 | if isinstance(sub, ast.AST) \ 49 | and hasattr(sub, "lineno") \ 50 | and hasattr(sub, "col_offset") \ 51 | and (sub.lineno, sub.col_offset) > (lineno, col_offset): 52 | 53 | lineno = sub.lineno 54 | col_offset = sub.col_offset 55 | 56 | fill_line_numbers(sub, lineno, col_offset) 57 | elif isinstance(tree, ast.AST): 58 | if not (hasattr(tree, "lineno") and hasattr(tree, "col_offset")): 59 | tree.lineno = lineno 60 | tree.col_offset = col_offset 61 | for name, sub in ast.iter_fields(tree): 62 | fill_line_numbers(sub, tree.lineno, tree.col_offset) 63 | elif isinstance(tree, (str, int, float)) or tree is None: 64 | pass 65 | else: 66 | raise TypeError("Invalid AST node '{!r}', type: '{!r}' " 67 | "after expansion".format(tree, type(tree))) 68 | return tree 69 | -------------------------------------------------------------------------------- /macropy/core/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ast 4 | import sys 5 | 6 | PY33 = sys.version_info >= (3, 3) 7 | PY34 = sys.version_info >= (3, 4) 8 | PY35 = sys.version_info >= (3, 5) 9 | PY36 = sys.version_info >= (3, 6) 10 | 11 | CPY = sys.implementation.name == 'cpython' 12 | PYPY = sys.implementation.name == 'pypy' 13 | 14 | HAS_FSTRING = CPY and PY36 or PYPY and PY35 15 | 16 | if PY34: 17 | function_nodes = (ast.FunctionDef,) 18 | else: 19 | function_nodes = (ast.AsyncFunctionDef, ast.FunctionDef) 20 | 21 | scope_nodes = function_nodes + (ast.ClassDef,) 22 | 23 | 24 | def Call(func, args, keywords): 25 | """A version of ``ast.Call`` that deals with compatibility. 26 | 27 | .. warning:: 28 | 29 | Currently it supports only one element for each *args and **kwargs. 30 | """ 31 | if PY35: 32 | return ast.Call(func, args, keywords) 33 | else: 34 | # see https://greentreesnakes.readthedocs.io/en/latest/nodes.html#Call 35 | starargs = [el.value for el in args if isinstance(el, ast.Starred)] 36 | if len(starargs) == 0: 37 | starargs = None 38 | elif len(starargs) == 1: 39 | starargs = starargs[0] 40 | else: 41 | raise ValueError("No more than one starargs.") 42 | kwargs = [el.value for el in keywords if el.arg is None] 43 | if len(kwargs) == 0: 44 | kwargs = None 45 | elif len(kwargs) == 1: 46 | kwargs = kwargs[0] 47 | else: 48 | raise ValueError("No more than one kwargs.") 49 | args = [el for el in args if not isinstance(el, ast.Starred)] 50 | keywords = [el for el in keywords if el.value is not kwargs] 51 | return ast.Call(func, args, keywords, starargs, kwargs) 52 | -------------------------------------------------------------------------------- /macropy/core/console.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Implementation and activation of a basic macro-powered REPL.""" 3 | 4 | import ast 5 | import code 6 | import importlib 7 | import sys 8 | 9 | from .macros import ModuleExpansionContext, detect_macros 10 | 11 | 12 | class MacroConsole(code.InteractiveConsole): 13 | 14 | def __init__(self, locals=None, filename=""): 15 | code.InteractiveConsole.__init__(self, locals, filename) 16 | self.bindings = [] 17 | 18 | def runsource(self, source, filename="", symbol="single"): 19 | try: 20 | code = self.compile(source, filename, symbol) 21 | except (OverflowError, SyntaxError, ValueError): 22 | code = "" 23 | pass 24 | 25 | if code is None: 26 | # This means it's incomplete 27 | return True 28 | 29 | try: 30 | tree = ast.parse(source) 31 | bindings = detect_macros(tree, '__main__') 32 | 33 | for mod, bind in bindings: 34 | self.bindings.append((importlib.import_module(mod), bind)) 35 | 36 | tree = ModuleExpansionContext(tree, source, self.bindings).expand_macros() 37 | 38 | tree = ast.Interactive(tree.body) 39 | code = compile(tree, filename, symbol, 40 | self.compile.compiler.flags, 1) 41 | except (OverflowError, SyntaxError, ValueError): 42 | # Case 1 43 | self.showsyntaxerror(filename) 44 | # This means there's a syntax error 45 | return False 46 | 47 | self.runcode(code) 48 | # This means it was successfully compiled; `runcode` takes care of 49 | # any runtime failures 50 | return False 51 | -------------------------------------------------------------------------------- /macropy/core/exact_src.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Logic related to lazily performing the computation necessary to finding 3 | the source extent of an AST. 4 | 5 | Exposed to each macro as an `exact_src` function.""" 6 | 7 | import ast 8 | 9 | from . import unparse 10 | from .macros import injected_vars 11 | from .util import Lazy, distinct, register 12 | from .walkers import Walker 13 | 14 | 15 | def linear_index(line_lengths, lineno, col_offset): 16 | prev_length = sum(line_lengths[:lineno-1]) + lineno-2 17 | out = prev_length + col_offset + 1 18 | return out 19 | 20 | 21 | @Walker 22 | def indexer(tree, collect, **kw): 23 | try: 24 | # print('Indexer: %s' % ast.dump(tree), file=sys.stderr) 25 | unparse(tree) 26 | collect((tree.lineno, tree.col_offset)) 27 | except (AttributeError, KeyError) as e: 28 | # If this handler gets executed it's because unparse() has 29 | # failed (it's being used as a poor man's syntax 30 | # checker). It's important to remember that unparse cannot 31 | # unparse *any* tree fragment. There are certain fragments, 32 | # (like an ast.Add without its parent ast.BinOp) that cannot 33 | # be unparsed alone 34 | pass 35 | # print("Failure in exact_src.py", e, file=sys.stderr) 36 | # raise 37 | 38 | 39 | _transforms = { 40 | ast.GeneratorExp: "(%s)", 41 | ast.ListComp: "[%s]", 42 | ast.SetComp: "{%s}", 43 | ast.DictComp: "{%s}" 44 | } 45 | 46 | 47 | @register(injected_vars) 48 | def exact_src(tree, src, **kw): 49 | 50 | def exact_src_imp(tree, src, indexes, line_lengths): 51 | all_child_pos = sorted(indexer.collect(tree)) 52 | start_index = linear_index(line_lengths(), *all_child_pos[0]) 53 | 54 | last_child_index = linear_index(line_lengths(), *all_child_pos[-1]) 55 | 56 | first_successor_index = indexes()[min(indexes().index(last_child_index)+1, 57 | len(indexes())-1)] 58 | 59 | for end_index in range(last_child_index, first_successor_index+1): 60 | 61 | prelim = src[start_index:end_index] 62 | prelim = _transforms.get(type(tree), "%s") % prelim 63 | 64 | if isinstance(tree, ast.stmt): 65 | prelim = prelim.replace("\n" + " " * tree.col_offset, "\n") 66 | 67 | if isinstance(tree, list): 68 | prelim = prelim.replace("\n" + " " * tree[0].col_offset, "\n") 69 | 70 | try: 71 | if isinstance(tree, ast.expr): 72 | x = "(" + prelim + ")" 73 | else: 74 | x = prelim 75 | parsed = ast.parse(x) 76 | if unparse(parsed).strip() == unparse(tree).strip(): 77 | return prelim 78 | 79 | except SyntaxError as e: 80 | pass 81 | raise ExactSrcException() 82 | 83 | positions = Lazy(lambda: indexer.collect(tree)) 84 | line_lengths = Lazy(lambda: list(map(len, src.split("\n")))) 85 | indexes = Lazy(lambda: distinct([linear_index(line_lengths(), l, c) 86 | for (l, c) in positions()] + [len(src)])) 87 | return lambda t: exact_src_imp(t, src, indexes, line_lengths) 88 | 89 | 90 | class ExactSrcException(Exception): 91 | pass 92 | -------------------------------------------------------------------------------- /macropy/core/exporters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Ways of dealing with macro-expanded code, e.g. caching or 3 | re-serializing it.""" 4 | 5 | import imp 6 | import logging 7 | import marshal 8 | import os 9 | import shutil 10 | 11 | from . import unparse 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def wr_long(f, x): 18 | """Internal; write a 32-bit int to a file in little-endian order.""" 19 | f.write(chr( x & 0xff)) 20 | f.write(chr((x >> 8) & 0xff)) 21 | f.write(chr((x >> 16) & 0xff)) 22 | f.write(chr((x >> 24) & 0xff)) 23 | 24 | 25 | class NullExporter(object): 26 | def export_transformed(self, code, tree, module_name, file_name): 27 | pass 28 | 29 | def find(self, file_path, pathname, description, module_name, 30 | package_path): 31 | pass 32 | 33 | 34 | class SaveExporter(object): 35 | def __init__(self, directory="exported", root=os.getcwd()): 36 | self.root = os.path.abspath(root) 37 | self.directory = os.path.abspath(directory) 38 | shutil.rmtree(self.directory, ignore_errors=True) 39 | shutil.copytree(self.root, directory) 40 | 41 | def export_transformed(self, code, tree, module_name, file_name): 42 | 43 | # do the export only if module's file_name is a subpath of the 44 | # root 45 | logger.debug('Asked to export module %r', file_name) 46 | if os.path.commonprefix([self.root, file_name]) == self.root: 47 | 48 | new_path = os.path.join( 49 | self.directory, 50 | os.path.relpath(file_name, self.root) 51 | ) 52 | 53 | with open(new_path, "w") as f: 54 | f.write(unparse(tree)) 55 | logger.debug('Exported module %r to %r', file_name, new_path) 56 | 57 | def find(self, file_path, pathname, description, module_name, package_path): 58 | pass 59 | 60 | 61 | suffix = __debug__ and 'c' or 'o' 62 | 63 | 64 | class PycExporter(object): 65 | def __init__(self, root=os.getcwd()): 66 | self.root = root 67 | 68 | def export_transformed(self, code, tree, module_name, file_name): 69 | """TODO: this needs to be updated, look into py_compile.compile, the 70 | following code was copied verbatim from there on python2""" 71 | f = open(file_name + suffix , 'wb') 72 | f.write('\0\0\0\0') 73 | timestamp = long(os.fstat(f.fileno()).st_mtime) 74 | wr_long(f, timestamp) 75 | marshal.dump(code, f) 76 | f.flush() 77 | f.seek(0, 0) 78 | f.write(imp.get_magic()) 79 | 80 | def find(self, file_path, pathname, description, module_name, package_path): 81 | try: 82 | file = open(file_path, 'rb') 83 | f = open(file.name + suffix, 'rb') 84 | py_time = os.fstat(file.fileno()).st_mtime 85 | pyc_time = os.fstat(f.fileno()).st_mtime 86 | 87 | if py_time > pyc_time: 88 | return None 89 | x = imp.load_compiled(module_name, pathname + suffix, f) 90 | return x 91 | except Exception as e: 92 | # print(e) 93 | raise 94 | -------------------------------------------------------------------------------- /macropy/core/failure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Transform macro expansion errors into runtime errors with nice 3 | stack traces. 4 | """ 5 | import traceback 6 | 7 | from .macros import filters 8 | from .hquotes import macros, hq # noqa: F401 9 | from .util import register 10 | 11 | 12 | class MacroExpansionError(Exception): 13 | def __init__(self, message): 14 | Exception.__init__(self, message) 15 | 16 | 17 | def raise_error(ex): 18 | raise ex 19 | 20 | 21 | @register(filters) 22 | def clear_errors(tree, **kw): 23 | if isinstance(tree, Exception): 24 | tb = "".join(traceback.format_tb(tree.__traceback__)) 25 | msg = str(tree) 26 | if type(tree) is not AssertionError or tree.args == (): 27 | msg = ("".join(tree.args) + "\nCaused by Macro-Expansion Error:\n" + 28 | tb) 29 | return hq[raise_error(MacroExpansionError(msg))] 30 | else: 31 | return tree 32 | -------------------------------------------------------------------------------- /macropy/core/gen_sym.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Logic related to generated a stream of unique symbols for macros to 3 | use. 4 | 5 | Exposes this functionality as the `gen_sym` function. 6 | """ 7 | 8 | import ast 9 | 10 | from . import compat, macros, util, walkers 11 | 12 | 13 | @util.register(macros.injected_vars) 14 | def gen_sym(tree, **kw): 15 | """Create a generator that creates symbols which are not used in the 16 | given `tree`. This means they will be hygienic, i.e. it guarantees 17 | that they will not cause accidental shadowing, as long as the 18 | scope of the new symbol is limited to `tree` e.g. by a lambda 19 | expression or a function body 20 | """ 21 | @walkers.Walker 22 | def name_finder(tree, collect, **kw): 23 | ttree = type(tree) 24 | if ttree is ast.Name: 25 | collect(tree.id) 26 | elif ttree is ast.arg: 27 | collect(tree.arg) 28 | elif ttree is ast.Import: 29 | names = [x.asname or x.name for x in tree.names] 30 | for name in names: 31 | collect(name) 32 | elif ttree is ast.ImportFrom: 33 | names = [x.asname or x.name for x in tree.names] 34 | for name in names: 35 | collect(name) 36 | elif ttree in compat.scope_nodes: 37 | collect(tree.name) 38 | 39 | found_names = set(name_finder.collect(tree)) 40 | 41 | def name_for(name="sym"): 42 | 43 | if name not in found_names: 44 | found_names.add(name) 45 | return name 46 | offset = 1 47 | while name + str(offset) in found_names: 48 | offset += 1 49 | found_names.add(name + str(offset)) 50 | return name + str(offset) 51 | return name_for 52 | -------------------------------------------------------------------------------- /macropy/core/hquotes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Hygienic Quasiquotes, which pull in names from their definition 3 | scope rather than their expansion scope. 4 | """ 5 | 6 | import ast 7 | import pickle 8 | 9 | from .macros import (Macros, check_annotated, filters, injected_vars, 10 | macro_stub, post_processing) 11 | 12 | from .quotes import (macros, q, unquote_search, u, ast_list, # noqa: F401 13 | name, ast_literal) 14 | from .analysis import Scoped 15 | 16 | from . import ast_repr, Captured, Literal 17 | from .util import register 18 | from .walkers import Walker 19 | 20 | 21 | # Monkey Patching pickle to pickle module objects properly See if 22 | # there is a way to do a better job with the dispatch tables, in the 23 | # meantime the code in post_proc() will use non-accelerated 24 | # pickler/unpickler. 25 | pickle._Pickler.dispatch[type(pickle)] = pickle._Pickler.save_global 26 | 27 | 28 | macros = Macros() # noqa F811 29 | 30 | 31 | @macro_stub 32 | def unhygienic(): 33 | """Used to delimit a section of a hq[...] that should not be 34 | hygienified.""" 35 | 36 | 37 | @register(injected_vars) 38 | def captured_registry(**kw): 39 | return [] 40 | 41 | 42 | @register(post_processing) # noqa: F811 43 | def post_proc(tree, captured_registry, gen_sym, **kw): 44 | if len(captured_registry) == 0: 45 | return tree 46 | 47 | unpickle_name = gen_sym("unpickled") 48 | with q as pickle_import: 49 | from pickle import _loads as x # noqa: F401 50 | 51 | pickle_import[0].names[0].asname = unpickle_name 52 | 53 | import pickle 54 | 55 | syms = [ast.Name(id=sym) for val, sym in captured_registry] 56 | vals = [val for val, sym in captured_registry] 57 | 58 | with q as stored: 59 | ast_list[syms] = name[unpickle_name](u[pickle._dumps(vals)]) 60 | 61 | from .cleanup import ast_ctx_fixer 62 | stored = ast_ctx_fixer.recurse(stored) 63 | 64 | tree.body = (list(map(ast.fix_missing_locations, pickle_import + stored)) + 65 | tree.body) 66 | 67 | return tree 68 | 69 | 70 | @register(filters) 71 | def hygienate(tree, captured_registry, gen_sym, **kw): 72 | # print('Hygienate %s' % ast.dump(tree) if isinstance(tree, ast.AST) 73 | # else tree, file=sys.stderr) 74 | @Walker 75 | def hygienator(tree, stop, **kw): 76 | if type(tree) is Captured: 77 | new_sym = [sym for val, sym in captured_registry 78 | if val is tree.val] 79 | if not new_sym: 80 | new_sym = gen_sym(tree.name) 81 | captured_registry.append((tree.val, new_sym)) 82 | else: 83 | new_sym = new_sym[0] 84 | return ast.Name(new_sym, ast.Load()) 85 | 86 | return hygienator.recurse(tree) 87 | 88 | 89 | @macros.block 90 | def hq(tree, target, **kw): 91 | tree = unquote_search.recurse(tree) 92 | tree = hygienator.recurse(tree) 93 | tree = ast_repr(tree) 94 | # print('Hquote block %s' % ast.dump(tree) if isinstance(tree, ast.AST) 95 | # else tree, file=sys.stderr) 96 | return [ast.Assign([target], tree)] 97 | 98 | 99 | @macros.expr # noqa: F811 100 | def hq(tree, **kw): 101 | """Hygienic Quasiquote macro, used to quote sections of code while 102 | ensuring that names within the quoted code will refer to the value 103 | bound to that name when the code was quoted. Used together with 104 | the `u`, `name`, `ast`, `ast_list`, `unhygienic` unquotes. 105 | """ 106 | tree = unquote_search.recurse(tree) 107 | # print('Hquote after search %s' % ast.dump(tree) 108 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 109 | tree = hygienator.recurse(tree) 110 | # print('Hquote after hygienator %s' % ast.dump(tree) 111 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 112 | tree = ast_repr(tree) 113 | # print('Hquote after repr %s' % ast.dump(tree) 114 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 115 | return tree 116 | 117 | 118 | @Scoped 119 | @Walker 120 | def hygienator(tree, stop, scope, **kw): 121 | if (type(tree) is ast.Name and type(tree.ctx) is ast.Load and 122 | tree.id not in scope.keys()): # noqa E129 123 | stop() 124 | return Captured(tree, tree.id) 125 | 126 | if type(tree) is Literal: 127 | stop() 128 | return tree 129 | 130 | res = check_annotated(tree) 131 | if res: 132 | id, subtree = res 133 | if 'unhygienic' == id: 134 | stop() 135 | tree.slice.value.ctx = None 136 | return tree.slice.value 137 | 138 | 139 | macros.expose_unhygienic(ast) 140 | macros.expose_unhygienic(ast_repr) 141 | macros.expose_unhygienic(Captured) 142 | macros.expose_unhygienic(Literal) 143 | -------------------------------------------------------------------------------- /macropy/core/import_hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Plumbing related to hooking into the import process, unrelated to 3 | MacroPy""" 4 | 5 | import ast 6 | import importlib 7 | from importlib.util import spec_from_loader 8 | import logging 9 | import sys 10 | 11 | import macropy.activate 12 | 13 | from . import macros # noqa: F401 14 | from . import exporters # noqa: F401 15 | from .util import singleton 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class _MacroLoader(object): 22 | """Performs the loading of a module with macro expansion.""" 23 | 24 | def __init__(self, module_name, mod): 25 | self.mod = mod 26 | sys.modules[module_name] = mod 27 | 28 | def load_module(self, fullname): 29 | return self.mod 30 | 31 | 32 | class MacroLoader: 33 | """Performs the real module loading in Python 3. the other is still 34 | there until the export stuff is fixed. 35 | """ 36 | 37 | def __init__(self, nomacro_spec, code, tree): 38 | self.nomacro_spec = nomacro_spec 39 | self.code = code 40 | self.tree = tree 41 | 42 | def create_module(self, spec): 43 | pass 44 | 45 | def exec_module(self, module): 46 | exec(self.code, module.__dict__) 47 | self.export() 48 | 49 | def export(self): 50 | try: 51 | macropy.exporter.export_transformed( 52 | self.code, self.tree, self.nomacro_spec.name, 53 | self.nomacro_spec.origin) 54 | except Exception as e: 55 | raise 56 | 57 | def get_filename(self, fullname): 58 | return self.nomacro_spec.loader.get_filename(fullname) 59 | 60 | def is_package(self, fullname): 61 | return self.nomacro_spec.loader.is_package(fullname) 62 | 63 | 64 | @singleton 65 | class MacroFinder(object): 66 | """Loads a module and looks for macros inside, only providing a loader 67 | if it finds some. 68 | """ 69 | 70 | def _find_spec_nomacro(self, fullname, path, target=None): 71 | """Try to find the original, non macro-expanded module using all the 72 | remaining meta_path finders. This one is installed by 73 | ``macropy.activate`` at index 0.""" 74 | spec = None 75 | for finder in sys.meta_path: 76 | # when testing with pytest, it installs a finder that for 77 | # some yet unknown reasons makes macros expansion 78 | # fail. For now it will just avoid using it and pass to 79 | # the next one 80 | if finder is self or 'pytest' in finder.__module__: 81 | continue 82 | if hasattr(finder, 'find_spec'): 83 | spec = finder.find_spec(fullname, path, target=target) 84 | elif hasattr(finder, 'load_module'): 85 | spec = spec_from_loader(fullname, finder) 86 | if spec is not None: 87 | break 88 | return spec 89 | 90 | def expand_macros(self, source_code, filename, spec): 91 | """ Parses the source_code and expands the resulting ast. 92 | Returns both the compiled ast and new ast. 93 | If no macros are found, returns None, None.""" 94 | if not source_code or "macros" not in source_code: 95 | return None, None 96 | 97 | logger.info('Expand macros in %s', filename) 98 | 99 | tree = ast.parse(source_code) 100 | bindings = macropy.core.macros.detect_macros(tree, spec.name, 101 | spec.parent, 102 | spec.name) 103 | 104 | if not bindings: 105 | return None, None 106 | 107 | modules = [] 108 | for mod, bind in bindings: 109 | modules.append((importlib.import_module(mod), bind)) 110 | new_tree = macropy.core.macros.ModuleExpansionContext( 111 | tree, source_code, modules).expand_macros() 112 | try: 113 | return compile(tree, filename, "exec"), new_tree 114 | except Exception: 115 | logger.exception("Error while compiling file %s", filename) 116 | raise 117 | 118 | def find_spec(self, fullname, path, target=None): 119 | spec = self._find_spec_nomacro(fullname, path, target) 120 | if spec is None or not (hasattr(spec.loader, 'get_source') and 121 | callable(spec.loader.get_source)): # noqa: E128 122 | if fullname != 'org': 123 | # stdlib pickle.py at line 94 contains a ``from 124 | # org.python.core for Jython which is always failing, 125 | # of course 126 | logging.debug('Failed finding spec for %s', fullname) 127 | return 128 | origin = spec.origin 129 | if origin == 'builtin': 130 | return 131 | # # try to find already exported module 132 | # # TODO: are these the right arguments? 133 | # # NOTE: This is a noop 134 | # module = macropy.core.exporters.NullExporter().find( 135 | # file_path, file_path, "", module_name, package_path) 136 | # if module: 137 | # return _MacroLoader(ast.mod) 138 | try: 139 | source = spec.loader.get_source(fullname) 140 | except ImportError: 141 | logging.debug('Loader for %s was unable to find the sources', 142 | fullname) 143 | return 144 | except Exception: 145 | logging.exception('Loader for %s raised an error', fullname) 146 | return 147 | code, tree = self.expand_macros(source, origin, spec) 148 | if not code: # no macros! 149 | return 150 | loader = MacroLoader(spec, code, tree) 151 | return spec_from_loader(fullname, loader) 152 | -------------------------------------------------------------------------------- /macropy/core/quotes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Implementation of the Quasiquotes macro. 3 | 4 | `u`, `name`, `ast_literal` and `ast_list` are the unquote delimiters, used to 5 | interpolate things into a quoted section. 6 | """ 7 | 8 | import ast 9 | 10 | from . import ast_repr, compat, Literal, walkers 11 | from .macros import Macros, check_annotated, macro_stub 12 | 13 | 14 | macros = Macros() 15 | 16 | 17 | @walkers.Walker 18 | def unquote_search(tree, **kw): 19 | "Get the values from the helper stubs." 20 | res = check_annotated(tree) # returns something if tree == 'foo[...]' 21 | if res: 22 | func, right = res 23 | for f in [u, name, ast_literal, ast_list]: 24 | # print('Unquote search %s' % f, file=sys.stderr) 25 | if f.__name__ == func: 26 | return f(right) 27 | 28 | 29 | @macros.expr 30 | def q(tree, **kw): 31 | tree = unquote_search.recurse(tree) 32 | # print('Quote expr after search %s' % ast.dump(tree) 33 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 34 | tree = ast_repr(tree) 35 | # print('Quote expr after repr %s' % ast.dump(tree) 36 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 37 | return tree 38 | 39 | 40 | @macros.block # noqa: F811 41 | def q(tree, target, **kw): 42 | """Quasiquote macro, used to lift sections of code into their AST 43 | representation which can be manipulated at runtime. Used together with 44 | the `u`, `name`, `ast_literal`, `ast_list` unquotes.""" 45 | body = unquote_search.recurse(tree) 46 | # print('Quote block after search %s' % ast.dump(tree) 47 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 48 | new_body = ast_repr(body) 49 | # print('Quote block after repr %s' % ast.dump(tree) 50 | # if isinstance(tree, ast.AST) else tree, file=sys.stderr) 51 | return [ast.Assign([target], new_body)] 52 | 53 | 54 | @macro_stub 55 | def u(tree): 56 | """Splices a value into the quoted code snippet, converting it into an AST 57 | via ast_repr.""" 58 | return Literal(compat.Call(ast.Name(id="ast_repr"), [tree], [])) 59 | 60 | 61 | @macro_stub 62 | def name(tree): 63 | "Splices a string value into the quoted code snippet as a Name." 64 | # TODO: another hard-coded call now assuming `ast.Name` 65 | return Literal(compat.Call(ast.Attribute( 66 | value=ast.Name(id='ast', ctx=ast.Load()), 67 | attr='Name', ctx=ast.Load()), [], [ast.keyword("id", tree)])) 68 | 69 | 70 | @macro_stub 71 | def ast_literal(tree): 72 | "Splices an AST into the quoted code snippet." 73 | return Literal(tree) 74 | 75 | 76 | @macro_stub 77 | def ast_list(tree): 78 | """Splices a list of ASTs into the quoted code snippet as a List node.""" 79 | return Literal(compat.Call(ast.Attribute( 80 | value=ast.Name(id='ast', ctx=ast.Load()), 81 | attr='List', ctx=ast.Load()), [], [ast.keyword("elts", tree)])) 82 | 83 | 84 | macros.expose_unhygienic(ast) 85 | macros.expose_unhygienic(ast_repr) 86 | macros.expose_unhygienic(Literal) 87 | -------------------------------------------------------------------------------- /macropy/core/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from macropy.test import test_suite 5 | 6 | 7 | class Cases: 8 | class Tests(unittest.TestCase): 9 | def test_exact_src(self): 10 | from . import exact_src 11 | assert exact_src.run0() == "1 * max(1, 2, 3)" 12 | assert exact_src.run1() == """1 * max((1,'2',"3"))""" 13 | assert exact_src.run_block() == """ 14 | print("omg") 15 | print("wtf") 16 | if 1: 17 | print('omg') 18 | else: 19 | import math 20 | math.acos(0.123) 21 | """.strip() 22 | 23 | def test_gen_sym(self): 24 | from . import gen_sym 25 | gen_sym.run() == 10 26 | 27 | def test_failure(self): 28 | from macropy.core.failure import MacroExpansionError 29 | from . import failure 30 | with self.assertRaises(MacroExpansionError) as ce: 31 | failure.run1() 32 | 33 | msg = str(ce.exception) 34 | # TODO: changed the behavior of this test by improving 35 | # tracebacks for errors. 36 | 37 | # this should contain at least two "i am a cow" and a 38 | # bunch of stack trace 39 | assert len(msg.splitlines()) >= 3, msg 40 | assert msg.rfind("i am a cow") != msg.find("i am a cow") 41 | 42 | # this one should only cotain the "i am a cow" message and 43 | # nothing else 44 | with self.assertRaises(MacroExpansionError) as ce: 45 | failure.run2() 46 | assert str(ce.exception) == "i am a cow" 47 | 48 | # with self.assertRaises(Exception) as ce: 49 | with self.assertRaises(MacroExpansionError): 50 | failure.run3() 51 | 52 | # with self.assertRaises(Exception) as ce: 53 | with self.assertRaises(MacroExpansionError): 54 | failure.run4() 55 | 56 | 57 | from . import quotes 58 | from . import unparse 59 | from . import walkers 60 | from . import macros 61 | from . import hquotes 62 | from . import exporters 63 | from . import analysis 64 | Tests = test_suite(cases = [ 65 | quotes, 66 | unparse, 67 | walkers, 68 | macros, 69 | Cases, 70 | hquotes, 71 | # exporters, 72 | analysis 73 | ]) 74 | -------------------------------------------------------------------------------- /macropy/core/test/analysis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | import unittest 4 | 5 | import macropy.core 6 | 7 | from macropy.core.walkers import Walker 8 | from macropy.core.analysis import Scoped, extract_arg_names 9 | 10 | 11 | @Scoped 12 | @Walker 13 | def scoped(tree, scope, collect, **kw): 14 | try: 15 | if scope != {}: 16 | collect((macropy.core.unparse(tree), 17 | {k: type(v) for k, v in scope.items()})) 18 | except (AttributeError, KeyError): 19 | # TODO: This is ignoring a bunch of errors related to the 20 | # unparser not knowing how to unparse some elements of Python 21 | # code. 22 | pass 23 | 24 | 25 | class Tests(unittest.TestCase): 26 | 27 | def test_extract_arg_names(self): 28 | from ast import parse, dump 29 | expr = parse("lambda a, b, f=6, *c, e=7, **d: 5") 30 | args = expr.body[0].value.args 31 | arg_names = extract_arg_names(args) 32 | convert_dict = lambda d: dict((k,v) if isinstance(v, str) 33 | else (k, dump(v)) for k, v in d.items()) 34 | self.assertEqual(convert_dict({ 35 | 'a': "arg(arg='a', annotation=None)", 36 | 'b': "arg(arg='b', annotation=None)", 37 | 'c': "arg(arg='c', annotation=None)", 38 | 'd': "arg(arg='d', annotation=None)", 39 | 'e': "arg(arg='e', annotation=None)", 40 | 'f': "arg(arg='f', annotation=None)" 41 | }), convert_dict(arg_names)) 42 | 43 | def test_simple_expr(self): 44 | tree = macropy.core.parse_expr("(lambda x: a)") 45 | 46 | self.assertEqual(scoped.collect(tree), [('a', {'x': ast.arg})]) 47 | 48 | tree = macropy.core.parse_expr("(lambda x, y: (lambda z: a))") 49 | 50 | self.assertEqual(scoped.collect(tree), [ 51 | ('(lambda z: a)', {'y': ast.arg, 'x': ast.arg}), 52 | ('z', {'y': ast.arg, 'x': ast.arg}), 53 | ('z', {'y': ast.arg, 'x': ast.arg}), 54 | ('a', {'y': ast.arg, 'x': ast.arg, 'z': ast.arg}) 55 | ]) 56 | 57 | tree = macropy.core.parse_expr("[e for (a, b) in c for d in e if f]") 58 | 59 | self.assertEqual(scoped.collect(tree), [ 60 | ('e', {'a': ast.Name, 'b': ast.Name, 'd': ast.Name}), 61 | ('d', {'a': ast.Name, 'b': ast.Name}), 62 | ('e', {'a': ast.Name, 'b': ast.Name}), 63 | ('f', {'a': ast.Name, 'b': ast.Name, 'd': ast.Name}) 64 | ]) 65 | 66 | 67 | tree = macropy.core.parse_expr("{k: v for k, v in d}") 68 | 69 | self.assertEqual(scoped.collect(tree), [ 70 | ('k', {'k': ast.Name, 'v': ast.Name}), 71 | ('v', {'k': ast.Name, 'v': ast.Name}) 72 | ]) 73 | 74 | def test_simple_stmt(self): 75 | tree = macropy.core.parse_stmt(""" 76 | def func(x, y): 77 | return x 78 | """) 79 | 80 | self.assertEqual(scoped.collect(tree), [ 81 | ('\n\ndef func(x, y):\n return x', {'func': ast.FunctionDef}), 82 | ('x, y', {'func': ast.FunctionDef}), 83 | ('x', {'func': ast.FunctionDef}), 84 | ('y', {'func': ast.FunctionDef}), 85 | ('\nreturn x', {'y': ast.arg, 'x': ast.arg, 86 | 'func': ast.FunctionDef}), 87 | ('x', {'y': ast.arg, 'x': ast.arg, 'func': ast.FunctionDef}) 88 | ]) 89 | 90 | tree = macropy.core.parse_stmt(""" 91 | def func(x, y): 92 | z = 10 93 | return x 94 | """) 95 | 96 | self.assertEqual(scoped.collect(tree), [ 97 | ('\n\ndef func(x, y):\n z = 10\n return x', { 98 | 'func': ast.FunctionDef}), 99 | ('x, y', {'func': ast.FunctionDef}), 100 | ('x', {'func': ast.FunctionDef}), 101 | ('y', {'func': ast.FunctionDef}), 102 | ('\nz = 10', {'y': ast.arg, 'x': ast.arg, 'z': ast.Name, 103 | 'func': ast.FunctionDef}), 104 | ('z', {'y': ast.arg, 'x': ast.arg, 'z': ast.Name, 105 | 'func': ast.FunctionDef}), 106 | ('10', {'y': ast.arg, 'x': ast.arg, 'z': ast.Name, 107 | 'func': ast.FunctionDef}), 108 | ('\nreturn x', {'y': ast.arg, 'x': ast.arg, 'z': ast.Name, 109 | 'func': ast.FunctionDef}), 110 | ('x', {'y': ast.arg, 'x': ast.arg, 'z': ast.Name, 111 | 'func': ast.FunctionDef}) 112 | ]) 113 | 114 | tree = macropy.core.parse_stmt(""" 115 | class C(A, B): 116 | z = 10 117 | printfunction(z) 118 | """) 119 | self.assertEqual(scoped.collect(tree), [ 120 | ('\n\nclass C(A, B):\n z = 10\n printfunction(z)', 121 | {'C': ast.ClassDef}), 122 | ('\nz = 10', {'z': ast.Name}), 123 | ('z', {'z': ast.Name}), 124 | ('10', {'z': ast.Name}), 125 | ('\nprintfunction(z)', {'z': ast.Name}), 126 | ('printfunction(z)', {'z': ast.Name}), 127 | ('printfunction', {'z': ast.Name}), 128 | ('z', {'z': ast.Name}) 129 | ]) 130 | 131 | tree = macropy.core.parse_stmt(""" 132 | def func(x, y): 133 | def do_nothing(): pass 134 | class C(): pass 135 | printfunction(10) 136 | """) 137 | 138 | 139 | self.assertEqual(scoped.collect(tree), [ 140 | ('\n\ndef func(x, y):\n\n def do_nothing():\n' 141 | ' pass\n\n class C:\n pass\n' 142 | ' printfunction(10)', {'func': ast.FunctionDef}), 143 | ('x, y', {'func': ast.FunctionDef}), 144 | ('x', {'func': ast.FunctionDef}), 145 | ('y', {'func': ast.FunctionDef}), 146 | ('\n\ndef do_nothing():\n pass', 147 | {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 148 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 149 | ('', {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 150 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 151 | ('\npass', {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 152 | 'do_nothing': ast.FunctionDef, 153 | 'func': ast.FunctionDef}), 154 | ('\n\nclass C:\n pass', 155 | {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 156 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 157 | ('\npass', 158 | {'y': ast.arg, 'x': ast.arg, 'do_nothing': ast.FunctionDef, 159 | 'func': ast.FunctionDef}), 160 | ('\nprintfunction(10)', 161 | {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 162 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 163 | ('printfunction(10)', 164 | {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 165 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 166 | ('printfunction', 167 | {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 168 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}), 169 | ('10', {'y': ast.arg, 'x': ast.arg, 'C': ast.ClassDef, 170 | 'do_nothing': ast.FunctionDef, 'func': ast.FunctionDef}) 171 | ]) 172 | 173 | tree = macropy.core.parse_stmt(""" 174 | try: 175 | pass 176 | except Exception as e: 177 | pass 178 | """) 179 | 180 | self.assertEqual(scoped.collect(tree), [ 181 | ('\npass', {'e': str}) 182 | ]) 183 | 184 | # This one still doesn't work right 185 | tree = macropy.core.parse_stmt(""" 186 | C = 1 187 | class C: 188 | C 189 | C 190 | """) 191 | -------------------------------------------------------------------------------- /macropy/core/test/exact_src.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.exact_src_macro import macros, f 2 | 3 | 4 | def run0(): 5 | return f[1 * max(1, 2, 3)] 6 | 7 | def run1(): 8 | return f[1 * max((1,'2',"3"))] 9 | 10 | def run_block(): 11 | with f as x: 12 | print("omg") 13 | print("wtf") 14 | if 1: 15 | print('omg') 16 | else: 17 | import math 18 | math.acos(0.123) 19 | 20 | return x 21 | -------------------------------------------------------------------------------- /macropy/core/test/exact_src_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | from macropy.core.quotes import macros, q 5 | 6 | macros = macropy.core.macros.Macros() 7 | 8 | @macros.expr 9 | def f(tree, exact_src, **kw): 10 | return ast.Str(s=exact_src(tree)) 11 | 12 | @macros.block 13 | def f(tree, exact_src, target, **kw): 14 | with q as s: 15 | x = y 16 | s[0].value = ast.Str(s=exact_src(tree)) 17 | return s 18 | -------------------------------------------------------------------------------- /macropy/core/test/exporters/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | pyc_cache_count = 0 5 | pyc_cache_macro_count = 0 6 | from macropy.core.exporters import NullExporter, PycExporter, SaveExporter 7 | 8 | THIS_FOLDER = os.path.dirname(__file__) 9 | 10 | class Tests(unittest.TestCase): 11 | def test_null_exporter(self): 12 | import pyc_cache 13 | # every load and reload should re-run both macro and file 14 | assert (pyc_cache_count, pyc_cache_macro_count) == (1, 1) 15 | reload(pyc_cache) 16 | assert (pyc_cache_count, pyc_cache_macro_count) == (2, 2) 17 | reload(pyc_cache) 18 | assert (pyc_cache_count, pyc_cache_macro_count) == (3, 3) 19 | 20 | def test_pyc_exporter(self): 21 | import macropy 22 | 23 | macropy.exporter = PycExporter() 24 | import pyc_cache 25 | assert (pyc_cache_count, pyc_cache_macro_count) == (3, 3) 26 | 27 | # reloading the file should re-run file but not macro 28 | reload(pyc_cache) 29 | assert (pyc_cache_count, pyc_cache_macro_count) == (4, 3) 30 | reload(pyc_cache) 31 | assert (pyc_cache_count, pyc_cache_macro_count) == (5, 3) 32 | 33 | 34 | cache_file = os.path.join(THIS_FOLDER, "pyc_cache.py") 35 | # unless you touch the file to bring its mtime up to that of the 36 | # stored .pyc, in which case the macro gets re-run too 37 | f = open(cache_file, "a") 38 | f.write(" ") 39 | f.close() 40 | 41 | reload(pyc_cache) 42 | assert (pyc_cache_count, pyc_cache_macro_count) == (6, 4) 43 | 44 | reload(pyc_cache) 45 | assert (pyc_cache_count, pyc_cache_macro_count) == (7, 4) 46 | 47 | f = open(cache_file, "a") 48 | f.write(" ") 49 | f.close() 50 | 51 | reload(pyc_cache) 52 | assert (pyc_cache_count, pyc_cache_macro_count) == (8, 5), (pyc_cache_count, pyc_cache_macro_count) 53 | 54 | def test_save_exporter(self): 55 | import macropy 56 | 57 | exported = os.path.join(THIS_FOLDER, "exported") 58 | macropy.exporter = SaveExporter(exported, THIS_FOLDER) 59 | 60 | # the original code should work 61 | import save 62 | assert save.run() == 14 63 | 64 | macropy.exporter = NullExporter() 65 | 66 | # the copy of the code saved in the ./exported folder should work too 67 | import macropy.core.test.exporters.exported.save as save_exported 68 | assert save_exported.run() == 14 69 | import shutil 70 | shutil.rmtree(exported) -------------------------------------------------------------------------------- /macropy/core/test/exporters/pyc_cache.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.exporters.pyc_cache_macro import macros, f 2 | from macropy.core.test import exporters 3 | 4 | exporters.pyc_cache_count += 1 5 | f[1] -------------------------------------------------------------------------------- /macropy/core/test/exporters/pyc_cache_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | 5 | from macropy.core.test import exporters 6 | macros = macropy.core.macros.Macros() 7 | 8 | @macros.expr 9 | def f(tree, **kw): 10 | 11 | exporters.pyc_cache_macro_count += 1 12 | return ast.Num(n = 10) 13 | -------------------------------------------------------------------------------- /macropy/core/test/exporters/save.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.exporters.save_macro import macros, f 2 | 3 | 4 | def run(): 5 | return f[1 + 1] 6 | -------------------------------------------------------------------------------- /macropy/core/test/exporters/save_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | 5 | from macropy.core.test import exporters 6 | from macropy.core.hquotes import macros, hq 7 | macros = macropy.core.macros.Macros() 8 | 9 | def double(x): 10 | return x * x 11 | @macros.expr 12 | def f(tree, **kw): 13 | n = 10 14 | return hq[double(ast_literal[tree]) + n] 15 | -------------------------------------------------------------------------------- /macropy/core/test/failure.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.test.failure_macro import macros, f, g, h, i 3 | 4 | 5 | def run1(): 6 | return f[0] 7 | 8 | 9 | def run2(): 10 | return g[0] 11 | 12 | 13 | def run3(): 14 | with h: 15 | pass 16 | 17 | 18 | def run4(): 19 | @i 20 | def x(): 21 | pass 22 | -------------------------------------------------------------------------------- /macropy/core/test/failure_macro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import macropy.core.macros 3 | 4 | macros = macropy.core.macros.Macros() 5 | 6 | 7 | @macros.expr 8 | def f(tree, gen_sym, **kw): 9 | raise Exception("i am a cow") 10 | 11 | 12 | @macros.expr 13 | def g(tree, gen_sym, **kw): 14 | assert False, "i am a cow" 15 | 16 | 17 | @macros.block 18 | def h(tree, gen_sym, **kw): 19 | raise Exception("i am a cow") 20 | 21 | 22 | @macros.decorator 23 | def i(tree, gen_sym, **kw): 24 | raise Exception("i am a cow") 25 | -------------------------------------------------------------------------------- /macropy/core/test/gen_sym.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.gen_sym_macro import macros, f 2 | sym1 = 10 3 | def run(): 4 | arg1 = 3 5 | sym3 = 10 6 | sym4 = 1 7 | f = 10 8 | return f[1 * max(1, 2, 3)] 9 | 10 | -------------------------------------------------------------------------------- /macropy/core/test/gen_sym_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | 5 | macros = macropy.core.macros.Macros() 6 | 7 | @macros.expr 8 | def f(tree, gen_sym, **kw): 9 | symbols = [gen_sym(), gen_sym(), gen_sym(), gen_sym(), gen_sym()] 10 | assert symbols == ["sym2", "sym5", "sym6", "sym7", "sym8"], symbols 11 | renamed = [gen_sym("max"), gen_sym("max"), gen_sym("run"), gen_sym("run")] 12 | assert renamed == ["max1", "max2", "run1", "run2"], renamed 13 | unchanged = [gen_sym("grar"), gen_sym("grar"), gen_sym("omg"), gen_sym("omg")] 14 | assert unchanged == ["grar", "grar1", "omg", "omg1"], unchanged 15 | return ast.Num(n = 10) 16 | -------------------------------------------------------------------------------- /macropy/core/test/hquotes/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | from . import hq 4 | 5 | class Tests(unittest.TestCase): 6 | def test_hq(self): 7 | 8 | assert hq.run1() == "2x: 1 double 1 double " 9 | 10 | assert hq.run2() == 5 11 | 12 | assert hq.run3() == 6 13 | 14 | def test_error(self): 15 | with self.assertRaises(TypeError) as ce: 16 | hq.run_error() 17 | 18 | assert str(ce.exception == ( 19 | "Stub `unhygienic` illegally invoked at runtime; " 20 | "is it used properly within a macro?" 21 | )) 22 | 23 | def test_hq2(self): 24 | from . import hq2 25 | 26 | assert hq2.run1() == "2x: 1 double 1 double " 27 | 28 | assert hq2.run2() == 20 29 | 30 | assert hq2.run3() == 480 -------------------------------------------------------------------------------- /macropy/core/test/hquotes/hq.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.hquotes.hq_macro import macros, expand, expand_unhygienic, unhygienic 2 | 3 | double = "double" 4 | value = 1 5 | def run1(): 6 | return expand[str(value) + " " + double + " "] 7 | 8 | def run2(): 9 | x = 1 10 | with expand: 11 | x = x + 1 12 | return x 13 | 14 | def run3(): 15 | x = 1 16 | with expand_unhygienic: 17 | x = x + 1 18 | return x 19 | 20 | def run_error(): 21 | unhygienic[10] 22 | -------------------------------------------------------------------------------- /macropy/core/test/hquotes/hq2.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.hquotes.hq_macro2 import macros, expand_block, expand, expand_block_complex 2 | 3 | double = "double" 4 | value = 1 5 | 6 | def run1(): 7 | return expand[str(value) + " " + double + " "] 8 | 9 | def run2(): 10 | x = 1 11 | with expand_block: 12 | pass 13 | return x 14 | 15 | def run3(): 16 | x = 1 17 | with expand_block_complex: 18 | pass 19 | return x 20 | -------------------------------------------------------------------------------- /macropy/core/test/hquotes/hq_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | from macropy.core.hquotes import macros, hq, unhygienic 5 | from macropy.core import Captured 6 | 7 | macros = macropy.core.macros.Macros() 8 | 9 | value = 2 10 | 11 | def double(x): 12 | return x * value 13 | 14 | @macros.expr 15 | def expand(tree, gen_sym, **kw): 16 | tree = hq[str(value) + "x: " + double(ast_literal[tree])] 17 | return tree 18 | 19 | @macros.block 20 | def expand(tree, gen_sym, **kw): 21 | v = 5 22 | with hq as new_tree: 23 | return v 24 | return new_tree 25 | 26 | @macros.block 27 | def expand_unhygienic(tree, gen_sym, **kw): 28 | 29 | v = 5 30 | with hq as new_tree: 31 | unhygienic[x] = unhygienic[x] + v 32 | 33 | return new_tree 34 | -------------------------------------------------------------------------------- /macropy/core/test/hquotes/hq_macro2.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | from macropy.core.hquotes import macros, hq, unhygienic 5 | from macropy.tracing import macros, show_expanded 6 | from macropy.core import Captured 7 | 8 | macros = macropy.core.macros.Macros() 9 | 10 | value = 2 11 | 12 | def double(x): 13 | return x * value 14 | 15 | @macros.expr 16 | def expand(tree, **kw): 17 | tree = hq[(lambda cow, prefix: prefix + "x: " + cow(ast_literal[tree]))(double, str(value))] 18 | return tree 19 | 20 | 21 | @macros.block 22 | def expand_block(tree, **kw): 23 | v = 5 24 | with hq as new_tree: 25 | x = v 26 | y = x + v 27 | z = x + y + v 28 | return z 29 | return new_tree 30 | 31 | @macros.block 32 | def expand_block_complex(tree, **kw): 33 | v = 5 34 | with hq as new_tree: 35 | x = v 36 | def multiply(start, *args): 37 | func = lambda a, b: a * b 38 | accum = 1 39 | for a in [start] + list(args): 40 | accum = func(accum, a) 41 | return accum 42 | y = x + v 43 | z = x + y + v 44 | return multiply(z, 2, 3, 4) 45 | 46 | return new_tree 47 | -------------------------------------------------------------------------------- /macropy/core/test/macros/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import sys 4 | 5 | from macropy.core import compat 6 | 7 | 8 | class Tests(unittest.TestCase): 9 | def test_basic_identification_and_expansion(self): 10 | from . import basic_expr 11 | assert basic_expr.run() == 10 12 | 13 | from . import basic_block 14 | assert basic_block.run() == 13 15 | 16 | from . import basic_decorator 17 | assert basic_decorator.run() == 15 18 | 19 | def test_progammatically_added_decorator_is_evaluated(self): 20 | from . import added_decorator 21 | assert added_decorator.run() == 8 22 | 23 | def test_arguments(self): 24 | from . import argument 25 | argument.run() == 31 26 | 27 | def test_ignore_macros_not_explicitly_imported(self): 28 | from . import not_imported 29 | assert not_imported.run1() == 1 30 | 31 | with self.assertRaises(TypeError) as c: 32 | assert not_imported.run2() == 1 33 | 34 | assert str(c.exception) == ( 35 | "Macro `f` illegally invoked at runtime; did you import it " + 36 | "properly using `from ... import macros, f`?" 37 | ) 38 | 39 | def test_line_numbers_should_match_source(self): 40 | from . import line_number_source 41 | assert line_number_source.run(0, False) == 10 42 | try: 43 | line_number_source.run(0, True) 44 | except Exception: 45 | exc_type, exc_value, exc_traceback = sys.exc_info() 46 | assert exc_traceback.tb_next.tb_lineno == 8 47 | 48 | def test_expanded_line_numbers_should_match_source(self): 49 | from . import line_number_error_source 50 | assert line_number_error_source.run(11) == 1 51 | 52 | if not compat.PY36: 53 | return 54 | 55 | # TODO: Find why in Py3.5 (and probably in 3.4) this fails 56 | # with gigantic line numbers 57 | try: 58 | line_number_error_source.run(10) 59 | except Exception: 60 | exc_type, exc_value, exc_traceback = sys.exc_info() 61 | assert exc_traceback.tb_next.tb_lineno == 9, "Lineno was: {}".format( 62 | exc_traceback.tb_next.tb_lineno) 63 | 64 | def test_quasiquote_expansion_line_numbers(self): 65 | from . import quote_source 66 | assert quote_source.run(8) == 1 67 | try: 68 | quote_source.run(4) 69 | except Exception: 70 | exc_type, exc_value, exc_traceback = sys.exc_info() 71 | assert exc_traceback.tb_next.tb_lineno == 6, "Lineno was: {}".format( 72 | exc_traceback.tb_next.tb_lineno) 73 | 74 | try: 75 | quote_source.run(2) 76 | except Exception: 77 | exc_type, exc_value, exc_traceback = sys.exc_info() 78 | assert exc_traceback.tb_next.tb_lineno == 6 79 | 80 | def test_aliases(self): 81 | from . import aliases 82 | assert aliases.run_normal() == "omg" 83 | assert aliases.run_aliased() == "wtf" 84 | with self.assertRaises(Exception): 85 | aliases.run_ignored() 86 | -------------------------------------------------------------------------------- /macropy/core/test/macros/added_decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .added_decorator_macro import macros, my_macro, my_macro2 # noqa: F401 3 | 4 | 5 | def outer(x): 6 | def wrapper(): 7 | return x() + 1 8 | return wrapper 9 | 10 | 11 | def middle(x): 12 | return x 13 | 14 | 15 | def inner(x): 16 | return x 17 | 18 | 19 | @outer 20 | @my_macro2 21 | @middle 22 | @my_macro 23 | @inner 24 | def run(): 25 | x = 10 26 | x = x + 1 27 | return x 28 | -------------------------------------------------------------------------------- /macropy/core/test/macros/added_decorator_macro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core import unparse 3 | from macropy.core.macros import Macros 4 | from macropy.core.hquotes import macros, hq 5 | 6 | macros = Macros() # noqa: F811 7 | 8 | 9 | def added_decorator(func): 10 | def wrapper(): 11 | return func() / 2 12 | return wrapper 13 | 14 | @macros.decorator 15 | def my_macro(tree, **kw): 16 | assert unparse(tree).strip() == "\n".join([ 17 | "@inner", 18 | "def run():", 19 | " x = 10", 20 | " x = (x + 1)", 21 | " return x"]), unparse(tree) 22 | 23 | b = tree.body 24 | tree.body = [b[0], b[1], b[1], b[1], b[1], b[2]] 25 | tree.decorator_list = [hq[added_decorator]] + tree.decorator_list 26 | return tree 27 | 28 | 29 | @macros.decorator 30 | def my_macro2(tree, **kw): 31 | assert unparse(tree).strip() == "\n".join([ 32 | "@middle", 33 | "@added_decorator", 34 | "@inner", 35 | "def run():", 36 | " x = 10", 37 | " x = (x + 1)", 38 | " x = (x + 1)", 39 | " x = (x + 1)", 40 | " x = (x + 1)", 41 | " return x"]), unparse(tree) 42 | 43 | return tree 44 | -------------------------------------------------------------------------------- /macropy/core/test/macros/aliases.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.aliases_macro import macros, e, f as f_new 2 | 3 | 4 | def run_normal(): 5 | return e[1 * max(1, 2, 3)] 6 | 7 | def run_aliased(): 8 | return f_new[1 * max((1,'2',"3"))] 9 | 10 | def run_ignored(): 11 | return g[1123] 12 | -------------------------------------------------------------------------------- /macropy/core/test/macros/aliases_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | from macropy.core.quotes import macros, q 5 | macros = macropy.core.macros.Macros() 6 | 7 | @macros.expr 8 | def e(tree, exact_src, **kw): 9 | return ast.Str("omg") 10 | 11 | @macros.expr 12 | def f(tree, exact_src, **kw): 13 | return ast.Str("wtf") 14 | 15 | @macros.expr 16 | def g(tree, exact_src, **kw): 17 | return ast.Str("bbq") 18 | -------------------------------------------------------------------------------- /macropy/core/test/macros/argument.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.argument_macros import macros, expr_macro, block_macro, decorator_macro 2 | import math 3 | 4 | def run(): 5 | x = expr_macro(1 + math.sqrt(5))[10 + 10 + 10] 6 | 7 | with block_macro(1 + math.sqrt(5)) as y: 8 | x = x + 1 9 | 10 | @decorator_macro(1 + math.sqrt(5)) 11 | def f(): 12 | pass 13 | 14 | return x -------------------------------------------------------------------------------- /macropy/core/test/macros/argument_macros.py: -------------------------------------------------------------------------------- 1 | import macropy.core 2 | import macropy.core.macros 3 | 4 | macros = macropy.core.macros.Macros() 5 | 6 | @macros.expr 7 | def expr_macro(tree, args, **kw): 8 | assert list(map(macropy.core.unparse, args)) == ["(1 + math.sqrt(5))"], macropy.core.unparse(args) 9 | return tree 10 | 11 | @macros.block 12 | def block_macro(tree, args, **kw): 13 | assert list(map(macropy.core.unparse, args)) == ["(1 + math.sqrt(5))"], macropy.core.unparse(args) 14 | return tree 15 | 16 | @macros.decorator 17 | def decorator_macro(tree, args, **kw): 18 | assert list(map(macropy.core.unparse, args)) == ["(1 + math.sqrt(5))"], macropy.core.unparse(args) 19 | return tree 20 | -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_block.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.basic_block_macro import macros, my_macro 2 | 3 | def run(): 4 | x = 10 5 | with my_macro as y: 6 | x = x + 1 7 | return x -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_block_macro.py: -------------------------------------------------------------------------------- 1 | import macropy.core 2 | import macropy.core.macros 3 | 4 | macros = macropy.core.macros.Macros() 5 | 6 | @macros.block 7 | def my_macro(tree, target, **kw): 8 | assert macropy.core.unparse(target) == "y" 9 | assert macropy.core.unparse(tree).strip() == "x = (x + 1)", macropy.core.unparse(tree) 10 | return tree * 3 11 | -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .basic_decorator_macro import macros, my_macro, my_macro2 # noqa: F401 3 | 4 | 5 | def outer(x): 6 | def wrapper(): 7 | return x() + 1 8 | return wrapper 9 | 10 | 11 | def middle(x): 12 | return x 13 | 14 | 15 | def inner(x): 16 | return x 17 | 18 | 19 | @outer 20 | @my_macro2 21 | @middle 22 | @my_macro 23 | @inner 24 | def run(): 25 | x = 10 26 | x = x + 1 27 | return x 28 | -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_decorator_macro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core import unparse 3 | from macropy.core.macros import Macros 4 | 5 | macros = Macros() 6 | 7 | 8 | @macros.decorator 9 | def my_macro(tree, **kw): 10 | assert unparse(tree).strip() == "\n".join([ 11 | "@inner", 12 | "def run():", 13 | " x = 10", 14 | " x = (x + 1)", 15 | " return x"]), unparse(tree) 16 | 17 | b = tree.body 18 | tree.body = [b[0], b[1], b[1], b[1], b[1], b[2]] 19 | return tree 20 | 21 | 22 | @macros.decorator 23 | def my_macro2(tree, **kw): 24 | assert unparse(tree).strip() == "\n".join([ 25 | "@middle", 26 | "@inner", 27 | "def run():", 28 | " x = 10", 29 | " x = (x + 1)", 30 | " x = (x + 1)", 31 | " x = (x + 1)", 32 | " x = (x + 1)", 33 | " return x"]), unparse(tree) 34 | 35 | return tree 36 | -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_expr.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.basic_expr_macro import macros, f 2 | 3 | def run(): 4 | f = 10 5 | return f[1 * max(1, 2, 3)] -------------------------------------------------------------------------------- /macropy/core/test/macros/basic_expr_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core 4 | import macropy.core.macros 5 | 6 | macros = macropy.core.macros.Macros() 7 | 8 | @macros.expr 9 | def f(tree, **kw): 10 | assert macropy.core.unparse(tree) == "(1 * max(1, 2, 3))", macropy.core.unparse(tree) 11 | return ast.Num(n = 10) 12 | -------------------------------------------------------------------------------- /macropy/core/test/macros/line_number_error_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .line_number_macro import macros, expand # noqa:F401 3 | 4 | 5 | def run(x): 6 | y = 0 7 | with expand: 8 | x = x - 1 9 | y = 1 / x # noqa: F841 10 | 11 | return x 12 | -------------------------------------------------------------------------------- /macropy/core/test/macros/line_number_macro.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.core.macros import Macros 3 | 4 | macros = Macros() 5 | 6 | 7 | @macros.block 8 | def expand(tree, **kw): 9 | import copy 10 | return tree * 10 11 | -------------------------------------------------------------------------------- /macropy/core/test/macros/line_number_source.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.line_number_macro import macros, expand 2 | 3 | def run(x, throw): 4 | with expand: 5 | x = x + 1 6 | 7 | if throw: 8 | raise Exception("lol") 9 | 10 | return x 11 | -------------------------------------------------------------------------------- /macropy/core/test/macros/not_imported.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.not_imported_macro import macros, g 2 | from macropy.core.test.macros.not_imported_macro import f 3 | 4 | def run1(): 5 | f = [1, 2, 3, 4, 5] 6 | g = 1 7 | return f[g[3]] 8 | 9 | def run2(): 10 | return f[g[3]] -------------------------------------------------------------------------------- /macropy/core/test/macros/not_imported_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | 5 | macros = macropy.core.macros.Macros() 6 | 7 | @macros.expr 8 | def g(tree, **kw): 9 | return ast.Num(n = 0) 10 | 11 | @macros.expr 12 | def f(tree, **kw): 13 | return ast.Num(n = 0) 14 | -------------------------------------------------------------------------------- /macropy/core/test/macros/quote_macro.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import macropy.core.macros 4 | 5 | from macropy.core.quotes import macros, q 6 | macros = macropy.core.macros.Macros() 7 | 8 | @macros.block 9 | def my_macro(tree, **kw): 10 | with q as code: 11 | x = x / 2 12 | y = 1 / x 13 | x = x / 2 14 | y = 1 / x 15 | x = x / 2 16 | y = 1 / x 17 | return code 18 | -------------------------------------------------------------------------------- /macropy/core/test/macros/quote_source.py: -------------------------------------------------------------------------------- 1 | from macropy.core.test.macros.quote_macro import macros, my_macro 2 | 3 | def run(x): 4 | pass 5 | pass 6 | with my_macro: 7 | pass 8 | pass 9 | return x 10 | -------------------------------------------------------------------------------- /macropy/core/test/quotes.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | import macropy.core 5 | from macropy.core.quotes import macros, q, u 6 | from macropy.core import ast_repr 7 | 8 | class Tests(unittest.TestCase): 9 | 10 | def test_simple(self): 11 | 12 | a = 10 13 | b = 2 14 | data1 = q[1 + u[a + b]] 15 | data2 = q[1 + (a + b)] 16 | 17 | assert eval(macropy.core.unparse(data1)) == 13 18 | assert eval(macropy.core.unparse(data2)) == 13 19 | a = 1 20 | assert eval(macropy.core.unparse(data1)) == 13 21 | assert eval(macropy.core.unparse(data2)) == 4 22 | 23 | 24 | def test_structured(self): 25 | 26 | a = [1, 2, "omg"] 27 | b = ["wtf", "bbq"] 28 | data1 = q[[x for x in u[a + b]]] 29 | 30 | assert(eval(macropy.core.unparse(data1)) == [1, 2, "omg", "wtf", "bbq"]) 31 | b = [] 32 | assert(eval(macropy.core.unparse(data1)) == [1, 2, "omg", "wtf", "bbq"]) 33 | 34 | 35 | def test_quote_unquote(self): 36 | 37 | x = 1 38 | y = 2 39 | a = q[u[x + y]] 40 | assert(eval(macropy.core.unparse(a)) == 3) 41 | x = 0 42 | y = 0 43 | assert(eval(macropy.core.unparse(a)) == 3) 44 | 45 | 46 | def test_unquote_name(self): 47 | n = "x" 48 | x = 1 49 | y = q[name[n] + name[n]] 50 | 51 | assert(eval(macropy.core.unparse(y)) == 2) 52 | 53 | def test_quote_unquote_ast(self): 54 | 55 | a = q[x + y] 56 | # TODO: This is almost certainly broken but I don't know what 57 | # it's supposed to do. 58 | b = q[ast_literal[a] + z] 59 | 60 | x, y, z = 1, 2, 3 61 | assert(eval(macropy.core.unparse(b)) == 6) 62 | x, y, z = 1, 3, 9 63 | assert(eval(macropy.core.unparse(b)) == 13) 64 | 65 | 66 | def test_quote_unquote_block(self): 67 | 68 | a = 10 69 | b = ["a", "b", "c"] 70 | c = [] 71 | with q as code: 72 | c.append(a) 73 | c.append(u[a]) 74 | c.extend(u[b]) 75 | 76 | exec(macropy.core.unparse(code)) 77 | assert(c == [10, 10, 'a', 'b', 'c']) 78 | c = [] 79 | a, b = None, None 80 | exec(macropy.core.unparse(code)) 81 | assert(c == [None, 10, 'a', 'b', 'c']) 82 | 83 | def test_bad_unquote_error(self): 84 | with self.assertRaises(TypeError) as ce: 85 | x = u[10] 86 | 87 | assert str(ce.exception) == ( 88 | "Stub `u` illegally invoked at runtime; " 89 | "is it used properly within a macro?" 90 | ) 91 | -------------------------------------------------------------------------------- /macropy/core/test/unparse.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | import macropy.core 5 | import macropy.core.compat as compat 6 | 7 | 8 | def convert(code): 9 | " string -> ast -> string " 10 | return macropy.core.unparse(macropy.core.parse_stmt(code)) 11 | 12 | 13 | class Tests(unittest.TestCase): 14 | 15 | def convert_test(self, code): 16 | # check if unparsing the ast of code yields the same source 17 | self.assertEqual(code.rstrip(), convert(code)) 18 | 19 | def test_expr(self): 20 | self.assertEqual(convert("1 +2 / a"), "\n(1 + (2 / a))") 21 | 22 | def test_stmts(self): 23 | test = """ 24 | import foo 25 | from foo import bar 26 | foo = something 27 | bar += 4 28 | return 29 | pass 30 | break 31 | continue 32 | del a, b, c 33 | assert foo, bar 34 | del foo 35 | global foo, bar, baz 36 | (yield foo) 37 | print('hello', 'world') 38 | nonlocal foo, bar, baz""" 39 | self.convert_test(test) 40 | 41 | def test_Exec(self): 42 | self.convert_test(""" 43 | exec('foo') 44 | exec('foo', bar) 45 | exec('foo', bar, {})""") 46 | 47 | def test_Raise(self): 48 | self.convert_test(""" 49 | raise 50 | raise Exception(e) 51 | raise Exception from init_arg""") 52 | 53 | def test_Try(self): 54 | self.convert_test(""" 55 | try: 56 | foo 57 | except: 58 | pass 59 | try: 60 | foo 61 | except Exeption as name: 62 | bar 63 | except Exception: 64 | 123 65 | except: 66 | pass 67 | else: 68 | baz 69 | finally: 70 | foo.close() 71 | try: 72 | foo 73 | finally: 74 | foo.close()""") 75 | 76 | def test_ClassDef(self): 77 | self.convert_test(""" 78 | 79 | @decorator 80 | @decorator2 81 | class Foo(bar, baz): 82 | pass 83 | 84 | class Bar(metaclass=Meta): 85 | pass""") 86 | 87 | def test_FunctionDef(self): 88 | # also tests the arguments object 89 | self.convert_test(""" 90 | 91 | @decorator 92 | @decorator2 93 | def foo(): 94 | bar 95 | 96 | def foo(arg, arg2, kw=5, *args, kwonly=4, **kwargs): 97 | pass""") 98 | 99 | def test_For(self): 100 | self.convert_test(""" 101 | for a in b: 102 | pass 103 | else: 104 | bar 105 | for a in b: 106 | pass""") 107 | 108 | def test_If(self): 109 | self.convert_test(""" 110 | if foo: 111 | if foo: 112 | pass 113 | else: 114 | pass 115 | if foo: 116 | pass 117 | elif c: 118 | if foo: 119 | pass 120 | elif a: 121 | pass 122 | elif b: 123 | pass 124 | else: 125 | pass 126 | """) 127 | 128 | def test_While(self): 129 | self.convert_test(""" 130 | while a: 131 | pass 132 | else: 133 | pass 134 | while a: 135 | pass""") 136 | 137 | def test_With(self): 138 | self.convert_test(""" 139 | with a as b: 140 | c""") 141 | 142 | def test_datatypes(self): 143 | self.convert_test(""" 144 | {1, 2, 3, 4} 145 | {1:2, 5:8} 146 | (1, 2, 3) 147 | """) 148 | self.convert_test("\n[1, 5.0, [(-(6))]]") 149 | self.convert_test("\n'abcd'") 150 | 151 | def test_comprehension(self): 152 | self.convert_test(""" 153 | (5 if foo else bar) 154 | (x for x in abc) 155 | (x for x in abc if foo) 156 | [x for x in abc if foo] 157 | {x for x in abc if foo} 158 | {x: y for x in abc if foo} 159 | """) 160 | 161 | def test_unaryop(self): 162 | self.convert_test(""" 163 | (not foo) 164 | (~ 9) 165 | (+ 1) 166 | """) 167 | 168 | def test_bnops(self): 169 | self.convert_test("\n(1 >> (2 | 3))") 170 | self.convert_test("\n(a >= b)") 171 | 172 | def test_misc(self): 173 | self.convert_test("\na.attr") # Attribute 174 | if compat.PY35: 175 | self.convert_test(""" 176 | f() 177 | f(a, *b, k=8, e=9, **c)""") # Call 178 | else: 179 | # Py3.4 version of ast.Call unparser doesn't deal with 180 | # starargs before keywords 181 | self.convert_test(""" 182 | f() 183 | f(a, k=8, e=9, *b, **c)""") # Call 184 | 185 | #self.convert_test("\n...") # Ellipsis 186 | self.convert_test(""" 187 | a[1] 188 | a[1:2] 189 | a[2:3:4] 190 | a[(1,)]""") # subscript, Index, Slice, extslice 191 | self.convert_test(""" 192 | (lambda k, f, a=6, *c, **kw: 7) 193 | (lambda: 7) 194 | """) 195 | 196 | def test_ann_assign(self): 197 | if not compat.PY36: 198 | return 199 | self.convert_test(""" 200 | a: Int 201 | (b.c): Bool = False 202 | (d[1]): Int 203 | """) 204 | 205 | def test_dict_star_star_expand(self): 206 | if not compat.PY35: 207 | return 208 | self.convert_test(""" 209 | {'a':1, **d}""") 210 | 211 | def test_joined_str(self): 212 | if not compat.HAS_FSTRING: 213 | return 214 | self.convert_test(""" 215 | f'bar {grande!r:foo} zoo' 216 | """) 217 | 218 | def test_async(self): 219 | if not compat.PY35: 220 | return 221 | # do not remove the empty line in the string, or the test will 222 | # not pass 223 | self.convert_test(""" 224 | 225 | async def foo(a: Int): 226 | async for foo in aiter: 227 | pass 228 | async with foo as bar: 229 | pass 230 | (await future) 231 | """) 232 | 233 | def test_async_comprehensions(self): 234 | if not compat.PY36: 235 | return 236 | self.convert_test(""" 237 | 238 | async def foo(): 239 | result = [i async for i in aiter() if (i % 2)] 240 | result = [(await fun()) for fun in funcs if (await condition())] 241 | """) 242 | 243 | def test_leftovers(self): 244 | self.assertEqual(macropy.core._ast_leftovers(), set()) 245 | -------------------------------------------------------------------------------- /macropy/core/test/walkers.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | import macropy.core 5 | import macropy.core.walkers 6 | from macropy.core.quotes import macros, q, u 7 | from macropy.core.walkers import Walker 8 | 9 | class Tests(unittest.TestCase): 10 | def test_transform(self): 11 | tree = macropy.core.parse_expr('(1 + 2) * "3" + ("4" + "5") * 6') 12 | goal = macropy.core.parse_expr('((("1" * "2") + 3) * ((4 * 5) + "6"))') 13 | 14 | @macropy.core.walkers.Walker 15 | def transform(tree, **kw): 16 | if type(tree) is ast.Num: 17 | return ast.Str(s = str(tree.n)) 18 | if type(tree) is ast.Str: 19 | return ast.Num(n = int(tree.s)) 20 | if type(tree) is ast.BinOp and type(tree.op) is ast.Mult: 21 | return ast.BinOp(tree.left, ast.Add(), tree.right) 22 | if type(tree) is ast.BinOp and type(tree.op) is ast.Add: 23 | return ast.BinOp(tree.left, ast.Mult(), tree.right) 24 | 25 | assert macropy.core.unparse(transform.recurse(tree)) == macropy.core.unparse(goal) 26 | 27 | def test_collect(self): 28 | 29 | tree = macropy.core.parse_expr('(((1 + 2) + (3 + 4)) + ((5 + 6) + (7 + 8)))') 30 | total = [0] 31 | @macropy.core.walkers.Walker 32 | def sum(tree, collect, **kw): 33 | if type(tree) is ast.Num: 34 | total[0] = total[0] + tree.n 35 | return collect(tree.n) 36 | 37 | tree, collected = sum.recurse_collect(tree) 38 | assert total[0] == 36 39 | assert collected == [1, 2, 3, 4, 5, 6, 7, 8] 40 | 41 | collected = sum.collect(tree) 42 | assert collected == [1, 2, 3, 4, 5, 6, 7, 8] 43 | 44 | def test_ctx(self): 45 | tree = macropy.core.parse_expr('(1 + (2 + (3 + (4 + (5)))))') 46 | 47 | @macropy.core.walkers.Walker 48 | def deepen(tree, ctx, set_ctx, **kw): 49 | if type(tree) is ast.Num: 50 | tree.n = tree.n + ctx 51 | else: 52 | return set_ctx(ctx=ctx + 1) 53 | 54 | new_tree = deepen.recurse(tree, ctx=0) 55 | goal = macropy.core.parse_expr('(2 + (4 + (6 + (8 + 9))))') 56 | assert macropy.core.unparse(new_tree) == macropy.core.unparse(goal) 57 | 58 | def test_stop(self): 59 | tree = macropy.core.parse_expr('(1 + 2 * 3 + 4 * (5 + 6) + 7)') 60 | goal = macropy.core.parse_expr('(0 + 2 * 3 + 4 * (5 + 6) + 0)') 61 | 62 | @macropy.core.walkers.Walker 63 | def stopper(tree, stop, **kw): 64 | if type(tree) is ast.Num: 65 | tree.n = 0 66 | if type(tree) is ast.BinOp and type(tree.op) is ast.Mult: 67 | stop() 68 | 69 | new_tree = stopper.recurse(tree) 70 | assert macropy.core.unparse(goal) == macropy.core.unparse(new_tree) 71 | -------------------------------------------------------------------------------- /macropy/core/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Functions that are nice to have but really should be in the python 3 | std lib. 4 | """ 5 | 6 | 7 | def flatten(xs): 8 | """Recursively flattens a list of lists of lists (arbitrarily, 9 | non-uniformly deep) into a single big list. 10 | """ 11 | res = [] 12 | 13 | def loop(ys): 14 | for i in ys: 15 | if isinstance(i, list): 16 | loop(i) 17 | elif i is None: 18 | pass 19 | else: 20 | res.append(i) 21 | loop(xs) 22 | return res 23 | 24 | 25 | def singleton(cls): 26 | """Decorates a class to turn it into a singleton.""" 27 | obj = cls() 28 | obj.__name__ = cls.__name__ 29 | 30 | return obj 31 | 32 | 33 | def merge_dicts(*my_dicts): 34 | """Combines a bunch of dictionaries together, later dictionaries taking 35 | precedence if there is a key conflict.""" 36 | return dict((k, v) for d in my_dicts for (k, v) in d.items()) 37 | 38 | 39 | class Lazy(object): 40 | 41 | def __init__(self, thunk): 42 | self.thunk = thunk 43 | self.val = None 44 | 45 | def __call__(self): 46 | if self.val is None: 47 | self.val = [self.thunk()] 48 | return self.val[0] 49 | 50 | 51 | def distinct(l): 52 | """Builds a new list with all duplicates removed.""" 53 | s = [] 54 | for i in l: 55 | if i not in s: 56 | s.append(i) 57 | return s 58 | 59 | 60 | def register(array): 61 | """A decorator to add things to lists without stomping over its 62 | value.""" 63 | def x(val): 64 | array.append(val) 65 | return val 66 | return x 67 | 68 | 69 | def box(x): 70 | "None | T => [T]" 71 | return [x] if x else [] 72 | -------------------------------------------------------------------------------- /macropy/core/walkers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Implementation of Walkers, a nice way of transforming and 3 | traversing ASTs.""" 4 | 5 | import ast 6 | 7 | from . import Captured, Literal 8 | 9 | 10 | class Walker(object): 11 | """@Walker decorates a function of the form: 12 | 13 | @Walker 14 | def transform(tree, **kw): 15 | ... 16 | return new_tree 17 | 18 | 19 | Which is used via: 20 | 21 | new_tree = transform.recurse(old_tree, initial_ctx) 22 | new_tree = transform.recurse(old_tree) 23 | new_tree, collected = transform.recurse_collect(old_tree, initial_ctx) 24 | new_tree, collected = transform.recurse_collect(old_tree) 25 | collected = transform.collect(old_tree, initial_ctx) 26 | collected = transform.collect(old_tree) 27 | 28 | The `transform` function takes the tree to be transformed, in addition to 29 | a set of `**kw` which provides additional functionality: 30 | 31 | 32 | - `set_ctx`: this is a function, used via `set_ctx(name=value)` 33 | anywhere in `transform`, which will cause any children of `tree` 34 | to receive `name` as an argument with a value `value. 35 | - `set_ctx_for`: this is similar to `set_ctx`, but takes an 36 | additional parameter `tree` (i.e. `set_ctx_for(tree, 37 | name=value)`) and `name` is only injected into the parameter 38 | list of `transform` when `tree` is the AST snippet being 39 | transformed. 40 | - `collect`: this is a function used via `collect(thing)`, which 41 | adds `thing` to the `collected` list returned by 42 | `recurse_collect`. 43 | - `stop`: when called via `stop()`, this prevents recursion on 44 | children of the current tree. 45 | 46 | These additional arguments can be declared in the signature, e.g.: 47 | 48 | @Walker 49 | def transform(tree, ctx, set_ctx, **kw): 50 | ... do stuff with ctx ... 51 | set_ctx(...) 52 | return new_tree 53 | 54 | for ease of use. 55 | 56 | """ 57 | def __init__(self, func): 58 | self.func = func 59 | 60 | def walk_children(self, tree, sub_kw=[], **kw): 61 | if isinstance(tree, ast.AST): 62 | aggregates = [] 63 | 64 | for field, old_value in ast.iter_fields(tree): 65 | 66 | old_value = getattr(tree, field, None) 67 | specific_sub_kw = [ 68 | (k, v) 69 | for item, kws in sub_kw 70 | if item is old_value 71 | for k, v in kws.items() 72 | ] 73 | new_value, new_aggregate = self.recurse_collect( 74 | old_value, sub_kw, 75 | **dict(list(kw.items()) + specific_sub_kw)) 76 | aggregates.extend(new_aggregate) 77 | setattr(tree, field, new_value) 78 | 79 | return aggregates 80 | 81 | elif isinstance(tree, list) and len(tree) > 0: 82 | aggregates = [] 83 | new_tree = [] 84 | 85 | for t in tree: 86 | new_t, new_a = self.recurse_collect(t, sub_kw, **kw) 87 | if type(new_t) is list: 88 | new_tree.extend(new_t) 89 | else: 90 | new_tree.append(new_t) 91 | aggregates.extend(new_a) 92 | 93 | tree[:] = new_tree 94 | return aggregates 95 | 96 | else: 97 | return [] 98 | 99 | def recurse(self, tree, **kw): 100 | """Traverse the given AST and return the transformed tree.""" 101 | return self.recurse_collect(tree, **kw)[0] 102 | 103 | def collect(self, tree, **kw): 104 | """Traverse the given AST and return the transformed tree.""" 105 | return self.recurse_collect(tree, **kw)[1] 106 | 107 | def recurse_collect(self, tree, sub_kw=[], **kw): 108 | """Traverse the given AST and return the transformed tree together 109 | with any values which were collected along with way.""" 110 | 111 | if (isinstance(tree, ast.AST) or type(tree) is Literal or 112 | type(tree) is Captured): # noqa: #E129 113 | aggregates = [] 114 | stop_now = [False] 115 | 116 | def stop(): 117 | stop_now[0] = True 118 | 119 | new_ctx = dict(**kw) 120 | new_ctx_for = sub_kw[:] 121 | 122 | def set_ctx(**new_kw): 123 | new_ctx.update(new_kw) 124 | 125 | def set_ctx_for(tree, **kw): 126 | new_ctx_for.append((tree, kw)) 127 | 128 | # Provide the function with a bunch of controls, in addition to 129 | # the tree itself. 130 | new_tree = self.func( 131 | tree=tree, 132 | collect=aggregates.append, 133 | set_ctx=set_ctx, 134 | set_ctx_for=set_ctx_for, 135 | stop=stop, 136 | **kw 137 | ) 138 | 139 | if new_tree is not None: 140 | tree = new_tree 141 | 142 | if not stop_now[0]: 143 | aggregates.extend(self.walk_children(tree, new_ctx_for, 144 | **new_ctx)) 145 | 146 | else: 147 | aggregates = self.walk_children(tree, sub_kw, **kw) 148 | 149 | return tree, aggregates 150 | -------------------------------------------------------------------------------- /macropy/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | """This directory directly contains a bunch of macros which are less 2 | well-tested and probably unstable, but nonetheless serve as pretty cool 3 | demonstrations of what is possible using MacroPy. Many of them have third 4 | party dependencies which are not installed by default (to avoid bloating the 5 | installing) and need to be installed manually before they can be used.""" -------------------------------------------------------------------------------- /macropy/experimental/js_snippets.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Imports added by remove_from_imports. 4 | 5 | import macropy.core.macros 6 | import ast 7 | 8 | from macropy.core import ast_repr 9 | from macropy.core.quotes import macros, q, u, ast 10 | import pjs 11 | from pjs.converter import Scope 12 | 13 | std_lib = [ 14 | 'modules.js', 15 | 'functions.js', 16 | 'classes.js', 17 | '__builtin__.js', 18 | ] 19 | 20 | import os 21 | path = os.path.dirname(pjs.__file__) + "/data/pjslib.js" 22 | std_lib_script = open(path).read() 23 | 24 | macros = macropy.core.macros.Macros() 25 | 26 | 27 | @macros.expr 28 | def js(tree, **kw): 29 | javascript = pjs.converter.Converter("").convert_node(tree, Scope()) 30 | return ast.Str(javascript) 31 | 32 | 33 | @macros.expr 34 | def pyjs(tree, **kw): 35 | javascript = pjs.converter.Converter("").convert_node(tree, Scope()) 36 | return q[(ast_literal[tree], u[javascript])] 37 | -------------------------------------------------------------------------------- /macropy/experimental/pinq.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | 4 | import sqlalchemy 5 | 6 | from ..core.macros import Macros 7 | from ..core.walkers import Walker 8 | 9 | from ..core import Captured # noqa: F401 10 | from ..core.quotes import ast_literal, name 11 | from ..core.hquotes import macros, hq, ast_list 12 | from ..quick_lambda import macros, f, _ # noqa: F401,F811 13 | 14 | 15 | macros = Macros() # noqa: F811 16 | 17 | 18 | @macros.expr 19 | def sql(tree, **kw): 20 | x = process(tree) 21 | x = expand_let_bindings.recurse(x) 22 | return x 23 | 24 | 25 | @macros.expr 26 | def query(tree, gen_sym, **kw): 27 | x = process(tree) 28 | x = expand_let_bindings.recurse(x) 29 | sym = gen_sym() 30 | # return q[(lambda query: query.bind.execute(query).fetchall())(ast[x])] 31 | new_tree = hq[(lambda query: name[sym].bind.execute( 32 | name[sym]).fetchall())(ast_literal[x])] 33 | new_tree.func.args = ast.arguments([ast.arg(sym, None)], None, [], [], 34 | None, []) 35 | return new_tree 36 | 37 | 38 | def process(tree): 39 | @Walker 40 | def recurse(tree, **kw): 41 | if type(tree) is ast.Compare and type(tree.ops[0]) is ast.In: 42 | return hq[(ast_literal[tree.left]).in_( 43 | ast_literal[tree.comparators[0]])] 44 | 45 | elif type(tree) is ast.GeneratorExp: 46 | elt_aliases = [] 47 | arg_names = [] 48 | call_params = [] 49 | for gen in tree.generators: 50 | elt_alias = gen.target 51 | it = gen.iter 52 | table_alias = hq[ast_literal[it] 53 | if isinstance(ast_literal[it], 54 | sqlalchemy.sql.Alias) 55 | else ast_literal[it].alias()] 56 | cols = hq[ast_literal[table_alias].c] 57 | elt_aliases.append(elt_alias) 58 | 59 | arg_names.extend((ast.Name(id=(elt_alias.id+'_alias'), 60 | ctx=ast.Load()), 61 | elt_alias)) 62 | call_params.extend((table_alias, cols)) 63 | 64 | elt = tree.elt 65 | if type(tree.elt) is ast.Tuple: 66 | sel = hq[ast_list[elt.elts]] 67 | else: 68 | sel = hq[[ast_literal[elt]]] 69 | 70 | out = hq[sqlalchemy.select(ast_literal[sel])] 71 | 72 | # take care of ifs in the genexpr 73 | for gen in tree.generators: 74 | for cond in gen.ifs: 75 | out = hq[ast_literal[out].where(ast_literal[cond])] 76 | 77 | # if there are varnames that match a genexpr target that 78 | # aren't subject of attribute get (like accessing column 79 | # names), replace those with `_alias`. In this 80 | # way the ``sqlalchemy.select()`` function will perform the 81 | # equivalent of a ``SELECT * FROM bar`` while respecting 82 | # the presence of multiple aliased tables. 83 | fix_columns.recurse(out, elt_aliases=elt_aliases) 84 | 85 | # 'out' is an ast.Call object where out.func is an 86 | # ast.Lambda taking a single param 'x' 87 | out = hq[(lambda x: ast_literal[out])()] 88 | 89 | # this replaces the 'x' param with the name of the targets 90 | # on each generator expression (the foo in 'for foo in 91 | # bar'). Each genexpr produces two arg names: ``foo_alias, 92 | # foo``. 93 | out.func.args.args = [ast.arg(n.id, None) for n in arg_names] 94 | # this replaces the ast.Call arguments with the 95 | # ``sqlalchemy.alias()`` calculated for each iterable the 96 | # (the bar in 'for foo in bar'). Each genexpr will produce 97 | # two args, the first is the aliased selectable and the 98 | # second its columns 99 | out.args = call_params 100 | return out 101 | return recurse.recurse(tree) 102 | 103 | 104 | def generate_schema(engine): 105 | metadata = sqlalchemy.MetaData(engine) 106 | metadata.reflect() 107 | 108 | class Db: 109 | pass 110 | db = Db() 111 | for table in metadata.sorted_tables: 112 | setattr(db, table.name, table) 113 | return db 114 | 115 | 116 | @Walker 117 | def _find_let_bindings(tree, stop, collect, **kw): 118 | if type(tree) is ast.Call and type(tree.func) is ast.Lambda: 119 | stop() 120 | collect(tree) 121 | return tree.func.body 122 | 123 | elif type(tree) in [ast.Lambda, ast.GeneratorExp, ast.ListComp, 124 | ast.SetComp, ast.DictComp]: 125 | stop() 126 | return tree 127 | 128 | 129 | @Walker 130 | def expand_let_bindings(tree, **kw): 131 | tree, chunks = _find_let_bindings.recurse_collect(tree) 132 | for v in chunks: 133 | let_tree = v 134 | let_tree.func.body = tree 135 | tree = let_tree 136 | return tree 137 | 138 | 139 | @Walker 140 | def fix_columns(tree, stop, elt_aliases, **kw): 141 | """if the "columns" name isn't attribute accessed, replace it with the 142 | same name witn '_alias' appended.""" 143 | if (type(tree) is ast.Attribute and type(tree.value) is ast.Name 144 | and any(map(lambda elt: elt.id == tree.value.id, elt_aliases))): 145 | stop() 146 | return tree 147 | elif (type(tree) is ast.Name and 148 | any(map(lambda elt: elt.id == tree.id, elt_aliases))): 149 | stop() 150 | return ast.Name(id=(tree.id+'_alias'), ctx=ast.Load()) 151 | else: 152 | return tree 153 | -------------------------------------------------------------------------------- /macropy/experimental/pyxl_strings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | from io import StringIO 4 | import tokenize 5 | 6 | try: 7 | from pyxl.codec.tokenizer import pyxl_tokenize 8 | from pyxl import html 9 | except ImportError as e: 10 | raise RuntimeError("External library missing, please install the 'pyxl3'" 11 | " package.") from e 12 | 13 | from macropy.core.macros import Macros 14 | 15 | macros = Macros() 16 | 17 | 18 | @macros.expr 19 | def p(tree, **kw): 20 | new_string = tokenize.untokenize(pyxl_tokenize(StringIO('(' + tree.s + ')') 21 | .readline)).rstrip().rstrip("\\") 22 | new_tree = ast.parse(new_string) 23 | return new_tree.body[0].value 24 | 25 | 26 | # expose to the calling module some symbols 27 | macros.expose_unhygienic(html, 'html') 28 | # these are needed due to bugs in the port to py3, I suppose 29 | rawhtml = html.rawhtml 30 | macros.expose_unhygienic(rawhtml) 31 | unicode = str 32 | macros.expose_unhygienic(unicode, 'unicode') 33 | -------------------------------------------------------------------------------- /macropy/experimental/tco.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import enum 3 | import functools 4 | 5 | from ..core import Captured # noqa: F401 6 | from ..core.hquotes import macros, hq # noqa: F811 7 | from ..core.macros import Macros 8 | from ..core.walkers import Walker 9 | from ..core.compat import PY35 10 | 11 | if not PY35: 12 | raise RuntimeError("Tail-call optimization works only on Py3.5+ for now.") 13 | 14 | from .pattern import ( # noqa: F401,F811 15 | macros, switch, ClassMatcher, NameMatcher) 16 | 17 | 18 | macros = Macros() # noqa: F811 19 | 20 | in_tc_stack = [False] 21 | 22 | TCOType = enum.Enum('TCOType', ('IGNORE', 'CALL')) 23 | 24 | 25 | def trampoline(func, args, kwargs): 26 | """ 27 | Repeatedly apply a function until it returns a value. 28 | 29 | The function may return (tco.CALL, func, args, kwargs) or (tco.IGNORE, 30 | func, args, kwargs) or just a value. 31 | """ 32 | 33 | ignoring = False 34 | while True: 35 | # We can only set this if we know it will be immediately unset by func 36 | if hasattr(func, 'tco'): 37 | in_tc_stack[0] = True 38 | result = func(*args, **kwargs) 39 | # for performance reasons, do not use pattern matching here 40 | if isinstance(result, tuple): 41 | if result[0] is TCOType.CALL: 42 | func = result[1] 43 | args = result[2] 44 | kwargs = result[3] 45 | continue 46 | elif result[0] is TCOType.IGNORE: 47 | ignoring = True 48 | func = result[1] 49 | args = result[2] 50 | kwargs = result[3] 51 | continue 52 | if ignoring: 53 | return None 54 | else: 55 | return result 56 | 57 | 58 | def trampoline_decorator(func): 59 | 60 | @functools.wraps(func) 61 | def trampolined(*args, **kwargs): 62 | if in_tc_stack[0]: 63 | in_tc_stack[0] = False 64 | return func(*args, **kwargs) 65 | in_tc_stack.append(False) 66 | return trampoline(func, args, kwargs) 67 | 68 | trampolined.tco = True 69 | return trampolined 70 | 71 | 72 | @macros.decorator 73 | def tco(tree, **kw): 74 | 75 | @Walker 76 | # Replace returns of calls 77 | def return_replacer(tree, **kw): 78 | with switch(tree): 79 | if ast.Return(value=ast.Call( 80 | func=func, 81 | args=args, 82 | keywords=keywords)): 83 | starred = [arg for arg in args if isinstance(arg, ast.Starred)] 84 | kwargs = [kw for kw in keywords if kw.arg is None] 85 | 86 | if len(kwargs): 87 | kwargs = kwargs[0].value 88 | if len(starred): 89 | starred = starred[0].value 90 | with hq as code: 91 | # get rid of starargs 92 | return (TCOType.CALL, 93 | ast_literal[func], 94 | (ast_literal[ast.List(args, ast.Load())] + 95 | list(ast_literal[starred])), 96 | ast_literal[kwargs or ast.Dict([],[])]) 97 | else: 98 | with hq as code: 99 | return (TCOType.CALL, 100 | ast_literal[func], 101 | ast_literal[ast.List(args, ast.Load())], 102 | ast_literal[kwargs or ast.Dict([], [])]) 103 | 104 | return code 105 | else: 106 | return tree 107 | 108 | # Replace calls (that aren't returned) which happen to be in a tail-call 109 | # position 110 | def replace_tc_pos(node): 111 | with switch(node): 112 | if ast.Expr(value=ast.Call( 113 | func=func, 114 | args=args, 115 | keywords=keywords)): 116 | starred = [arg for arg in args if isinstance(arg, ast.Starred)] 117 | kwargs = [kw for kw in keywords if kw.arg is None] 118 | 119 | if len(kwargs): 120 | kwargs = kwargs[0].value 121 | if len(starred): 122 | starred = starred[0].value 123 | with hq as code: 124 | # get rid of starargs 125 | return (TCOType.IGNORE, 126 | ast_literal[func], 127 | (ast_literal[ast.List(args, ast.Load())] + 128 | list(ast_literal[starred])), 129 | ast_literal[kwargs or ast.Dict([],[])]) 130 | else: 131 | with hq as code: 132 | return (TCOType.IGNORE, 133 | ast_literal[func], 134 | ast_literal[ast.List(args, ast.Load())], 135 | ast_literal[kwargs or ast.Dict([], [])]) 136 | return code 137 | elif ast.If(test=test, body=body, orelse=orelse): 138 | body[-1] = replace_tc_pos(body[-1]) 139 | if orelse: 140 | orelse[-1] = replace_tc_pos(orelse[-1]) 141 | return ast.If(test, body, orelse) 142 | else: 143 | return node 144 | 145 | tree = return_replacer.recurse(tree) 146 | 147 | tree.decorator_list = ([hq[trampoline_decorator]] + 148 | tree.decorator_list) 149 | 150 | tree.body[-1] = replace_tc_pos(tree.body[-1]) 151 | return tree 152 | -------------------------------------------------------------------------------- /macropy/experimental/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from macropy.test import test_suite 3 | from macropy.core.compat import PY35 4 | 5 | from . import pattern 6 | # This doesn't currently work in MacroPy3 due to a missing dependency 7 | # from . import js_snippets 8 | 9 | cases = [pattern] 10 | 11 | try: 12 | import sqlalchemy # noqa: F401 13 | from . import pinq 14 | cases.append(pinq) 15 | except ImportError: 16 | print('Excluding pinq tests') 17 | 18 | if PY35: 19 | from . import tco 20 | cases.append(tco) 21 | else: 22 | print('Exluding tco tests') 23 | 24 | try: 25 | from . import pyxl_snippets 26 | cases.append(pyxl_snippets) 27 | except RuntimeError: 28 | print('Excluding pyxl tests') 29 | 30 | Tests = test_suite(cases=cases) 31 | -------------------------------------------------------------------------------- /macropy/experimental/test/js_snippets.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from macropy.experimental.js_snippets import macros, pyjs, js, std_lib_script 4 | from macropy.tracing import macros, require 5 | 6 | 7 | from selenium import webdriver 8 | 9 | class Tests(unittest.TestCase): 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.driver = webdriver.Chrome() 13 | @classmethod 14 | def tearDownClass(cls): 15 | cls.driver.close() 16 | 17 | def exec_js(self, script): 18 | return Tests.driver.execute_script( 19 | std_lib_script + "return " + script 20 | ) 21 | 22 | def exec_js_func(self, script, *args): 23 | arg_list = ", ".join("arguments[%s]" % i for i in range(len(args))) 24 | return Tests.driver.execute_script( 25 | std_lib_script + "return (" + script + ")(%s)" % arg_list, 26 | *args 27 | ) 28 | def test_literals(self): 29 | # these work 30 | with require: 31 | self.exec_js(js[10]) == 10 32 | self.exec_js(js["i am a cow"]) == "i am a cow" 33 | 34 | # these literals are buggy, and it seems to be PJs' fault 35 | # ??? all the results seem to turn into strings ??? 36 | with require: 37 | self.exec_js(js[3.14]) == str(3.14) 38 | self.exec_js(js[[1, 2, 'lol']]) == str([1, 2, 'lol']) 39 | self.exec_js(js[{"moo": 2, "cow": 1}]) == str({"moo": 2, "cow": 1}) 40 | 41 | # set literals don't work so this throws an exception at macro-expansion time 42 | #self.exec_js(js%{1, 2, 'lol'}) 43 | 44 | def test_executions(self): 45 | with require: 46 | self.exec_js(js[(lambda x: x * 2)(10)]) == 20 47 | self.exec_js(js[sum([x for x in range(10) if x > 5])]) == 30 48 | 49 | def test_pyjs(self): 50 | # cross-compiling a trivial predicate 51 | code, javascript = pyjs[lambda x: x > 5 and x % 2 == 0] 52 | 53 | for i in range(10): 54 | with require: 55 | code(i) == self.exec_js_func(javascript, i) 56 | 57 | 58 | code, javascript = pyjs[lambda n: [ 59 | x for x in range(n) 60 | if 0 == len([ 61 | y for y in range(2, x-2) 62 | if x % y == 0 63 | ]) 64 | ]] 65 | # this is also wrongly stringifying the result =( 66 | with require: 67 | str(code(20)) == str(self.exec_js_func(javascript, 20)) 68 | -------------------------------------------------------------------------------- /macropy/experimental/test/pyxl_snippets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import unittest 4 | from xml.etree import ElementTree 5 | 6 | from macropy.case_classes import macros, case 7 | from macropy.experimental.pyxl_strings import macros, p # noqa: F811 8 | from macropy.tracing import macros, require # noqa: F811, F401 9 | 10 | from pyxl import html # noqa: F401 11 | 12 | 13 | def normalize(string): 14 | return ElementTree.tostring( 15 | ElementTree.fromstring( 16 | re.sub("\n *", "", string) 17 | ), 18 | encoding='utf8', method='xml') 19 | 20 | 21 | class Tests(unittest.TestCase): 22 | def test_inline_python(self): 23 | 24 | image_name = "bolton.png" 25 | image = p[''] 26 | 27 | text = "Michael Bolton" 28 | block = p['
{image}{text}
'] 29 | 30 | element_list = [image, text] 31 | block2 = p['
{element_list}
'] 32 | 33 | with require: 34 | block2.to_string() == '
Michael Bolton
' 35 | 36 | def test_dynamic(self): 37 | items = ['Puppies', 'Dragons'] 38 | nav = p['
    '] 39 | for text in items: 40 | nav.append(p['
  • {text}
  • ']) 41 | 42 | with require: 43 | str(nav) == "
    • Puppies
    • Dragons
    " 44 | 45 | def test_attributes(self): 46 | fruit = p['
    '] 47 | with require: 48 | fruit.data_text == "tangerine" 49 | fruit.set_attr('data-text', 'clementine') 50 | with require: 51 | fruit.attr('data-text') == "clementine" 52 | 53 | def test_interpreter(self): 54 | safe_value = "Puppies!" 55 | unsafe_value = "" 56 | unsafe_attr = '">' 57 | pyxl_blob = p["""
    58 | {unsafe_value} 59 | {rawhtml(safe_value)} 60 |
    """] 61 | target_blob = '
    <script>bad();</script> Puppies!
    ' 62 | with require: 63 | normalize(pyxl_blob.to_string()) == normalize(target_blob) 64 | 65 | def test_modules(self): 66 | from pyxl.element import x_element 67 | 68 | @case 69 | class User(name, profile_picture): 70 | pass 71 | 72 | class x_user_badge(x_element): 73 | __attrs__ = { 74 | 'user': object, 75 | } 76 | 77 | def render(self): 78 | return p[""" 79 |
    80 | 81 |
    82 |
    {self.user.name}
    83 | {self.children()} 84 |
    85 |
    """] 86 | 87 | user = User("cowman", "http:/www.google.com") 88 | content = p['
    Any arbitrary content...
    '] 89 | pyxl_blob = p['{content}'] 90 | target_blob = """ 91 |
    92 | 93 |
    cowman
    94 |
    Any arbitrary content...
    95 |
    """ 96 | 97 | with require: 98 | normalize(pyxl_blob.to_string()) == normalize(target_blob) 99 | -------------------------------------------------------------------------------- /macropy/experimental/test/tco.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from macropy.experimental.tco import macros, tco 3 | from macropy.case_classes import macros, case 4 | from macropy.experimental.pattern import macros, switch, _matching, ClassMatcher 5 | 6 | 7 | class Tests(unittest.TestCase): 8 | def test_tco_basic(self): 9 | @tco 10 | def foo(n): 11 | if n == 0: 12 | return 1 13 | return foo(n-1) 14 | self.assertEquals(1, foo(3000)) 15 | 16 | 17 | def test_tco_returns(self): 18 | 19 | @case 20 | class Cons(x, rest): pass 21 | 22 | @case 23 | class Nil(): pass 24 | 25 | def my_range(n): 26 | cur = Nil() 27 | for i in reversed(range(n)): 28 | cur = Cons(i, cur) 29 | return cur 30 | 31 | @tco 32 | def oddLength(xs): 33 | with switch(xs): 34 | if Nil(): 35 | return False 36 | else: 37 | return evenLength(xs.rest) 38 | 39 | @tco 40 | def evenLength(xs): 41 | with switch(xs): 42 | if Nil(): 43 | return True 44 | else: 45 | return oddLength(xs.rest) 46 | 47 | self.assertTrue(True, evenLength(my_range(2000))) 48 | self.assertTrue(True, oddLength(my_range(2001))) 49 | # if we get here, then we haven't thrown a stack overflow. success. 50 | 51 | def test_implicit_tailcall(self): 52 | """Tests for when there is an implicit return None""" 53 | blah = [] 54 | 55 | @tco 56 | def appendStuff(n): 57 | if n != 0: 58 | blah.append(n) 59 | appendStuff(n-1) 60 | 61 | appendStuff(10000) 62 | self.assertEquals(10000, len(blah)) 63 | 64 | def test_util_func_compatibility(self): 65 | def util(): 66 | return 3 + 4 67 | 68 | @tco 69 | def f(n): 70 | if n == 0: 71 | return util() 72 | else: 73 | return f(n-1) 74 | 75 | self.assertEquals(7, f(1000)) 76 | 77 | def util2(): 78 | return None 79 | 80 | @tco 81 | def f2(n): 82 | if n == 0: 83 | return util2() 84 | else: 85 | return f2(n-1) 86 | 87 | self.assertEquals(None, f2(1000)) 88 | 89 | def test_tailcall_methods(self): 90 | 91 | class Blah(object): 92 | @tco 93 | def foo(self, n): 94 | if n == 0: 95 | return 1 96 | return self.foo(n-1) 97 | 98 | self.assertEquals(1, Blah().foo(5000)) 99 | 100 | def test_cross_calls(self): 101 | def odd(n): 102 | if n == 0: 103 | return False 104 | return even(n-1) 105 | 106 | @tco 107 | def even(n): 108 | if n == 0: 109 | return True 110 | return odd(n-1) 111 | 112 | def fact(n): 113 | @tco 114 | def helper(n, cumulative): 115 | if n == 0: 116 | return cumulative 117 | return helper(n - 1, n * cumulative) 118 | return helper(n, 1) 119 | 120 | self.assertEquals(120, fact(5)) 121 | 122 | 123 | if __name__ == '__main__': 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /macropy/logging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # :Project: macropy3 -- enable basic logging 3 | # :Created: gio 01 mar 2018 02:43:14 CET 4 | # :Author: Alberto Berti 5 | # :License: GNU General Public License version 3 or later 6 | # :Copyright: © 2018 Alberto Berti 7 | # 8 | 9 | import logging 10 | log = logging.getLogger(__name__) 11 | 12 | logging.basicConfig(level=logging.DEBUG) 13 | logging.getLogger().setLevel(logging.DEBUG) 14 | log.debug('Log started') 15 | -------------------------------------------------------------------------------- /macropy/quick_lambda.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | 4 | from .core.macros import Macros, injected_vars, post_processing 5 | from .core.util import Lazy, register 6 | from .core.quotes import macros, name, q, ast_literal, u 7 | from .core.hquotes import macros, hq, u # noqa: F811 8 | from .core.cleanup import ast_ctx_fixer 9 | from .core import ast_repr, Captured # noqa: F401 10 | from .core.walkers import Walker 11 | 12 | 13 | macros = Macros() # noqa: F811 14 | 15 | 16 | def _(): 17 | """Placeholder for a function argument in the `f` macro.""" 18 | 19 | 20 | @macros.expr # noqa: F811 21 | def f(tree, gen_sym, **kw): 22 | """Macro to concisely create function literals; any `_`s within the 23 | wrapped expression becomes an argument to the generated function.""" 24 | @Walker 25 | def underscore_search(tree, collect, **kw): 26 | if isinstance(tree, ast.Name) and tree.id == "_": 27 | name = gen_sym("_") 28 | tree.id = name 29 | collect(name) 30 | return tree 31 | 32 | tree, used_names = underscore_search.recurse_collect(tree) 33 | 34 | new_tree = q[lambda: ast_literal[tree]] 35 | new_tree.args.args = [ast.arg(arg=x) for x in used_names] 36 | return new_tree 37 | 38 | 39 | @macros.expr 40 | def lazy(tree, **kw): 41 | """Macro to wrap an expression in a lazy memoizing thunk. This can be 42 | called via `thing()` to extract the value. The wrapped expression is 43 | only evaluated the first time the thunk is called and the result cached 44 | for all subsequent evaluations.""" 45 | return hq[Lazy(lambda: ast_literal[tree])] 46 | 47 | 48 | def get_interned(store, index, thunk): 49 | if store[index] is None: 50 | store[index] = [thunk()] 51 | return store[index][0] 52 | 53 | 54 | @register(injected_vars) 55 | def interned_count(**kw): 56 | return [0] 57 | 58 | 59 | @register(injected_vars) # noqa: F811 60 | def interned_name(gen_sym, **kw): 61 | return gen_sym() 62 | 63 | 64 | @register(post_processing) # noqa: F811 65 | def interned_processing(tree, gen_sym, interned_count, interned_name, **kw): 66 | 67 | if interned_count[0] != 0: 68 | with q as code: 69 | name[interned_name] = [None for x in range(u[interned_count[0]])] 70 | 71 | code = ast_ctx_fixer.recurse(code) 72 | code = list(map(ast.fix_missing_locations, code)) 73 | 74 | tree.body = code + tree.body 75 | 76 | return tree 77 | 78 | 79 | @macros.expr 80 | def interned(tree, interned_name, interned_count, **kw): 81 | """Macro to intern the wrapped expression on a per-module basis""" 82 | interned_count[0] += 1 83 | 84 | hq[name[interned_name]] 85 | 86 | return hq[get_interned(name[interned_name], interned_count[0] - 1, lambda: ast_literal[tree])] 87 | -------------------------------------------------------------------------------- /macropy/string_interp.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | 4 | import macropy.core 5 | import macropy.core.macros 6 | 7 | from macropy.core.quotes import u, ast_list 8 | from macropy.core.hquotes import macros, hq 9 | from macropy.core import ast_repr, Captured 10 | 11 | macros = macropy.core.macros.Macros() 12 | 13 | @macros.expr 14 | def s(tree, **kw): 15 | """Macro to easily interpolate values into string literals.""" 16 | captured = [] 17 | new_string = "" 18 | chunks = re.split("{(.*?)}", tree.s) 19 | for i in range(0, len(chunks)): 20 | if i % 2 == 0: 21 | new_string += chunks[i] 22 | else: 23 | new_string += "%s" 24 | captured += [chunks[i]] 25 | 26 | result = hq[u[new_string] % tuple(ast_list[list(map(macropy.core.parse_expr, captured))])] 27 | 28 | return result 29 | -------------------------------------------------------------------------------- /macropy/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import macropy.activate 4 | import unittest 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | def test_suite(suites=[], cases=[]): 9 | new_suites = [x.Tests for x in suites] 10 | new_cases = [unittest.makeSuite(x.Tests) for x in cases] 11 | return unittest.TestSuite(new_cases + new_suites) 12 | 13 | 14 | from . import case_classes 15 | from . import quick_lambda 16 | from . import string_interp 17 | from . import tracing 18 | from . import peg 19 | import macropy.experimental.test 20 | import macropy.core.test 21 | 22 | 23 | Tests = test_suite(cases=[ 24 | case_classes, 25 | quick_lambda, 26 | string_interp, 27 | tracing, 28 | peg 29 | ], suites=[ 30 | macropy.experimental.test, 31 | macropy.core.test 32 | ]) 33 | -------------------------------------------------------------------------------- /macropy/test/peg_json/fail1.json: -------------------------------------------------------------------------------- 1 | "A JSON payload should be an object or array, not a string." -------------------------------------------------------------------------------- /macropy/test/peg_json/fail10.json: -------------------------------------------------------------------------------- 1 | {"Extra value after close": true} "misplaced quoted value" -------------------------------------------------------------------------------- /macropy/test/peg_json/fail11.json: -------------------------------------------------------------------------------- 1 | {"Illegal expression": 1 + 2} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail12.json: -------------------------------------------------------------------------------- 1 | {"Illegal invocation": alert()} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail13.json: -------------------------------------------------------------------------------- 1 | {"Numbers cannot have leading zeroes": 013} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail14.json: -------------------------------------------------------------------------------- 1 | {"Numbers cannot be hex": 0x14} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail15.json: -------------------------------------------------------------------------------- 1 | ["Illegal backslash escape: \x15"] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail16.json: -------------------------------------------------------------------------------- 1 | [\naked] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail17.json: -------------------------------------------------------------------------------- 1 | ["Illegal backslash escape: \017"] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail18.json: -------------------------------------------------------------------------------- 1 | [[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail19.json: -------------------------------------------------------------------------------- 1 | {"Missing colon" null} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail2.json: -------------------------------------------------------------------------------- 1 | ["Unclosed array" -------------------------------------------------------------------------------- /macropy/test/peg_json/fail20.json: -------------------------------------------------------------------------------- 1 | {"Double colon":: null} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail21.json: -------------------------------------------------------------------------------- 1 | {"Comma instead of colon", null} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail22.json: -------------------------------------------------------------------------------- 1 | ["Colon instead of comma": false] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail23.json: -------------------------------------------------------------------------------- 1 | ["Bad value", truth] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail24.json: -------------------------------------------------------------------------------- 1 | ['single quote'] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail25.json: -------------------------------------------------------------------------------- 1 | [" tab character in string "] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail26.json: -------------------------------------------------------------------------------- 1 | ["tab\ character\ in\ string\ "] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail27.json: -------------------------------------------------------------------------------- 1 | ["line 2 | break"] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail28.json: -------------------------------------------------------------------------------- 1 | ["line\ 2 | break"] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail29.json: -------------------------------------------------------------------------------- 1 | [0e] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail3.json: -------------------------------------------------------------------------------- 1 | {unquoted_key: "keys must be quoted"} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail30.json: -------------------------------------------------------------------------------- 1 | [0e+] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail31.json: -------------------------------------------------------------------------------- 1 | [0e+-1] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail32.json: -------------------------------------------------------------------------------- 1 | {"Comma instead if closing brace": true, -------------------------------------------------------------------------------- /macropy/test/peg_json/fail33.json: -------------------------------------------------------------------------------- 1 | ["mismatch"} -------------------------------------------------------------------------------- /macropy/test/peg_json/fail4.json: -------------------------------------------------------------------------------- 1 | ["extra comma",] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail5.json: -------------------------------------------------------------------------------- 1 | ["double extra comma",,] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail6.json: -------------------------------------------------------------------------------- 1 | [ , "<-- missing value"] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail7.json: -------------------------------------------------------------------------------- 1 | ["Comma after the close"], -------------------------------------------------------------------------------- /macropy/test/peg_json/fail8.json: -------------------------------------------------------------------------------- 1 | ["Extra close"]] -------------------------------------------------------------------------------- /macropy/test/peg_json/fail9.json: -------------------------------------------------------------------------------- 1 | {"Extra comma": true,} -------------------------------------------------------------------------------- /macropy/test/peg_json/pass1.json: -------------------------------------------------------------------------------- 1 | [ 2 | "JSON Test Pattern pass1", 3 | {"object with 1 member":["array with 1 element"]}, 4 | {}, 5 | [], 6 | -42, 7 | true, 8 | false, 9 | null, 10 | { 11 | "integer": 1234567890, 12 | "real": -9876.543210, 13 | "e": 0.123456789e-12, 14 | "E": 1.234567890E+34, 15 | "": 23456789012E66, 16 | "zero": 0, 17 | "one": 1, 18 | "space": " ", 19 | "quote": "\"", 20 | "backslash": "\\", 21 | "controls": "\b\f\n\r\t", 22 | "slash": "/ & \/", 23 | "alpha": "abcdefghijklmnopqrstuvwyz", 24 | "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", 25 | "digit": "0123456789", 26 | "0123456789": "digit", 27 | "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", 28 | "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", 29 | "true": true, 30 | "false": false, 31 | "null": null, 32 | "array":[ ], 33 | "object":{ }, 34 | "address": "50 St. James Street", 35 | "url": "http://www.JSON.org/", 36 | "comment": "// /* */": " ", 38 | " s p a c e d " :[1,2 , 3 39 | 40 | , 41 | 42 | 4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7], 43 | "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", 44 | "quotes": "" \u0022 %22 0x22 034 "", 45 | "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" 46 | : "A key can be any string" 47 | }, 48 | 0.5 ,98.6 49 | , 50 | 99.44 51 | , 52 | 53 | 1066, 54 | 1e1, 55 | 0.1e1, 56 | 1e-1, 57 | 1e00,2e+00,2e-00 58 | ,"rosebud"] -------------------------------------------------------------------------------- /macropy/test/peg_json/pass2.json: -------------------------------------------------------------------------------- 1 | [[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] -------------------------------------------------------------------------------- /macropy/test/peg_json/pass3.json: -------------------------------------------------------------------------------- 1 | { 2 | "JSON Test Pattern pass3": { 3 | "The outermost value": "must be an object or array.", 4 | "In this test": "It is an object." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /macropy/test/quick_lambda.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | from macropy.quick_lambda import macros, f, _, lazy, interned 5 | from macropy.tracing import macros, show_expanded 6 | from functools import reduce 7 | 8 | class Tests(unittest.TestCase): 9 | def test_basic(self): 10 | assert list(map(f[_ - 1], [1, 2, 3])) == [0, 1, 2] 11 | assert reduce(f[_ + _], [1, 2, 3]) == 6 12 | 13 | def test_partial(self): 14 | basetwo = f[int(_, base=2)] 15 | assert basetwo('10010') == 18 16 | 17 | def test_attribute(self): 18 | assert list(map(f[_.split(' ')[0]], ["i am cow", "hear me moo"])) == ["i", "hear"] 19 | 20 | def test_no_args(self): 21 | from random import random 22 | thunk = f[random()] 23 | assert thunk() != thunk() 24 | 25 | def test_name_collision(self): 26 | sym0 = 1 27 | sym1 = 2 28 | func1 = f[_ + sym0] 29 | assert func1(10) == 11 30 | func2 = f[_ + sym0 + _ + sym1] 31 | assert func2(10, 10) == 23 32 | 33 | def test_lazy(self): 34 | wrapped = [0] 35 | def func(): 36 | wrapped[0] += 1 37 | 38 | thunk = lazy[func()] 39 | 40 | assert wrapped[0] == 0 41 | 42 | thunk() 43 | assert wrapped[0] == 1 44 | thunk() 45 | assert wrapped[0] == 1 46 | 47 | def test_interned(self): 48 | 49 | wrapped = [0] 50 | def func(): 51 | wrapped[0] += 1 52 | 53 | def wrapped_func(): 54 | return interned[func()] 55 | 56 | assert wrapped[0] == 0 57 | wrapped_func() 58 | assert wrapped[0] == 1 59 | wrapped_func() 60 | assert wrapped[0] == 1 61 | -------------------------------------------------------------------------------- /macropy/test/string_interp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | from macropy.string_interp import macros, s 5 | 6 | 7 | class Tests(unittest.TestCase): 8 | def test_string_interpolate(self): 9 | a, b = 1, 2 10 | c = s["{a} apple and {b} bananas"] 11 | assert(c == "1 apple and 2 bananas") 12 | 13 | 14 | def test_string_interpolate_2(self): 15 | apple_count = 10 16 | banana_delta = 4 17 | c = s["{apple_count} {'apples'} and {apple_count + banana_delta} {''.join(['b', 'a', 'n', 'a', 'n', 'a', 's'])}"] 18 | 19 | assert(c == "10 apples and 14 bananas") 20 | -------------------------------------------------------------------------------- /macropy/test/tracing.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | from macropy.tracing import macros, trace, log, require, show_expanded 5 | from macropy.core.quotes import macros, q 6 | result = [] 7 | 8 | def log(x): 9 | result.append(x) 10 | 11 | 12 | class Tests(unittest.TestCase): 13 | 14 | def test_basic(self): 15 | 16 | log[1 + 2] 17 | log["omg" * 3] 18 | 19 | assert(result[-2:] == [ 20 | "1 + 2 -> 3", 21 | "\"omg\" * 3 -> 'omgomgomg'" 22 | ]) 23 | 24 | def test_combo(self): 25 | 26 | trace[1 + 2 + 3 + 4] 27 | 28 | self.assertEqual(result[-3:], [ 29 | "1 + 2 -> 3", 30 | "1 + 2 + 3 -> 6", 31 | "1 + 2 + 3 + 4 -> 10" 32 | ]) 33 | 34 | def test_fancy(self): 35 | trace[[len(x)*3 for x in ['omg', 'wtf', 'b' * 2 + 'q', 'lo' * 3 + 'l']]] 36 | 37 | assert(result[-14:] == [ 38 | "'b' * 2 -> 'bb'", 39 | "'b' * 2 + 'q' -> 'bbq'", 40 | "'lo' * 3 -> 'lololo'", 41 | "'lo' * 3 + 'l' -> 'lololol'", 42 | "['omg', 'wtf', 'b' * 2 + 'q', 'lo' * 3 + 'l'] -> ['omg', 'wtf', 'bbq', 'lololol']", 43 | "len(x) -> 3", 44 | "len(x)*3 -> 9", 45 | "len(x) -> 3", 46 | "len(x)*3 -> 9", 47 | "len(x) -> 3", 48 | "len(x)*3 -> 9", 49 | "len(x) -> 7", 50 | "len(x)*3 -> 21", 51 | "[len(x)*3 for x in ['omg', 'wtf', 'b' * 2 + 'q', 'lo' * 3 + 'l']] -> [9, 9, 9, 21]" 52 | ]) 53 | 54 | def test_function_call(self): 55 | trace[sum([sum([1, 2, 3]), min(4, 5, 6), max(7, 8, 9)])] 56 | assert(result[-5:] == [ 57 | "sum([1, 2, 3]) -> 6", 58 | "min(4, 5, 6) -> 4", 59 | "max(7, 8, 9) -> 9", 60 | "[sum([1, 2, 3]), min(4, 5, 6), max(7, 8, 9)] -> [6, 4, 9]", 61 | "sum([sum([1, 2, 3]), min(4, 5, 6), max(7, 8, 9)]) -> 19" 62 | ]) 63 | 64 | 65 | def test_require(self): 66 | with self.assertRaises(AssertionError) as cm: 67 | require[1 == 10] 68 | 69 | assert str(cm.exception) == "Require Failed\n1 == 10 -> False" 70 | 71 | require[1 == 1] 72 | 73 | with self.assertRaises(AssertionError) as cm: 74 | require[3**2 + 4**2 != 5**2] 75 | 76 | 77 | require[3**2 + 4**2 == 5**2] 78 | 79 | def test_require_block(self): 80 | with self.assertRaises(AssertionError) as cm: 81 | a = 10 82 | b = 2 83 | with require: 84 | a > 5 85 | a * b == 20 86 | a < 2 87 | assert str(cm.exception) == "Require Failed\na < 2 -> False" 88 | 89 | 90 | def test_show_expanded(self): 91 | 92 | from macropy.core import ast_repr 93 | show_expanded[q[1 + 2]] 94 | 95 | assert ("ast.BinOp(left=ast.Num(n=1), op=ast.Add(), " 96 | "right=ast.Num(n=2))" in result[-1]) 97 | 98 | with show_expanded: 99 | a = 1 100 | b = 2 101 | with q as code: 102 | return(a + u[b + 1]) 103 | 104 | assert result[-3] == '\na = 1' 105 | assert result[-2] == '\nb = 2' 106 | self.assertEqual("\ncode = [ast.Return(value=ast.BinOp(" 107 | "left=ast.Name(id='a'"", ctx=ast.Load()), " 108 | "op=ast.Add(), right=ast_repr((b + 1))))]", 109 | result[-1]) 110 | -------------------------------------------------------------------------------- /macropy/tracing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ast 3 | import copy 4 | 5 | import macropy.core 6 | import macropy.core.macros 7 | import macropy.core.walkers 8 | 9 | from macropy.core.quotes import ast_literal, u 10 | from macropy.core.hquotes import macros, hq, unhygienic 11 | 12 | 13 | macros = macropy.core.macros.Macros() # noqa: F811 14 | 15 | 16 | def literal_eval(node_or_string): 17 | """ 18 | Safely evaluate an expression node or a string containing a Python 19 | expression. The string or node provided may only consist of the 20 | following Python literal structures: strings, numbers, tuples, 21 | lists, dicts, booleans, and None. 22 | """ 23 | _safe_names = {'None': None, 'True': True, 'False': False} 24 | if isinstance(node_or_string, str): 25 | node_or_string = ast.parse(node_or_string, mode='eval') 26 | if isinstance(node_or_string, ast.Expression): 27 | node_or_string = node_or_string.body 28 | 29 | def _convert(node): 30 | if isinstance(node, ast.Str): 31 | return node.s 32 | elif isinstance(node, ast.Num): 33 | return node.n 34 | elif isinstance(node, ast.Tuple): 35 | return tuple(map(_convert, node.elts)) 36 | elif isinstance(node, ast.List): 37 | return list(map(_convert, node.elts)) 38 | elif isinstance(node, ast.Dict): 39 | return dict((_convert(k), _convert(v)) for k, v 40 | in zip(node.keys, node.values)) 41 | elif isinstance(node, ast.Name): 42 | if node.id in _safe_names: 43 | return _safe_names[node.id] 44 | elif (isinstance(node, ast.BinOp) and 45 | isinstance(node.op, (ast.Add, ast.Sub)) and 46 | isinstance(node.right, ast.Num) and 47 | isinstance(node.right.n, complex) and 48 | isinstance(node.left, ast.Num) and 49 | isinstance(node.left.n, (int, float))): # TODO: long, 50 | left = node.left.n 51 | right = node.right.n 52 | if isinstance(node.op, ast.Add): 53 | return left + right 54 | else: 55 | return left - right 56 | raise ValueError('malformed string') 57 | return _convert(node_or_string) 58 | 59 | 60 | def wrap(printer, txt, x): 61 | string = txt + " -> " + repr(x) 62 | printer(string) 63 | return x 64 | 65 | 66 | def wrap_simple(printer, txt, x): 67 | string = txt 68 | printer(string) 69 | return x 70 | 71 | 72 | @macros.expr 73 | def log(tree, exact_src, **kw): 74 | """Prints out source code of the wrapped expression and the value it 75 | evaluates to""" 76 | new_tree = hq[wrap(unhygienic[log], u[exact_src(tree)], ast_literal[tree])] 77 | yield new_tree 78 | 79 | 80 | @macros.expr 81 | def show_expanded(tree, expand_macros, **kw): 82 | """Prints out the expanded version of the wrapped source code, after all 83 | macros inside it have been expanded""" 84 | new_tree = hq[wrap_simple( 85 | unhygienic[log], u[macropy.core.unparse(tree)], 86 | ast_literal[tree])] 87 | return new_tree 88 | 89 | 90 | @macros.block # noqa: F811 91 | def show_expanded(tree, expand_macros, **kw): 92 | """Prints out the expanded version of the wrapped source code, after all 93 | macros inside it have been expanded""" 94 | new_tree = [] 95 | for stmt in tree: 96 | with hq as code: 97 | unhygienic[log](u[macropy.core.unparse(stmt)]) 98 | new_tree.append(code) 99 | new_tree.append(stmt) 100 | 101 | return new_tree 102 | 103 | 104 | def trace_walk_func(tree, exact_src): 105 | @macropy.core.walkers.Walker 106 | def trace_walk(tree, stop, **kw): 107 | 108 | if (isinstance(tree, ast.expr) and 109 | tree._fields != () and 110 | type(tree) is not ast.Name): # noqa: E129 111 | 112 | try: 113 | literal_eval(tree) 114 | stop() 115 | return tree 116 | except ValueError as e: 117 | txt = exact_src(tree) 118 | trace_walk.walk_children(tree) 119 | wrapped = hq[wrap(unhygienic[log], u[txt], ast_literal[tree])] 120 | stop() 121 | return wrapped 122 | 123 | elif isinstance(tree, ast.stmt): 124 | txt = exact_src(tree) 125 | trace_walk.walk_children(tree) 126 | with hq as code: 127 | unhygienic[log](u[txt]) 128 | stop() 129 | return [code, tree] 130 | 131 | return trace_walk.recurse(tree) 132 | 133 | 134 | @macros.expr 135 | def trace(tree, exact_src, **kw): 136 | """Traces the wrapped code, printing out the source code and evaluated 137 | result of every statement and expression contained within it""" 138 | ret = trace_walk_func(tree, exact_src) 139 | yield ret 140 | 141 | 142 | @macros.block # noqa: F811 143 | def trace(tree, exact_src, **kw): 144 | """Traces the wrapped code, printing out the source code and evaluated 145 | result of every statement and expression contained within it""" 146 | ret = trace_walk_func(tree, exact_src) 147 | yield ret 148 | 149 | 150 | def require_transform(tree, exact_src): 151 | ret = trace_walk_func(copy.deepcopy(tree), exact_src) 152 | trace_walk_func(copy.deepcopy(tree), exact_src) 153 | new = hq[ast_literal[tree] or wrap_require(lambda log: ast_literal[ret])] 154 | return new 155 | 156 | 157 | def wrap_require(thunk): 158 | out = [] 159 | thunk(out.append) 160 | raise AssertionError("Require Failed\n" + "\n".join(out)) 161 | 162 | 163 | @macros.expr 164 | def require(tree, exact_src, **kw): 165 | """A version of assert that traces the expression's evaluation in the 166 | case of failure. If used as a block, performs this on every expression 167 | within the block""" 168 | yield require_transform(tree, exact_src) 169 | 170 | 171 | @macros.block # noqa: F811 172 | def require(tree, exact_src, **kw): 173 | """A version of assert that traces the expression's evaluation in the 174 | case of failure. If used as a block, performs this on every expression 175 | within the block""" 176 | for expr in tree: 177 | expr.value = require_transform(expr.value, exact_src) 178 | 179 | yield tree 180 | 181 | 182 | @macros.expose_unhygienic # noqa: F811 183 | def log(x): 184 | print(x) 185 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | .. -*- coding: utf-8 -*- 2 | 3 | ============================= 4 | MacroPy3 1.1.0b2 5 | ============================= 6 | 7 | .. image:: https://travis-ci.org/azazel75/macropy.svg?branch=master 8 | :target: https://travis-ci.org/azazel75/macropy 9 | 10 | **MacroPy** is an implementation of `Syntactic Macros 11 | `_ in the `Python Programming Language 12 | `_. MacroPy provides a mechanism for user-defined 13 | functions (macros) to perform transformations on the `abstract syntax 14 | tree `_ (AST) of a 15 | Python program at *import time*. This is an easy way to enhance the 16 | semantics of a Python program in ways which are otherwise impossible, 17 | for example providing an extremely concise way of declaring classes. 18 | 19 | Python like you've never seen before 20 | ==================================== 21 | 22 | MacroPy allows you to create constructs which are impossible to have 23 | in normal python code, such as: 24 | 25 | Tracing 26 | ------- 27 | 28 | .. code:: python 29 | 30 | with trace: 31 | sum([x + 5 for x in range(3)]) 32 | 33 | # sum([x + 5 for x in range(3)]) 34 | # range(3) -> [0, 1, 2] 35 | # x + 5 -> 5 36 | # x + 5 -> 6 37 | # x + 5 -> 7 38 | # [x + 5 for x in range(3)] -> [5, 6, 7] 39 | # sum([x + 5 for x in range(3)]) -> 18 40 | 41 | Quick Lambdas 42 | ------------- 43 | 44 | .. code:: python 45 | 46 | print(list(map(f[_[0]], ['omg', 'wtf', 'bbq']))) 47 | # ['o', 'w', 'b'] 48 | 49 | print(list(reduce(f[_ + _], ['omg', 'wtf', 'bbq']))) 50 | # 'omgwtfbbq 51 | 52 | Case Classes 53 | ------------ 54 | 55 | .. code:: python 56 | 57 | @case 58 | class Point(x, y): pass 59 | 60 | p = Point(1, 2) 61 | 62 | print str(p) #Point(1, 2) 63 | print p.x #1 64 | print p.y #2 65 | print Point(1, 2) == Point(1, 2) # True 66 | 67 | and more! See the docs at 68 | ``_. 69 | 70 | Requirements 71 | ============ 72 | 73 | MacroPy3 is tested to run on `CPython 3.4 74 | `_ or newer and `PyPy 75 | `_ 3.5. I has no current support for `Jython 76 | `_. MacroPy3 is also available on `PyPI 77 | `_. 78 | 79 | Installation 80 | ============ 81 | 82 | Just execute a: 83 | 84 | .. code:: console 85 | 86 | $ pip install macropy3 87 | 88 | if you want to use macros that require external libraries in order to 89 | work, you can automatically install those dependencies by installing 90 | one of the ``pinq`` or ``pyxl`` extras like this: 91 | 92 | .. code:: console 93 | 94 | $ pip install macropy3[pinq,pyxl] 95 | 96 | 97 | then have a look at the docs at ``_. 98 | 99 | How to contribute 100 | ================= 101 | 102 | We're open to contributions, so send us your 103 | ideas/questions/issues/pull-requests and we'll do our best to 104 | accommodate you! You can ask questions on the `Google Group 105 | `_ and on the 106 | `Gitter channel `_ or file bugs on 107 | thee `issues`__ page. 108 | 109 | __ https://github.com/lihaoyi/macropy/issues 110 | 111 | Credits 112 | ======= 113 | 114 | MacroPy was initially created as a final project for the `MIT 115 | `_ class `6.945: Adventures in Advanced Symbolic 116 | Programming `_, 117 | taught by `Gerald Jay Sussman 118 | `_ and `Pavel Panchekha 119 | `_. Inspiration was taken from project such 120 | as `Scala Macros `_, `Karnickel 121 | `_ and `Pyxl 122 | `_. 123 | 124 | The MIT License (MIT) 125 | 126 | Copyright (c) 2013-2018, `Li Haoyi `_, `Justin 127 | Holmgren `_, `Alberto Berti 128 | `_ and all the other contributors 129 | 130 | Permission is hereby granted, free of charge, to any person obtaining a copy 131 | of this software and associated documentation files (the "Software"), to deal 132 | in the Software without restriction, including without limitation the rights 133 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 134 | copies of the Software, and to permit persons to whom the Software is 135 | furnished to do so, subject to the following conditions: 136 | 137 | The above copyright notice and this permission notice shall be included in 138 | all copies or substantial portions of the Software. 139 | 140 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 141 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 142 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 143 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 144 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 145 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 146 | THE SOFTWARE. 147 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | import macropy.activate 4 | 5 | import macropy.test 6 | 7 | res = unittest.TextTestRunner().run(macropy.test.Tests) 8 | sys.exit(len(res.failures)) 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: python -*- 2 | from pathlib import Path 3 | from setuptools import find_packages, setup 4 | from macropy import __version__ 5 | 6 | here = Path(__file__).absolute().parent 7 | with (here / 'CHANGES.rst').open(encoding='utf-8') as f: 8 | CHANGES = f.read() 9 | with (here / 'readme.rst').open(encoding='utf-8') as f: 10 | README = f.read() 11 | 12 | setup( 13 | name='macropy3', 14 | version=__version__, 15 | description='Macros for Python: Quasiquotes, Case Classes, LINQ and more!', 16 | long_description=README + '\n\n' + CHANGES, 17 | license='MIT', 18 | author='Li Haoyi, Justin Holmgren', 19 | author_email='haoyi.sg@gmail.com, justin.holmgren@gmail.com', 20 | maintainer='Alberto Berti', 21 | maintainer_email='alberto@metapensiero.it', 22 | url='https://github.com/lihaoyi/macropy', 23 | packages=find_packages(exclude=["*.test", "*.test.*"]), 24 | extras_require={ 25 | 'pyxl': ["pyxl3"], 26 | 'pinq': ["SQLAlchemy"], 27 | 'js_snippets': ["selenium", "pjs"], 28 | }, 29 | install_requires=[], 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.5', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: Implementation :: CPython', 38 | 'Programming Language :: Python :: Implementation :: PyPy', 39 | 'Topic :: Software Development :: Code Generators', 40 | ], 41 | python_requires='>=3.4,<3.8', 42 | project_urls={ 43 | 'Documentation': 'http://macropy3.readthedocs.io/en/latest/index.html', 44 | } 45 | ) 46 | --------------------------------------------------------------------------------